Skip to content

Commit

Permalink
Added Instrument class. Closes highcharts#9334.
Browse files Browse the repository at this point in the history
  • Loading branch information
oysteinmoseng committed Nov 23, 2018
1 parent 8d21ee0 commit 124c1b9
Show file tree
Hide file tree
Showing 18 changed files with 449 additions and 1,773 deletions.
6 changes: 6 additions & 0 deletions errors/29/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Browser does not support WebAudio

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.
7 changes: 6 additions & 1 deletion js/error-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ H.errorMessages = {
"title": "Fallback to export server disabled",
"text": "<h1>Fallback to export server disabled</h1><p>This happens when the offline export module encounters a chart that it can't</p><p>export successfully, and the fallback to the online export server is disabled. The offline exporting module will fail for certain browsers, and certain</p><p>features (e.g. <a href=\"https://api.highcharts.com/highcharts/exporting.allowHTML\">exporting.allowHTML</a> ), depending on the type of image exporting to. For a compatibility overview, see <a href=\"https://www.highcharts.com/docs/export-module/client-side-export\">Client Side Export</a>.</p><p>For very complex charts, it's possible that exporting fail in browsers that don't support Blob objects, due to data URL length limits. It's always recommended to define the <a href=\"https://api.highcharts.com/highcharts/exporting.error\">exporting.error</a> callback when disabling the fallback, so that details can be provided to the end-user if offline export isn't working for them.</p>"
},
"29": {
"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>"
},
"meta": {
"files": [
"errors/10/readme.md",
Expand All @@ -112,7 +116,8 @@ H.errorMessages = {
"errors/25/readme.md",
"errors/26/readme.md",
"errors/27/readme.md",
"errors/28/readme.md"
"errors/28/readme.md",
"errors/29/readme.md"
],
"version": "6.2.0"
}
Expand Down
2 changes: 1 addition & 1 deletion js/masters/modules/sonification.src.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@

'use strict';

import '../../modules/sonification.src.js';
import '../../modules/sonification/sonification.js';
10 changes: 0 additions & 10 deletions js/modules/sonification.src.js

This file was deleted.

286 changes: 286 additions & 0 deletions js/modules/sonification/Instrument.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
/**
* (c) 2009-2018 Øystein Moseng
*
* Instrument class for sonification module.
*
* License: www.highcharts.com/license
*/

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

var stopOffset = 15, // Time it takes to fade out note
stopImmediateThreshold = 30; // No fade out if duration is less

// Default options for Instrument constructor
var defaultOptions = {
type: 'oscillator',
oscillator: {
waveformShape: 'sine'
}
};


/**
* The Instrument class. Instrument objects represent an instrument capable of
* playing a certain pitch for a specified duration.
*
* @class Instrument
*
* @param {Object} options
* Options for the instrument instance.
*
* @param {String} [options.type='oscillator']
* The type of instrument. Currently only `oscillator` is supported.
*
* @param {String} [options.id]
* The unique ID of the instrument. Generated if not supplied.
*
* @param {Object} [options.oscillator]
* Options specific to oscillator instruments.
*
* @param {String} [options.oscillator.waveformShape='sine']
* The waveform shape to use for oscillator instruments.
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
*/
function Instrument(options) {
this.init(options);
}
Instrument.prototype.init = function (options) {
if (!this.initAudioContext()) {
H.error(29);
return;
}
this.options = H.merge(defaultOptions, options);
this.id = this.options.id = options && options.id || H.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 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;
if (Context) {
H.audioContext = H.audioContext || new Context();
return !!(
H.audioContext &&
H.audioContext.createOscillator &&
H.audioContext.createGain
);
}
return false;
};


/**
* Init an oscillator instrument.
* @private
*
* @param {Object} oscillatorOptions
* The oscillator options passed to Instrument.init.
*/
Instrument.prototype.initOscillator = function (options) {
var ctx = H.audioContext;
this.oscillator = ctx.createOscillator();
this.oscillator.type = options.waveformShape;
this.oscillator.frequency.value = 0; // Start frequency at 0
this.oscillator.connect(this.gainNode);
this.oscillatorStarted = false;
};


/**
* Set pan position.
* @private
*
* @param {number} panValue
* The pan position to set for the instrument.
*/
Instrument.prototype.setPan = function (panValue) {
if (this.panNode) {
this.panNode.pan.value = panValue;
}
};


/**
* Set gain level.
* @private
*
* @param {number} gainValue
* The gain level to set for the instrument.
*/
Instrument.prototype.setGain = function (gainValue) {
if (this.gainNode) {
this.gainNode.gain.value = gainValue;
}
};


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

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


/**
* Play the instrument according to 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} options.duration
* The duration of the note in milliseconds.
*
* @param {number} [options.volume=1]
* The volume of the instrument.
*
* @param {number} [options.pan=0]
* The panning of the instrument.
*
* @sample highcharts/sonification/instrument/
* Using Instruments directly
*/
Instrument.prototype.play = function (options) {
var instrument = this;
if (!instrument.id) {
// No audio support - do nothing
return;
}

// 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 instrument after the duration of the note
var immediate = options.duration < stopImmediateThreshold;
instrument.stopTimeout = setTimeout(function () {
delete instrument.stopTimeout;
instrument.stop(immediate, function () {
// After stop, call the stop callback for the play we finished
if (instrument.stopCallback) {
instrument.stopCallback();
}
});
}, immediate ? options.duration : options.duration - stopOffset);
instrument.stopCallback = options.onEnd;

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

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


/**
* Mute an instrument that is playing. If the instrument is not currently
* playing, this function does nothing.
*/
Instrument.prototype.mute = function () {
if (this.gainNode) {
this.gainNode.gain.setValueAtTime(
this.gainNode.gain.value, H.audioContext.currentTime
);
this.gainNode.gain.exponentialRampToValueAtTime(
0.0001, H.audioContext.currentTime + 0.008
);
}
};


/**
* Stop the instrument playing.
*
* @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.
*/
Instrument.prototype.stop = function (immediately, onStopped) {
var instr = this,
reset = function () {
// The oscillator may have stopped in the meantime here, so allow
// this function to fail if so.
try {
instr.oscillator.stop();
} catch (e) {}
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
if (onStopped) {
onStopped();
}
};
if (immediately) {
instr.setGain(0);
reset();
} else {
instr.mute();
// Stop the oscillator after the mute fade-out has finished
setTimeout(reset, 10);
}
};


export default Instrument;
20 changes: 20 additions & 0 deletions js/modules/sonification/instrumentDefinitions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* (c) 2009-2018 Øystein Moseng
*
* Instrument definitions for sonification module.
*
* License: www.highcharts.com/license
*/

'use strict';

import Instrument from 'Instrument.js';

var instruments = {
sine: new Instrument(),
square: new Instrument({ oscillator: { waveformShape: 'square' } }),
triangle: new Instrument({ oscillator: { waveformShape: 'triangle' } }),
sawtooth: new Instrument({ oscillator: { waveformShape: 'sawtooth' } })
};

export default instruments;
19 changes: 19 additions & 0 deletions js/modules/sonification/sonification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* (c) 2009-2018 Øystein Moseng
*
* Sonification module for Highcharts
*
* License: www.highcharts.com/license
*/

'use strict';
import H from '../../parts/Globals.js';
import Instrument from 'Instrument.js';
import instruments from 'instrumentDefinitions.js';

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

Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
authors:
- Øystein Moseng
requiresManualTesting: true
js_wrap: b
...
5 changes: 5 additions & 0 deletions samples/highcharts/sonification/instrument/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/sonification.js"></script>
<button id="play1">Play instrument A</button>
<button id="play2">Play instrument B</button>
<button id="play3">Play instrument C</button>
Loading

0 comments on commit 124c1b9

Please sign in to comment.