/** * @license Highcharts JS v11.4.1 (2024-04-04) * * Sonification module * * (c) 2010-2024 Highsoft AS * Author: Øystein Moseng * * License: www.highcharts.com/license */ (function (factory) { if (typeof module === 'object' && module.exports) { factory['default'] = factory; module.exports = factory; } else if (typeof define === 'function' && define.amd) { define('highcharts/modules/sonification', ['highcharts'], function (Highcharts) { factory(Highcharts); factory.Highcharts = Highcharts; return factory; }); } else { factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined); } }(function (Highcharts) { 'use strict'; var _modules = Highcharts ? Highcharts._modules : {}; function _registerModule(obj, path, args, fn) { if (!obj.hasOwnProperty(path)) { obj[path] = fn.apply(null, args); if (typeof CustomEvent === 'function') { window.dispatchEvent(new CustomEvent( 'HighchartsModuleLoaded', { detail: { path: path, module: obj[path] } } )); } } } _registerModule(_modules, 'Extensions/Sonification/Options.js', [], function () { /* * * * (c) 2009-2024 Øystein Moseng * * Default options for sonification. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const Options = { /** * Options for configuring sonification and audio charts. Requires the * [sonification module](https://code.highcharts.com/modules/sonification.js) * to be loaded. * * @sample highcharts/demo/all-instruments * All predefined instruments * @sample highcharts/demo/audio-boxplot * Audio boxplots * @sample highcharts/demo/plotline-context * Context tracks * @sample highcharts/demo/sonification-music * Musical chart * * @since 11.0.0 * @requires modules/sonification * @optionparent sonification */ sonification: { /** * Global tracks to add to every series. * * Defined as an array of either instrument or speech tracks, * or a combination. * * @declare Highcharts.SonificationTracksOptions * @extends sonification.defaultSpeechOptions * @extends sonification.defaultInstrumentOptions * @type {Array<*>} * @apioption sonification.globalTracks */ /** * Rate mapping for speech tracks. * * @declare Highcharts.SonificationTracksRateOptions * @extends sonification.defaultSpeechOptions.mapping.rate * @apioption sonification.globalTracks.mapping.rate */ /** * Text mapping for speech tracks. * * @declare Highcharts.SonificationTracksTextOptions * @extends sonification.defaultSpeechOptions.mapping.text * @apioption sonification.globalTracks.mapping.text */ /** * Context tracks to add globally, an array of either instrument * tracks, speech tracks, or a mix. * * Context tracks are not tied to data points, but play at a set * interval - either based on time or on prop values. * * @sample highcharts/demo/plotline-context * Using contexts * * @declare Highcharts.SonificationContextTracksOptions * @extends sonification.globalTracks * @type {Array<*>} * @apioption sonification.globalContextTracks */ /** * Set a context track to play periodically every `timeInterval` * milliseconds while the sonification is playing. * * @sample highcharts/demo/plotline-context * Using contexts * * @type {number} * @apioption sonification.globalContextTracks.timeInterval */ /** * Set a context track to play periodically every `valueInterval` * units of a data property `valueProp` while the sonification is * playing. * * For example, setting `valueProp` to `x` and `valueInterval` to 5 * will play the context track for every 5th X value. * * The context audio events will be mapped to time according to the * prop value relative to the min/max values for that prop. * * @sample highcharts/demo/plotline-context * Using contexts * * @type {number} * @apioption sonification.globalContextTracks.valueInterval */ /** * The point property to play context for when using `valueInterval`. * * @type {string} * @default "x" * @apioption sonification.globalContextTracks.valueProp */ /** * How to map context events to time when using the `valueInterval` * option. * * @type {"linear"|"logarithmic"} * @default "linear" * @apioption sonification.globalContextTracks.valueMapFunction */ /** * Set up event handlers for the sonification * * @apioption sonification.events */ /** * Called on play. * * A context object is passed to the function, with properties `chart` * and `timeline`. * * @type {Function} * @apioption sonification.events.onPlay */ /** * Called on pause, cancel, or if play is completed. * * A context object is passed to the function, with properties `chart`, * `timeline` and `pointsPlayed`. `pointsPlayed` is an array of `Point` * objects, referencing data points that were related to the audio * events played. * * @type {Function} * @apioption sonification.events.onStop */ /** * Called when play is completed. * * A context object is passed to the function, with properties `chart`, * `timeline` and `pointsPlayed`. `pointsPlayed` is an array of `Point` * objects, referencing data points that were related to the audio * events played. * * @type {Function} * @apioption sonification.events.onEnd */ /** * Called immediately when a play is requested. * * A context object is passed to the function, with properties `chart` * and `timeline`. * * @type {Function} * @apioption sonification.events.beforePlay */ /** * Called before updating the sonification. * * A context object is passed to the function, with properties `chart` * and `timeline`. * * @type {Function} * @apioption sonification.events.beforeUpdate */ /** * Called after updating the sonification. * * A context object is passed to the function, with properties `chart` * and `timeline`. * * @type {Function} * @apioption sonification.events.afterUpdate */ /** * Called on the beginning of playing a series. * * A context object is passed to the function, with properties `series` * and `timeline`. * * @type {Function} * @apioption sonification.events.onSeriesStart */ /** * Called when finished playing a series. * * A context object is passed to the function, with properties `series` * and `timeline`. * * @type {Function} * @apioption sonification.events.onSeriesEnd */ /** * Called when attempting to play an adjacent point or series, and * there is none. * * By default a percussive sound is played. * * A context object is passed to the function, with properties `chart`, * `timeline`, and `attemptedNext`. `attemptedNext` is a boolean * property that is `true` if the boundary hit was from trying to play * the next series/point, and `false` if it was from trying to play the * previous. * * @type {Function} * @apioption sonification.events.onBoundaryHit */ /** * Enable sonification functionality for the chart. */ enabled: true, /** * The total duration of the sonification, in milliseconds. */ duration: 6000, /** * The time to wait in milliseconds after each data series when playing * the series one after the other. * * @sample highcharts/sonification/chart-earcon * Notification after series * * @see [order](#sonification.order) */ afterSeriesWait: 700, /** * How long to wait between each recomputation of the sonification, if * the chart updates rapidly. This avoids slowing down processes like * panning. Given in milliseconds. */ updateInterval: 200, /** * Overall/master volume for the sonification, from 0 to 1. */ masterVolume: 0.7, /** * What order to play the data series in, either `sequential` where * the series play individually one after the other, or `simultaneous` * where the series play all at once. * * @sample highcharts/sonification/chart-simultaneous * Simultaneous sonification * * @type {"sequential"|"simultaneous"} */ order: 'sequential', /** * Show tooltip as the chart plays. * * Note that if multiple tracks that play at different times try to * show the tooltip, it can be glitchy, so it is recommended in * those cases to turn this on/off for individual tracks using the * [showPlayMarker](#plotOptions.series.sonification.tracks.showPlayMarker) * option. * * @see [showCrosshair](#sonification.showCrosshair) */ showTooltip: true, /** * Show X and Y axis crosshairs (if they exist) as the chart plays. * * Note that if multiple tracks that play at different times try to * show the crosshairs, it can be glitchy, so it is recommended in * those cases to turn this on/off for individual tracks using the * [showPlayMarker](#plotOptions.series.sonification.tracks.showPlayMarker) * option. * * @see [showTooltip](#sonification.showTooltip) * @see [crosshair](#xAxis.crosshair) */ showCrosshair: true, /** * Options for grouping data points together when sonifying. This * allows for the visual presentation to contain more points than what * is being played. If not enabled, all visible / uncropped points are * played. * * @see [series.cropThreshold](#plotOptions.series.cropThreshold) */ pointGrouping: { /** * Whether or not to group points */ enabled: true, /** * The size of each group in milliseconds. Audio events closer than * this are grouped together. */ groupTimespan: 15, /** * The grouping algorithm, deciding which points to keep when * grouping a set of points together. By default `"minmax"` is * used, which keeps both the minimum and maximum points. * * The other algorithms will either keep the first point in the * group (time wise), last point, middle point, or both the first * and the last point. * * The timing of the resulting point(s) is then adjusted to play * evenly, regardless of its original position within the group. * * @type {"minmax"|"first"|"last"|"middle"|"firstlast"} */ algorithm: 'minmax', /** * The data property for each point to compare when deciding which * points to keep in the group. * * By default it is "y", which means that if the `"minmax"` * algorithm is used, the two points the group with the lowest and * highest `y` value will be kept, and the others not played. */ prop: 'y' }, /** * Default sonification options for all instrument tracks. * * If specific options are also set on individual tracks or per * series, those will override these options. * * @sample highcharts/sonification/point-sonify * Sonify points on click * * @declare Highcharts.SonificationInstrumentOptions */ defaultInstrumentOptions: { /** * Round pitch mapping to musical notes. * * If `false`, will play the exact mapped note, even if it is out * of tune compared to the musical notes as defined by 440Hz * standard tuning. */ roundToMusicalNotes: true, /** * Type of track. Always `"instrument"` for instrument tracks, and * `"speech"` for speech tracks. * * @declare Highcharts.SonifcationTypeValue * @type {string} * @default instrument * @validvalue ["instrument","speech"] * @apioption sonification.defaultInstrumentOptions.type */ /** * Show play marker (tooltip and/or crosshair) for a track. * * @type {boolean} * @default true * @apioption sonification.defaultInstrumentOptions.showPlayMarker */ /** * Name to use for a track when exporting to MIDI. * By default it uses the series name if the track is related to * a series. * * @type {string} * @apioption sonification.defaultInstrumentOptions.midiName */ /** * Options for point grouping, specifically for instrument tracks. * * @declare Highcharts.SonificationInstrumentPointGroupingOptions * @extends sonification.pointGrouping * @apioption sonification.defaultInstrumentOptions.pointGrouping */ /** * Define a condition for when a track should be active and not. * * Can either be a function callback or a configuration object. * * If a function is used, it should return a `boolean` for whether * or not the track should be active. The function is called for * each audio event, and receives a parameter object with `time`, * and potentially `point` and `value` properties depending on the * track. `point` is available if the audio event is related to a * data point. `value` is available if the track is used as a * context track, and `valueInterval` is used. * * @sample highcharts/sonification/mapping-zones * Mapping zones * * @declare Highcharts.SonificationInstrumentActiveWhenOptions * @type {Function|*} * @apioption sonification.defaultInstrumentOptions.activeWhen */ /** * Track is only active when `prop` is above or at this value. * * @type {number} * @apioption sonification.defaultInstrumentOptions.activeWhen.min */ /** * Track is only active when `prop` is below or at this value. * * @type {number} * @apioption sonification.defaultInstrumentOptions.activeWhen.max */ /** * Track is only active when `prop` was below, and is now at or * above this value. * * If both `crossingUp` and `crossingDown` are defined, the track * is active if either condition is met. * * @type {number} * @apioption sonification.defaultInstrumentOptions.activeWhen.crossingUp */ /** * Track is only active when `prop` was above, and is now at or * below this value. * * If both `crossingUp` and `crossingDown` are defined, the track * is active if either condition is met. * * @type {number} * @apioption sonification.defaultInstrumentOptions.activeWhen.crossingDown */ /** * The point property to compare, for example `y` or `x`. * * @type {string} * @apioption sonification.defaultInstrumentOptions.activeWhen.prop */ /** * Instrument to use for playing. * * Can either be a string referencing a synth preset, or it can be * a synth configuration object. * * @sample highcharts/demo/all-instruments * Overview of available presets * @sample highcharts/sonification/custom-instrument * Custom instrument * * @type {string|Highcharts.SynthPatchOptionsObject} */ instrument: 'piano', /** * Mapping options for the audio parameters. * * All parameters can be either: * - A string, referencing a point property to map to. * - A number, setting the value of the audio parameter directly. * - A callback function, returning the value programmatically. * - An object defining detailed configuration of the mapping. * * If a function is used, it should return the desired value for * the audio parameter. The function is called for each audio event * to be played, and receives a context object parameter with * `time`, and potentially `point` and `value` depending on the * track. `point` is available if the audio event is related to a * data point, and `value` is available if the track is used for a * context track using `valueInterval`. * * @sample highcharts/sonification/mapping-overview * Overview of common mapping parameters * @sample highcharts/sonification/pitch-mapping * Various types of mapping used * @sample highcharts/sonification/polarity-invert * Inverted mapping to property * @sample highcharts/sonification/log-mapping * Logarithmic mapping to property * * @declare Highcharts.SonificationInstrumentMappingOptions */ mapping: { /** * The volume of notes, from 0 to 1. * * @declare Highcharts.SonificationInstrumentVolumeOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @default 1 * @apioption sonification.defaultInstrumentOptions.mapping.volume */ /** * Frequency in Hertz of notes. Overrides pitch mapping if set. * * @declare Highcharts.SonificationInstrumentFrequencyOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @apioption sonification.defaultInstrumentOptions.mapping.frequency */ /** * Milliseconds to wait before playing, comes in addition to * the time determined by the `time` mapping. * * Can also be negative to play before the mapped time. * * @declare Highcharts.SonificationInstrumentPlayDelayOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @apioption sonification.defaultInstrumentOptions.mapping.playDelay */ /** * Mapping options for tremolo effects. * * Tremolo is repeated changes of volume over time. * * @declare Highcharts.SonificationInstrumentTremoloOptions * @apioption sonification.defaultInstrumentOptions.mapping.tremolo */ /** * Map to tremolo depth, from 0 to 1. * * This determines the intensity of the tremolo effect, how * much the volume changes. * * @declare Highcharts.SonificationInstrumentTremoloDepthOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @apioption sonification.defaultInstrumentOptions.mapping.tremolo.depth */ /** * Map to tremolo speed, from 0 to 1. * * This determines the speed of the tremolo effect, how fast * the volume changes. * * @declare Highcharts.SonificationInstrumentTremoloSpeedOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @apioption sonification.defaultInstrumentOptions.mapping.tremolo.speed */ /** * Mapping options for the lowpass filter. * * A lowpass filter lets low frequencies through, but stops high * frequencies, making the sound more dull. * * @declare Highcharts.SonificationInstrumentLowpassOptions * @apioption sonification.defaultInstrumentOptions.mapping.lowpass */ /** * Map to filter frequency in Hertz from 1 to 20,000Hz. * * @declare Highcharts.SonificationInstrumentLowpassFrequencyOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @apioption sonification.defaultInstrumentOptions.mapping.lowpass.frequency */ /** * Map to filter resonance in dB. Can be negative to cause a * dip, or positive to cause a bump. * * @declare Highcharts.SonificationInstrumentLowpassResonanceOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @apioption sonification.defaultInstrumentOptions.mapping.lowpass.resonance */ /** * Mapping options for the highpass filter. * * A highpass filter lets high frequencies through, but stops * low frequencies, making the sound thinner. * * @declare Highcharts.SonificationInstrumentHighpassOptions * @extends sonification.defaultInstrumentOptions.mapping.lowpass * @apioption sonification.defaultInstrumentOptions.mapping.highpass */ /** * Time mapping determines what time each point plays. It is * defined as an offset in milliseconds, where 0 means it * plays immediately when the chart is sonified. * * By default time is mapped to `x`, meaning points with the * lowest `x` value plays first, and points with the highest * `x` value plays last. * * Can be set to a fixed value, a prop to map to, a function, * or a mapping object. * * @sample highcharts/sonification/point-play-time * Play points in order of Y value * * @declare Highcharts.SonificationInstrumentTimeOptions * @type {string|number|Function|*} * @default "x" */ time: 'x', /** * A point property to map the mapping parameter to. * * A negative sign `-` can be placed before the property name * to make mapping inverted. * * @sample highcharts/sonification/polarity-invert * Inverted mapping to property * * @type {string} * @apioption sonification.defaultInstrumentOptions.mapping.time.mapTo */ /** * The minimum value for the audio parameter. This is the * lowest value the audio parameter will be mapped to. * * @type {number} * @apioption sonification.defaultInstrumentOptions.mapping.time.min */ /** * The maximum value for the audio parameter. This is the * highest value the audio parameter will be mapped to. * * @type {number} * @apioption sonification.defaultInstrumentOptions.mapping.time.max */ /** * What data values to map the parameter within. * * Mapping within `"series"` will make the lowest value point * in the series map to the min audio parameter value, and the * highest value will map to the max audio parameter. * * Mapping within `"chart"` will make the lowest value point in * the whole chart map to the min audio parameter value, and * the highest value in the whole chart will map to the max * audio parameter. * * You can also map within the X or Y axis of each series. * * @sample highcharts/sonification/mapping-within * Mapping within demonstrated * * @type {"chart"|"series"|"xAxis"|"yAxis"} * @apioption sonification.defaultInstrumentOptions.mapping.time.within */ /** * How to perform the mapping. * @sample highcharts/sonification/log-mapping * Logarithmic mapping to property * * @type {"linear"|"logarithmic"} * @apioption sonification.defaultInstrumentOptions.mapping.time.mapFunction */ /** * A fixed value to use for the prop when mapping. * * For example, if mapping to `y`, setting value to `4` will * map as if all points had a y value of 4. * * @sample highcharts/demo/plotline-context * Map to fixed y value * * @type {number} * @apioption sonification.defaultInstrumentOptions.mapping.time.value */ /** * Pan refers to the stereo panning position of the sound. * It is defined from -1 (left) to 1 (right). * * By default it is mapped to `x`, making the sound move from * left to right as the chart plays. * * Can be set to a fixed value, a prop to map to, a function, * or a mapping object. * * @extends sonification.defaultInstrumentOptions.mapping.time * @default "x" */ pan: 'x', /** * Note duration determines for how long a note plays, in * milliseconds. * * It only affects instruments that are able to play * continuous sustained notes. Examples of these instruments * from the presets include `flute`, `saxophone`, `trumpet`, * `sawsynth`, `wobble`, `basic1`, `basic2`, `sine`, * `sineGlide`, `triangle`, `square`, `sawtooth`, `noise`, * `filteredNoise`, and `wind`. * * Can be set to a fixed value, a prop to map to, a function, * or a mapping object. * * @extends sonification.defaultInstrumentOptions.mapping.time * @default 200 */ noteDuration: 200, /** * Musical pitch refers to how high or low notes are played. * * By default it is mapped to `y` so low `y` values are played * with a lower pitch, and high values are played with a higher * pitch. * * Pitch mapping has a few extra features compared to other * audio parameters. * * Firstly, it accepts not only number values, but also string * values denoting note names. These are given in the form * ``, for example `"c6"` or `"F#2"`. * * Secondly, it is possible to map pitch to an array of notes. * In this case, the `[gapBetweenNotes](#sonification.defaultInstrumentOptions.mapping.gapBetweenNotes)` * mapping determines the delay between these notes. * * Thirdly, it is possible to define a musical scale to follow * when mapping. * * Can be set to a fixed value, an array, a prop to map to, a * function, or a mapping object. * * @sample highcharts/sonification/pitch-mapping * Various types of mapping used * @sample highcharts/sonification/polarity-invert * Inverted mapping to property * @sample highcharts/sonification/log-mapping * Logarithmic mapping to property * * @declare Highcharts.SonificationInstrumentPitchOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @type {string|number|Function|Array|*} */ pitch: { mapTo: 'y', min: 'c2', max: 'c6', within: 'yAxis' }, /** * Map pitches to a musical scale. The scale is defined as an * array of semitone offsets from the root note. * * @sample highcharts/sonification/all-scales * Predefined scale presets * * @type {Array} * @apioption sonification.defaultInstrumentOptions.mapping.pitch.scale */ /** * Gap in milliseconds between notes if pitch is mapped to an * array of notes. * * Can be set to a fixed value, a prop to map to, a function, * or a mapping object. * * @sample maps/demo/audio-map * Mapping to gap between notes * * @extends sonification.defaultInstrumentOptions.mapping.time * @default 100 */ gapBetweenNotes: 100 } }, /** * Default sonification options for all speech tracks. * * If specific options are also set on individual tracks or per * series, those will override these options. * * @sample highcharts/sonification/speak-values * Speak values * * @declare Highcharts.SonificationSpeechOptions * @extends sonification.defaultInstrumentOptions * @excluding roundToMusicalNotes, midiName, instrument */ defaultSpeechOptions: { /** * Type of track. Always `"instrument"` for instrument tracks, and * `"speech"` for speech tracks. * * @declare Highcharts.SonifcationTypeValue * @type {string} * @default speech * @validvalue ["instrument","speech"] * @apioption sonification.defaultSpeechOptions.type */ /** * Name of the voice synthesis to prefer for speech tracks. * * If not available, falls back to the default voice for the * selected language. * * Different platforms provide different voices for web speech * synthesis. * * @type {string} * @apioption sonification.defaultSpeechOptions.preferredVoice */ /** * The language to speak in for speech tracks, as an IETF BCP 47 * language tag. * * @sample maps/demo/audio-map * French language speech */ language: 'en-US', /** * Mapping configuration for the speech/audio parameters. * * All parameters except `text` can be either: * - A string, referencing a point property to map to. * - A number, setting the value of the speech parameter directly. * - A callback function, returning the value programmatically. * - An object defining detailed configuration of the mapping. * * If a function is used, it should return the desired value for * the speech parameter. The function is called for each speech * event to be played, and receives a context object parameter with * `time`, and potentially `point` and `value` depending on the * track. `point` is available if the audio event is related to a * data point, and `value` is available if the track is used for a * context track using `valueInterval`. * * @declare Highcharts.SonificationSpeechMappingOptions * @extends sonification.defaultInstrumentOptions.mapping * @excluding frequency, gapBetweenNotes, highpass, lowpass, * tremolo, noteDuration, pan * @apioption sonification.defaultSpeechOptions.mapping */ mapping: { /** * Milliseconds to wait before playing, comes in addition to * the time determined by the `time` mapping. * * Can also be negative to play before the mapped time. * * @declare Highcharts.SonificationSpeechPlayDelayOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @apioption sonification.defaultSpeechOptions.mapping.playDelay */ /** * Speech pitch (how high/low the voice is) multiplier. * @sample highcharts/sonification/speak-values * Speak values * * @declare Highcharts.SonificationSpeechPitchOptions * @extends sonification.defaultInstrumentOptions.mapping.time * @excluding scale * @type {string|number|Function|*} * @default 1 * @apioption sonification.defaultSpeechOptions.mapping.pitch */ /** * @default undefined * @apioption sonification.defaultSpeechOptions.mapping.pitch.mapTo */ /** * @default undefined * @apioption sonification.defaultSpeechOptions.mapping.pitch.min */ /** * @default undefined * @apioption sonification.defaultSpeechOptions.mapping.pitch.max */ /** * @default undefined * @apioption sonification.defaultSpeechOptions.mapping.pitch.within */ /** * The text to announce for speech tracks. Can either be a * format string or a function. * * If it is a function, it should return the format string to * announce. The function is called for each audio event, and * receives a parameter object with `time`, and potentially * `point` and `value` properties depending on the track. * `point` is available if the audio event is related to a data * point. `value` is available if the track is used as a * context track, and `valueInterval` is used. * * If it is a format string, in addition to normal string * content, format values can be accessed using bracket * notation. For example `"Value is {point.y}%"`. * * `time`, `point` and `value` are available to the format * strings similarly to with functions. Nested properties can * be accessed with dot notation, for example * `"Density: {point.options.custom.density}"` * * @sample highcharts/sonification/speak-values * Speak values * * @type {string|Function} * @apioption sonification.defaultSpeechOptions.mapping.text */ /** * @extends sonification.defaultInstrumentOptions.mapping.time * @default "x" */ time: 'x', /** * Speech rate (speed) multiplier. * @extends sonification.defaultInstrumentOptions.mapping.time * @default 1.3 */ rate: 1.3, /** * Volume of the speech announcement. * @extends sonification.defaultInstrumentOptions.mapping.volume * @default 0.4 */ volume: 0.4 }, pointGrouping: { algorithm: 'last' } } }, exporting: { menuItemDefinitions: { downloadMIDI: { textKey: 'downloadMIDI', onclick: function () { if (this.sonification) { this.sonification.downloadMIDI(); } } }, playAsSound: { textKey: 'playAsSound', onclick: function () { const s = this.sonification; if (s && s.isPlaying()) { s.cancel(); } else { this.sonify(); } } } } }, /** * @optionparent lang * @private */ lang: { /** * The text for the MIDI download menu item in the export menu. * @requires modules/sonification * @since 11.0.0 */ downloadMIDI: 'Download MIDI', /** * The text for the Play as sound menu item in the export menu. * @requires modules/sonification * @since 11.0.0 */ playAsSound: 'Play as sound' } }; /* * * * API declarations * * */ /** * Sonification/audio chart options for a series. * * @declare Highcharts.SeriesSonificationOptions * @since 11.0.0 * @requires modules/sonification * @apioption plotOptions.series.sonification */ /** * Whether or not sonification is enabled for this series. * * @type {boolean} * @default true * @apioption plotOptions.series.sonification.enabled */ /** * Context tracks for this series. Context tracks are tracks that are not * tied to data points. * * Given as an array of instrument tracks, speech tracks, or a mix of both. * * @declare Highcharts.SeriesSonificationContextTracksOptions * @type {Array<*>} * @extends sonification.globalContextTracks * @apioption plotOptions.series.sonification.contextTracks */ /** * Tracks for this series. * * Given as an array of instrument tracks, speech tracks, or a mix of both. * * @declare Highcharts.SeriesSonificationTracksOptions * @type {Array<*>} * @extends sonification.globalTracks * @apioption plotOptions.series.sonification.tracks */ /** * Default options for all this series' instrument tracks. * * @declare Highcharts.SeriesSonificationInstrumentOptions * @extends sonification.defaultInstrumentOptions * @apioption plotOptions.series.sonification.defaultInstrumentOptions */ /** * Default options for all this series' speech tracks. * * @declare Highcharts.SeriesSonificationSpeechOptions * @extends sonification.defaultSpeechOptions * @apioption plotOptions.series.sonification.defaultSpeechOptions */ /** * Sonification point grouping options for this series. * * @declare Highcharts.SeriesSonificationPointGroupingOptions * @extends sonification.pointGrouping * @apioption plotOptions.series.sonification.pointGrouping */ /** * Event context object sent to sonification chart events. * @requires modules/sonification * @interface Highcharts.SonificationChartEventCallbackContext */ /** * The relevant chart * @name Highcharts.SonificationChartEventCallbackContext#chart * @type {Highcharts.Chart|undefined} */ /** * The points that were played, if any * @name Highcharts.SonificationChartEventCallbackContext#pointsPlayed * @type {Array|undefined} */ /** * The playing timeline object with advanced and internal content * @name Highcharts.SonificationChartEventCallbackContext#timeline * @type {object|undefined} */ /** * Event context object sent to sonification series events. * @requires modules/sonification * @interface Highcharts.SonificationSeriesEventCallbackContext */ /** * The relevant series * @name Highcharts.SonificationSeriesEventCallbackContext#series * @type {Highcharts.Series|undefined} */ /** * The playing timeline object with advanced and internal content * @name Highcharts.SonificationSeriesEventCallbackContext#timeline * @type {object|undefined} */ /** * Callback function for sonification events on chart. * @callback Highcharts.SonificationChartEventCallback * @param {Highcharts.SonificationChartEventCallbackContext} e Sonification chart event context */ /** * Callback function for sonification events on series. * @callback Highcharts.SonificationSeriesEventCallback * @param {Highcharts.SonificationSeriesEventCallbackContext} e Sonification series event context */ (''); // Keep above doclets in JS file return Options; }); _registerModule(_modules, 'Extensions/Sonification/SynthPatch.js', [_modules['Core/Utilities.js']], function (U) { /* * * * (c) 2009-2024 Øystein Moseng * * Class representing a Synth Patch, used by Instruments in the * sonification.js module. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { clamp, defined, pick } = U; /** * Get the multiplier value from a pitch tracked multiplier. The parameter * specifies the multiplier at ca 3200Hz. It is 1 at ca 50Hz. In between * it is mapped logarithmically. * @private * @param {number} multiplier The multiplier to track. * @param {number} freq The current frequency. */ function getPitchTrackedMultiplierVal(multiplier, freq) { const a = 0.2414 * multiplier - 0.2414, b = (3.5 - 1.7 * multiplier) / 1.8; return a * Math.log(freq) + b; } /** * Schedule a mini ramp to volume at time - avoid clicks/pops. * @private * @param {Object} gainNode The gain node to schedule for. * @param {number} time The time in seconds to start ramp. * @param {number} vol The volume to ramp to. */ function miniRampToVolAtTime(gainNode, time, vol) { gainNode.gain.cancelScheduledValues(time); gainNode.gain.setTargetAtTime(vol, time, SynthPatch.stopRampTime / 4); gainNode.gain.setValueAtTime(vol, time + SynthPatch.stopRampTime); } /** * Schedule a gain envelope for a gain node. * @private * @param {Array} envelope The envelope to schedule. * @param {string} type Type of envelope, attack or release. * @param {number} time At what time (in seconds) to start envelope. * @param {Object} gainNode The gain node to schedule on. * @param {number} [volumeMultiplier] Volume multiplier for the envelope. */ function scheduleGainEnvelope(envelope, type, time, gainNode, volumeMultiplier = 1) { const isAtk = type === 'attack', gain = gainNode.gain; gain.cancelScheduledValues(time); if (!envelope.length) { miniRampToVolAtTime(gainNode, time, isAtk ? volumeMultiplier : 0); return; } if (envelope[0].t > 1) { envelope.unshift({ t: 0, vol: isAtk ? 0 : 1 }); } envelope.forEach((ep, ix) => { const prev = envelope[ix - 1], delta = prev ? (ep.t - prev.t) / 1000 : 0, startTime = time + (prev ? prev.t / 1000 + SynthPatch.stopRampTime : 0); gain.setTargetAtTime(ep.vol * volumeMultiplier, startTime, Math.max(delta, SynthPatch.stopRampTime) / 2); }); } /** * Internal class used by Oscillator, representing a Pulse Oscillator node. * Combines two sawtooth oscillators to create a pulse by phase inverting and * delaying one of them. * @class * @private */ class PulseOscNode { constructor(context, options) { this.pulseWidth = Math.min(Math.max(0, options.pulseWidth || 0.5)); const makeOsc = () => new OscillatorNode(context, { type: 'sawtooth', detune: options.detune, frequency: Math.max(1, options.frequency || 350) }); this.sawOscA = makeOsc(); this.sawOscB = makeOsc(); this.phaseInverter = new GainNode(context, { gain: -1 }); this.masterGain = new GainNode(context); this.delayNode = new DelayNode(context, { delayTime: this.pulseWidth / this.sawOscA.frequency.value }); this.sawOscA.connect(this.masterGain); this.sawOscB.connect(this.phaseInverter); this.phaseInverter.connect(this.delayNode); this.delayNode.connect(this.masterGain); } connect(destination) { this.masterGain.connect(destination); } // Polymorph with normal osc.frequency API getFrequencyFacade() { const pulse = this; return { cancelScheduledValues(fromTime) { pulse.sawOscA.frequency.cancelScheduledValues(fromTime); pulse.sawOscB.frequency.cancelScheduledValues(fromTime); pulse.delayNode.delayTime.cancelScheduledValues(fromTime); return pulse.sawOscA.frequency; }, setValueAtTime(frequency, time) { this.cancelScheduledValues(time); pulse.sawOscA.frequency.setValueAtTime(frequency, time); pulse.sawOscB.frequency.setValueAtTime(frequency, time); pulse.delayNode.delayTime.setValueAtTime(Math.round(10000 * pulse.pulseWidth / frequency) / 10000, time); return pulse.sawOscA.frequency; }, setTargetAtTime(frequency, time, timeConstant) { this.cancelScheduledValues(time); pulse.sawOscA.frequency .setTargetAtTime(frequency, time, timeConstant); pulse.sawOscB.frequency .setTargetAtTime(frequency, time, timeConstant); pulse.delayNode.delayTime.setTargetAtTime(Math.round(10000 * pulse.pulseWidth / frequency) / 10000, time, timeConstant); return pulse.sawOscA.frequency; } }; } getPWMTarget() { return this.delayNode.delayTime; } start() { this.sawOscA.start(); this.sawOscB.start(); } stop(time) { this.sawOscA.stop(time); this.sawOscB.stop(time); } } /** * Internal class used by SynthPatch * @class * @private */ class Oscillator { constructor(audioContext, options, destination) { this.audioContext = audioContext; this.options = options; this.fmOscillatorIx = options.fmOscillator; this.vmOscillatorIx = options.vmOscillator; this.createSoundSource(); this.createGain(); this.createFilters(); this.createVolTracking(); if (destination) { this.connect(destination); } } // Connect the node tree from destination down to oscillator, // depending on which nodes exist. Done automatically unless // no destination was passed to constructor. connect(destination) { [ this.lowpassNode, this.highpassNode, this.volTrackingNode, this.vmNode, this.gainNode, this.whiteNoise, this.pulseNode, this.oscNode ].reduce((prev, cur) => (cur ? (cur.connect(prev), cur) : prev), destination); } start() { if (this.oscNode) { this.oscNode.start(); } if (this.whiteNoise) { this.whiteNoise.start(); } if (this.pulseNode) { this.pulseNode.start(); } } stopAtTime(time) { if (this.oscNode) { this.oscNode.stop(time); } if (this.whiteNoise) { this.whiteNoise.stop(time); } if (this.pulseNode) { this.pulseNode.stop(time); } } setFreqAtTime(time, frequency, glideDuration = 0) { const opts = this.options, f = clamp(pick(opts.fixedFrequency, frequency) * (opts.freqMultiplier || 1), 0, 21000), oscTarget = this.getOscTarget(), timeConstant = glideDuration / 5000; if (oscTarget) { oscTarget.cancelScheduledValues(time); if (glideDuration && time - (this.lastUpdateTime || -1) > 0.01) { oscTarget.setTargetAtTime(f, time, timeConstant); oscTarget.setValueAtTime(f, time + timeConstant); } else { oscTarget.setValueAtTime(f, time); } } this.scheduleVolTrackingChange(f, time, glideDuration); this.scheduleFilterTrackingChange(f, time, glideDuration); this.lastUpdateTime = time; } // Get target for FM synthesis if another oscillator wants to modulate. // Pulse nodes don't do FM, but do PWM instead. getFMTarget() { return this.oscNode && this.oscNode.detune || this.whiteNoise && this.whiteNoise.detune || this.pulseNode && this.pulseNode.getPWMTarget(); } // Get target for volume modulation if another oscillator wants to modulate. getVMTarget() { return this.vmNode && this.vmNode.gain; } // Schedule one of the oscillator envelopes at a specified time in // seconds (in AudioContext timespace). runEnvelopeAtTime(type, time) { if (!this.gainNode) { return; } const env = (type === 'attack' ? this.options.attackEnvelope : this.options.releaseEnvelope) || []; scheduleGainEnvelope(env, type, time, this.gainNode, this.options.volume); } // Cancel any envelopes or frequency changes currently scheduled cancelScheduled() { if (this.gainNode) { this.gainNode.gain .cancelScheduledValues(this.audioContext.currentTime); } const oscTarget = this.getOscTarget(); if (oscTarget) { oscTarget.cancelScheduledValues(0); } if (this.lowpassNode) { this.lowpassNode.frequency.cancelScheduledValues(0); } if (this.highpassNode) { this.highpassNode.frequency.cancelScheduledValues(0); } if (this.volTrackingNode) { this.volTrackingNode.gain.cancelScheduledValues(0); } } // Set the pitch dependent volume to fit some frequency at some time scheduleVolTrackingChange(frequency, time, glideDuration) { if (this.volTrackingNode) { const v = getPitchTrackedMultiplierVal(this.options.volumePitchTrackingMultiplier || 1, frequency), rampTime = glideDuration ? glideDuration / 1000 : SynthPatch.stopRampTime; this.volTrackingNode.gain.cancelScheduledValues(time); this.volTrackingNode.gain.setTargetAtTime(v, time, rampTime / 5); this.volTrackingNode.gain.setValueAtTime(v, time + rampTime); } } // Set the pitch dependent filter frequency to fit frequency at some time scheduleFilterTrackingChange(frequency, time, glideDuration) { const opts = this.options, rampTime = glideDuration ? glideDuration / 1000 : SynthPatch.stopRampTime, scheduleFilterTarget = (filterNode, filterOptions) => { const multiplier = getPitchTrackedMultiplierVal(filterOptions.frequencyPitchTrackingMultiplier || 1, frequency), f = clamp((filterOptions.frequency || 1000) * multiplier, 0, 21000); filterNode.frequency.cancelScheduledValues(time); filterNode.frequency.setTargetAtTime(f, time, rampTime / 5); filterNode.frequency.setValueAtTime(f, time + rampTime); }; if (this.lowpassNode && opts.lowpass) { scheduleFilterTarget(this.lowpassNode, opts.lowpass); } if (this.highpassNode && opts.highpass) { scheduleFilterTarget(this.highpassNode, opts.highpass); } } createGain() { const opts = this.options, needsGainNode = defined(opts.volume) || opts.attackEnvelope && opts.attackEnvelope.length || opts.releaseEnvelope && opts.releaseEnvelope.length; if (needsGainNode) { this.gainNode = new GainNode(this.audioContext, { gain: pick(opts.volume, 1) }); } // We always need VM gain, so make that this.vmNode = new GainNode(this.audioContext); } // Create the oscillator or audio buffer acting as the sound source createSoundSource() { const opts = this.options, ctx = this.audioContext, frequency = (opts.fixedFrequency || 0) * (opts.freqMultiplier || 1); if (opts.type === 'whitenoise') { const bSize = ctx.sampleRate * 2, buffer = ctx.createBuffer(1, bSize, ctx.sampleRate), data = buffer.getChannelData(0); for (let i = 0; i < bSize; ++i) { // More pleasant "white" noise with less variance than -1 to +1 data[i] = Math.random() * 1.2 - 0.6; } const wn = this.whiteNoise = ctx.createBufferSource(); wn.buffer = buffer; wn.loop = true; } else if (opts.type === 'pulse') { this.pulseNode = new PulseOscNode(ctx, { detune: opts.detune, pulseWidth: opts.pulseWidth, frequency }); } else { this.oscNode = new OscillatorNode(ctx, { type: opts.type || 'sine', detune: opts.detune, frequency }); } } // Lowpass/Highpass filters createFilters() { const opts = this.options; if (opts.lowpass && opts.lowpass.frequency) { this.lowpassNode = new BiquadFilterNode(this.audioContext, { type: 'lowpass', Q: opts.lowpass.Q || 1, frequency: opts.lowpass.frequency }); } if (opts.highpass && opts.highpass.frequency) { this.highpassNode = new BiquadFilterNode(this.audioContext, { type: 'highpass', Q: opts.highpass.Q || 1, frequency: opts.highpass.frequency }); } } // Gain node used for frequency dependent volume tracking createVolTracking() { const opts = this.options; if (opts.volumePitchTrackingMultiplier && opts.volumePitchTrackingMultiplier !== 1) { this.volTrackingNode = new GainNode(this.audioContext, { gain: 1 }); } } // Get the oscillator frequency target getOscTarget() { return this.oscNode ? this.oscNode.frequency : this.pulseNode && this.pulseNode.getFrequencyFacade(); } } /** * The SynthPatch class. This class represents an instance and configuration * of the built-in Highcharts synthesizer. It can be used to play various * generated sounds. * * @sample highcharts/sonification/manual-using-synth * Using Synth directly to sonify manually * @sample highcharts/sonification/custom-instrument * Using custom Synth options with chart * * @requires modules/sonification * * @class * @name Highcharts.SynthPatch * * @param {AudioContext} audioContext * The AudioContext to use. * @param {Highcharts.SynthPatchOptionsObject} options * Configuration for the synth. */ class SynthPatch { constructor(audioContext, options) { this.audioContext = audioContext; this.options = options; this.eqNodes = []; this.midiInstrument = options.midiInstrument || 1; this.outputNode = new GainNode(audioContext, { gain: 0 }); this.createEqChain(this.outputNode); const inputNode = this.eqNodes.length ? this.eqNodes[0] : this.outputNode; this.oscillators = (this.options.oscillators || []).map((oscOpts) => new Oscillator(audioContext, oscOpts, defined(oscOpts.fmOscillator) || defined(oscOpts.vmOscillator) ? void 0 : inputNode)); // Now that we have all oscillators, connect the ones // that are used for modulation. this.oscillators.forEach((osc) => { const connectTarget = (targetFunc, targetOsc) => { if (targetOsc) { const target = targetOsc[targetFunc](); if (target) { osc.connect(target); } } }; if (defined(osc.fmOscillatorIx)) { connectTarget('getFMTarget', this.oscillators[osc.fmOscillatorIx]); } if (defined(osc.vmOscillatorIx)) { connectTarget('getVMTarget', this.oscillators[osc.vmOscillatorIx]); } }); } /** * Start the oscillators, but don't output sound. * @function Highcharts.SynthPatch#startSilently */ startSilently() { this.outputNode.gain.value = 0; this.oscillators.forEach((o) => o.start()); } /** * Stop the synth. It can't be started again. * @function Highcharts.SynthPatch#stop */ stop() { const curTime = this.audioContext.currentTime, endTime = curTime + SynthPatch.stopRampTime; miniRampToVolAtTime(this.outputNode, curTime, 0); this.oscillators.forEach((o) => o.stopAtTime(endTime)); this.outputNode.disconnect(); } /** * Mute sound at time (in seconds). * Will still run release envelope. Note: If scheduled multiple times in * succession, the release envelope will run, and that could make sound. * @function Highcharts.SynthPatch#silenceAtTime * @param {number} time Time offset from now, in seconds */ silenceAtTime(time) { if (!time && this.outputNode.gain.value < 0.01) { this.outputNode.gain.value = 0; return; // Skip if not needed } this.releaseAtTime((time || 0) + this.audioContext.currentTime); } /** * Mute sound immediately. * @function Highcharts.SynthPatch#mute */ mute() { this.cancelScheduled(); miniRampToVolAtTime(this.outputNode, this.audioContext.currentTime, 0); } /** * Play a frequency at time (in seconds). * Time denotes when the attack ramp starts. Note duration is given in * milliseconds. If note duration is not given, the note plays indefinitely. * @function Highcharts.SynthPatch#silenceAtTime * @param {number} time Time offset from now, in seconds * @param {number} frequency The frequency to play at * @param {number|undefined} noteDuration Duration to play, in milliseconds */ playFreqAtTime(time, frequency, noteDuration) { const t = (time || 0) + this.audioContext.currentTime, opts = this.options; this.oscillators.forEach((o) => { o.setFreqAtTime(t, frequency, opts.noteGlideDuration); o.runEnvelopeAtTime('attack', t); }); scheduleGainEnvelope(opts.masterAttackEnvelope || [], 'attack', t, this.outputNode, opts.masterVolume); if (noteDuration) { this.releaseAtTime(t + noteDuration / 1000); } } /** * Cancel any scheduled actions * @function Highcharts.SynthPatch#cancelScheduled */ cancelScheduled() { this.outputNode.gain.cancelScheduledValues(this.audioContext.currentTime); this.oscillators.forEach((o) => o.cancelScheduled()); } /** * Connect the SynthPatch output to an audio node / destination. * @function Highcharts.SynthPatch#connect * @param {AudioNode} destinationNode The node to connect to. * @return {AudioNode} The destination node, to allow chaining. */ connect(destinationNode) { return this.outputNode.connect(destinationNode); } /** * Create nodes for master EQ * @private */ createEqChain(outputNode) { this.eqNodes = (this.options.eq || []).map((eqDef) => new BiquadFilterNode(this.audioContext, { type: 'peaking', ...eqDef })); // Connect nodes this.eqNodes.reduceRight((chain, node) => { node.connect(chain); return node; }, outputNode); } /** * Fade by release envelopes at time * @private */ releaseAtTime(time) { let maxReleaseDuration = 0; this.oscillators.forEach((o) => { const env = o.options.releaseEnvelope; if (env && env.length) { maxReleaseDuration = Math.max(maxReleaseDuration, env[env.length - 1].t); o.runEnvelopeAtTime('release', time); } }); const masterEnv = this.options.masterReleaseEnvelope || []; if (masterEnv.length) { scheduleGainEnvelope(masterEnv, 'release', time, this.outputNode, this.options.masterVolume); maxReleaseDuration = Math.max(maxReleaseDuration, masterEnv[masterEnv.length - 1].t); } miniRampToVolAtTime(this.outputNode, time + maxReleaseDuration / 1000, 0); } } SynthPatch.stopRampTime = 0.012; // Ramp time to 0 when stopping sound /* * * * Default Export * * */ /* * * * API declarations * * */ /** * An EQ filter definition for a low/highpass filter. * @requires modules/sonification * @interface Highcharts.SynthPatchPassFilter */ /** * Filter frequency. * @name Highcharts.SynthPatchPassFilter#frequency * @type {number|undefined} */ /** * A pitch tracking multiplier similarly to the one for oscillator volume. * Affects the filter frequency. * @name Highcharts.SynthPatchPassFilter#frequencyPitchTrackingMultiplier * @type {number|undefined} */ /** * Filter resonance bump/dip in dB. Defaults to 0. * @name Highcharts.SynthPatchPassFilter#Q * @type {number|undefined} */ /** * @typedef {Highcharts.Record<"t"|"vol",number>} Highcharts.SynthEnvelopePoint * @requires modules/sonification */ /** * @typedef {Array} Highcharts.SynthEnvelope * @requires modules/sonification */ /** * @typedef {"sine"|"square"|"sawtooth"|"triangle"|"whitenoise"|"pulse"} Highcharts.SynthPatchOscillatorType * @requires modules/sonification */ /** * Configuration for an oscillator for the synth. * @requires modules/sonification * @interface Highcharts.SynthPatchOscillatorOptionsObject */ /** * The type of oscillator. This describes the waveform of the oscillator. * @name Highcharts.SynthPatchOscillatorOptionsObject#type * @type {Highcharts.SynthPatchOscillatorType|undefined} */ /** * A volume modifier for the oscillator. Defaults to 1. * @name Highcharts.SynthPatchOscillatorOptionsObject#volume * @type {number|undefined} */ /** * A multiplier for the input frequency of the oscillator. Defaults to 1. If * this is for example set to 4, an input frequency of 220Hz will cause the * oscillator to play at 880Hz. * @name Highcharts.SynthPatchOscillatorOptionsObject#freqMultiplier * @type {number|undefined} */ /** * Play a fixed frequency for the oscillator - ignoring input frequency. The * frequency multiplier is still applied. * @name Highcharts.SynthPatchOscillatorOptionsObject#fixedFrequency * @type {number|undefined} */ /** * Applies a detuning of all frequencies. Set in cents. Defaults to 0. * @name Highcharts.SynthPatchOscillatorOptionsObject#detune * @type {number|undefined} */ /** * Width of the pulse waveform. Only applies to "pulse" type oscillators. A * width of 0.5 is roughly equal to a square wave. This is the default. * @name Highcharts.SynthPatchOscillatorOptionsObject#pulseWidth * @type {number|undefined} */ /** * Index of another oscillator to use as carrier, with this oscillator being * used as a volume modulator. The first oscillator in the array has index 0, * and so on. This option can be used to produce tremolo effects. * @name Highcharts.SynthPatchOscillatorOptionsObject#vmOscillator * @type {number|undefined} */ /** * Index of another oscillator to use as carrier, with this oscillator being * used as a frequency modulator. Note: If the carrier is a pulse oscillator, * the modulation will be on pulse width instead of frequency, allowing for * PWM effects. * @name Highcharts.SynthPatchOscillatorOptionsObject#fmOscillator * @type {number|undefined} */ /** * A tracking multiplier used for frequency dependent behavior. For example, by * setting the volume tracking multiplier to 0.01, the volume will be lower at * higher notes. The multiplier is a logarithmic function, where 1 is at ca * 50Hz, and you define the output multiplier for an input frequency around * 3.2kHz. * @name Highcharts.SynthPatchOscillatorOptionsObject#volumePitchTrackingMultiplier * @type {number|undefined} */ /** * Volume envelope for note attack, specific to this oscillator. * @name Highcharts.SynthPatchOscillatorOptionsObject#attackEnvelope * @type {Highcharts.SynthEnvelope|undefined} */ /** * Volume envelope for note release, specific to this oscillator. * @name Highcharts.SynthPatchOscillatorOptionsObject#releaseEnvelope * @type {Highcharts.SynthEnvelope|undefined} */ /** * Highpass filter options for the oscillator. * @name Highcharts.SynthPatchOscillatorOptionsObject#highpass * @type {Highcharts.SynthPatchPassFilter|undefined} */ /** * Lowpass filter options for the oscillator. * @name Highcharts.SynthPatchOscillatorOptionsObject#lowpass * @type {Highcharts.SynthPatchPassFilter|undefined} */ /** * An EQ filter definition for a bell filter. * @requires modules/sonification * @interface Highcharts.SynthPatchEQFilter */ /** * Filter frequency. * @name Highcharts.SynthPatchEQFilter#frequency * @type {number|undefined} */ /** * Filter gain. Defaults to 0. * @name Highcharts.SynthPatchEQFilter#gain * @type {number|undefined} */ /** * Filter Q. Defaults to 1. Lower numbers mean a wider bell. * @name Highcharts.SynthPatchEQFilter#Q * @type {number|undefined} */ /** * A set of options for the SynthPatch class. * * @requires modules/sonification * * @interface Highcharts.SynthPatchOptionsObject */ /** * Global volume modifier for the synth. Defaults to 1. Note that if the total * volume of all oscillators is too high, the browser's audio engine can * distort. * @name Highcharts.SynthPatchOptionsObject#masterVolume * @type {number|undefined} */ /** * Time in milliseconds to glide between notes. Causes a glissando effect. * @name Highcharts.SynthPatchOptionsObject#noteGlideDuration * @type {number|undefined} */ /** * MIDI instrument ID for the synth. Used with MIDI export of Timelines to have * tracks open with a similar instrument loaded when imported into other * applications. Defaults to 1, "Acoustic Grand Piano". * @name Highcharts.SynthPatchOptionsObject#midiInstrument * @type {number|undefined} */ /** * Volume envelope for the overall attack of a note - what happens to the * volume when a note first plays. If the volume goes to 0 in the attack * envelope, the synth will not be able to play the note continuously/ * sustained, and the notes will be staccato. * @name Highcharts.SynthPatchOptionsObject#masterAttackEnvelope * @type {Highcharts.SynthEnvelope|undefined} */ /** * Volume envelope for the overall release of a note - what happens to the * volume when a note stops playing. If the release envelope starts at a higher * volume than the attack envelope ends, the volume will first rise to that * volume before falling when releasing a note. If the note is released while * the attack envelope is still in effect, the attack envelope is interrupted, * and the release envelope plays instead. * @name Highcharts.SynthPatchOptionsObject#masterReleaseEnvelope * @type {Highcharts.SynthEnvelope|undefined} */ /** * Master EQ filters for the synth, affecting the overall sound. * @name Highcharts.SynthPatchOptionsObject#eq * @type {Array|undefined} */ /** * Array of oscillators to add to the synth. * @name Highcharts.SynthPatchOptionsObject#oscillators * @type {Array|undefined} */ (''); // Keep above doclets in JS file return SynthPatch; }); _registerModule(_modules, 'Extensions/Sonification/InstrumentPresets.js', [], function () { /* * * * (c) 2009-2024 Øystein Moseng * * Presets for SynthPatch. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const InstrumentPresets = { // PIANO ---------------------------- piano: { masterVolume: 0.45, masterAttackEnvelope: [ { t: 1, vol: 0.71 }, { t: 40, vol: 0.79 }, { t: 82, vol: 0.64 }, { t: 147, vol: 0.29 }, { t: 260, vol: 0.15 }, { t: 417, vol: 0.05 }, { t: 589, vol: 0 } ], eq: [ { frequency: 200, Q: 0.7, gain: 6 }, { frequency: 450, gain: 6 }, { frequency: 1300, gain: 2 }, { frequency: 2600, Q: 0.8, gain: 8 }, { frequency: 3500, Q: 0.8, gain: 6 }, { frequency: 6200, Q: 0.8, gain: 10 }, { frequency: 8000, gain: -23 }, { frequency: 10000, Q: 0.4, gain: -12 } ], oscillators: [{ type: 'pulse', volume: 0.5, pulseWidth: 0.55, volumePitchTrackingMultiplier: 0.1, lowpass: { frequency: 4.5, frequencyPitchTrackingMultiplier: 900, Q: -2 }, highpass: { frequency: 270 }, attackEnvelope: [{ t: 1, vol: 1 }], releaseEnvelope: [ { t: 1, vol: 1 }, { t: 282, vol: 0.64 }, { t: 597, vol: 0 } ] }, { type: 'whitenoise', volume: 0.8, lowpass: { frequency: 400 }, highpass: { frequency: 300 }, attackEnvelope: [ { t: 1, vol: 1 }, { t: 19, vol: 0 } ] }] }, // PLUCKED -------------------------- plucked: { masterVolume: 0.5, midiInstrument: 25, masterAttackEnvelope: [ { t: 1, vol: 0.71 }, { t: 4, vol: 0.71 }, { t: 31, vol: 0.4 }, { t: 109, vol: 0.12 }, { t: 234, vol: 0.04 }, { t: 442, vol: 0 } ], eq: [ { frequency: 800, gain: -8 }, { frequency: 1400, Q: 4, gain: 4 }, { frequency: 1600, gain: -14 }, { frequency: 2200, gain: -8 }, { frequency: 3600, gain: -2 }, { frequency: 6400, Q: 2, gain: -6 } ], oscillators: [{ type: 'sawtooth', volume: 0.9, volumePitchTrackingMultiplier: 0.6, highpass: { frequency: 100 }, lowpass: { frequency: 8000 }, releaseEnvelope: [ { t: 1, vol: 1 }, { t: 315, vol: 0.56 }, { t: 550, vol: 0 } ] }] }, // FLUTE ---------------------------- flute: { masterVolume: 1.1, midiInstrument: 74, noteGlideDuration: 30, masterAttackEnvelope: [ { t: 0, vol: 0 }, { t: 29, vol: 1 }, { t: 76, vol: 0.48 }, { t: 600, vol: 0.36 } ], masterReleaseEnvelope: [ { t: 1, vol: 0.36 }, { t: 24, vol: 0.15 }, { t: 119, vol: 0 } ], eq: [ { frequency: 150, Q: 0.6, gain: -10 }, { frequency: 500, gain: 4 }, { frequency: 1100, gain: -4 }, { frequency: 2200, gain: -14 }, { frequency: 5000, gain: 8 }, { frequency: 6400, gain: 10 }, { frequency: 8000, gain: 12 }, { frequency: 10800, gain: 8 } ], oscillators: [{ type: 'triangle', volume: 1, volumePitchTrackingMultiplier: 0.4, lowpass: { frequency: 12, frequencyPitchTrackingMultiplier: 100 }, highpass: { frequency: 200 } }, { type: 'sine', fixedFrequency: 5, volume: 0.2, vmOscillator: 0, attackEnvelope: [ { t: 1, vol: 1 }, { t: 48, vol: 0 }, { t: 225, vol: 0.05 }, { t: 600, vol: 0.77 } ] }, { type: 'whitenoise', volume: 0.13, lowpass: { frequency: 9000, Q: 3 }, highpass: { frequency: 6000, Q: 3 }, vmOscillator: 0, attackEnvelope: [ { t: 0, vol: 0 }, { t: 26, vol: 1 }, { t: 93, vol: 0.8 } ] }] }, // LEAD ----------------------------- lead: { masterVolume: 1, midiInstrument: 20, masterAttackEnvelope: [ { t: 1, vol: 0.81 }, { t: 98, vol: 0.5 }, { t: 201, vol: 0.18 }, { t: 377, vol: 0.04 }, { t: 586, vol: 0 }, { t: 586, vol: 0 } ], eq: [ { frequency: 200, gain: -6 }, { frequency: 400, gain: -8 }, { frequency: 800, Q: 0.5, gain: -10 }, { frequency: 1200, gain: 4 }, { frequency: 3600, gain: -4 }, { frequency: 4200, gain: -12 }, { frequency: 7400, gain: -14 }, { frequency: 10000, gain: 2 } ], oscillators: [{ type: 'triangle', volume: 1.1, volumePitchTrackingMultiplier: 0.6, lowpass: { frequency: 5000 }, highpass: { frequency: 100 } }, { type: 'sawtooth', volume: 0.4, lowpass: { frequency: 7000 }, highpass: { frequency: 800, Q: 6 }, releaseEnvelope: [ { t: 0, vol: 0.99 }, { t: 200, vol: 0.83 }, { t: 495, vol: 0 } ] }] }, // VIBRAPHONE ----------------------- vibraphone: { masterVolume: 1, midiInstrument: 12, masterAttackEnvelope: [ { t: 1, vol: 0 }, { t: 10, vol: 0.63 }, { t: 82, vol: 0.64 }, { t: 149, vol: 0.26 }, { t: 600, vol: 0 } ], eq: [ { frequency: 200, Q: 0.8, gain: -12 }, { frequency: 400, gain: -4 }, { frequency: 1600, Q: 0.5, gain: 6 }, { frequency: 2200, Q: 0.5, gain: 6 }, { frequency: 6400, gain: 4 }, { frequency: 12800, gain: 4 } ], oscillators: [{ type: 'sine', volume: 1.5, volumePitchTrackingMultiplier: 0.0000001, attackEnvelope: [{ t: 1, vol: 1 }], releaseEnvelope: [ { t: 1, vol: 1 }, { t: 146, vol: 0.39 }, { t: 597, vol: 0 } ] }, { type: 'whitenoise', volume: 0.03, volumePitchTrackingMultiplier: 0.0001, lowpass: { frequency: 900 }, highpass: { frequency: 800 }, attackEnvelope: [ { t: 1, vol: 1 }, { t: 9, vol: 0 } ] }, { type: 'sine', freqMultiplier: 4, volume: 0.15, volumePitchTrackingMultiplier: 0.0001 }, { type: 'sine', fixedFrequency: 3, volume: 6, fmOscillator: 0, releaseEnvelope: [ { t: 1, vol: 1 }, { t: 190, vol: 0.41 }, { t: 600, vol: 0 } ] }, { type: 'sine', fixedFrequency: 6, volume: 3, fmOscillator: 2 }, { type: 'sine', freqMultiplier: 9, volume: 0.0005, volumePitchTrackingMultiplier: 0.0001, releaseEnvelope: [ { t: 1, vol: 0.97 }, { t: 530, vol: 0 } ] }] }, // SAXOPHONE ------------------------ saxophone: { masterVolume: 1, midiInstrument: 67, noteGlideDuration: 10, masterAttackEnvelope: [ { t: 1, vol: 0.57 }, { t: 35, vol: 1 }, { t: 87, vol: 0.84 }, { t: 111, vol: 0.6 }, { t: 296, vol: 0.49 }, { t: 600, vol: 0.58 } ], masterReleaseEnvelope: [ { t: 1, vol: 0.58 }, { t: 47, vol: 0.16 }, { t: 119, vol: 0 } ], eq: [ { frequency: 200, gain: -2 }, { frequency: 600, gain: 2 }, { frequency: 800, gain: -10 }, { frequency: 1100, gain: -2 }, { frequency: 2200, gain: -2 }, { frequency: 3500, gain: 10 }, { frequency: 12800, gain: 4 } ], oscillators: [{ type: 'sawtooth', volume: 0.45, volumePitchTrackingMultiplier: 0.06, lowpass: { frequency: 18, frequencyPitchTrackingMultiplier: 200 }, highpass: { frequency: 300 } }, { type: 'whitenoise', fixedFrequency: 1, volume: 0.4, highpass: { frequency: 7000 }, vmOscillator: 0, attackEnvelope: [ { t: 1, vol: 1 }, { t: 51, vol: 1 }, { t: 86, vol: 0.84 }, { t: 500, vol: 0.78 } ] }, { type: 'sine', fixedFrequency: 4, volume: 2, fmOscillator: 0, attackEnvelope: [ { t: 0, vol: 0 }, { t: 15, vol: 0.94 }, { t: 79, vol: 1 }, { t: 172, vol: 0.47 }, { t: 500, vol: 0.26 } ] }, { type: 'sine', fixedFrequency: 7, volume: 6, fmOscillator: 0, attackEnvelope: [ { t: 0, vol: 0 }, { t: 25, vol: 0.99 }, { t: 85, vol: 0 }, { t: 85, vol: 0 }, { t: 387, vol: 0.02 }, { t: 511, vol: 0.43 }, { t: 600, vol: 0 } ] }] }, // TRUMPET ------------------------ trumpet: { masterVolume: 0.3, midiInstrument: 57, noteGlideDuration: 40, masterAttackEnvelope: [ { t: 1, vol: 0 }, { t: 17, vol: 1 }, { t: 42, vol: 0.85 }, { t: 76, vol: 1 }, { t: 202, vol: 0.65 }, { t: 226, vol: 0.86 }, { t: 282, vol: 0.63 } ], masterReleaseEnvelope: [ { t: 1, vol: 0.62 }, { t: 34, vol: 0.14 }, { t: 63, vol: 0.21 }, { t: 96, vol: 0 } ], eq: [ { frequency: 200, Q: 0.6, gain: 10 }, { frequency: 600, Q: 0.5, gain: 6 }, { frequency: 1500, Q: 0.7, gain: 14 }, { frequency: 3200, Q: 2, gain: 8 }, { frequency: 3800, Q: 0.8, gain: 10 }, { frequency: 6200, gain: 12 }, { frequency: 8400, gain: -20 }, { frequency: 12800, Q: 0.5, gain: -18 } ], oscillators: [{ type: 'sawtooth', volume: 0.15, pulseWidth: 0.5, volumePitchTrackingMultiplier: 0.5, lowpass: { frequency: 1900, Q: 3 } }, { type: 'sine', fixedFrequency: 6, volume: 0.2, vmOscillator: 0, attackEnvelope: [ { t: 1, vol: 1 }, { t: 102, vol: 0.13 }, { t: 556, vol: 0.24 } ] }, { type: 'whitenoise', volume: 0.45, highpass: { frequency: 7000, Q: 9 }, vmOscillator: 0, attackEnvelope: [ { t: 1, vol: 1 }, { t: 89, vol: 0.51 }, { t: 577, vol: 0.29 } ] }, { type: 'sine', fixedFrequency: 5.7, volume: 20, fmOscillator: 0, attackEnvelope: [ { t: 1, vol: 1 }, { t: 89, vol: 1 }, { t: 137, vol: 0.46 }, { t: 283, vol: 0.15 }, { t: 600, vol: 0.28 } ] }] }, // SAWSYNTH -------------------------- sawsynth: { masterVolume: 0.3, midiInstrument: 51, noteGlideDuration: 40, masterAttackEnvelope: [ { t: 0, vol: 0.6 }, { t: 9, vol: 1 }, { t: 102, vol: 0.48 } ], eq: [{ frequency: 200, gain: -6 }], oscillators: [{ type: 'sawtooth', volume: 0.4, volumePitchTrackingMultiplier: 0.3 }, { type: 'sawtooth', volume: 0.4, detune: 11, volumePitchTrackingMultiplier: 0.3 }, { type: 'sawtooth', volume: 0.4, detune: -11, volumePitchTrackingMultiplier: 0.3 }] }, // BASIC1 --------------------------- basic1: { masterVolume: 1, noteGlideDuration: 0, masterReleaseEnvelope: [ { t: 1, vol: 0.36 }, { t: 24, vol: 0.15 }, { t: 119, vol: 0 } ], eq: [ { frequency: 150, Q: 0.6, gain: -12 }, { frequency: 1100, gain: -2 }, { frequency: 2200, gain: -16 }, { frequency: 5000, gain: 8 }, { frequency: 6400, gain: 10 }, { frequency: 8000, gain: 12 }, { frequency: 10800, gain: 8 } ], oscillators: [{ type: 'triangle', volume: 1, volumePitchTrackingMultiplier: 0.05, lowpass: { frequency: 17, frequencyPitchTrackingMultiplier: 100 }, highpass: { frequency: 200 } }, { type: 'whitenoise', volume: 0.04, lowpass: { frequency: 9000, Q: 3 }, highpass: { frequency: 6000, Q: 3 }, vmOscillator: 0, attackEnvelope: [ { t: 0, vol: 0 }, { t: 26, vol: 1 }, { t: 71, vol: 0.73 } ] }] }, // BASIC2 --------------------------- basic2: { masterVolume: 0.3, eq: [ { frequency: 200, Q: 0.7, gain: 6 }, { frequency: 450, gain: 2 }, { frequency: 1300, gain: -2 }, { frequency: 2600, Q: 0.8, gain: 6 }, { frequency: 3500, Q: 0.8, gain: 6 }, { frequency: 6200, Q: 0.8, gain: 10 }, { frequency: 8000, gain: -18 }, { frequency: 10000, Q: 0.4, gain: -12 } ], oscillators: [{ type: 'pulse', volume: 0.4, pulseWidth: 0.55, volumePitchTrackingMultiplier: 0.1, lowpass: { frequency: 4.5, frequencyPitchTrackingMultiplier: 900, Q: -2 }, highpass: { frequency: 270 } }] }, // CHORD ------------------------------- chord: { masterVolume: 1, masterAttackEnvelope: [ { t: 1, vol: 0.79 }, { t: 27, vol: 0.86 }, { t: 62, vol: 0.81 }, { t: 150, vol: 0.35 }, { t: 408, vol: 0.04 }, { t: 600, vol: 0 } ], eq: [ { frequency: 200, gain: -8 }, { frequency: 600, Q: 2, gain: 4 }, { frequency: 800, gain: -10 }, { frequency: 1600, gain: -2 }, { frequency: 2200, gain: -6 }, { frequency: 3600, Q: 0.7, gain: -2 }, { frequency: 6400, gain: 6 }, { frequency: 12800, gain: 6 } ], oscillators: [{ type: 'triangle', volume: 1.1, volumePitchTrackingMultiplier: 0.05, lowpass: { frequency: 8000 }, highpass: { frequency: 100 }, releaseEnvelope: [ { t: 1, vol: 1 }, { t: 315, vol: 0.56 }, { t: 540, vol: 0 } ] }, { type: 'triangle', freqMultiplier: 1.17, volume: 0.4, volumePitchTrackingMultiplier: 0.07, lowpass: { frequency: 5000 }, highpass: { frequency: 100 }, releaseEnvelope: [ { t: 0, vol: 1 }, { t: 476, vol: 0 } ] }, { type: 'triangle', freqMultiplier: 1.58333, volume: 0.7, volumePitchTrackingMultiplier: 0.02, highpass: { frequency: 200 }, releaseEnvelope: [ { t: 0, vol: 1 }, { t: 422, vol: 0.56 }, { t: 577, vol: 0 } ] }, { type: 'sine', fixedFrequency: 10, volume: 4, fmOscillator: 0, attackEnvelope: [ { t: 1, vol: 1 }, { t: 157, vol: 0.65 } ] }, { type: 'sine', fixedFrequency: 5, volume: 0.3, vmOscillator: 2, attackEnvelope: [ { t: 1, vol: 1 }, { t: 155, vol: 0.91 }, { t: 289, vol: 0.78 } ] }] }, // WOBBLE --------------------------- wobble: { masterVolume: 0.9, masterReleaseEnvelope: [ { t: 1, vol: 0.36 }, { t: 24, vol: 0.15 }, { t: 119, vol: 0 } ], eq: [ { frequency: 150, Q: 0.6, gain: -12 }, { frequency: 1100, gain: -2 }, { frequency: 2200, gain: -16 }, { frequency: 5000, gain: 8 }, { frequency: 6400, gain: 10 }, { frequency: 8000, gain: 12 }, { frequency: 10800, gain: 8 } ], oscillators: [{ type: 'triangle', volume: 0.9, volumePitchTrackingMultiplier: 0.1, lowpass: { frequency: 17, frequencyPitchTrackingMultiplier: 100 }, highpass: { frequency: 200 } }, { type: 'whitenoise', volume: 0.04, lowpass: { frequency: 9000, Q: 3 }, highpass: { frequency: 6000, Q: 3 }, vmOscillator: 0, attackEnvelope: [ { t: 0, vol: 0 }, { t: 26, vol: 1 }, { t: 71, vol: 0.73 } ] }, { type: 'sine', freqMultiplier: 0.011, volume: 30, fmOscillator: 0 }] }, // SINE ----------------------------- sine: { masterVolume: 1, oscillators: [{ type: 'sine', volumePitchTrackingMultiplier: 0.07 }] }, // SINE GLIDE ----------------------- sineGlide: { masterVolume: 1, noteGlideDuration: 100, oscillators: [{ type: 'sine', volumePitchTrackingMultiplier: 0.07 }] }, // TRIANGLE ------------------------- triangle: { masterVolume: 0.5, oscillators: [{ type: 'triangle', volume: 1, volumePitchTrackingMultiplier: 0.07 }] }, // SAWTOOTH ------------------------- sawtooth: { masterVolume: 0.25, midiInstrument: 82, oscillators: [{ type: 'sawtooth', volume: 0.3, volumePitchTrackingMultiplier: 0.07 }] }, // SQUARE --------------------------- square: { masterVolume: 0.3, midiInstrument: 81, oscillators: [{ type: 'square', volume: 0.2, volumePitchTrackingMultiplier: 0.07 }] }, // PERCUSSION INSTRUMENTS ---------- chop: { masterVolume: 1, midiInstrument: 116, masterAttackEnvelope: [{ t: 1, vol: 1 }, { t: 44, vol: 0 }], oscillators: [{ type: 'whitenoise', volume: 1, lowpass: { frequency: 600 }, highpass: { frequency: 200 } }] }, shaker: { masterVolume: 0.4, midiInstrument: 116, masterAttackEnvelope: [{ t: 1, vol: 1 }, { t: 44, vol: 0 }], oscillators: [{ type: 'whitenoise', volume: 1, lowpass: { frequency: 6500 }, highpass: { frequency: 5000 } }] }, step: { masterVolume: 1, midiInstrument: 116, masterAttackEnvelope: [{ t: 1, vol: 1 }, { t: 44, vol: 0 }], eq: [ { frequency: 200, gain: -1 }, { frequency: 400, gain: -14 }, { frequency: 800, gain: 8 }, { frequency: 1000, Q: 5, gain: -24 }, { frequency: 1600, gain: 8 }, { frequency: 2200, gain: -10 }, { frequency: 5400, gain: 4 }, { frequency: 12800, gain: -36 } ], oscillators: [{ type: 'whitenoise', volume: 1.5, lowpass: { frequency: 300 }, highpass: { frequency: 100, Q: 6 } }] }, kick: { masterVolume: 0.55, masterAttackEnvelope: [ { t: 1, vol: 0.8 }, { t: 15, vol: 1 }, { t: 45, vol: 0.35 }, { t: 121, vol: 0.11 }, { t: 242, vol: 0 } ], eq: [ { frequency: 50, gain: 6 }, { frequency: 400, gain: -18 }, { frequency: 1600, gain: 18 } ], oscillators: [{ type: 'triangle', fixedFrequency: 90, volume: 1, lowpass: { frequency: 300 }, attackEnvelope: [ { t: 1, vol: 1 }, { t: 6, vol: 1 }, { t: 45, vol: 0.01 } ] }, { type: 'whitenoise', volume: 0.4, lowpass: { frequency: 200 }, attackEnvelope: [ { t: 1, vol: 1 }, { t: 30, vol: 0 } ] }, { type: 'triangle', freqMultiplier: 0.1, volume: 1, lowpass: { frequency: 200 } }] }, shortnote: { masterVolume: 0.8, midiInstrument: 116, masterAttackEnvelope: [ { t: 1, vol: 1 }, { t: 15, vol: 0 } ], eq: [ { frequency: 400, gain: -4 }, { frequency: 800, gain: -12 }, { frequency: 2400, gain: 4 }, { frequency: 7200, gain: -20 }, { frequency: 1000, Q: 5, gain: -12 }, { frequency: 5400, gain: -32 }, { frequency: 12800, gain: -14 } ], oscillators: [{ type: 'sawtooth', volume: 0.6, lowpass: { frequency: 1000 } }, { type: 'whitenoise', volume: 0.2, lowpass: { frequency: 10000 }, highpass: { frequency: 7000 }, attackEnvelope: [ { t: 1, vol: 1 }, { t: 10, vol: 0 } ] }, { type: 'whitenoise', volume: 1.3, lowpass: { frequency: 700, Q: 4 }, highpass: { frequency: 250 } }] }, // NOISE ---------------------------- noise: { masterVolume: 0.3, midiInstrument: 122, oscillators: [{ type: 'whitenoise' }] }, // FILTERED NOISE ------------------- filteredNoise: { masterVolume: 0.3, midiInstrument: 122, eq: [ { frequency: 1600, gain: -8 }, { frequency: 2200, gain: -4 } ], oscillators: [{ type: 'whitenoise', lowpass: { frequency: 5, frequencyPitchTrackingMultiplier: 1300, Q: 6 }, highpass: { frequency: 5, frequencyPitchTrackingMultiplier: 300, Q: 6 } }] }, // WIND ------------------------------- wind: { masterVolume: 0.75, midiInstrument: 122, noteGlideDuration: 150, masterReleaseEnvelope: [ { t: 0, vol: 1 }, { t: 124, vol: 0.24 }, { t: 281, vol: 0 } ], oscillators: [{ type: 'whitenoise', volume: 1, lowpass: { frequency: 100, frequencyPitchTrackingMultiplier: 6, Q: 23 }, highpass: { frequency: 170, frequencyPitchTrackingMultiplier: 6 } }, { type: 'sine', freqMultiplier: 0.016, volume: 1000, fmOscillator: 0 }] } }; /* * * * Default Export * * */ /* * * * API declarations * * */ /** * @typedef {"piano"|"plucked"|"flute"|"lead"|"vibraphone"|"saxophone"|"trumpet"|"sawsynth"|"basic1"|"basic2"|"chord"|"wobble"|"sine"|"sineGlide"|"triangle"|"sawtooth"|"square"|"chop"|"shaker"|"step"|"kick"|"shortnote"|"noise"|"filteredNoise"|"wind"} Highcharts.SonificationSynthPreset * @requires modules/sonification */ (''); // Keep above doclets in JS file return InstrumentPresets; }); _registerModule(_modules, 'Extensions/Sonification/SonificationInstrument.js', [_modules['Extensions/Sonification/SynthPatch.js'], _modules['Extensions/Sonification/InstrumentPresets.js'], _modules['Core/Utilities.js']], function (SynthPatch, InstrumentPresets, U) { /* * * * (c) 2009-2024 Øystein Moseng * * Class representing an Instrument with mappable parameters for sonification. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { defined, extend } = U; /** * The SonificationInstrument class. This class represents an instrument with * mapping capabilities. The instrument wraps a SynthPatch object, and extends * it with functionality such as panning, tremolo, and global low/highpass * filters. * * @sample highcharts/sonification/instrument-raw * Using SonificationInstrument directly, with no chart. * * @requires modules/sonification * * @class * @name Highcharts.SonificationInstrument * * @param {AudioContext} audioContext * The AudioContext to use. * @param {AudioNode} outputNode * The destination node to connect to. * @param {Highcharts.SonificationInstrumentOptionsObject} options * Configuration for the instrument. */ class SonificationInstrument { constructor(audioContext, outputNode, options) { this.audioContext = audioContext; this.curParams = {}; this.midiTrackName = options.midiTrackName; this.masterVolNode = new GainNode(audioContext); this.masterVolNode.connect(outputNode); this.volumeNode = new GainNode(audioContext); this.createNodesFromCapabilities(extend({ pan: true }, options.capabilities || {})); this.connectCapabilityNodes(this.volumeNode, this.masterVolNode); this.synthPatch = new SynthPatch(audioContext, typeof options.synthPatch === 'string' ? InstrumentPresets[options.synthPatch] : options.synthPatch); this.midiInstrument = this.synthPatch.midiInstrument || 1; this.synthPatch.startSilently(); this.synthPatch.connect(this.volumeNode); } /** * Set the overall volume. * @function Highcharts.SonificationInstrument#setMasterVolume * @param {number} volume The volume to set, from 0 to 1. */ setMasterVolume(volume) { this.masterVolNode.gain.setTargetAtTime(volume, 0, SonificationInstrument.rampTime); } /** * Schedule an instrument event at a given time offset, whether it is * playing a note or changing the parameters of the instrument. * @function Highcharts.SonificationInstrument#scheduleEventAtTime * @param {number} time Time is given in seconds, where 0 is now. * @param {Highcharts.SonificationInstrumentScheduledEventOptionsObject} params * Parameters for the instrument event. */ scheduleEventAtTime(time, params) { const mergedParams = extend(this.curParams, params), freq = defined(params.frequency) ? params.frequency : defined(params.note) ? SonificationInstrument.musicalNoteToFrequency(params.note) : 220; if (defined(freq)) { this.synthPatch.playFreqAtTime(time, freq, mergedParams.noteDuration); } if (defined(mergedParams.tremoloDepth) || defined(mergedParams.tremoloSpeed)) { this.setTremoloAtTime(time, mergedParams.tremoloDepth, mergedParams.tremoloSpeed); } if (defined(mergedParams.pan)) { this.setPanAtTime(time, mergedParams.pan); } if (defined(mergedParams.volume)) { this.setVolumeAtTime(time, mergedParams.volume); } if (defined(mergedParams.lowpassFreq) || defined(mergedParams.lowpassResonance)) { this.setFilterAtTime('lowpass', time, mergedParams.lowpassFreq, mergedParams.lowpassResonance); } if (defined(mergedParams.highpassFreq) || defined(mergedParams.highpassResonance)) { this.setFilterAtTime('highpass', time, mergedParams.highpassFreq, mergedParams.highpassResonance); } } /** * Schedule silencing the instrument at a given time offset. * @function Highcharts.SonificationInstrument#silenceAtTime * @param {number} time Time is given in seconds, where 0 is now. */ silenceAtTime(time) { this.synthPatch.silenceAtTime(time); } /** * Cancel currently playing sounds and any scheduled actions. * @function Highcharts.SonificationInstrument#cancel */ cancel() { this.synthPatch.mute(); [ this.tremoloDepth && this.tremoloDepth.gain, this.tremoloOsc && this.tremoloOsc.frequency, this.lowpassNode && this.lowpassNode.frequency, this.lowpassNode && this.lowpassNode.Q, this.highpassNode && this.highpassNode.frequency, this.highpassNode && this.highpassNode.Q, this.panNode && this.panNode.pan, this.volumeNode.gain ].forEach((p) => (p && p.cancelScheduledValues(0))); } /** * Stop instrument and destroy it, cleaning up used resources. * @function Highcharts.SonificationInstrument#destroy */ destroy() { this.cancel(); this.synthPatch.stop(); if (this.tremoloOsc) { this.tremoloOsc.stop(); } [ this.tremoloDepth, this.tremoloOsc, this.lowpassNode, this.highpassNode, this.panNode, this.volumeNode, this.masterVolNode ].forEach(((n) => n && n.disconnect())); } /** * Schedule a pan value at a given time offset. * @private */ setPanAtTime(time, pan) { if (this.panNode) { this.panNode.pan.setTargetAtTime(pan, time + this.audioContext.currentTime, SonificationInstrument.rampTime); } } /** * Schedule a filter configuration at a given time offset. * @private */ setFilterAtTime(filter, time, frequency, resonance) { const node = this[filter + 'Node'], audioTime = this.audioContext.currentTime + time; if (node) { if (defined(resonance)) { node.Q.setTargetAtTime(resonance, audioTime, SonificationInstrument.rampTime); } if (defined(frequency)) { node.frequency.setTargetAtTime(frequency, audioTime, SonificationInstrument.rampTime); } } } /** * Schedule a volume value at a given time offset. * @private */ setVolumeAtTime(time, volume) { if (this.volumeNode) { this.volumeNode.gain.setTargetAtTime(volume, time + this.audioContext.currentTime, SonificationInstrument.rampTime); } } /** * Schedule a tremolo configuration at a given time offset. * @private */ setTremoloAtTime(time, depth, speed) { const audioTime = this.audioContext.currentTime + time; if (this.tremoloDepth && defined(depth)) { this.tremoloDepth.gain.setTargetAtTime(depth, audioTime, SonificationInstrument.rampTime); } if (this.tremoloOsc && defined(speed)) { this.tremoloOsc.frequency.setTargetAtTime(15 * speed, audioTime, SonificationInstrument.rampTime); } } /** * Create audio nodes according to instrument capabilities * @private */ createNodesFromCapabilities(capabilities) { const ctx = this.audioContext; if (capabilities.pan) { this.panNode = new StereoPannerNode(ctx); } if (capabilities.tremolo) { this.tremoloOsc = new OscillatorNode(ctx, { type: 'sine', frequency: 3 }); this.tremoloDepth = new GainNode(ctx); this.tremoloOsc.connect(this.tremoloDepth); this.tremoloDepth.connect(this.masterVolNode.gain); this.tremoloOsc.start(); } if (capabilities.filters) { this.lowpassNode = new BiquadFilterNode(ctx, { type: 'lowpass', frequency: 20000 }); this.highpassNode = new BiquadFilterNode(ctx, { type: 'highpass', frequency: 0 }); } } /** * Connect audio node chain from output down to input, depending on which * nodes exist. * @private */ connectCapabilityNodes(input, output) { [ this.panNode, this.lowpassNode, this.highpassNode, input ].reduce((prev, cur) => (cur ? (cur.connect(prev), cur) : prev), output); } /** * Get number of notes from C0 from a string like "F#4" * @static * @private */ static noteStringToC0Distance(note) { // eslint-disable-next-line require-unicode-regexp const match = note.match(/^([a-g][#b]?)([0-8])$/i), semitone = match ? match[1] : 'a', wholetone = semitone[0].toLowerCase(), accidental = semitone[1], octave = match ? parseInt(match[2], 10) : 4, accidentalOffset = accidental === '#' ? 1 : accidental === 'b' ? -1 : 0; return ({ c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[wholetone] || 0) + accidentalOffset + octave * 12; } /** * Convert a note value to a frequency. * @static * @function Highcharts.SonificationInstrument#musicalNoteToFrequency * @param {string|number} note * Note to convert. Can be a string 'c0' to 'b8' or a number of semitones * from c0. * @return {number} The converted frequency */ static musicalNoteToFrequency(note) { const notesFromC0 = typeof note === 'string' ? this.noteStringToC0Distance(note) : note; return 16.3516 * Math.pow(2, Math.min(notesFromC0, 107) / 12); } } SonificationInstrument.rampTime = SynthPatch.stopRampTime / 4; /* * * * Default Export * * */ /* * * * API definitions * * */ /** * Capabilities configuration for a SonificationInstrument. * @requires modules/sonification * @interface Highcharts.SonificationInstrumentCapabilitiesOptionsObject */ /** * Whether or not instrument should be able to pan. Defaults to `true`. * @name Highcharts.SonificationInstrumentCapabilitiesOptionsObject#pan * @type {boolean|undefined} */ /** * Whether or not instrument should be able to use tremolo effects. Defaults * to `false`. * @name Highcharts.SonificationInstrumentCapabilitiesOptionsObject#tremolo * @type {boolean|undefined} */ /** * Whether or not instrument should be able to use filter effects. Defaults * to `false`. * @name Highcharts.SonificationInstrumentCapabilitiesOptionsObject#filters * @type {boolean|undefined} */ /** * Configuration for a SonificationInstrument. * @requires modules/sonification * @interface Highcharts.SonificationInstrumentOptionsObject */ /** * The synth configuration for the instrument. Can be either a string, * referencing the instrument presets, or an actual SynthPatch configuration. * @name Highcharts.SonificationInstrumentOptionsObject#synthPatch * @type {Highcharts.SonificationSynthPreset|Highcharts.SynthPatchOptionsObject} * @sample highcharts/demo/all-instruments * All instrument presets * @sample highcharts/sonification/custom-instrument * Custom instrument preset */ /** * Define additional capabilities for the instrument, such as panning, filters, * and tremolo effects. * @name Highcharts.SonificationInstrumentOptionsObject#capabilities * @type {Highcharts.SonificationInstrumentCapabilitiesOptionsObject|undefined} */ /** * A track name to use for this instrument in MIDI export. * @name Highcharts.SonificationInstrumentOptionsObject#midiTrackName * @type {string|undefined} */ /** * Options for a scheduled event for a SonificationInstrument * @requires modules/sonification * @interface Highcharts.SonificationInstrumentScheduledEventOptionsObject */ /** * Number of semitones from c0, or a note string - such as "c4" or "F#6". * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#note * @type {number|string|undefined} */ /** * Note frequency in Hertz. Overrides note, if both are given. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#frequency * @type {number|undefined} */ /** * Duration to play the note in milliseconds. If not given, the note keeps * playing indefinitely * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#noteDuration * @type {number|undefined} */ /** * Depth/intensity of the tremolo effect - which is a periodic change in * volume. From 0 to 1. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#tremoloDepth * @type {number|undefined} */ /** * Speed of the tremolo effect, from 0 to 1. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#tremoloSpeed * @type {number|undefined} */ /** * Stereo panning value, from -1 (left) to 1 (right). * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#pan * @type {number|undefined} */ /** * Volume of the instrument, from 0 to 1. Can be set independent of the * master/overall volume. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#volume * @type {number|undefined} */ /** * Frequency of the lowpass filter, in Hertz. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#lowpassFreq * @type {number|undefined} */ /** * Resonance of the lowpass filter, in dB. Can be negative for a dip, or * positive for a bump. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#lowpassResonance * @type {number|undefined} */ /** * Frequency of the highpass filter, in Hertz. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#highpassFreq * @type {number|undefined} */ /** * Resonance of the highpass filter, in dB. Can be negative for a dip, or * positive for a bump. * @name Highcharts.SonificationInstrumentScheduledEventOptionsObject#highpassResonance * @type {number|undefined} */ (''); // Keep above doclets in JS file return SonificationInstrument; }); _registerModule(_modules, 'Extensions/Sonification/SonificationSpeaker.js', [_modules['Core/Utilities.js']], function (U) { /* * * * (c) 2009-2024 Øystein Moseng * * Class representing a speech synthesis voice. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { pick } = U; /** * The SonificationSpeaker class. This class represents an announcer using * speech synthesis. It allows for scheduling speech announcements, as well * as speech parameter changes - including rate, volume and pitch. * * @sample highcharts/demo/sonification-navigation * Demo using SonificationSpeaker directly for some announcements * * @requires modules/sonification * * @class * @name Highcharts.SonificationSpeaker * * @param {Highcharts.SonificationSpeakerOptionsObject} options * Configuration for the speaker */ class SonificationSpeaker { constructor(options) { this.options = options; this.masterVolume = 1; this.synthesis = window.speechSynthesis; if (typeof speechSynthesis.onvoiceschanged !== 'undefined') { speechSynthesis.onvoiceschanged = this.setVoice.bind(this); } this.setVoice(); this.scheduled = []; } /** * Say a message using the speaker voice. Interrupts other currently * speaking announcements from this speaker. * @function Highcharts.SonificationSpeaker#say * @param {string} message The message to speak. * @param {SonificationSpeakerOptionsObject} [options] * Optionally override speaker configuration. */ say(message, options) { if (this.synthesis) { this.synthesis.cancel(); const utterance = new SpeechSynthesisUtterance(message); if (this.voice) { utterance.voice = this.voice; } utterance.rate = options && options.rate || this.options.rate || 1; utterance.pitch = options && options.pitch || this.options.pitch || 1; utterance.volume = pick(options && options.volume, this.options.volume, 1) * this.masterVolume; this.synthesis.speak(utterance); } } /** * Schedule a message using the speaker voice. * @function Highcharts.SonificationSpeaker#sayAtTime * @param {number} time * The time offset to speak at, in milliseconds from now. * @param {string} message * The message to speak. * @param {SonificationSpeakerOptionsObject} [options] * Optionally override speaker configuration. */ sayAtTime(time, message, options) { this.scheduled.push(setTimeout(this.say.bind(this, message, options), time)); } /** * Clear scheduled announcements, and stop current speech. * @function Highcharts.SonificationSpeaker#cancel */ cancel() { this.scheduled.forEach(clearTimeout); this.scheduled = []; this.synthesis.cancel(); } /** * Stop speech and release any used resources * @private */ destroy() { // Ran on TimelineChannel.destroy // (polymorphism with SonificationInstrument). // Currently all we need to do is cancel. this.cancel(); } /** * Set speaker overall/master volume modifier. This affects all * announcements, and applies in addition to the individual announcement * volume. * @function Highcharts.SonificationSpeaker#setMasterVolume * @param {number} vol Volume from 0 to 1. */ setMasterVolume(vol) { this.masterVolume = vol; } /** * Set the active synthesis voice for the speaker. * @private */ setVoice() { if (this.synthesis) { const name = this.options.name, lang = this.options.language || 'en-US', voices = this.synthesis.getVoices(), len = voices.length; let langFallback; for (let i = 0; i < len; ++i) { if (name && voices[i].name === name) { this.voice = voices[i]; return; } if (!langFallback && voices[i].lang === lang) { langFallback = voices[i]; if (!name) { break; } } } this.voice = langFallback; } } } /* * * * Default Export * * */ /* * * * API declarations * * */ /** * Configuration for a SonificationSpeaker. * @requires modules/sonification * @interface Highcharts.SonificationSpeakerOptionsObject */ /** * Name of the voice synthesis to use. If not found, reverts to the default * voice for the language chosen. * @name Highcharts.SonificationSpeakerOptionsObject#name * @type {string|undefined} */ /** * The language of the voice synthesis. Defaults to `"en-US"`. * @name Highcharts.SonificationSpeakerOptionsObject#language * @type {string|undefined} */ /** * The pitch modifier of the voice. Defaults to `1`. Set higher for a higher * voice pitch. * @name Highcharts.SonificationSpeakerOptionsObject#pitch * @type {number|undefined} */ /** * The speech rate modifier. Defaults to `1`. * @name Highcharts.SonificationSpeakerOptionsObject#rate * @type {number|undefined} */ /** * The speech volume, from 0 to 1. Defaults to `1`. * @name Highcharts.SonificationSpeakerOptionsObject#volume * @type {number|undefined} */ (''); // Keep above doclets in JS file return SonificationSpeaker; }); _registerModule(_modules, 'Extensions/Sonification/TimelineChannel.js', [], function () { /* * * * (c) 2009-2024 Øystein Moseng * * Class representing a TimelineChannel with sonification events to play. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /** * Represents a channel of TimelineEvents for an engine (either an instrument * or a speaker). * @private */ class TimelineChannel { constructor(type, engine, showPlayMarker = false, events, muted) { this.type = type; this.engine = engine; this.showPlayMarker = showPlayMarker; this.muted = muted; this.events = events || []; } addEvent(event) { const lastEvent = this.events[this.events.length - 1]; if (lastEvent && event.time < lastEvent.time) { // Ensure we are sorted by time, so insert at the right place let i = this.events.length; while (i-- && this.events[i].time > event.time) { /* */ } this.events.splice(i + 1, 0, event); } else { this.events.push(event); } return event; } mute() { this.muted = true; } unmute() { this.muted = false; } cancel() { this.engine.cancel(); } destroy() { this.engine.destroy(); } } /* * * * Default Export * * */ /* * * * API declarations * * */ /** * A TimelineEvent object represents a scheduled audio event to play for a * SonificationTimeline. * @requires modules/sonification * @interface Highcharts.SonificationTimelineEvent */ /** * Time is given in milliseconds, where 0 is now. * @name Highcharts.SonificationTimelineEvent#time * @type {number} */ /** * A reference to a data point related to the TimelineEvent. Populated when * sonifying points. * @name Highcharts.SonificationTimelineEvent#relatedPoint * @type {Highcharts.Point|undefined} */ /** * Options for an instrument event to be played. * @name Highcharts.SonificationTimelineEvent#instrumentEventOptions * @type {Highcharts.SonificationInstrumentScheduledEventOptionsObject|undefined} */ /** * Options for a speech event to be played. * @name Highcharts.SonificationTimelineEvent#speechOptions * @type {Highcharts.SonificationSpeakerOptionsObject|undefined} */ /** * The message to speak for speech events. * @name Highcharts.SonificationTimelineEvent#message * @type {string|undefined} */ /** * Callback to call when playing the event. * @name Highcharts.SonificationTimelineEvent#callback * @type {Function|undefined} */ (''); // Keep above doclets in JS file return TimelineChannel; }); _registerModule(_modules, 'Extensions/Sonification/MIDI.js', [_modules['Extensions/Sonification/SonificationInstrument.js'], _modules['Core/Utilities.js']], function (SonificationInstrument, U) { /* * * * (c) 2009-2024 Øystein Moseng * * Small MIDI file writer for sonification export. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /* eslint-disable no-multi-spaces */ const { pick } = U; const freqToNote = (f) => Math.round(12 * Math.log(f) / Math.LN2 - 48.37632), b = (byte, n) => n >>> 8 * byte & 0xFF, getHeader = (nTracks) => [ 0x4D, 0x54, 0x68, 0x64, 0, 0, 0, 6, 0, nTracks > 1 ? 1 : 0, b(1, nTracks), b(0, nTracks), // SMTPE: 0xE7 0x28 // -25/40 time div gives us millisecond SMTPE, but not widely supported. 1, 0xF4 // HD_TIMEDIV, 500 ticks per beat = millisecond at 120bpm ], timeInfo = [0, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20], // META_TEMPO varLenEnc = (n) => { let buf = n & 0x7F; const res = []; while (n >>= 7) { // eslint-disable-line no-cond-assign buf <<= 8; buf |= (n & 0x7F) | 0x80; } while (true) { // eslint-disable-line no-constant-condition res.push(buf & 0xFF); if (buf & 0x80) { buf >>= 8; } else { break; } } return res; }, toMIDIEvents = (events) => { let cachedVel, cachedDur; const res = [], add = (el) => { let ix = res.length; while (ix-- && res[ix].timeMS > el.timeMS) { /* */ } res.splice(ix + 1, 0, el); }; events.forEach((e) => { const o = e.instrumentEventOptions || {}, t = e.time, dur = cachedDur = pick(o.noteDuration, cachedDur), tNOF = dur && e.time + dur, ctrl = [{ valMap: (n) => 64 + 63 * n & 0x7F, data: { 0x0A: o.pan, 0x5C: o.tremoloDepth, 0x5D: o.tremoloSpeed } }, { valMap: (n) => 127 * n / 20000 & 0x7F, data: { 0x4A: o.lowpassFreq, 0x4B: o.highpassFreq } }, { valMap: (n) => 63 * Math.min(18, Math.max(-18, n)) / 18 + 63 & 0x7F, data: { 0x47: o.lowpassResonance, 0x4C: o.highpassResonance } }], v = cachedVel = o.volume === void 0 ? pick(cachedVel, 127) : 127 * o.volume & 0x7F, freq = o.frequency, note = o.note || 0, noteVal = 12 + (freq ? freqToNote(freq) : // MIDI note #0 is C-1 typeof note === 'string' ? SonificationInstrument .noteStringToC0Distance(note) : note) & 0x7F; // CTRL_CHANGE events ctrl.forEach((ctrlDef) => Object.keys(ctrlDef.data) .forEach((ctrlSignal) => { const val = ctrlDef.data[ctrlSignal]; if (val !== void 0) { add({ timeMS: t, type: 'CTRL_CHG', data: [ 0xB0, parseInt(ctrlSignal, 10), ctrlDef.valMap(val) ] }); } })); // NON/NOF if (tNOF) { add({ timeMS: t, type: 'NON', data: [0x90, noteVal, v] }); add({ timeMS: tNOF, type: 'NOF', data: [0x80, noteVal, v] }); } }); return res; }, getMetaEvents = (midiTrackName, midiInstrument) => { const events = []; if (midiInstrument) { // Program Change MIDI event events.push(0, 0xC0, midiInstrument & 0x7F); } if (midiTrackName) { // Track name meta event const textArr = []; for (let i = 0; i < midiTrackName.length; ++i) { const code = midiTrackName.charCodeAt(i); if (code < 128) { // Keep ASCII only textArr.push(code); } } return events.concat([0, 0xFF, 0x03], varLenEnc(textArr.length), textArr); } return events; }, getTrackChunk = (events, addTimeInfo, midiTrackName, midiInstrument) => { let prevTime = 0; const metaEvents = getMetaEvents(midiTrackName, midiInstrument), trackEvents = toMIDIEvents(events).reduce((data, e) => { const t = varLenEnc(e.timeMS - prevTime); prevTime = e.timeMS; return data.concat(t, e.data); }, []); const trackEnd = [0, 0xFF, 0x2F, 0], size = (addTimeInfo ? timeInfo.length : 0) + metaEvents.length + trackEvents.length + trackEnd.length; return [ 0x4D, 0x54, 0x72, 0x6B, b(3, size), b(2, size), b(1, size), b(0, size) ].concat(addTimeInfo ? timeInfo : [], metaEvents, trackEvents, trackEnd // SYSEX_TRACK_END ); }; /** * Get MIDI data from a set of Timeline instrument channels. * * Outputs multi-track MIDI for Timelines with multiple channels. * * @private */ function toMIDI(channels) { const channelsToAdd = channels.filter((c) => !!c.events.length), numCh = channelsToAdd.length, multiCh = numCh > 1; return new Uint8Array(getHeader(multiCh ? numCh + 1 : numCh).concat(multiCh ? getTrackChunk([], true) : [], // Time info only channelsToAdd.reduce((chunks, channel) => { const engine = channel.engine; return chunks.concat(getTrackChunk(channel.events, !multiCh, engine.midiTrackName, engine.midiInstrument)); }, []))); } return toMIDI; }); _registerModule(_modules, 'Extensions/DownloadURL.js', [_modules['Core/Globals.js']], function (H) { /* * * * (c) 2015-2024 Oystein Moseng * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * Mixin for downloading content in the browser * * */ /* * * * Imports * * */ const { isSafari, win, win: { document: doc } } = H; /* * * * Constants * * */ const domurl = win.URL || win.webkitURL || win; /* * * * Functions * * */ /** * Convert base64 dataURL to Blob if supported, otherwise returns undefined. * @private * @function Highcharts.dataURLtoBlob * @param {string} dataURL * URL to convert * @return {string|undefined} * Blob */ function dataURLtoBlob(dataURL) { const parts = dataURL .replace(/filename=.*;/, '') .match(/data:([^;]*)(;base64)?,([0-9A-Za-z+/]+)/); if (parts && parts.length > 3 && (win.atob) && win.ArrayBuffer && win.Uint8Array && win.Blob && (domurl.createObjectURL)) { // Try to convert data URL to Blob const binStr = win.atob(parts[3]), buf = new win.ArrayBuffer(binStr.length), binary = new win.Uint8Array(buf); for (let i = 0; i < binary.length; ++i) { binary[i] = binStr.charCodeAt(i); } return domurl .createObjectURL(new win.Blob([binary], { 'type': parts[1] })); } } /** * Download a data URL in the browser. Can also take a blob as first param. * * @private * @function Highcharts.downloadURL * @param {string|global.URL} dataURL * The dataURL/Blob to download * @param {string} filename * The name of the resulting file (w/extension) * @return {void} */ function downloadURL(dataURL, filename) { const nav = win.navigator, a = doc.createElement('a'); // IE specific blob implementation // Don't use for normal dataURLs if (typeof dataURL !== 'string' && !(dataURL instanceof String) && nav.msSaveOrOpenBlob) { nav.msSaveOrOpenBlob(dataURL, filename); return; } dataURL = '' + dataURL; const // Some browsers have limitations for data URL lengths. Try to convert // to Blob or fall back. Edge always needs that blob. isOldEdgeBrowser = /Edge\/\d+/.test(nav.userAgent), // Safari on iOS needs Blob in order to download PDF safariBlob = (isSafari && typeof dataURL === 'string' && dataURL.indexOf('data:application/pdf') === 0); if (safariBlob || isOldEdgeBrowser || dataURL.length > 2000000) { dataURL = dataURLtoBlob(dataURL) || ''; if (!dataURL) { throw new Error('Failed to convert to blob'); } } // Try HTML5 download attr if supported if (typeof a.download !== 'undefined') { a.href = dataURL; a.download = filename; // HTML5 download attribute doc.body.appendChild(a); a.click(); doc.body.removeChild(a); } else { // No download attr, just opening data URI try { if (!win.open(dataURL, 'chart')) { throw new Error('Failed to open window'); } } catch { // If window.open failed, try location.href win.location.href = dataURL; } } } /* * * * Default Export * * */ const DownloadURL = { dataURLtoBlob, downloadURL }; return DownloadURL; }); _registerModule(_modules, 'Extensions/Sonification/SonificationTimeline.js', [_modules['Extensions/Sonification/TimelineChannel.js'], _modules['Extensions/Sonification/MIDI.js'], _modules['Extensions/DownloadURL.js'], _modules['Core/Utilities.js']], function (TimelineChannel, toMIDI, DU, U) { /* * * * (c) 2009-2024 Øystein Moseng * * Class representing a Timeline with sonification events to play. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { downloadURL } = DU; const { defined, find, merge } = U; /** * Get filtered channels. Timestamps are compensated, so that the first * event starts immediately. * @private */ function filterChannels(filter, channels) { const filtered = channels.map((channel) => { channel.cancel(); return { channel, filteredEvents: channel.muted ? [] : channel.events.filter(filter) }; }), minTime = filtered.reduce((acc, cur) => Math.min(acc, cur.filteredEvents.length ? cur.filteredEvents[0].time : Infinity), Infinity); return filtered.map((c) => (new TimelineChannel(c.channel.type, c.channel.engine, c.channel.showPlayMarker, c.filteredEvents.map((e) => merge(e, { time: e.time - minTime })), c.channel.muted))); } /** * The SonificationTimeline class. This class represents a timeline of * audio events scheduled to play. It provides functionality for manipulating * and navigating the timeline. * @private */ class SonificationTimeline { constructor(options, chart) { this.chart = chart; this.isPaused = false; this.isPlaying = false; this.channels = []; this.scheduledCallbacks = []; this.playTimestamp = 0; this.resumeFromTime = 0; this.options = options || {}; } // Add a channel, optionally with events, to be played. // Note: Only one speech channel is supported at a time. addChannel(type, engine, showPlayMarker = false, events) { if (type === 'instrument' && !engine.scheduleEventAtTime || type === 'speech' && !engine.sayAtTime) { throw new Error('Highcharts Sonification: Invalid channel engine.'); } const channel = new TimelineChannel(type, engine, showPlayMarker, events); this.channels.push(channel); return channel; } // Play timeline, optionally filtering out only some of the events to play. // Note that if not all instrument parameters are updated on each event, // parameters may update differently depending on the events filtered out, // since some of the events that update parameters can be filtered out too. // The filterPersists argument determines whether or not the filter persists // after e.g. pausing and resuming. Usually this should be true. play(filter, filterPersists = true, resetAfter = true, onEnd) { if (this.isPlaying) { this.cancel(); } else { this.clearScheduledCallbacks(); } this.onEndArgument = onEnd; this.playTimestamp = Date.now(); this.resumeFromTime = 0; this.isPaused = false; this.isPlaying = true; const skipThreshold = this.options.skipThreshold || 2, onPlay = this.options.onPlay, showTooltip = this.options.showTooltip, showCrosshair = this.options.showCrosshair, channels = filter ? filterChannels(filter, this.playingChannels || this.channels) : this.channels, getEventKeysSignature = (e) => Object.keys(e.speechOptions || {}) .concat(Object.keys(e.instrumentEventOptions || {})) .join(), pointsPlayed = []; if (filterPersists) { this.playingChannels = channels; } if (onPlay) { onPlay({ chart: this.chart, timeline: this }); } let maxTime = 0; channels.forEach((channel) => { if (channel.muted) { return; } const numEvents = channel.events.length; let lastCallbackTime = -Infinity, lastEventTime = -Infinity, lastEventKeys = ''; maxTime = Math.max(channel.events[numEvents - 1] && channel.events[numEvents - 1].time || 0, maxTime); for (let i = 0; i < numEvents; ++i) { const e = channel.events[i], keysSig = getEventKeysSignature(e); // Optimize by skipping extremely close events (<2ms apart by // default), as long as they don't introduce new event options if (keysSig === lastEventKeys && e.time - lastEventTime < skipThreshold) { continue; } lastEventKeys = keysSig; lastEventTime = e.time; if (channel.type === 'instrument') { channel.engine .scheduleEventAtTime(e.time / 1000, e.instrumentEventOptions || {}); } else { channel.engine.sayAtTime(e.time, e.message || '', e.speechOptions || {}); } const point = e.relatedPoint, chart = point && point.series && point.series.chart, needsCallback = e.callback || point && (showTooltip || showCrosshair) && channel.showPlayMarker !== false && (e.time - lastCallbackTime > 50 || i === numEvents - 1); if (point) { pointsPlayed.push(point); } if (needsCallback) { this.scheduledCallbacks.push(setTimeout(() => { if (e.callback) { e.callback(); } if (point) { if (showCrosshair) { const s = point.series; if (s && s.xAxis && s.xAxis.crosshair) { s.xAxis.drawCrosshair(void 0, point); } if (s && s.yAxis && s.yAxis.crosshair) { s.yAxis.drawCrosshair(void 0, point); } } if (showTooltip && !( // Don't re-hover if shared tooltip chart && chart.hoverPoints && chart.hoverPoints.length > 1 && find(chart.hoverPoints, (p) => p === point) && // Stock issue w/Navigator point.onMouseOver)) { point.onMouseOver(); } } }, e.time)); lastCallbackTime = e.time; } } }); const onEndOpt = this.options.onEnd, onStop = this.options.onStop; this.scheduledCallbacks.push(setTimeout(() => { const chart = this.chart, context = { chart, timeline: this, pointsPlayed }; this.isPlaying = false; if (resetAfter) { this.resetPlayState(); } if (onStop) { onStop(context); } if (onEndOpt) { onEndOpt(context); } if (onEnd) { onEnd(context); } if (chart) { if (chart.tooltip) { chart.tooltip.hide(0); } if (chart.hoverSeries) { chart.hoverSeries.onMouseOut(); } chart.axes.forEach((a) => a.hideCrosshair()); } }, maxTime + 250)); this.resumeFromTime = filterPersists ? maxTime : this.getLength(); } // Pause for later resuming. Returns current timestamp to resume from. pause() { this.isPaused = true; this.cancel(); this.resumeFromTime = Date.now() - this.playTimestamp - 10; return this.resumeFromTime; } // Get current time getCurrentTime() { return this.isPlaying ? Date.now() - this.playTimestamp : this.resumeFromTime; } // Get length of timeline in milliseconds getLength() { return this.channels.reduce((maxTime, channel) => { const lastEvent = channel.events[channel.events.length - 1]; return lastEvent ? Math.max(lastEvent.time, maxTime) : maxTime; }, 0); } // Resume from paused resume() { if (this.playingChannels) { const resumeFrom = this.resumeFromTime - 50; this.play((e) => e.time > resumeFrom, false, false, this.onEndArgument); this.playTimestamp -= resumeFrom; } else { this.play(void 0, false, false, this.onEndArgument); } } // Play a short moment, then pause, setting the cursor to the final // event's time. anchorPlayMoment(eventFilter, onEnd) { if (this.isPlaying) { this.pause(); } let finalEventTime = 0; this.play((e, ix, arr) => { // We have to keep track of final event time ourselves, since // play() messes with the time internally upon filtering. const res = eventFilter(e, ix, arr); if (res && e.time > finalEventTime) { finalEventTime = e.time; } return res; }, false, false, onEnd); this.playingChannels = this.playingChannels || this.channels; this.isPaused = true; this.isPlaying = false; this.resumeFromTime = finalEventTime; } // Play event(s) occurring next/prev from paused state. playAdjacent(next, onEnd, onBoundaryHit, eventFilter) { if (this.isPlaying) { this.pause(); } const fromTime = this.resumeFromTime, closestTime = this.channels.reduce((time, channel) => { // Adapted binary search since events are sorted by time const events = eventFilter ? channel.events.filter(eventFilter) : channel.events; let s = 0, e = events.length, lastValidTime = time; while (s < e) { const mid = (s + e) >> 1, t = events[mid].time, cmp = t - fromTime; if (cmp > 0) { // Ahead if (next && t < lastValidTime) { lastValidTime = t; } e = mid; } else if (cmp < 0) { // Behind if (!next && t > lastValidTime) { lastValidTime = t; } s = mid + 1; } else { // Same as from time if (next) { s = mid + 1; } else { e = mid; } } } return lastValidTime; }, next ? Infinity : -Infinity), margin = 0.02; if (closestTime === Infinity || closestTime === -Infinity) { if (onBoundaryHit) { onBoundaryHit({ chart: this.chart, timeline: this, attemptedNext: next }); } return; } this.anchorPlayMoment((e, ix, arr) => { const withinTime = next ? e.time > fromTime && e.time <= closestTime + margin : e.time < fromTime && e.time >= closestTime - margin; return eventFilter ? withinTime && eventFilter(e, ix, arr) : withinTime; }, onEnd); } // Play event with related point, where the value of a prop on the // related point is closest to a target value. // Note: not very efficient. playClosestToPropValue(prop, targetVal, onEnd, onBoundaryHit, eventFilter) { const filter = (e, ix, arr) => !!(eventFilter ? eventFilter(e, ix, arr) && e.relatedPoint : e.relatedPoint); let closestValDiff = Infinity, closestEvent = null; (this.playingChannels || this.channels).forEach((channel) => { const events = channel.events; let i = events.length; while (i--) { if (!filter(events[i], i, events)) { continue; } const val = events[i].relatedPoint[prop], diff = defined(val) && Math.abs(targetVal - val); if (diff !== false && diff < closestValDiff) { closestValDiff = diff; closestEvent = events[i]; } } }); if (closestEvent) { this.play((e) => !!(closestEvent && e.time < closestEvent.time + 1 && e.time > closestEvent.time - 1 && e.relatedPoint === closestEvent.relatedPoint), false, false, onEnd); this.playingChannels = this.playingChannels || this.channels; this.isPaused = true; this.isPlaying = false; this.resumeFromTime = closestEvent.time; } else if (onBoundaryHit) { onBoundaryHit({ chart: this.chart, timeline: this }); } } // Get timeline events that are related to a certain point. // Note: Point grouping may cause some points not to have a // related point in the timeline. getEventsForPoint(point) { return this.channels.reduce((events, channel) => { const pointEvents = channel.events .filter((e) => e.relatedPoint === point); return events.concat(pointEvents); }, []); } // Divide timeline into 100 parts of equal time, and play one of them. // Used for scrubbing. // Note: Should be optimized? playSegment(segment, onEnd) { const numSegments = 100; const eventTimes = { first: Infinity, last: -Infinity }; this.channels.forEach((c) => { if (c.events.length) { eventTimes.first = Math.min(c.events[0].time, eventTimes.first); eventTimes.last = Math.max(c.events[c.events.length - 1].time, eventTimes.last); } }); if (eventTimes.first < Infinity) { const segmentSize = (eventTimes.last - eventTimes.first) / numSegments, fromTime = eventTimes.first + segment * segmentSize, toTime = fromTime + segmentSize; // Binary search, do we have any events within time range? if (!this.channels.some((c) => { const events = c.events; let s = 0, e = events.length; while (s < e) { const mid = (s + e) >> 1, t = events[mid].time; if (t < fromTime) { // Behind s = mid + 1; } else if (t > toTime) { // Ahead e = mid; } else { return true; } } return false; })) { return; // If not, don't play - avoid cancelling current play } this.play((e) => e.time >= fromTime && e.time <= toTime, false, false, onEnd); this.playingChannels = this.playingChannels || this.channels; this.isPaused = true; this.isPlaying = false; this.resumeFromTime = toTime; } } // Get last played / current point // Since events are scheduled we can't just store points as we play them getLastPlayedPoint(filter) { const curTime = this.getCurrentTime(), channels = this.playingChannels || this.channels; let closestDiff = Infinity, closestPoint = null; channels.forEach((c) => { const events = c.events.filter((e, ix, arr) => !!(e.relatedPoint && e.time <= curTime && (!filter || filter(e, ix, arr)))), closestEvent = events[events.length - 1]; if (closestEvent) { const closestTime = closestEvent.time, diff = Math.abs(closestTime - curTime); if (diff < closestDiff) { closestDiff = diff; closestPoint = closestEvent.relatedPoint; } } }); return closestPoint; } // Reset play/pause state so that a later call to resume() will start over reset() { if (this.isPlaying) { this.cancel(); } this.resetPlayState(); } cancel() { const onStop = this.options.onStop; if (onStop) { onStop({ chart: this.chart, timeline: this }); } this.isPlaying = false; this.channels.forEach((c) => c.cancel()); if (this.playingChannels && this.playingChannels !== this.channels) { this.playingChannels.forEach((c) => c.cancel()); } this.clearScheduledCallbacks(); this.resumeFromTime = 0; } destroy() { this.cancel(); if (this.playingChannels && this.playingChannels !== this.channels) { this.playingChannels.forEach((c) => c.destroy()); } this.channels.forEach((c) => c.destroy()); } setMasterVolume(vol) { this.channels.forEach((c) => c.engine.setMasterVolume(vol)); } getMIDIData() { return toMIDI(this.channels.filter((c) => c.type === 'instrument')); } downloadMIDI(filename) { const data = this.getMIDIData(), name = (filename || this.chart && this.chart.options.title && this.chart.options.title.text || 'chart') + '.mid', blob = new Blob([data], { type: 'application/octet-stream' }), url = window.URL.createObjectURL(blob); downloadURL(url, name); window.URL.revokeObjectURL(url); } resetPlayState() { delete this.playingChannels; delete this.onEndArgument; this.playTimestamp = this.resumeFromTime = 0; this.isPaused = false; } clearScheduledCallbacks() { this.scheduledCallbacks.forEach(clearTimeout); this.scheduledCallbacks = []; } } /* * * * Default Export * * */ /* * * * API declarations * * */ /** * Filter callback for filtering timeline events on a SonificationTimeline. * * @callback Highcharts.SonificationTimelineFilterCallback * * @param {Highcharts.SonificationTimelineEvent} e TimelineEvent being filtered * * @param {number} ix Index of TimelineEvent in current event array * * @param {Array} arr The current event array * * @return {boolean} * The function should return true if the TimelineEvent should be included, * false otherwise. */ (''); // Keep above doclets in JS file return SonificationTimeline; }); _registerModule(_modules, 'Extensions/Sonification/TimelineFromChart.js', [_modules['Extensions/Sonification/SonificationTimeline.js'], _modules['Extensions/Sonification/SonificationInstrument.js'], _modules['Extensions/Sonification/SonificationSpeaker.js'], _modules['Core/Utilities.js'], _modules['Core/Templating.js']], function (SonificationTimeline, SonificationInstrument, SonificationSpeaker, U, T) { /* * * * (c) 2009-2024 Øystein Moseng * * Build a timeline from a chart. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const { clamp, defined, extend, getNestedProperty, merge, pick } = U; const { format } = T; const isNoteDefinition = (str) => // eslint-disable-next-line require-unicode-regexp (/^([a-g][#b]?)[0-8]$/i).test(str); /** * Get the value of a point property from string. * @private */ function getPointPropValue(point, prop) { let ret; if (prop) { ret = point[prop]; if (typeof ret === 'number') { return ret; } ret = getNestedProperty(prop, point); } return typeof ret === 'number' ? ret : void 0; } /** * Get chart wide min/max for a set of props, as well as per * series min/max for selected props. * @private */ function getChartExtremesForProps(chart, props, perSeriesProps) { const series = chart.series, numProps = props.length, numSeriesProps = perSeriesProps.length, initCache = (propList) => propList.reduce((cache, prop) => { ((cache[prop] = { min: Infinity, max: -Infinity }), cache); return cache; }, {}), updateCache = (cache, point, prop) => { let val = point[prop]; if (val === void 0) { val = getNestedProperty(prop, point); } if (typeof val === 'number') { cache[prop].min = Math.min(cache[prop].min, val); cache[prop].max = Math.max(cache[prop].max, val); } }, globalExtremes = initCache(props); let i = series.length; const allSeriesExtremes = new Array(i); while (i--) { const seriesExtremes = initCache(perSeriesProps); const opts = series[i].options; if (!series[i].visible || opts && opts.sonification && opts.sonification.enabled === false) { continue; } const points = series[i].points || []; let j = points.length; while (j--) { let k = numProps; while (k--) { updateCache(globalExtremes, points[j], props[k]); } k = numSeriesProps; while (k--) { updateCache(seriesExtremes, points[j], perSeriesProps[k]); } } allSeriesExtremes[i] = seriesExtremes; } return { globalExtremes, seriesExtremes: allSeriesExtremes }; } /** * Build a cache of prop extremes for the chart. Goes through * options to find out which props are needed. * @private */ function getPropMetrics(chart) { const globalOpts = chart.options.sonification || {}, defaultInstrMapping = (globalOpts.defaultInstrumentOptions || {}) .mapping || { time: 'x', pitch: 'y' }, defaultSpeechMapping = globalOpts.defaultSpeechOptions && globalOpts.defaultSpeechOptions.mapping || {}, seriesTimeProps = [], commonTimeProps = {}, addTimeProp = (prop, seriesIx) => { if (seriesIx !== null) { seriesTimeProps[seriesIx] = seriesTimeProps[seriesIx] || {}; seriesTimeProps[seriesIx][prop] = true; } else { commonTimeProps[prop] = true; } }, props = {}, perSeriesProps = {}, addPropFromMappingParam = (param, val, seriesIx) => { const removeInvertedFlag = (s) => (s.charAt(0) === '-' ? s.slice(1) : s); if (typeof val === 'string' && param !== 'text') { if (param === 'pitch' && isNoteDefinition(val)) { return; } if (param === 'time') { perSeriesProps[val] = true; addTimeProp(val, seriesIx); } props[removeInvertedFlag(val)] = true; return; } const paramOpts = val; if (paramOpts && paramOpts.mapTo && typeof paramOpts.mapTo === 'string') { const mapTo = removeInvertedFlag(paramOpts.mapTo); if (param === 'time') { addTimeProp(mapTo, seriesIx); } if (param === 'time' || paramOpts.within === 'series') { perSeriesProps[mapTo] = true; } props[mapTo] = true; return; } if (['tremolo', 'lowpass', 'highpass'].indexOf(param) > -1 && typeof val === 'object') { Object.keys(val).forEach((subParam) => addPropFromMappingParam(subParam, val[subParam], seriesIx)); } }, addPropsFromMappingOptions = (mapping, seriesIx) => { (Object.keys(mapping)).forEach((param) => addPropFromMappingParam(param, mapping[param], seriesIx)); }, addPropsFromContextTracks = (tracks) => tracks.forEach((track) => { props[track.valueProp || 'x'] = perSeriesProps[track.valueProp || 'x'] = true; }); addPropsFromMappingOptions(defaultInstrMapping, null); addPropsFromMappingOptions(defaultSpeechMapping, null); addPropsFromContextTracks(globalOpts.globalContextTracks || []); const hasCommonTimeProps = Object.keys(commonTimeProps).length; chart.series.forEach((series) => { const sOpts = series.options.sonification; if (series.visible && !(sOpts && sOpts.enabled === false)) { if (hasCommonTimeProps) { seriesTimeProps[series.index] = merge(commonTimeProps); } if (sOpts) { const defaultInstrMapping = (sOpts.defaultInstrumentOptions || {}).mapping, defaultSpeechMapping = (sOpts.defaultSpeechOptions || {}).mapping; if (defaultInstrMapping) { addPropsFromMappingOptions(defaultInstrMapping, series.index); } if (defaultSpeechMapping) { addPropsFromMappingOptions(defaultSpeechMapping, series.index); } addPropsFromContextTracks(sOpts.contextTracks || []); (sOpts.tracks || []) .concat(sOpts.contextTracks || []) .forEach((trackOpts) => { if (trackOpts.mapping) { addPropsFromMappingOptions(trackOpts.mapping, series.index); } }); } } }); return { seriesTimeProps, ...getChartExtremesForProps(chart, Object.keys(props), Object.keys(perSeriesProps)) }; } /** * Map a relative value onto a virtual axis. * @private */ function mapToVirtualAxis(value, valueExtremes, virtualAxisExtremes, invert, logarithmic // Virtual axis is logarithmic ) { const lenValueAxis = valueExtremes.max - valueExtremes.min; if (lenValueAxis <= 0) { return virtualAxisExtremes.min; } const lenVirtualAxis = virtualAxisExtremes.max - virtualAxisExtremes.min, valueDelta = value - valueExtremes.min; let virtualValueDelta = lenVirtualAxis * valueDelta / lenValueAxis; if (logarithmic) { const log = valueExtremes.min > 0 ? // Normal log formula (x) => Math.log(x) / Math.LOG10E : // Negative logarithmic support needed (x) => { let adjustedNum = Math.abs(x); if (adjustedNum < 10) { adjustedNum += (10 - adjustedNum) / 10; } const res = Math.log(adjustedNum) / Math.LN10; return x < 0 ? -res : res; }; const logValMin = log(valueExtremes.min); virtualValueDelta = lenVirtualAxis * (log(value) - logValMin) / (log(valueExtremes.max) - logValMin); } const val = invert ? virtualAxisExtremes.max - virtualValueDelta : virtualAxisExtremes.min + virtualValueDelta; return clamp(val, virtualAxisExtremes.min, virtualAxisExtremes.max); } /** * Get the value of a mapped parameter for a point. * @private */ function getMappingParameterValue(context, propMetrics, useSeriesExtremes, defaultMapping, mappingOptions, contextValueProp) { if (typeof mappingOptions === 'number') { return mappingOptions; } if (typeof mappingOptions === 'function') { return mappingOptions(extend({ time: 0 }, context)); } let mapTo = mappingOptions, mapFunc = defaultMapping.mapFunction, min = defaultMapping.min, max = defaultMapping.max, within = defaultMapping.within, scale; if (typeof mappingOptions === 'object') { mapTo = mappingOptions.mapTo; mapFunc = mappingOptions.mapFunction || mapFunc; min = pick(mappingOptions.min, min); max = pick(mappingOptions.max, max); within = mappingOptions.within || defaultMapping.within; scale = mappingOptions.scale; } if (!mapTo) { return null; } const isInverted = mapTo.charAt(0) === '-'; if (isInverted) { mapTo = mapTo.slice(1); } let value = context.value; const useContextValue = mapTo === 'value' && value !== void 0 && contextValueProp; if (!useContextValue) { const fixedValue = mappingOptions.value; if (fixedValue !== void 0) { value = fixedValue; } else { if (!context.point) { return null; } value = context.point[mapTo]; } if (value === void 0) { value = getNestedProperty(mapTo, context.point); } } if (typeof value !== 'number' || value === null) { return null; } // Figure out extremes for this mapping let extremes = null; if (context.point) { if (within === 'xAxis' || within === 'yAxis') { const axis = context.point.series[within]; if (axis && defined(axis.dataMin) && defined(axis.dataMax)) { extremes = { min: axis.dataMin, max: axis.dataMax }; } } else if ((within === 'series' || useSeriesExtremes) && context.point.series) { extremes = propMetrics.seriesExtremes[context.point.series.index][useContextValue ? contextValueProp : mapTo]; } } if (!extremes) { // Chart extremes extremes = propMetrics.globalExtremes[useContextValue ? contextValueProp : mapTo]; } if (scale) { // Build a musical scale from array const scaleAxis = [], minOctave = Math.floor(min / 12), maxOctave = Math.ceil(max / 12) + 1, lenScale = scale.length; for (let octave = minOctave; octave < maxOctave; ++octave) { for (let scaleIx = 0; scaleIx < lenScale; ++scaleIx) { const note = 12 * octave + scale[scaleIx]; if (note >= min && note <= max) { scaleAxis.push(note); } } } // Map to the scale const noteNum = mapToVirtualAxis(value, extremes, { min: 0, max: scaleAxis.length - 1 }, isInverted, mapFunc === 'logarithmic'); return scaleAxis[Math.round(noteNum)]; } return mapToVirtualAxis(value, extremes, { min, max }, isInverted, mapFunc === 'logarithmic'); } /** * Get mapping parameter value with defined fallback and defaults. * @private */ function getParamValWithDefault(context, propMetrics, useSeriesExtremes, mappingParamOptions, fallback, defaults, contextValueProp) { return pick(getMappingParameterValue(context, propMetrics, useSeriesExtremes, extend({ min: 0, max: 1, mapTo: 'y', mapFunction: 'linear', within: 'chart' }, (defaults || {})), mappingParamOptions, contextValueProp), fallback); } /** * Get time value for a point event. * @private */ function getPointTime(point, startTime, duration, timeMappingOptions, propMetrics, useSeriesExtremes) { const time = getParamValWithDefault({ point, time: 0 }, propMetrics, useSeriesExtremes, timeMappingOptions, 0, { min: 0, max: duration, mapTo: 'x' }); return time + startTime; } /** * Get duration for a series * @private */ function getAvailableDurationForSeries(series, totalDuration, propMetrics, afterSeriesWait) { let timeProp, seriesDuration; const availableDuration = totalDuration - (series.chart.series.length - 1) * afterSeriesWait, hasGlobalTimeProp = propMetrics.seriesTimeProps.every((timeProps) => { const props = Object.keys(timeProps); if (props.length > 1) { return false; } if (!timeProp) { timeProp = props[0]; } return timeProp === props[0]; }); if (hasGlobalTimeProp) { // Chart-wide single time prop, use time prop extremes const seriesExtremes = propMetrics .seriesExtremes[series.index][timeProp], seriesTimeLen = seriesExtremes.max - seriesExtremes.min, totalTimeLen = propMetrics.seriesExtremes.reduce((sum, s) => (s[timeProp] ? sum + s[timeProp].max - s[timeProp].min : sum), 0); seriesDuration = Math.round(seriesTimeLen / totalTimeLen * availableDuration); } else { // No common time prop, so use percent of total points const totalPoints = series.chart.series.reduce((sum, s) => sum + s.points.length, 0); seriesDuration = Math.round((series.points || []).length / totalPoints * availableDuration); } return Math.max(50, seriesDuration); } /** * Build and add a track to the timeline. * @private */ function addTimelineChannelFromTrack(timeline, audioContext, destinationNode, options) { const speechOpts = options, instrMappingOpts = (options.mapping || {}), engine = options.type === 'speech' ? new SonificationSpeaker({ language: speechOpts.language, name: speechOpts.preferredVoice }) : new SonificationInstrument(audioContext, destinationNode, { capabilities: { pan: !!instrMappingOpts.pan, tremolo: !!instrMappingOpts.tremolo, filters: !!(instrMappingOpts.highpass || instrMappingOpts.lowpass) }, synthPatch: options.instrument, midiTrackName: options.midiName }); return timeline.addChannel(options.type || 'instrument', engine, pick(options.showPlayMarker, true)); } /** * Add event from a point to a mapped instrument track. * @private */ function addMappedInstrumentEvent(context, channel, mappingOptions, propMetrics, roundToMusicalNotes, contextValueProp) { const getParam = (param, fallback, defaults, parent) => getParamValWithDefault(context, propMetrics, false, (parent || mappingOptions)[param], fallback, defaults, contextValueProp); const eventsAdded = [], eventOpts = { noteDuration: getParam('noteDuration', 200, { min: 40, max: 1000 }), pan: getParam('pan', 0, { min: -1, max: 1 }), volume: getParam('volume', 1, { min: 0.1, max: 1 }) }; if (mappingOptions.frequency) { eventOpts.frequency = getParam('frequency', 440, { min: 50, max: 6000 }); } if (mappingOptions.lowpass) { eventOpts.lowpassFreq = getParam('frequency', 20000, { min: 0, max: 20000 }, mappingOptions.lowpass); eventOpts.lowpassResonance = getParam('resonance', 0, { min: -6, max: 12 }, mappingOptions.lowpass); } if (mappingOptions.highpass) { eventOpts.highpassFreq = getParam('frequency', 20000, { min: 0, max: 20000 }, mappingOptions.highpass); eventOpts.highpassResonance = getParam('resonance', 0, { min: -6, max: 12 }, mappingOptions.highpass); } if (mappingOptions.tremolo) { eventOpts.tremoloDepth = getParam('depth', 0, { min: 0, max: 0.8 }, mappingOptions.tremolo); eventOpts.tremoloSpeed = getParam('speed', 0, { min: 0, max: 0.8 }, mappingOptions.tremolo); } const gapBetweenNotes = getParam('gapBetweenNotes', 150, { min: 50, max: 1000 }), playDelay = getParam('playDelay', 0, { max: 200 }); const addNoteEvent = (noteDef, ix = 0) => { let opts = noteDef; if (noteDef.mapTo) { // Transform the pitch mapping options to normal mapping options if (typeof noteDef.min === 'string') { opts.min = SonificationInstrument .noteStringToC0Distance(noteDef.min); } if (typeof noteDef.max === 'string') { opts.max = SonificationInstrument .noteStringToC0Distance(noteDef.max); } } else if (typeof noteDef === 'string' && isNoteDefinition(noteDef)) { opts = SonificationInstrument.noteStringToC0Distance(noteDef); } eventOpts.note = getParamValWithDefault(context, propMetrics, false, opts, -1, { min: 0, max: 107 }, contextValueProp); if (eventOpts.note > -1) { if (roundToMusicalNotes) { eventOpts.note = Math.round(eventOpts.note); } eventsAdded.push(channel.addEvent({ time: context.time + playDelay + gapBetweenNotes * ix, relatedPoint: context.point, instrumentEventOptions: ix !== void 0 ? extend({}, eventOpts) : eventOpts })); } }; if (mappingOptions.pitch && mappingOptions.pitch.constructor === Array) { mappingOptions.pitch.forEach(addNoteEvent); } else if (mappingOptions.pitch) { addNoteEvent(mappingOptions.pitch); } else if (mappingOptions.frequency) { eventsAdded.push(channel.addEvent({ time: context.time + playDelay, relatedPoint: context.point, instrumentEventOptions: eventOpts })); } return eventsAdded; } /** * Get the message value to speak for a point. * @private */ function getSpeechMessageValue(context, messageParam) { return format(typeof messageParam === 'function' ? messageParam(context) : messageParam, context, context.point && context.point.series.chart); } /** * Add an event from a point to a mapped speech track. * @private */ function addMappedSpeechEvent(context, channel, mappingOptions, propMetrics, contextValueProp) { const getParam = (param, fallback, defaults) => getParamValWithDefault(context, propMetrics, false, mappingOptions[param], fallback, defaults, contextValueProp); const playDelay = getParam('playDelay', 0, { max: 200 }), pitch = getParam('pitch', 1, { min: 0.3, max: 2 }), rate = getParam('rate', 1, { min: 0.4, max: 4 }), volume = getParam('volume', 1, { min: 0.1 }), message = getSpeechMessageValue(context, mappingOptions.text); if (message) { return channel.addEvent({ time: context.time + playDelay, relatedPoint: context.point, speechOptions: { pitch, rate, volume }, message }); } } /** * Add events to a channel for a point&track combo. * @private */ function addMappedEventForPoint(context, channel, trackOptions, propMetrics) { let eventsAdded = []; if (trackOptions.type === 'speech' && trackOptions.mapping) { const eventAdded = addMappedSpeechEvent(context, channel, trackOptions.mapping, propMetrics); if (eventAdded) { eventsAdded = [eventAdded]; } } else if (trackOptions.mapping) { eventsAdded = addMappedInstrumentEvent(context, channel, trackOptions.mapping, propMetrics, pick(trackOptions .roundToMusicalNotes, true)); } return eventsAdded; } /** * Get a reduced set of points from a list, depending on grouping opts. * @private */ function getGroupedPoints(pointGroupOpts, points) { const alg = pointGroupOpts.algorithm || 'minmax', r = (ix) => (points[ix] ? [points[ix].point] : []); if (alg === 'first') { return r(0); } if (alg === 'last') { return r(points.length - 1); } if (alg === 'middle') { return r(points.length >> 1); } if (alg === 'firstlast') { return r(0).concat(r(points.length - 1)); } if (alg === 'minmax') { const prop = pointGroupOpts.prop || 'y'; let min, max, minVal, maxVal; points.forEach((p) => { const val = getPointPropValue(p.point, prop); if (val === void 0) { return; } if (!min || val < minVal) { min = p; minVal = val; } if (!max || val > maxVal) { max = p; maxVal = val; } }); if (min && max) { if (min.point === max.point) { return [min.point]; } return min.time > max.time ? [max.point, min.point] : [min.point, max.point]; } } return []; } /** * Should a track be active for this event? * @private */ function isActive(context, activeWhen, lastPropValue) { if (typeof activeWhen === 'function') { return activeWhen(context); } if (typeof activeWhen === 'object') { const prop = activeWhen.prop, val = pick(context.value, context.point && getPointPropValue(context.point, prop)); if (typeof val !== 'number') { return false; } let crossingOk = true; const crossingUp = activeWhen.crossingUp, crossingDown = activeWhen.crossingDown, hasLastValue = typeof lastPropValue === 'number'; if (crossingUp && crossingDown) { crossingOk = hasLastValue && (lastPropValue < crossingUp && val >= crossingUp || lastPropValue > crossingDown && val <= crossingDown); } else { crossingOk = (crossingUp === void 0 || hasLastValue && lastPropValue < crossingUp && val >= crossingUp) && (crossingDown === void 0 || hasLastValue && lastPropValue > crossingDown && val <= crossingDown); } const max = pick(activeWhen.max, Infinity), min = pick(activeWhen.min, -Infinity); return val <= max && val >= min && crossingOk; } return true; } /** * Build a new timeline object from a chart. * @private */ function timelineFromChart(audioContext, destinationNode, chart) { const options = chart.options.sonification || {}, defaultInstrOpts = options.defaultInstrumentOptions, defaultSpeechOpts = options.defaultSpeechOptions, defaultPointGroupOpts = merge({ enabled: true, groupTimespan: 15, algorithm: 'minmax', prop: 'y' }, options.pointGrouping), globalTracks = options.globalTracks || [], globalContextTracks = options.globalContextTracks || [], isSequential = options.order === 'sequential', // Slight margin for note end totalDuration = Math.max(50, options.duration - 300), afterSeriesWait = options.afterSeriesWait, eventOptions = options.events || {}, propMetrics = getPropMetrics(chart), timeline = new SonificationTimeline({ onPlay: eventOptions.onPlay, onEnd: eventOptions.onEnd, onStop: eventOptions.onStop, showCrosshair: options.showCrosshair, showTooltip: options.showTooltip }, chart); // Expose PropMetrics for tests if (chart.sonification) { chart.sonification.propMetrics = propMetrics; } let startTime = 0; chart.series.forEach((series, seriesIx) => { const sOptions = series.options.sonification || {}; if (series.visible && sOptions.enabled !== false) { const seriesDuration = isSequential ? getAvailableDurationForSeries(series, totalDuration, propMetrics, afterSeriesWait) : totalDuration, seriesDefaultInstrOpts = merge(defaultInstrOpts, sOptions.defaultInstrumentOptions), seriesDefaultSpeechOpts = merge(defaultSpeechOpts, sOptions.defaultSpeechOptions), seriesPointGroupOpts = merge(defaultPointGroupOpts, sOptions.pointGrouping), mainTracks = (sOptions.tracks || [seriesDefaultInstrOpts]) .concat(globalTracks), hasAddedSeries = !!timeline.channels.length, contextTracks = hasAddedSeries && !isSequential ? sOptions.contextTracks || [] : (sOptions.contextTracks || []).concat(globalContextTracks), eventsAdded = []; // For crossing threshold notifications let lastPropValue; // Add events for the mapped tracks mainTracks.forEach((trackOpts) => { const mergedOpts = merge({ pointGrouping: seriesPointGroupOpts, midiName: trackOpts.midiName || series.name }, trackOpts.type === 'speech' ? seriesDefaultSpeechOpts : seriesDefaultInstrOpts, trackOpts), pointGroupOpts = mergedOpts.pointGrouping, activeWhen = mergedOpts.activeWhen, updateLastPropValue = (point) => { if (typeof activeWhen === 'object' && activeWhen.prop) { lastPropValue = getPointPropValue(point, activeWhen.prop); } }; const channel = addTimelineChannelFromTrack(timeline, audioContext, destinationNode, mergedOpts), add = (c) => eventsAdded.push( // Note arrays add multiple events ...addMappedEventForPoint(c, channel, mergedOpts, propMetrics)); // Go through the points and add events to channel let pointGroup = [], pointGroupTime = 0; const addCurrentPointGroup = (groupSpanTime) => { if (pointGroup.length === 1) { add({ point: pointGroup[0].point, time: pointGroupTime + groupSpanTime / 2 }); } else { const points = getGroupedPoints(pointGroupOpts, pointGroup), t = groupSpanTime / points.length; points.forEach((p, ix) => add({ point: p, time: pointGroupTime + t / 2 + t * ix })); } pointGroup = []; }; (series.points || []).forEach((point, pointIx) => { const isLastPoint = pointIx === series.points.length - 1; const time = getPointTime(point, startTime, seriesDuration, mergedOpts.mapping && mergedOpts.mapping.time || 0, propMetrics, isSequential); const context = { point, time }; // Is this point active? if (!mergedOpts.mapping || !isActive(context, activeWhen, lastPropValue)) { updateLastPropValue(point); // Remaining points in group if (isLastPoint && pointGroup.length) { addCurrentPointGroup(pointGroup[pointGroup.length - 1].time - pointGroup[0].time); } return; } updateLastPropValue(point); // Add the events if (!pointGroupOpts.enabled) { add(context); } else { const dT = time - pointGroupTime, groupSpan = pointGroupOpts.groupTimespan, spanTime = isLastPoint && dT <= groupSpan ? dT : groupSpan; if (isLastPoint || dT > groupSpan) { if (dT <= groupSpan) { // Only happens if last point is within group pointGroup.push(context); } addCurrentPointGroup(spanTime); pointGroupTime = Math.floor(time / groupSpan) * groupSpan; if (isLastPoint && dT > groupSpan) { add({ point: context.point, time: pointGroupTime + spanTime / 2 }); } else { pointGroup = [context]; } } else { pointGroup.push(context); } } }); }); // Add callbacks to first/last events const firstEvent = eventsAdded.reduce((first, e) => (e.time < first.time ? e : first), { time: Infinity }); const lastEvent = eventsAdded.reduce((last, e) => (e.time > last.time ? e : last), { time: -Infinity }); firstEvent.callback = eventOptions.onSeriesStart ? eventOptions.onSeriesStart.bind(null, { series, timeline }) : void 0; lastEvent.callback = eventOptions.onSeriesEnd ? eventOptions.onSeriesEnd.bind(null, { series, timeline }) : void 0; // Add the context tracks that are not related to points contextTracks.forEach((trackOpts) => { const mergedOpts = trackOpts.type === 'speech' ? merge(defaultSpeechOpts, trackOpts) : merge(defaultInstrOpts, { mapping: { pitch: { mapTo: 'value' } } }, trackOpts); const contextChannel = addTimelineChannelFromTrack(timeline, audioContext, destinationNode, mergedOpts); lastPropValue = void 0; const { timeInterval, valueInterval } = mergedOpts, valueProp = mergedOpts.valueProp || 'x', activeWhen = mergedOpts.activeWhen, contextExtremes = propMetrics .seriesExtremes[seriesIx][valueProp], addContextEvent = (time, value) => { if (!mergedOpts.mapping || !isActive({ time, value }, typeof activeWhen === 'object' ? extend({ prop: valueProp }, activeWhen) : activeWhen, lastPropValue)) { lastPropValue = value; return; } lastPropValue = value; if (mergedOpts.type === 'speech') { addMappedSpeechEvent({ time, value }, contextChannel, mergedOpts.mapping, propMetrics, valueProp); } else { addMappedInstrumentEvent({ time, value }, contextChannel, mergedOpts.mapping, propMetrics, pick(mergedOpts.roundToMusicalNotes, true), valueProp); } }; if (timeInterval) { let time = 0; while (time <= seriesDuration) { const val = mapToVirtualAxis(time, { min: 0, max: seriesDuration }, contextExtremes); addContextEvent(time + startTime, val); time += timeInterval; } } if (valueInterval) { let val = contextExtremes.min; while (val <= contextExtremes.max) { const time = mapToVirtualAxis(val, contextExtremes, { min: 0, max: seriesDuration }, false, mergedOpts.valueMapFunction === 'logarithmic'); addContextEvent(time + startTime, val); val += valueInterval; } } }); if (isSequential) { startTime += seriesDuration + afterSeriesWait; } } }); return timeline; } /* * * * Default Export * * */ return timelineFromChart; }); _registerModule(_modules, 'Extensions/Sonification/Sonification.js', [_modules['Core/Defaults.js'], _modules['Core/Utilities.js'], _modules['Core/Globals.js'], _modules['Extensions/Sonification/Options.js'], _modules['Extensions/Sonification/SonificationInstrument.js'], _modules['Extensions/Sonification/SonificationSpeaker.js'], _modules['Extensions/Sonification/SynthPatch.js'], _modules['Extensions/Sonification/InstrumentPresets.js'], _modules['Extensions/Sonification/TimelineFromChart.js']], function (D, U, H, defaultSonificationOptions, SonificationInstrument, SonificationSpeaker, SynthPatch, InstrumentPresets, timelineFromChart) { /* * * * (c) 2009-2024 Øystein Moseng * * Sonification module. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ /* * * * Imports * * */ const { defaultOptions, getOptions } = D; const { addEvent, extend, fireEvent, merge, pick } = U; const { doc, win } = H; /** * The Sonification class. This class represents a chart's sonification * capabilities. A chart automatically gets an instance of this class when * applicable. * * @sample highcharts/sonification/chart-events * Basic demo accessing some of the chart.sonification methods. * @sample highcharts/demo/sonification-navigation * More advanced demo using more functionality. * * @requires modules/sonification * * @class * @name Highcharts.Sonification * * @param {Highcharts.Chart} chart The chart to tie the sonification to */ class Sonification { constructor(chart) { this.chart = chart; this.retryContextCounter = 0; this.lastUpdate = 0; this.unbindKeydown = addEvent(doc, 'keydown', function (e) { if (chart && chart.sonification && (e.key === 'Esc' || e.key === 'Escape')) { chart.sonification.cancel(); } }); try { this.audioContext = new win.AudioContext(); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.audioContext.suspend(); this.audioDestination = this.audioContext.destination; } catch (e) { /* Ignore */ } } /** * Set the audio destination node to something other than the default * output. This allows for inserting custom WebAudio chains after the * sonification. * @function Highcharts.Sonification#setAudioDestination * @param {AudioDestinationNode} audioDestination The destination node */ setAudioDestination(audioDestination) { this.audioDestination = audioDestination; this.update(); } /** * Check if sonification is playing currently * @function Highcharts.Sonification#isPlaying * @return {boolean} `true` if currently playing, `false` if not */ isPlaying() { return !!this.timeline && this.timeline.isPlaying; } /** * Divide timeline into 100 parts of equal time, and play one of them. * Can be used for scrubbing navigation. * @function Highcharts.Sonification#playSegment * * @sample highcharts/sonification/scrubbing * Scrubbing with slider * * @param {number} segment The segment to play, from 0 to 100 * @param {Highcharts.SonificationChartEventCallback} [onEnd] Callback to call after play completed */ playSegment(segment, onEnd) { if (!this.ready(this.playSegment.bind(this, segment, onEnd))) { return; } if (this.timeline) { this.timeline.playSegment(segment, onEnd); } } /** * Play point(s)/event(s) adjacent to current timeline cursor location. * @function Highcharts.Sonification#playAdjacent * * @sample highcharts/demo/sonification-navigation * Sonification keyboard navigation * * @param {number} next Pass `true` to play next point, `false` for previous * @param {Highcharts.SonificationChartEventCallback} [onEnd] * Callback to call after play completed * @param {Highcharts.SonificationTimelineFilterCallback} [eventFilter] * Filter to apply to the events before finding adjacent to play */ playAdjacent(next, onEnd, eventFilter) { if (!this.ready(this.playAdjacent.bind(this, next, onEnd, eventFilter))) { return; } if (this.timeline) { const opts = this.chart.options.sonification, onHit = opts && opts.events && opts.events.onBoundaryHit; if (!onHit) { this.initBoundaryInstrument(); } this.timeline.playAdjacent(next, onEnd, onHit || (() => { this.defaultBoundaryHit(); }), eventFilter); } } /** * Play next/previous series, picking the point closest to a prop value * from last played point. By default picks the point in the adjacent * series with the closest x value as the last played point. * @function Highcharts.Sonification#playAdjacentSeries * * @sample highcharts/demo/sonification-navigation * Sonification keyboard navigation * * @param {number} next Pass `true` to play next series, `false` for previous * @param {string} [prop] Prop to find closest value of, defaults to `x`. * @param {Highcharts.SonificationChartEventCallback} [onEnd] * Callback to call after play completed * * @return {Highcharts.Series|null} The played series, or `null` if none found */ playAdjacentSeries(next, prop = 'x', onEnd) { const lastPlayed = this.getLastPlayedPoint(); if (lastPlayed) { const targetSeriesIx = lastPlayed.series.index + (next ? 1 : -1); this.playClosestToProp(prop, lastPlayed[prop], (e) => !!e.relatedPoint && e.relatedPoint.series.index === targetSeriesIx, onEnd); return this.chart.series[targetSeriesIx] || null; } return null; } /** * Play point(s)/event(s) closest to a prop relative to a reference value. * @function Highcharts.Sonification#playClosestToProp * * @param {string} prop Prop to compare. * @param {number} targetValue Target value to find closest value of. * @param {Highcharts.SonificationTimelineFilterCallback} [targetFilter] * Filter to apply to the events before finding closest point(s) * @param {Highcharts.SonificationChartEventCallback} [onEnd] * Callback to call after play completed */ playClosestToProp(prop, targetValue, targetFilter, onEnd) { if (!this.ready(this.playClosestToProp.bind(this, prop, targetValue, targetFilter, onEnd))) { return; } if (this.timeline) { const opts = this.chart.options.sonification, onHit = opts && opts.events && opts.events.onBoundaryHit; if (!onHit) { this.initBoundaryInstrument(); } this.timeline.playClosestToPropValue(prop, targetValue, onEnd, onHit || (() => this.defaultBoundaryHit()), targetFilter); } } /** * Get last played point * @function Highcharts.Sonification#getLastPlayedPoint * * @sample highcharts/demo/sonification-navigation * Sonification keyboard navigation * * @return {Highcharts.Point|null} The point, or null if none */ getLastPlayedPoint() { if (this.timeline) { return this.timeline.getLastPlayedPoint(); } return null; } /** * Play a note with a specific instrument, and optionally a time offset. * @function Highcharts.Sonification#playNote * * @sample highcharts/sonification/chart-events * Custom notifications * * @param {Highcharts.SonificationSynthPreset|Highcharts.SynthPatchOptionsObject} instrument * The instrument to play. Can be either a string referencing the * instrument presets, or an actual SynthPatch configuration. * @param {Highcharts.SonificationInstrumentScheduledEventOptionsObject} options * Configuration for the instrument event to play. * @param {number} [delayMs] * Time offset from now, in milliseconds. Defaults to 0. */ playNote(instrument, options, delayMs = 0) { if (!this.ready(this.playNote.bind(this, instrument, options))) { return; } const duration = options.noteDuration = options.noteDuration || 500; const instr = new SonificationInstrument(this.audioContext, this.audioDestination, { synthPatch: instrument, capabilities: { filters: true, tremolo: true, pan: true } }); instr.scheduleEventAtTime(delayMs / 1000, options); setTimeout(() => instr && instr.destroy(), delayMs + duration + 500); } /** * Speak a text string, optionally with a custom speaker configuration * @function Highcharts.Sonification#speak * * @sample highcharts/sonification/chart-events * Custom notifications * * @param {string} text Text to announce * @param {Highcharts.SonificationSpeakerOptionsObject} [speakerOptions] * Options for the announcement * @param {number} [delayMs] * Time offset from now, in milliseconds. Defaults to 0. */ speak(text, speakerOptions, delayMs = 0) { const speaker = new SonificationSpeaker(merge({ language: 'en-US', rate: 1.5, volume: 0.4 }, speakerOptions || {})); speaker.sayAtTime(delayMs, text); } /** * Cancel current playing audio and reset the timeline. * @function Highcharts.Sonification#cancel */ cancel() { if (this.timeline) { this.timeline.cancel(); } fireEvent(this, 'cancel'); } /** * Start download of a MIDI file export of the timeline. * @function Highcharts.Sonification#downloadMIDI */ downloadMIDI() { if (!this.ready(this.downloadMIDI.bind(this))) { return; } if (this.timeline) { this.timeline.reset(); this.timeline.downloadMIDI(); } } /** * Implementation of chart.sonify * @private */ sonifyChart(resetAfter, onEnd) { if (!this.ready(this.sonifyChart.bind(this, resetAfter, onEnd))) { return; } if (this.timeline) { this.timeline.reset(); this.beforePlay(); this.timeline.play(void 0, void 0, resetAfter, onEnd); } } /** * Implementation of series.sonify * @private */ sonifySeries(series, resetAfter, onEnd) { if (!this.ready(this.sonifySeries.bind(this, series, resetAfter, onEnd))) { return; } if (this.timeline) { this.timeline.reset(); this.beforePlay(); this.timeline.play((e) => !!e.relatedPoint && e.relatedPoint.series === series, void 0, resetAfter, onEnd); } } /** * Implementation of point.sonify * @private */ sonifyPoint(point, onEnd) { if (!this.ready(this.sonifyPoint.bind(this, point, onEnd))) { return; } if (this.timeline) { this.timeline.reset(); this.beforePlay(); this.timeline.anchorPlayMoment((e) => e.relatedPoint === point, onEnd); } } /** * Set the overall/master volume for the sonification. * Usually handled through chart update. * @private */ setMasterVolume(vol) { if (this.timeline) { this.timeline.setMasterVolume(vol); } } /** * Destroy the sonification capabilities * @private */ destroy() { this.unbindKeydown(); if (this.timeline) { this.timeline.destroy(); delete this.timeline; } if (this.boundaryInstrument) { this.boundaryInstrument.stop(); } if (this.audioContext) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.audioContext.close(); delete this.audioContext; } } /** * Update the timeline with latest chart changes. Usually handled * automatically. Note that the [sonification.updateInterval](https://api.highcharts.com/highcharts/sonification.updateInterval) * option can stop updates from happening in rapid succession, including * manual calls to this function. * @private */ update() { const sOpts = this.chart.options && this.chart.options.sonification; if (!this.ready(this.update.bind(this)) || !sOpts) { return; } // Don't update too often, it gets performance intensive const now = Date.now(), updateInterval = sOpts.updateInterval; if (now - this.lastUpdate < updateInterval && !this.forceReady) { clearTimeout(this.scheduledUpdate); this.scheduledUpdate = setTimeout(this.update.bind(this), updateInterval / 2); return; } const events = sOpts.events || {}; if (events.beforeUpdate) { events.beforeUpdate({ chart: this.chart, timeline: this.timeline }); } this.lastUpdate = now; if (this.timeline) { this.timeline.destroy(); } if (this.audioContext && this.audioDestination) { this.timeline = timelineFromChart(this.audioContext, this.audioDestination, this.chart); const sOpts = this.chart.options.sonification; this.timeline.setMasterVolume(pick(sOpts && sOpts.masterVolume, 1)); } if (events.afterUpdate) { events.afterUpdate({ chart: this.chart, timeline: this.timeline }); } } /** * Only continue if sonification enabled. If audioContext is * suspended, retry up to 20 times with a small delay. * @private */ ready(whenReady) { if (!this.audioContext || !this.audioDestination || !this.chart.options || this.chart.options.sonification && this.chart.options.sonification.enabled === false) { return false; } if (this.audioContext.state === 'suspended' && !this.forceReady) { if (this.retryContextCounter++ < 20) { setTimeout(() => { if (this.audioContext && this.audioContext.state === 'suspended') { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.audioContext.resume().then(whenReady); } else { whenReady(); } }, 5); } return false; } this.retryContextCounter = 0; return true; } /** * Call beforePlay event handler if exists * @private */ beforePlay() { const opts = this.chart.options.sonification, beforePlay = opts && opts.events && opts.events.beforePlay; if (beforePlay) { beforePlay({ chart: this.chart, timeline: this.timeline }); } } /** * Initialize the builtin boundary hit instrument * @private */ initBoundaryInstrument() { if (!this.boundaryInstrument) { this.boundaryInstrument = new SynthPatch(this.audioContext, merge(InstrumentPresets.chop, { masterVolume: 0.3 })); this.boundaryInstrument.startSilently(); this.boundaryInstrument.connect(this.audioDestination); } } /** * The default boundary hit sound * @private */ defaultBoundaryHit() { if (this.boundaryInstrument) { this.boundaryInstrument.playFreqAtTime(0.1, 1, 200); this.boundaryInstrument.playFreqAtTime(0.2, 1, 200); } } } (function (Sonification) { const composedClasses = []; /** * Update sonification object on chart. * @private */ function updateSonificationEnabled() { const sonification = this.sonification, sOptions = this.options && this.options.sonification; if (sOptions && sOptions.enabled) { if (sonification) { sonification.update(); } else { this.sonification = new Sonification(this); this.sonification.update(); } } else if (sonification) { sonification.destroy(); delete this.sonification; } } /** * Destroy with chart. * @private */ function chartOnDestroy() { if (this && this.sonification) { this.sonification.destroy(); } } /** * Update on render * @private */ function chartOnRender() { if (this.updateSonificationEnabled) { this.updateSonificationEnabled(); } } /** * Update * @private */ function chartOnUpdate(e) { const newOptions = e.options.sonification; if (newOptions) { merge(true, this.options.sonification, newOptions); chartOnRender.call(this); } } /** * Compose * @private */ function compose(ChartClass, SeriesClass, PointClass) { // Extend chart if (composedClasses.indexOf(ChartClass) === -1) { composedClasses.push(ChartClass); extend(ChartClass.prototype, { updateSonificationEnabled, sonify: function (onEnd) { if (this.sonification) { this.sonification.sonifyChart(false, onEnd); } }, toggleSonify: function (reset = true, onEnd) { if (!this.sonification) { return; } const timeline = this.sonification.timeline; if (win.speechSynthesis) { win.speechSynthesis.cancel(); } if (timeline && this.sonification.isPlaying()) { if (reset) { this.sonification.cancel(); } else { timeline.pause(); } } else if (timeline && timeline.isPaused) { timeline.resume(); } else { this.sonification.sonifyChart(reset, onEnd); } } }); addEvent(ChartClass, 'destroy', chartOnDestroy); addEvent(ChartClass, 'render', chartOnRender); addEvent(ChartClass, 'update', chartOnUpdate); } // Extend series if (composedClasses.indexOf(SeriesClass) === -1) { composedClasses.push(SeriesClass); SeriesClass.prototype.sonify = function (onEnd) { if (this.chart.sonification) { this.chart.sonification.sonifySeries(this, false, onEnd); } }; } // Extend points if (composedClasses.indexOf(PointClass) === -1) { composedClasses.push(PointClass); PointClass.prototype.sonify = function (onEnd) { if (this.series.chart.sonification) { this.series.chart.sonification.sonifyPoint(this, onEnd); } }; } // Add items to the exporting menu const exportingOptions = getOptions().exporting; if (exportingOptions && exportingOptions.buttons && exportingOptions.buttons.contextButton.menuItems) { exportingOptions.buttons.contextButton.menuItems.push('separator', 'downloadMIDI', 'playAsSound'); } } Sonification.compose = compose; })(Sonification || (Sonification = {})); // Add default options merge(true, defaultOptions, defaultSonificationOptions); /* * * * Default Export * * */ /* * * * API declarations * * */ /** * Play a sonification of a chart. * * @function Highcharts.Chart#sonify * @param {Highcharts.SonificationChartEventCallback} [onEnd] * Callback to call after play completed * * @requires modules/sonification */ /** * Play/pause sonification of a chart. * * @function Highcharts.Chart#toggleSonify * * @param {boolean} [reset] * Reset the playing cursor after play completed. Defaults to `true`. * @param {Highcharts.SonificationChartEventCallback} [onEnd] * Callback to call after play completed * * @requires modules/sonification */ /** * Play a sonification of a series. * * @function Highcharts.Series#sonify * @param {Highcharts.SonificationChartEventCallback} [onEnd] * Callback to call after play completed * * @requires modules/sonification */ /** * Play a sonification of a point. * * @function Highcharts.Point#sonify * @param {Highcharts.SonificationChartEventCallback} [onEnd] * Callback to call after play completed * * @requires modules/sonification */ /** * Sonification capabilities for the chart. * * @name Highcharts.Chart#sonification * @type {Highcharts.Sonification|undefined} * * @requires modules/sonification */ /** * Collection of Sonification classes and objects. * @requires modules/sonification * @interface Highcharts.SonificationGlobalObject */ /** * SynthPatch presets * @name Highcharts.SonificationGlobalObject#InstrumentPresets * @type {Record|undefined} */ /** * Musical scale presets * @name Highcharts.SonificationGlobalObject#Scales * @type {Highcharts.SonificationScalePresetsObject|undefined} */ /** * SynthPatch class * @name Highcharts.SonificationGlobalObject#SynthPatch * @type {Highcharts.SynthPatch|undefined} */ /** * SonificationInstrument class * @name Highcharts.SonificationGlobalObject#SonificationInstrument * @type {Highcharts.SonificationInstrument|undefined} */ /** * SonificationSpeaker class * @name Highcharts.SonificationGlobalObject#SonificationSpeaker * @type {Highcharts.SonificationSpeaker|undefined} */ /** * Global Sonification classes and objects. * * @name Highcharts.sonification * @type {Highcharts.SonificationGlobalObject} * * @requires modules/sonification */ (''); // Keep above doclets in JS file return Sonification; }); _registerModule(_modules, 'Extensions/Sonification/Scales.js', [], function () { /* * * * (c) 2009-2024 Øystein Moseng * * Musical scales for sonification. * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ const Scales = { minor: [0, 2, 3, 5, 7, 8, 10], dorian: [0, 2, 3, 5, 7, 9, 10], harmonicMinor: [0, 2, 3, 5, 7, 8, 11], phrygian: [0, 1, 3, 5, 7, 8, 11], major: [0, 2, 4, 5, 7, 9, 11], lydian: [0, 2, 4, 6, 7, 9, 11], mixolydian: [0, 2, 4, 5, 7, 9, 10], majorPentatonic: [0, 2, 4, 7, 9], minorPentatonic: [0, 3, 5, 7, 10] }; /* * * * Default Export * * */ /* * * * API declarations * * */ /** * Preset scales for pitch mapping. * @requires modules/sonification * @interface Highcharts.SonificationScalePresetsObject */ /** * Minor scale (aeolian) * @name Highcharts.SonificationScalePresetsObject#minor * @type {Array} */ /** * Dorian scale * @name Highcharts.SonificationScalePresetsObject#dorian * @type {Array} */ /** * Harmonic minor scale * @name Highcharts.SonificationScalePresetsObject#harmonicMinor * @type {Array} */ /** * Phrygian scale * @name Highcharts.SonificationScalePresetsObject#phrygian * @type {Array} */ /** * Major (ionian) scale * @name Highcharts.SonificationScalePresetsObject#major * @type {Array} */ /** * Lydian scale * @name Highcharts.SonificationScalePresetsObject#lydian * @type {Array} */ /** * Mixolydian scale * @name Highcharts.SonificationScalePresetsObject#mixolydian * @type {Array} */ /** * Major pentatonic scale * @name Highcharts.SonificationScalePresetsObject#majorPentatonic * @type {Array} */ /** * Minor pentatonic scale * @name Highcharts.SonificationScalePresetsObject#minorPentatonic * @type {Array} */ (''); // Keep above doclets in JS file return Scales; }); _registerModule(_modules, 'masters/modules/sonification.src.js', [_modules['Core/Globals.js'], _modules['Extensions/Sonification/Sonification.js'], _modules['Extensions/Sonification/SynthPatch.js'], _modules['Extensions/Sonification/InstrumentPresets.js'], _modules['Extensions/Sonification/Scales.js'], _modules['Extensions/Sonification/SonificationInstrument.js'], _modules['Extensions/Sonification/SonificationSpeaker.js'], _modules['Extensions/Sonification/SonificationTimeline.js']], function (Highcharts, Sonification, SynthPatch, InstrumentPresets, Scales, SonificationInstrument, SonificationSpeaker, SonificationTimeline) { const G = Highcharts; // Global objects G.sonification = { InstrumentPresets, Scales, SynthPatch, SonificationInstrument, SonificationSpeaker, SonificationTimeline, Sonification }; Sonification.compose(G.Chart, G.Series, G.Point); return Highcharts; }); }));