Skip to content

Commit

Permalink
Added Earcons and advanced Instruments. Closes highcharts#9336.
Browse files Browse the repository at this point in the history
  • Loading branch information
oysteinmoseng committed Nov 23, 2018
1 parent 124c1b9 commit 6c72fd3
Show file tree
Hide file tree
Showing 13 changed files with 458 additions and 21 deletions.
4 changes: 4 additions & 0 deletions errors/30/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Invalid instrument

This happens when you try to use a sonification instrument that is not valid. If
you are using a predefined instrument, make sure your spelling is correct.
7 changes: 6 additions & 1 deletion js/error-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ H.errorMessages = {
"title": "Browser does not support WebAudio",
"text": "<h1>Browser does not support WebAudio</h1><p>This happens when you attempt to use the sonification module on a chart in a browser or environment that does not support the WebAudio API. This API is supported on all modern browsers, including Microsoft Edge, Google Chrome and Mozilla Firefox.</p>"
},
"30": {
"title": "Invalid instrument",
"text": "<h1>Invalid instrument</h1><p>This happens when you try to use a sonification instrument that is not valid. If you are using a predefined instrument, make sure your spelling is correct.</p>"
},
"meta": {
"files": [
"errors/10/readme.md",
Expand All @@ -117,7 +121,8 @@ H.errorMessages = {
"errors/26/readme.md",
"errors/27/readme.md",
"errors/28/readme.md",
"errors/29/readme.md"
"errors/29/readme.md",
"errors/30/readme.md"
],
"version": "6.2.0"
}
Expand Down
99 changes: 99 additions & 0 deletions js/modules/sonification/Earcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* (c) 2009-2018 Øystein Moseng
*
* Earcons for the sonification module in Highcharts.
*
* License: www.highcharts.com/license
*/

'use strict';

import H from '../../parts/Globals.js';

/**
* @typedef {Object} EarconInstrument
* @property {Highcharts.Instrument|String} instrument - An instrument instance
* or the name of the instrument in the Highcharts.sonification.instruments
* map.
* @property {Object} playOptions - The options to pass to Instrument.play
*/

/**
* The Earcon class. Earcon objects represent a certain sound consisting of
* one or more instruments playing a predefined sound.
*
* @class Earcon
*
* @param {Object} options
* Options for the Earcon instance.
*
* @param {Array<EarconInstrument>} options.instruments
* The instruments and their options defining this earcon.
*
* @param {String} [options.id]
* The unique ID of the Earcon. Generated if not supplied.
*
* @param {number} [options.pan]
* Global panning of all instruments. Overrides all panning on
* individual instruments. Can be a number between -1 and 1.
*
* @param {number} [options.volume=1]
* Master volume for all instruments. Volume settings on individual
* instruments can still be used for relative volume between the
* instruments. This setting does not affect volumes set by functions
* in individual instruments. Can be a number between 0 and 1.
*
* @sample highcharts/sonification/earcon/
* Using earcons directly
*/
function Earcon(options) {
this.init(options || {});
}
Earcon.prototype.init = function (options) {
this.options = options;
if (!this.options.id) {
this.options.id = this.id = H.uniqueKey();
}
};

/**
* Play the earcon, optionally overriding init options.
*
* @param {Object} options
* Override existing options. Same as for Earcon.init.
*
* @sample highcharts/sonification/earcon/
* Using earcons directly
*/
Earcon.prototype.play = function (options) {
this.options = H.merge(this.options, options);

// Find master volume/pan settings
var masterVolume = H.pick(this.options.volume, 1),
masterPan = this.options.pan;

// Go through the instruments and play them
this.options.instruments.forEach(function (opts) {
var instrument = typeof opts.instrument === 'string' ?
H.sonification.instruments[opts.instrument] : opts.instrument,
playOpts = H.merge(opts.playOptions);
if (instrument && instrument.play) {
if (playOpts) {
// Handle master pan/volume
if (typeof opts.playOptions.volume !== 'function') {
playOpts.volume = H.pick(masterVolume, 1) *
H.pick(opts.playOptions.volume, 1);
}
playOpts.pan = H.pick(masterPan, playOpts.pan);

// Play the instrument. Use a copy so we can play multiple at
// the same time.
instrument.copy().play(playOpts);
}
} else {
H.error(30);
}
});
};

export default Earcon;
132 changes: 114 additions & 18 deletions js/modules/sonification/Instrument.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var stopOffset = 15, // Time it takes to fade out note
// Default options for Instrument constructor
var defaultOptions = {
type: 'oscillator',
playCallbackInterval: 20,
oscillator: {
waveformShape: 'sine'
}
Expand All @@ -36,6 +37,11 @@ var defaultOptions = {
* @param {String} [options.id]
* The unique ID of the instrument. Generated if not supplied.
*
* @param {number} [playCallbackInterval=20]
* 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.
*
* @param {Object} [options.oscillator]
* Options specific to oscillator instruments.
*
Expand All @@ -44,6 +50,8 @@ var defaultOptions = {
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
* @sample highcharts/sonification/instrument-advanced/
* Using callbacks for instrument parameters
*/
function Instrument(options) {
this.init(options);
Expand Down Expand Up @@ -73,6 +81,25 @@ Instrument.prototype.init = function (options) {
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.
*
* @param {Object} 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(H.merge(this.options, { id: null }, options));
};


Expand Down Expand Up @@ -127,34 +154,54 @@ Instrument.prototype.setPan = function (panValue) {


/**
* Set gain level.
* 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.
*/
Instrument.prototype.setGain = function (gainValue) {
if (this.gainNode) {
if (gainValue > 1.2) {
console.warn( // eslint-disable-line
'Highcharts sonification warning: ' +
'Volume of instrument set too high.'
);
gainValue = 1.2;
}
this.gainNode.gain.value = gainValue;
}
};


/**
* Clear existing play callback timers.
* @private
*/
Instrument.prototype.clearPlayCallbackTimers = function () {
this.playCallbackTimers.forEach(function (timer) {
clearInterval(timer);
});
this.playCallbackTimers = [];
};


/**
* Play oscillator instrument.
* @private
*
* @param {Object} options
* Play options, same as Instrument.play.
* @param {number} frequency
* The frequency to play.
*/
Instrument.prototype.oscillatorPlay = function (options) {
Instrument.prototype.oscillatorPlay = function (frequency) {
if (!this.oscillatorStarted) {
this.oscillator.start();
this.oscillatorStarted = true;
}

this.oscillator.frequency.linearRampToValueAtTime(
options.frequency, H.audioContext.currentTime + 0.002
frequency, H.audioContext.currentTime + 0.002
);
};

Expand All @@ -165,31 +212,76 @@ Instrument.prototype.oscillatorPlay = function (options) {
* @param {Object} options
* Options for the playback of the instrument.
*
* @param {Function} options.onEnd
* Callback function to be called when the play is completed.
*
* @param {number} options.frequency
* The frequency of the note to play.
* @param {number|Function} options.frequency
* 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.
*
* @param {number} options.duration
* The duration of the note in milliseconds.
*
* @param {number} [options.volume=1]
* The volume of the instrument.
* @param {Function} [options.onEnd]
* Callback function to be called when the play is completed.
*
* @param {number|Function} [options.volume=1]
* 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.
*
* @param {number} [options.pan=0]
* The panning of the instrument.
* @param {number|Function} [options.pan=0]
* 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.
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
* @sample highcharts/sonification/instrument-advanced/
* Using callbacks for instrument parameters
*/
Instrument.prototype.play = function (options) {
var instrument = this;
var instrument = this,
// Set a value, or if it is a function, set it continously as a timer
setOrStartTimer = function (value, setter) {
var target = options.duration,
currentDurationIx = 0,
callbackInterval = instrument.options.playCallbackInterval;
if (typeof value === 'function') {
instrument[setter](value(0)); // Init
var timer = setInterval(function () {
currentDurationIx++;
var curTime = currentDurationIx * callbackInterval / target;
if (curTime >= 1) {
instrument[setter](value(1));
clearInterval(timer);
} else {
instrument[setter](value(curTime));
}
}, callbackInterval);
instrument.playCallbackTimers.push(timer);
} else {
instrument[setter](value);
}
};

if (!instrument.id) {
// No audio support - do nothing
return;
}

// Clear any existing play timers
if (instrument.playCallbackTimers.length) {
instrument.clearPlayCallbackTimers();
}

// If a note is playing right now, clear the stop timeout, and call the
// callback.
if (instrument.stopTimeout) {
Expand Down Expand Up @@ -221,12 +313,12 @@ Instrument.prototype.play = function (options) {
instrument.stopCallback = options.onEnd;

// Set the volume and panning
instrument.setGain(H.pick(options.volume, 1));
instrument.setPan(H.pick(options.pan, 0));
setOrStartTimer(H.pick(options.volume, 1), 'setGain');
setOrStartTimer(H.pick(options.pan, 0), 'setPan');

// Play, depending on instrument type
if (instrument.options.type === 'oscillator') {
instrument.oscillatorPlay(options);
setOrStartTimer(options.frequency, 'oscillatorPlay');
}
};

Expand Down Expand Up @@ -272,6 +364,10 @@ Instrument.prototype.stop = function (immediately, onStopped) {
onStopped();
}
};
// Clear any existing play timers
if (instr.playCallbackTimers.length) {
instr.clearPlayCallbackTimers();
}
if (immediately) {
instr.setGain(0);
reset();
Expand Down
6 changes: 4 additions & 2 deletions js/modules/sonification/sonification.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import H from '../../parts/Globals.js';
import Instrument from 'Instrument.js';
import instruments from 'instrumentDefinitions.js';
import Earcon from 'Earcon.js';

// Expose on Highcharts object
// Expose on the Highcharts object
H.sonification = {
Instrument: Instrument,
instruments: instruments
instruments: instruments,
Earcon: Earcon
};

6 changes: 6 additions & 0 deletions samples/highcharts/sonification/earcon/demo.details
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
name: Highcharts Demo
authors:
- Øystein Moseng
requiresManualTesting: true
...
4 changes: 4 additions & 0 deletions samples/highcharts/sonification/earcon/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/sonification.js"></script>
<button id="playA">Play earcon A</button>
<button id="playB">Play earcon B</button>
Loading

0 comments on commit 6c72fd3

Please sign in to comment.