tupali/librerias/gantt/code/es-modules/modules/sonification/Instrument.js

516 lines
18 KiB
JavaScript
Raw Normal View History

2020-05-23 20:45:54 +00:00
/* *
*
* (c) 2009-2020 Øystein Moseng
*
* Instrument class for sonification module.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import H from '../../parts/Globals.js';
import U from '../../parts/Utilities.js';
var error = U.error, merge = U.merge, pick = U.pick, uniqueKey = U.uniqueKey;
/**
* A set of options for the Instrument class.
*
* @requires module:modules/sonification
*
* @interface Highcharts.InstrumentOptionsObject
*/ /**
* The type of instrument. Currently only `oscillator` is supported. Defaults
* to `oscillator`.
* @name Highcharts.InstrumentOptionsObject#type
* @type {string|undefined}
*/ /**
* The unique ID of the instrument. Generated if not supplied.
* @name Highcharts.InstrumentOptionsObject#id
* @type {string|undefined}
*/ /**
* When using functions to determine frequency or other parameters during
* playback, this options specifies how often to call the callback functions.
* Number given in milliseconds. Defaults to 20.
* @name Highcharts.InstrumentOptionsObject#playCallbackInterval
* @type {number|undefined}
*/ /**
* A list of allowed frequencies for this instrument. If trying to play a
* frequency not on this list, the closest frequency will be used. Set to `null`
* to allow all frequencies to be used. Defaults to `null`.
* @name Highcharts.InstrumentOptionsObject#allowedFrequencies
* @type {Array<number>|undefined}
*/ /**
* Options specific to oscillator instruments.
* @name Highcharts.InstrumentOptionsObject#oscillator
* @type {Highcharts.OscillatorOptionsObject|undefined}
*/
/**
* Options for playing an instrument.
*
* @requires module:modules/sonification
*
* @interface Highcharts.InstrumentPlayOptionsObject
*/ /**
* The frequency of the note to play. Can be a fixed number, or a function. The
* function receives one argument: the relative time of the note playing (0
* being the start, and 1 being the end of the note). It should return the
* frequency number for each point in time. The poll interval of this function
* is specified by the Instrument.playCallbackInterval option.
* @name Highcharts.InstrumentPlayOptionsObject#frequency
* @type {number|Function}
*/ /**
* The duration of the note in milliseconds.
* @name Highcharts.InstrumentPlayOptionsObject#duration
* @type {number}
*/ /**
* The minimum frequency to allow. If the instrument has a set of allowed
* frequencies, the closest frequency is used by default. Use this option to
* stop too low frequencies from being used.
* @name Highcharts.InstrumentPlayOptionsObject#minFrequency
* @type {number|undefined}
*/ /**
* The maximum frequency to allow. If the instrument has a set of allowed
* frequencies, the closest frequency is used by default. Use this option to
* stop too high frequencies from being used.
* @name Highcharts.InstrumentPlayOptionsObject#maxFrequency
* @type {number|undefined}
*/ /**
* The volume of the instrument. Can be a fixed number between 0 and 1, or a
* function. The function receives one argument: the relative time of the note
* playing (0 being the start, and 1 being the end of the note). It should
* return the volume for each point in time. The poll interval of this function
* is specified by the Instrument.playCallbackInterval option. Defaults to 1.
* @name Highcharts.InstrumentPlayOptionsObject#volume
* @type {number|Function|undefined}
*/ /**
* The panning of the instrument. Can be a fixed number between -1 and 1, or a
* function. The function receives one argument: the relative time of the note
* playing (0 being the start, and 1 being the end of the note). It should
* return the panning value for each point in time. The poll interval of this
* function is specified by the Instrument.playCallbackInterval option.
* Defaults to 0.
* @name Highcharts.InstrumentPlayOptionsObject#pan
* @type {number|Function|undefined}
*/ /**
* Callback function to be called when the play is completed.
* @name Highcharts.InstrumentPlayOptionsObject#onEnd
* @type {Function|undefined}
*/
/**
* @requires module:modules/sonification
*
* @interface Highcharts.OscillatorOptionsObject
*/ /**
* The waveform shape to use for oscillator instruments. Defaults to `sine`.
* @name Highcharts.OscillatorOptionsObject#waveformShape
* @type {string|undefined}
*/
// Default options for Instrument constructor
var defaultOptions = {
type: 'oscillator',
playCallbackInterval: 20,
oscillator: {
waveformShape: 'sine'
}
};
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* The Instrument class. Instrument objects represent an instrument capable of
* playing a certain pitch for a specified duration.
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
* @sample highcharts/sonification/instrument-advanced/
* Using callbacks for instrument parameters
*
* @requires module:modules/sonification
*
* @class
* @name Highcharts.Instrument
*
* @param {Highcharts.InstrumentOptionsObject} options
* Options for the instrument instance.
*/
function Instrument(options) {
this.init(options);
}
Instrument.prototype.init = function (options) {
if (!this.initAudioContext()) {
error(29);
return;
}
this.options = merge(defaultOptions, options);
this.id = this.options.id = options && options.id || uniqueKey();
// Init the audio nodes
var ctx = H.audioContext;
this.gainNode = ctx.createGain();
this.setGain(0);
this.panNode = ctx.createStereoPanner && ctx.createStereoPanner();
if (this.panNode) {
this.setPan(0);
this.gainNode.connect(this.panNode);
this.panNode.connect(ctx.destination);
}
else {
this.gainNode.connect(ctx.destination);
}
// Oscillator initialization
if (this.options.type === 'oscillator') {
this.initOscillator(this.options.oscillator);
}
// Init timer list
this.playCallbackTimers = [];
};
/**
* Return a copy of an instrument. Only one instrument instance can play at a
* time, so use this to get a new copy of the instrument that can play alongside
* it. The new instrument copy will receive a new ID unless one is supplied in
* options.
*
* @function Highcharts.Instrument#copy
*
* @param {Highcharts.InstrumentOptionsObject} [options]
* Options to merge in for the copy.
*
* @return {Highcharts.Instrument}
* A new Instrument instance with the same options.
*/
Instrument.prototype.copy = function (options) {
return new Instrument(merge(this.options, { id: null }, options));
};
/**
* Init the audio context, if we do not have one.
* @private
* @return {boolean} True if successful, false if not.
*/
Instrument.prototype.initAudioContext = function () {
var Context = H.win.AudioContext || H.win.webkitAudioContext, hasOldContext = !!H.audioContext;
if (Context) {
H.audioContext = H.audioContext || new Context();
if (!hasOldContext &&
H.audioContext &&
H.audioContext.state === 'running') {
H.audioContext.suspend(); // Pause until we need it
}
return !!(H.audioContext &&
H.audioContext.createOscillator &&
H.audioContext.createGain);
}
return false;
};
/**
* Init an oscillator instrument.
* @private
* @param {Highcharts.OscillatorOptionsObject} oscillatorOptions
* The oscillator options passed to Highcharts.Instrument#init.
* @return {void}
*/
Instrument.prototype.initOscillator = function (options) {
var ctx = H.audioContext;
this.oscillator = ctx.createOscillator();
this.oscillator.type = options.waveformShape;
this.oscillator.connect(this.gainNode);
this.oscillatorStarted = false;
};
/**
* Set pan position.
* @private
* @param {number} panValue
* The pan position to set for the instrument.
* @return {void}
*/
Instrument.prototype.setPan = function (panValue) {
if (this.panNode) {
this.panNode.pan.setValueAtTime(panValue, H.audioContext.currentTime);
}
};
/**
* Set gain level. A maximum of 1.2 is allowed before we emit a warning. The
* actual volume is not set above this level regardless of input.
* @private
* @param {number} gainValue
* The gain level to set for the instrument.
* @param {number} [rampTime=0]
* Gradually change the gain level, time given in milliseconds.
* @return {void}
*/
Instrument.prototype.setGain = function (gainValue, rampTime) {
if (this.gainNode) {
if (gainValue > 1.2) {
console.warn(// eslint-disable-line
'Highcharts sonification warning: ' +
'Volume of instrument set too high.');
gainValue = 1.2;
}
if (rampTime) {
this.gainNode.gain.setValueAtTime(this.gainNode.gain.value, H.audioContext.currentTime);
this.gainNode.gain.linearRampToValueAtTime(gainValue, H.audioContext.currentTime + rampTime / 1000);
}
else {
this.gainNode.gain.setValueAtTime(gainValue, H.audioContext.currentTime);
}
}
};
/**
* Cancel ongoing gain ramps.
* @private
* @return {void}
*/
Instrument.prototype.cancelGainRamp = function () {
if (this.gainNode) {
this.gainNode.gain.cancelScheduledValues(0);
}
};
/**
* Get the closest valid frequency for this instrument.
* @private
* @param {number} frequency - The target frequency.
* @param {number} [min] - Minimum frequency to return.
* @param {number} [max] - Maximum frequency to return.
* @return {number} The closest valid frequency to the input frequency.
*/
Instrument.prototype.getValidFrequency = function (frequency, min, max) {
var validFrequencies = this.options.allowedFrequencies, maximum = pick(max, Infinity), minimum = pick(min, -Infinity);
return !validFrequencies || !validFrequencies.length ?
// No valid frequencies for this instrument, return the target
frequency :
// Use the valid frequencies and return the closest match
validFrequencies.reduce(function (acc, cur) {
// Find the closest allowed value
return Math.abs(cur - frequency) < Math.abs(acc - frequency) &&
cur < maximum && cur > minimum ?
cur : acc;
}, Infinity);
};
/**
* Clear existing play callback timers.
* @private
* @return {void}
*/
Instrument.prototype.clearPlayCallbackTimers = function () {
this.playCallbackTimers.forEach(function (timer) {
clearInterval(timer);
});
this.playCallbackTimers = [];
};
/**
* Set the current frequency being played by the instrument. The closest valid
* frequency between the frequency limits is used.
* @param {number} frequency
* The frequency to set.
* @param {Highcharts.Dictionary<number>} [frequencyLimits]
* Object with maxFrequency and minFrequency
* @return {void}
*/
Instrument.prototype.setFrequency = function (frequency, frequencyLimits) {
var limits = frequencyLimits || {}, validFrequency = this.getValidFrequency(frequency, limits.min, limits.max);
if (this.options.type === 'oscillator') {
this.oscillatorPlay(validFrequency);
}
};
/**
* Play oscillator instrument.
* @private
* @param {number} frequency - The frequency to play.
*/
Instrument.prototype.oscillatorPlay = function (frequency) {
if (!this.oscillatorStarted) {
this.oscillator.start();
this.oscillatorStarted = true;
}
this.oscillator.frequency.setValueAtTime(frequency, H.audioContext.currentTime);
};
/**
* Prepare instrument before playing. Resumes the audio context and starts the
* oscillator.
* @private
*/
Instrument.prototype.preparePlay = function () {
this.setGain(0.001);
if (H.audioContext.state === 'suspended') {
H.audioContext.resume();
}
if (this.oscillator && !this.oscillatorStarted) {
this.oscillator.start();
this.oscillatorStarted = true;
}
};
/**
* Play the instrument according to options.
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
* @sample highcharts/sonification/instrument-advanced/
* Using callbacks for instrument parameters
*
* @function Highcharts.Instrument#play
*
* @param {Highcharts.InstrumentPlayOptionsObject} options
* Options for the playback of the instrument.
*
* @return {void}
*/
Instrument.prototype.play = function (options) {
var instrument = this, duration = options.duration || 0,
// Set a value, or if it is a function, set it continously as a timer.
// Pass in the value/function to set, the setter function, and any
// additional data to pass through to the setter function.
setOrStartTimer = function (value, setter, setterData) {
var target = options.duration, currentDurationIx = 0, callbackInterval = instrument.options.playCallbackInterval;
if (typeof value === 'function') {
var timer = setInterval(function () {
currentDurationIx++;
var curTime = (currentDurationIx * callbackInterval / target);
if (curTime >= 1) {
instrument[setter](value(1), setterData);
clearInterval(timer);
}
else {
instrument[setter](value(curTime), setterData);
}
}, callbackInterval);
instrument.playCallbackTimers.push(timer);
}
else {
instrument[setter](value, setterData);
}
};
if (!instrument.id) {
// No audio support - do nothing
return;
}
// If the AudioContext is suspended we have to resume it before playing
if (H.audioContext.state === 'suspended' ||
this.oscillator && !this.oscillatorStarted) {
instrument.preparePlay();
// Try again in 10ms
setTimeout(function () {
instrument.play(options);
}, 10);
return;
}
// Clear any existing play timers
if (instrument.playCallbackTimers.length) {
instrument.clearPlayCallbackTimers();
}
// Clear any gain ramps
instrument.cancelGainRamp();
// Clear stop oscillator timer
if (instrument.stopOscillatorTimeout) {
clearTimeout(instrument.stopOscillatorTimeout);
delete instrument.stopOscillatorTimeout;
}
// If a note is playing right now, clear the stop timeout, and call the
// callback.
if (instrument.stopTimeout) {
clearTimeout(instrument.stopTimeout);
delete instrument.stopTimeout;
if (instrument.stopCallback) {
// We have a callback for the play we are interrupting. We do not
// allow this callback to start a new play, because that leads to
// chaos. We pass in 'cancelled' to indicate that this note did not
// finish, but still stopped.
instrument._play = instrument.play;
instrument.play = function () { };
instrument.stopCallback('cancelled');
instrument.play = instrument._play;
}
}
// Stop the note without fadeOut if the duration is too short to hear the
// note otherwise.
var immediate = duration < H.sonification.fadeOutDuration + 20;
// Stop the instrument after the duration of the note
instrument.stopCallback = options.onEnd;
var onStop = function () {
delete instrument.stopTimeout;
instrument.stop(immediate);
};
if (duration) {
instrument.stopTimeout = setTimeout(onStop, immediate ? duration :
duration - H.sonification.fadeOutDuration);
// Play the note
setOrStartTimer(options.frequency, 'setFrequency', {
minFrequency: options.minFrequency,
maxFrequency: options.maxFrequency
});
// Set the volume and panning
setOrStartTimer(pick(options.volume, 1), 'setGain', 4); // Slight ramp
setOrStartTimer(pick(options.pan, 0), 'setPan');
}
else {
// No note duration, so just stop immediately
onStop();
}
};
/**
* Mute an instrument that is playing. If the instrument is not currently
* playing, this function does nothing.
*
* @function Highcharts.Instrument#mute
*/
Instrument.prototype.mute = function () {
this.setGain(0.0001, H.sonification.fadeOutDuration * 0.8);
};
/**
* Stop the instrument playing.
*
* @function Highcharts.Instrument#stop
*
* @param {boolean} immediately
* Whether to do the stop immediately or fade out.
*
* @param {Function} [onStopped]
* Callback function to be called when the stop is completed.
*
* @param {*} [callbackData]
* Data to send to the onEnd callback functions.
*
* @return {void}
*/
Instrument.prototype.stop = function (immediately, onStopped, callbackData) {
var instr = this, reset = function () {
// Remove timeout reference
if (instr.stopOscillatorTimeout) {
delete instr.stopOscillatorTimeout;
}
// The oscillator may have stopped in the meantime here, so allow
// this function to fail if so.
try {
instr.oscillator.stop();
}
catch (e) {
// silent error
}
instr.oscillator.disconnect(instr.gainNode);
// We need a new oscillator in order to restart it
instr.initOscillator(instr.options.oscillator);
// Done stopping, call the callback from the stop
if (onStopped) {
onStopped(callbackData);
}
// Call the callback for the play we finished
if (instr.stopCallback) {
instr.stopCallback(callbackData);
}
};
// Clear any existing timers
if (instr.playCallbackTimers.length) {
instr.clearPlayCallbackTimers();
}
if (instr.stopTimeout) {
clearTimeout(instr.stopTimeout);
}
if (immediately) {
instr.setGain(0);
reset();
}
else {
instr.mute();
// Stop the oscillator after the mute fade-out has finished
instr.stopOscillatorTimeout =
setTimeout(reset, H.sonification.fadeOutDuration + 100);
}
};
export default Instrument;