From fb355abb7d3399c5e28790c45475a060a76d101b Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 25 Jun 2018 10:46:54 -0400 Subject: [PATCH 1/3] 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. --- src/AudioEngine.js | 11 +++++++++++ src/SoundPlayer.js | 3 ++- src/effects/VolumeEffect.js | 6 +++--- test/SoundPlayer.js | 17 +++++++++-------- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/AudioEngine.js b/src/AudioEngine.js index 80122ab..45f91ec 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -113,6 +113,17 @@ class AudioEngine { 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. * @return {AudioNode} - audio node that is the input for this effect diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index 7cabf2d..cb411b0 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -245,7 +245,8 @@ class SoundPlayer extends EventEmitter { taken.finished().then(() => taken.dispose()); 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); } /** diff --git a/src/effects/VolumeEffect.js b/src/effects/VolumeEffect.js index 1b9942a..b7f9ab4 100644 --- a/src/effects/VolumeEffect.js +++ b/src/effects/VolumeEffect.js @@ -38,9 +38,9 @@ class VolumeEffect extends Effect { this.value = value; const {gain} = this.outputNode; - const {audioContext: {currentTime}, DECAY_TIME} = this.audioEngine; - gain.setValueAtTime(gain.value, currentTime); - gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_TIME); + const {audioContext: {currentTime}, DECAY_TIME, DECAY_SOON} = this.audioEngine; + gain.setValueAtTime(gain.value, currentTime + DECAY_SOON); + gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_SOON + DECAY_TIME); } /** diff --git a/test/SoundPlayer.js b/test/SoundPlayer.js index aa4588c..12c4a8b 100644 --- a/test/SoundPlayer.js +++ b/test/SoundPlayer.js @@ -20,10 +20,10 @@ tap.test('SoundPlayer', suite => { suite.beforeEach(() => { audioContext = new AudioContext(); audioEngine = new AudioEngine(audioContext); - // sound will be 0.1 seconds long - audioContext.DECODE_AUDIO_DATA_RESULT = audioContext.createBuffer(2, 4410, 44100); + // sound will be 0.2 seconds long + audioContext.DECODE_AUDIO_DATA_RESULT = audioContext.createBuffer(2, 8820, 44100); audioContext.DECODE_AUDIO_DATA_FAILED = false; - const data = new Uint8Array(44100); + const data = new Uint8Array(0); return audioEngine.decodeSoundPlayer({data}).then(result => { soundPlayer = result; }); @@ -88,13 +88,13 @@ tap.test('SoundPlayer', suite => { inputs: [outputNode.toJSON()] }], '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; 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.equal(outputNode.$state, 'PLAYING'); - audioContext.$processTo(audioEngine.DECAY_TIME); + audioContext.$processTo(audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.001); t.deepEqual(help.engineInputs, [{ name: 'GainNode', gain: { @@ -180,18 +180,19 @@ tap.test('SoundPlayer', suite => { t.equal(soundPlayer.outputNode.$state, 'PLAYING'); 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, '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(firstPlayNode.$state, 'FINISHED'); t.equal(help.engineInputs[0].gain.value, 0, 'faded old sound to 0'); 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 return Promise.resolve(); From ee73fd7a8aa061fef38fc4ac76fddb4469789c82 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 25 Jun 2018 14:01:15 -0400 Subject: [PATCH 2/3] rename DECAY_* constants to DECAY_WAIT and DECAY_DURATION --- src/AudioEngine.js | 4 ++-- src/SoundPlayer.js | 6 +++--- src/effects/Effect.js | 2 +- src/effects/PanEffect.js | 4 ++-- src/effects/VolumeEffect.js | 6 +++--- test/SoundPlayer.js | 18 +++++++++--------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/AudioEngine.js b/src/AudioEngine.js index 45f91ec..ddc7ef4 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -109,7 +109,7 @@ class AudioEngine { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime} * @const {number} */ - get DECAY_TIME () { + get DECAY_DURATION () { return 0.025; } @@ -120,7 +120,7 @@ class AudioEngine { * @see {@link https://bugzilla.mozilla.org/show_bug.cgi?id=1228207} * @const {number} */ - get DECAY_SOON () { + get DECAY_WAIT () { return 0.05; } diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index cb411b0..7a2f7d5 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -219,7 +219,7 @@ class SoundPlayer extends EventEmitter { this.isPlaying = true; - this.startingUntil = this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME; + this.startingUntil = this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_DURATION; this.emit('play'); } @@ -245,8 +245,8 @@ class SoundPlayer extends EventEmitter { taken.finished().then(() => taken.dispose()); taken.volumeEffect.set(0); - const {audioContext, DECAY_TIME, DECAY_SOON} = this.audioEngine; - taken.outputNode.stop(audioContext.currentTime + DECAY_SOON + DECAY_TIME); + const {audioContext, DECAY_DURATION, DECAY_WAIT} = this.audioEngine; + taken.outputNode.stop(audioContext.currentTime + DECAY_WAIT + DECAY_DURATION); } /** diff --git a/src/effects/Effect.js b/src/effects/Effect.js index 97ddee1..c8e91ad 100644 --- a/src/effects/Effect.js +++ b/src/effects/Effect.js @@ -144,7 +144,7 @@ class Effect { this.outputNode.disconnect(); } - if (this._isPatch || this._lastPatch + this.audioEngine.DECAY_TIME < this.audioEngine.currentTime) { + if (this._isPatch || this._lastPatch + this.audioEngine.DECAY_DURATION < this.audioEngine.currentTime) { this.outputNode.connect(target.getInputNode()); } diff --git a/src/effects/PanEffect.js b/src/effects/PanEffect.js index 3255ee4..2fa4360 100644 --- a/src/effects/PanEffect.js +++ b/src/effects/PanEffect.js @@ -65,12 +65,12 @@ class PanEffect extends Effect { this.leftGain.gain.setTargetAtTime( leftVal, this.audioEngine.audioContext.currentTime, - this.audioEngine.DECAY_TIME + this.audioEngine.DECAY_DURATION ); this.rightGain.gain.setTargetAtTime( rightVal, this.audioEngine.audioContext.currentTime, - this.audioEngine.DECAY_TIME + this.audioEngine.DECAY_DURATION ); } diff --git a/src/effects/VolumeEffect.js b/src/effects/VolumeEffect.js index b7f9ab4..bf38e5f 100644 --- a/src/effects/VolumeEffect.js +++ b/src/effects/VolumeEffect.js @@ -38,9 +38,9 @@ class VolumeEffect extends Effect { this.value = value; const {gain} = this.outputNode; - const {audioContext: {currentTime}, DECAY_TIME, DECAY_SOON} = this.audioEngine; - gain.setValueAtTime(gain.value, currentTime + DECAY_SOON); - gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_SOON + DECAY_TIME); + const {audioContext: {currentTime}, DECAY_DURATION, DECAY_WAIT} = this.audioEngine; + gain.setValueAtTime(gain.value, currentTime + DECAY_WAIT); + gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_WAIT + DECAY_DURATION); } /** diff --git a/test/SoundPlayer.js b/test/SoundPlayer.js index 12c4a8b..1f7b089 100644 --- a/test/SoundPlayer.js +++ b/test/SoundPlayer.js @@ -88,13 +88,13 @@ tap.test('SoundPlayer', suite => { inputs: [outputNode.toJSON()] }], 'output node connects to gain node to input node'); - audioContext.$processTo(audioEngine.DECAY_SOON + audioEngine.DECAY_TIME / 2); + audioContext.$processTo(audioEngine.DECAY_WAIT + audioEngine.DECAY_DURATION / 2); const engineInputs = help.engineInputs; 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.equal(outputNode.$state, 'PLAYING'); - audioContext.$processTo(audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.001); + audioContext.$processTo(audioEngine.DECAY_WAIT + audioEngine.DECAY_DURATION + 0.001); t.deepEqual(help.engineInputs, [{ name: 'GainNode', gain: { @@ -129,14 +129,14 @@ tap.test('SoundPlayer', suite => { t.deepEqual(log, ['finished first', 'finished second', 'finished third'], 'finished in order'); // fast forward to one ms before decay time - audioContext.$processTo(audioEngine.DECAY_TIME - 0.001); + audioContext.$processTo(audioEngine.DECAY_DURATION - 0.001); soundPlayer.play(); t.equal(originalNode, soundPlayer.outputNode, 'same output node'); - // now at DECAY_TIME, we should meet a new player as the old one is taken/stopped - audioContext.$processTo(audioEngine.DECAY_TIME); + // now at DECAY_DURATION, we should meet a new player as the old one is taken/stopped + audioContext.$processTo(audioEngine.DECAY_DURATION); t.equal(soundPlayer.isStarting, false, 'player.isStarting now false'); @@ -157,7 +157,7 @@ tap.test('SoundPlayer', suite => { const firstPlayNode = soundPlayer.outputNode; // go past debounce time and play again - audioContext.$processTo(audioEngine.DECAY_TIME); + audioContext.$processTo(audioEngine.DECAY_DURATION); return Promise.resolve() .then(() => { @@ -181,18 +181,18 @@ tap.test('SoundPlayer', suite => { t.equal(help.engineInputs[0].gain.value, 1, 'old sound connectect to gain node with volume 1'); const {currentTime} = audioContext; - audioContext.$processTo(currentTime + audioEngine.DECAY_SOON + 0.001); + audioContext.$processTo(currentTime + audioEngine.DECAY_WAIT + 0.001); t.notEqual(help.engineInputs[0].gain.value, 1, 'old sound connected to gain node which will fade'); - audioContext.$processTo(currentTime + audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.001); + audioContext.$processTo(currentTime + audioEngine.DECAY_WAIT + audioEngine.DECAY_DURATION + 0.001); t.equal(soundPlayer.outputNode.$state, 'PLAYING'); t.equal(firstPlayNode.$state, 'FINISHED'); t.equal(help.engineInputs[0].gain.value, 0, 'faded old sound to 0'); t.equal(log.length, 1); - audioContext.$processTo(currentTime + audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.3); + audioContext.$processTo(currentTime + audioEngine.DECAY_WAIT + audioEngine.DECAY_DURATION + 0.3); // wait for a micro-task loop to fire our previous events return Promise.resolve(); From e01a32a83d2de1e1d887295a028b2ec3030336d5 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 25 Jun 2018 14:27:21 -0400 Subject: [PATCH 3/3] use AudioEngine.currentTime and DECAY_WAIT in PanEffect, etc --- src/SoundPlayer.js | 9 +++++---- src/effects/PanEffect.js | 13 +++---------- src/effects/VolumeEffect.js | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index 7a2f7d5..6c3154c 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -46,7 +46,7 @@ class SoundPlayer extends EventEmitter { * @type {boolean} */ get isStarting () { - return this.isPlaying && this.startingUntil > this.audioEngine.audioContext.currentTime; + return this.isPlaying && this.startingUntil > this.audioEngine.currentTime; } /** @@ -219,7 +219,8 @@ class SoundPlayer extends EventEmitter { this.isPlaying = true; - this.startingUntil = this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_DURATION; + const {currentTime, DECAY_DURATION} = this.audioEngine; + this.startingUntil = currentTime + DECAY_DURATION; this.emit('play'); } @@ -245,8 +246,8 @@ class SoundPlayer extends EventEmitter { taken.finished().then(() => taken.dispose()); taken.volumeEffect.set(0); - const {audioContext, DECAY_DURATION, DECAY_WAIT} = this.audioEngine; - taken.outputNode.stop(audioContext.currentTime + DECAY_WAIT + DECAY_DURATION); + const {currentTime, DECAY_WAIT, DECAY_DURATION} = this.audioEngine; + taken.outputNode.stop(currentTime + DECAY_WAIT + DECAY_DURATION); } /** diff --git a/src/effects/PanEffect.js b/src/effects/PanEffect.js index 2fa4360..01239da 100644 --- a/src/effects/PanEffect.js +++ b/src/effects/PanEffect.js @@ -62,16 +62,9 @@ class PanEffect extends Effect { const leftVal = Math.cos(p * Math.PI / 2); const rightVal = Math.sin(p * Math.PI / 2); - this.leftGain.gain.setTargetAtTime( - leftVal, - this.audioEngine.audioContext.currentTime, - this.audioEngine.DECAY_DURATION - ); - this.rightGain.gain.setTargetAtTime( - rightVal, - this.audioEngine.audioContext.currentTime, - this.audioEngine.DECAY_DURATION - ); + const {currentTime, DECAY_WAIT, DECAY_DURATION} = this.audioEngine; + this.leftGain.gain.setTargetAtTime(leftVal, currentTime + DECAY_WAIT, DECAY_DURATION); + this.rightGain.gain.setTargetAtTime(rightVal, currentTime + DECAY_WAIT, DECAY_DURATION); } /** diff --git a/src/effects/VolumeEffect.js b/src/effects/VolumeEffect.js index bf38e5f..1d1506d 100644 --- a/src/effects/VolumeEffect.js +++ b/src/effects/VolumeEffect.js @@ -38,7 +38,7 @@ class VolumeEffect extends Effect { this.value = value; const {gain} = this.outputNode; - const {audioContext: {currentTime}, DECAY_DURATION, DECAY_WAIT} = this.audioEngine; + const {currentTime, DECAY_WAIT, DECAY_DURATION} = this.audioEngine; gain.setValueAtTime(gain.value, currentTime + DECAY_WAIT); gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_WAIT + DECAY_DURATION); }