mirror of
https://github.com/scratchfoundation/scratch-audio.git
synced 2024-12-22 05:53:43 -05:00
wip: greenplayer tests
This commit is contained in:
parent
7dd6d557bb
commit
18cb9787ae
8 changed files with 159 additions and 22 deletions
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
10
src/StartAudioContext.js
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
17
test/AudioEngine.js
Normal 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
102
test/SoundPlayer.js
Normal 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();
|
||||||
|
});
|
Loading…
Reference in a new issue