wip: greenplayer tests

This commit is contained in:
Corey Frang 2018-06-19 13:37:00 -04:00 committed by Michael "Z" Goddard
parent 7dd6d557bb
commit 18cb9787ae
No known key found for this signature in database
GPG key ID: 762CD40DD5349872
8 changed files with 159 additions and 22 deletions

View file

@ -33,8 +33,9 @@
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"eslint": "^3.19.0", "eslint": "^3.19.0",
"eslint-config-scratch": "^3.1.0", "eslint-config-scratch": "^3.1.0",
"tap": "^12.0.1",
"web-audio-test-api": "^0.5.2",
"webpack": "^4.8.0", "webpack": "^4.8.0",
"webpack-cli": "^2.0.15", "webpack-cli": "^2.0.15"
"tap": "^12.0.1"
} }
} }

View file

@ -1,4 +1,4 @@
const StartAudioContext = require('startaudiocontext'); const StartAudioContext = require('./StartAudioContext');
const AudioContext = require('audio-context'); const AudioContext = require('audio-context');
const log = require('./log'); const log = require('./log');
@ -35,13 +35,13 @@ const decodeAudioData = function (audioContext, buffer) {
* sprites. * sprites.
*/ */
class AudioEngine { class AudioEngine {
constructor () { constructor (audioContext = new AudioContext()) {
/** /**
* AudioContext to play and manipulate sounds with a graph of source * AudioContext to play and manipulate sounds with a graph of source
* and effect nodes. * and effect nodes.
* @type {AudioContext} * @type {AudioContext}
*/ */
this.audioContext = new AudioContext(); this.audioContext = audioContext;
StartAudioContext(this.audioContext); StartAudioContext(this.audioContext);
/** /**
@ -65,6 +65,10 @@ class AudioEngine {
this.loudness = null; this.loudness = null;
} }
get currentTime () {
return this.audioContext.currentTime;
}
/** /**
* Names of the audio effects. * Names of the audio effects.
* @enum {string} * @enum {string}

View file

@ -33,6 +33,8 @@ class SoundPlayer extends EventEmitter {
this.isPlaying = false; this.isPlaying = false;
this.startingUntil = 0; this.startingUntil = 0;
this.playbackRate = 1; this.playbackRate = 1;
this.handleEvent = this.handleEvent.bind(this);
} }
/** /**
@ -68,7 +70,7 @@ class SoundPlayer extends EventEmitter {
*/ */
_createSource () { _createSource () {
if (this.outputNode !== null) { if (this.outputNode !== null) {
this.outputNode.removeEventListener(ON_ENDED, this); this.outputNode.removeEventListener(ON_ENDED, this.handleEvent);
this.outputNode.disconnect(); this.outputNode.disconnect();
} }
@ -76,7 +78,7 @@ class SoundPlayer extends EventEmitter {
this.outputNode.playbackRate.value = this.playbackRate; this.outputNode.playbackRate.value = this.playbackRate;
this.outputNode.buffer = this.buffer; this.outputNode.buffer = this.buffer;
this.outputNode.addEventListener(ON_ENDED, this); this.outputNode.addEventListener(ON_ENDED, this.handleEvent);
if (this.target !== null) { if (this.target !== null) {
this.connect(this.target); this.connect(this.target);
@ -149,7 +151,7 @@ class SoundPlayer extends EventEmitter {
*/ */
take () { take () {
if (this.outputNode) { if (this.outputNode) {
this.outputNode.removeEventListener(ON_ENDED, this); this.outputNode.removeEventListener(ON_ENDED, this.handleEvent);
} }
const taken = new SoundPlayer(this.audioEngine, this); const taken = new SoundPlayer(this.audioEngine, this);
@ -160,7 +162,7 @@ class SoundPlayer extends EventEmitter {
taken.initialize(); taken.initialize();
taken.outputNode.disconnect(); taken.outputNode.disconnect();
taken.outputNode = this.outputNode; taken.outputNode = this.outputNode;
taken.outputNode.addEventListener(ON_ENDED, taken); taken.outputNode.addEventListener(ON_ENDED, taken.handleEvent);
taken.volumeEffect.set(this.volumeEffect.value); taken.volumeEffect.set(this.volumeEffect.value);
if (this.target !== null) { if (this.target !== null) {
taken.connect(this.target); taken.connect(this.target);
@ -202,10 +204,10 @@ class SoundPlayer extends EventEmitter {
this.take().stop(); this.take().stop();
} }
if (!this.initialized) { if (this.initialized) {
this.initialize();
} else {
this._createSource(); this._createSource();
} else {
this.initialize();
} }
this.volumeEffect.set(this.volumeEffect.DEFAULT_VALUE); this.volumeEffect.set(this.volumeEffect.DEFAULT_VALUE);

10
src/StartAudioContext.js Normal file
View file

@ -0,0 +1,10 @@
// StartAudioContext assumes that we are in a window/document setting and messes with the unit
// tests, this is our own version just checking to see if we have a global document to listen
// to before we even try to "start" it. Our test api audio context is started by default.
const StartAudioContext = require('startaudiocontext');
module.exports = function (context) {
if (typeof document !== 'undefined') {
return StartAudioContext(context);
}
};

View file

@ -82,14 +82,17 @@ class Effect {
} }
// Store whether the graph should currently affected by this effect. // Store whether the graph should currently affected by this effect.
const _isPatch = this._isPatch; const wasPatch = this._isPatch;
if (wasPatch) {
this._lastPatch = this.audioEngine.currentTime;
}
// Call the internal implementation per this Effect. // Call the internal implementation per this Effect.
this._set(value); this._set(value);
// Connect or disconnect from the graph if this now applies or no longer // Connect or disconnect from the graph if this now applies or no longer
// applies an effect. // applies an effect.
if (this._isPatch !== _isPatch && this.target !== null) { if (this._isPatch !== wasPatch && this.target !== null) {
this.connect(this.target); this.connect(this.target);
} }
} }
@ -133,7 +136,7 @@ class Effect {
this.outputNode.disconnect(); this.outputNode.disconnect();
} }
if (this._isPatch) { if (this._isPatch || this._lastPatch + this.audioEngine.DECAY_TIME < this.audioEngine.currentTime) {
this.outputNode.connect(target.getInputNode()); this.outputNode.connect(target.getInputNode());
} }

View file

@ -32,13 +32,11 @@ class VolumeEffect extends Effect {
*/ */
_set (value) { _set (value) {
this.value = value; this.value = value;
// A gain of 1 is normal. Scale down scratch's volume value. Apply the
// change over a tiny period of time. const {gain} = this.outputNode;
this.outputNode.gain.setTargetAtTime( const {audioContext: {currentTime}, DECAY_TIME} = this.audioEngine;
value / 100, gain.setValueAtTime(gain.value, currentTime);
this.audioEngine.audioContext.currentTime, gain.linearRampToValueAtTime(value / 100, currentTime + DECAY_TIME);
this.audioEngine.DECAY_TIME
);
} }
/** /**

17
test/AudioEngine.js Normal file
View file

@ -0,0 +1,17 @@
const tap = require('tap');
const AudioEngine = require('../src/AudioEngine');
const {AudioContext} = require('web-audio-test-api');
tap.test('AudioEngine', t => {
const audioEngine = new AudioEngine(new AudioContext());
t.deepEqual(audioEngine.inputNode.toJSON(), {
gain: {
inputs: [],
value: 1
},
inputs: [],
name: 'GainNode'
}, 'JSON Representation of inputNode');
});

102
test/SoundPlayer.js Normal file
View file

@ -0,0 +1,102 @@
/* global Uint8Array */
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;
suite.beforeEach(async () => {
audioContext = new AudioContext();
audioEngine = new AudioEngine(audioContext);
audioEngine.DECODE_AUDIO_DATA_RESULT = audioContext.createBuffer(2, 1024, 44100);
const data = new Uint8Array(1024);
soundPlayer = await audioEngine.decodeSoundPlayer({data});
});
suite.afterEach(() => {
soundPlayer.dispose();
soundPlayer = null;
audioEngine = null;
audioContext.$reset();
audioContext = null;
});
suite.plan(3);
suite.test('play initializes and creates chain', t => {
t.plan(3);
t.equal(soundPlayer.initialized, false, 'not yet initialized');
soundPlayer.play();
t.equal(soundPlayer.initialized, true, 'now is initialized');
let buffer = audioEngine.DECODE_AUDIO_DATA_RESULT.toJSON();
t.deepEqual(soundPlayer.outputNode.toJSON(), {
buffer,
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(audioEngine.inputNode.toJSON().inputs, [
soundPlayer.outputNode.toJSON()
], 'output node connects to input node');
t.end();
});
suite.test('stop decay', t => {
t.plan(6);
soundPlayer.play();
soundPlayer.connect(audioEngine);
audioContext.$processTo(0);
soundPlayer.stop();
t.deepEqual(audioEngine.inputNode.toJSON().inputs, [{
name: 'GainNode',
gain: {
value: 1,
inputs: []
},
inputs: [soundPlayer.outputNode.toJSON()]
}], 'output node connects to gain node to input node');
audioContext.$processTo(audioEngine.DECAY_TIME / 2);
const engineInputs = audioEngine.inputNode.toJSON().inputs;
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(soundPlayer.outputNode.$state, 'PLAYING');
audioContext.$processTo(audioEngine.DECAY_TIME);
t.deepEqual(audioEngine.inputNode.toJSON().inputs, [{
name: 'GainNode',
gain: {
value: 0,
inputs: []
},
inputs: [soundPlayer.outputNode.toJSON()]
}], 'output node connects to gain node to input node decayed');
t.equal(soundPlayer.outputNode.$state, 'FINISHED');
t.end();
});
suite.end();
});