schedule stop DECAY in the future for firefox

Firefox at this time cannot smoothly schedule audio parameter changes
to happen immediately. Immediately scheduled changes clip when firefox
tries to catch up. Smoothly fading out a sound immediately instead of
clipping the end of the sound, in firefox at this time clips the sound
between where the fade out starts and where firefox catches up and
finishes the scheduled fade.
This commit is contained in:
Michael "Z" Goddard 2018-06-25 10:46:54 -04:00
parent a79684a720
commit fb355abb7d
No known key found for this signature in database
GPG key ID: 762CD40DD5349872
4 changed files with 25 additions and 12 deletions

View file

@ -113,6 +113,17 @@ class AudioEngine {
return 0.025; return 0.025;
} }
/**
* Some environments cannot smoothly change parameters immediately, provide
* a small delay before decaying.
*
* @see {@link https://bugzilla.mozilla.org/show_bug.cgi?id=1228207}
* @const {number}
*/
get DECAY_SOON () {
return 0.05;
}
/** /**
* Get the input node. * Get the input node.
* @return {AudioNode} - audio node that is the input for this effect * @return {AudioNode} - audio node that is the input for this effect

View file

@ -245,7 +245,8 @@ class SoundPlayer extends EventEmitter {
taken.finished().then(() => taken.dispose()); taken.finished().then(() => taken.dispose());
taken.volumeEffect.set(0); taken.volumeEffect.set(0);
taken.outputNode.stop(this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME); const {audioContext, DECAY_TIME, DECAY_SOON} = this.audioEngine;
taken.outputNode.stop(audioContext.currentTime + DECAY_SOON + DECAY_TIME);
} }
/** /**

View file

@ -38,9 +38,9 @@ class VolumeEffect extends Effect {
this.value = value; this.value = value;
const {gain} = this.outputNode; const {gain} = this.outputNode;
const {audioContext: {currentTime}, DECAY_TIME} = this.audioEngine; const {audioContext: {currentTime}, DECAY_TIME, DECAY_SOON} = this.audioEngine;
gain.setValueAtTime(gain.value, currentTime); gain.setValueAtTime(gain.value, currentTime + DECAY_SOON);
gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_TIME); gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_SOON + DECAY_TIME);
} }
/** /**

View file

@ -20,10 +20,10 @@ tap.test('SoundPlayer', suite => {
suite.beforeEach(() => { suite.beforeEach(() => {
audioContext = new AudioContext(); audioContext = new AudioContext();
audioEngine = new AudioEngine(audioContext); audioEngine = new AudioEngine(audioContext);
// sound will be 0.1 seconds long // sound will be 0.2 seconds long
audioContext.DECODE_AUDIO_DATA_RESULT = audioContext.createBuffer(2, 4410, 44100); audioContext.DECODE_AUDIO_DATA_RESULT = audioContext.createBuffer(2, 8820, 44100);
audioContext.DECODE_AUDIO_DATA_FAILED = false; audioContext.DECODE_AUDIO_DATA_FAILED = false;
const data = new Uint8Array(44100); const data = new Uint8Array(0);
return audioEngine.decodeSoundPlayer({data}).then(result => { return audioEngine.decodeSoundPlayer({data}).then(result => {
soundPlayer = result; soundPlayer = result;
}); });
@ -88,13 +88,13 @@ tap.test('SoundPlayer', suite => {
inputs: [outputNode.toJSON()] inputs: [outputNode.toJSON()]
}], 'output node connects to gain node to input node'); }], 'output node connects to gain node to input node');
audioContext.$processTo(audioEngine.DECAY_TIME / 2); audioContext.$processTo(audioEngine.DECAY_SOON + audioEngine.DECAY_TIME / 2);
const engineInputs = help.engineInputs; const engineInputs = help.engineInputs;
t.notEqual(engineInputs[0].gain.value, 1, 'gain value should not be 1'); t.notEqual(engineInputs[0].gain.value, 1, 'gain value should not be 1');
t.notEqual(engineInputs[0].gain.value, 0, 'gain value should not be 0'); t.notEqual(engineInputs[0].gain.value, 0, 'gain value should not be 0');
t.equal(outputNode.$state, 'PLAYING'); t.equal(outputNode.$state, 'PLAYING');
audioContext.$processTo(audioEngine.DECAY_TIME); audioContext.$processTo(audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.001);
t.deepEqual(help.engineInputs, [{ t.deepEqual(help.engineInputs, [{
name: 'GainNode', name: 'GainNode',
gain: { gain: {
@ -180,18 +180,19 @@ tap.test('SoundPlayer', suite => {
t.equal(soundPlayer.outputNode.$state, 'PLAYING'); t.equal(soundPlayer.outputNode.$state, 'PLAYING');
t.equal(help.engineInputs[0].gain.value, 1, 'old sound connectect to gain node with volume 1'); t.equal(help.engineInputs[0].gain.value, 1, 'old sound connectect to gain node with volume 1');
audioContext.$processTo(audioContext.currentTime + 0.001); const {currentTime} = audioContext;
audioContext.$processTo(currentTime + audioEngine.DECAY_SOON + 0.001);
t.notEqual(help.engineInputs[0].gain.value, 1, t.notEqual(help.engineInputs[0].gain.value, 1,
'old sound connected to gain node which will fade'); 'old sound connected to gain node which will fade');
audioContext.$processTo(audioContext.currentTime + audioEngine.DECAY_TIME + 0.001); audioContext.$processTo(currentTime + audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.001);
t.equal(soundPlayer.outputNode.$state, 'PLAYING'); t.equal(soundPlayer.outputNode.$state, 'PLAYING');
t.equal(firstPlayNode.$state, 'FINISHED'); t.equal(firstPlayNode.$state, 'FINISHED');
t.equal(help.engineInputs[0].gain.value, 0, 'faded old sound to 0'); t.equal(help.engineInputs[0].gain.value, 0, 'faded old sound to 0');
t.equal(log.length, 1); t.equal(log.length, 1);
audioContext.$processTo(audioContext.currentTime + 0.2); audioContext.$processTo(currentTime + audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.3);
// wait for a micro-task loop to fire our previous events // wait for a micro-task loop to fire our previous events
return Promise.resolve(); return Promise.resolve();