UNPKG

highcharts

Version:
367 lines (366 loc) 14 kB
/* * * * (c) 2009-2025 Ø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. !!!!!!! * * */ 'use strict'; import SynthPatch from './SynthPatch.js'; import InstrumentPresets from './InstrumentPresets.js'; import U from '../../Core/Utilities.js'; 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) { 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 * * */ export default SonificationInstrument; /* * * * 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