scratch-audio/test/SoundPlayer.js
Michael "Z" Goddard fb355abb7d
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.
2018-06-25 13:15:19 -04:00

211 lines
7.1 KiB
JavaScript

/* global Uint8Array Promise */
const tap = require('tap');
const {AudioContext} = require('web-audio-test-api');
const AudioEngine = require('../src/AudioEngine');
tap.test('SoundPlayer', suite => {
let audioContext;
let audioEngine;
let soundPlayer;
const help = {
get engineInputs () {
return audioEngine.inputNode.toJSON().inputs;
}
};
suite.beforeEach(() => {
audioContext = new AudioContext();
audioEngine = new AudioEngine(audioContext);
// 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(0);
return audioEngine.decodeSoundPlayer({data}).then(result => {
soundPlayer = result;
});
});
suite.afterEach(() => {
soundPlayer.dispose();
soundPlayer = null;
audioEngine = null;
audioContext.$reset();
audioContext = null;
});
suite.plan(5);
suite.test('play initializes and creates source node', t => {
t.plan(3);
t.equal(soundPlayer.initialized, false, 'not yet initialized');
soundPlayer.play();
t.equal(soundPlayer.initialized, true, 'now is initialized');
t.deepEqual(soundPlayer.outputNode.toJSON(), {
buffer: audioContext.DECODE_AUDIO_DATA_RESULT.toJSON(),
inputs: [],
loop: false,
loopEnd: 0,
loopStart: 0,
name: 'AudioBufferSourceNode',
playbackRate: {
inputs: [],
value: 1
}
});
t.end();
});
suite.test('connect', t => {
t.plan(1);
soundPlayer.play();
soundPlayer.connect(audioEngine);
t.deepEqual(help.engineInputs, [
soundPlayer.outputNode.toJSON()
], 'output node connects to input node');
t.end();
});
suite.test('stop decay', t => {
t.plan(7);
soundPlayer.play();
soundPlayer.connect(audioEngine);
const outputNode = soundPlayer.outputNode;
audioContext.$processTo(0);
soundPlayer.stop();
t.equal(soundPlayer.outputNode, null, 'nullify outputNode immediately (taken sound is stopping)');
t.deepEqual(help.engineInputs, [{
name: 'GainNode',
gain: {
value: 1,
inputs: []
},
inputs: [outputNode.toJSON()]
}], 'output node connects to gain node to input node');
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_SOON + audioEngine.DECAY_TIME + 0.001);
t.deepEqual(help.engineInputs, [{
name: 'GainNode',
gain: {
value: 0,
inputs: []
},
inputs: [outputNode.toJSON()]
}], 'output node connects to gain node to input node decayed');
t.equal(outputNode.$state, 'FINISHED');
t.end();
});
suite.test('play while playing debounces', t => {
t.plan(7);
const log = [];
soundPlayer.connect(audioEngine);
soundPlayer.play();
t.equal(soundPlayer.isStarting, true, 'player.isStarting');
const originalNode = soundPlayer.outputNode;
// the second play should still "finish" this play
soundPlayer.finished().then(() => log.push('finished first'));
soundPlayer.play();
soundPlayer.finished().then(() => log.push('finished second'));
soundPlayer.play();
soundPlayer.finished().then(() => log.push('finished third'));
soundPlayer.play();
t.equal(originalNode, soundPlayer.outputNode, 'same output node');
t.equal(soundPlayer.outputNode.$state, 'PLAYING');
return Promise.resolve().then(() => {
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);
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);
t.equal(soundPlayer.isStarting, false, 'player.isStarting now false');
soundPlayer.play();
t.notEqual(originalNode, soundPlayer.outputNode, 'New output node');
t.end();
});
});
suite.test('play while playing', t => {
t.plan(15);
const log = [];
soundPlayer.play();
soundPlayer.finished().then(() => log.push('play 1 finished'));
soundPlayer.connect(audioEngine);
const firstPlayNode = soundPlayer.outputNode;
// go past debounce time and play again
audioContext.$processTo(audioEngine.DECAY_TIME);
return Promise.resolve()
.then(() => {
t.equal(soundPlayer.outputNode.$state, 'PLAYING');
soundPlayer.play();
soundPlayer.finished().then(() => log.push('play 2 finished'));
// wait for a micro-task loop to fire our previous events
return Promise.resolve();
})
.then(() => {
t.equal(log[0], 'play 1 finished');
t.notEqual(soundPlayer.outputNode, firstPlayNode, 'created new player node');
t.equal(help.engineInputs.length, 2, 'there should be 2 players connected');
t.equal(firstPlayNode.$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');
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(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(currentTime + audioEngine.DECAY_SOON + audioEngine.DECAY_TIME + 0.3);
// wait for a micro-task loop to fire our previous events
return Promise.resolve();
})
.then(() => {
t.equal(log[1], 'play 2 finished');
t.equal(help.engineInputs.length, 1, 'old sound disconneted itself after done');
t.equal(log.length, 2);
t.end();
});
});
suite.end();
});