Remove dependency on Tone.js

This commit is contained in:
Eric Rosenbaum 2017-06-20 16:50:02 -04:00
parent 865d3cde88
commit 03034dd2f7
7 changed files with 75 additions and 60 deletions

View file

@ -30,7 +30,6 @@
"json": "^9.0.6", "json": "^9.0.6",
"minilog": "^3.0.1", "minilog": "^3.0.1",
"soundfont-player": "0.10.5", "soundfont-player": "0.10.5",
"tone": "0.9.0",
"travis-after-all": "^1.4.4", "travis-after-all": "^1.4.4",
"webpack": "2.4.0" "webpack": "2.4.0"
} }

View file

@ -1,5 +1,4 @@
const ArrayBufferStream = require('./ArrayBufferStream'); const ArrayBufferStream = require('./ArrayBufferStream');
const Tone = require('tone');
const log = require('./log'); const log = require('./log');
/** /**
@ -10,6 +9,9 @@ const log = require('./log');
* https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as * https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as
*/ */
class ADPCMSoundDecoder { class ADPCMSoundDecoder {
constructor (context) {
this.context = context;
}
/** /**
* Data used by the decompression algorithm * Data used by the decompression algorithm
* @type {Array} * @type {Array}
@ -40,7 +42,7 @@ class ADPCMSoundDecoder {
* Decode an ADPCM sound stored in an ArrayBuffer and return a promise * Decode an ADPCM sound stored in an ArrayBuffer and return a promise
* with the decoded audio buffer. * with the decoded audio buffer.
* @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio * @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio
* @return {Tone.Buffer} the decoded audio buffer * @return {AudioBuffer} the decoded audio buffer
*/ */
decode (audioData) { decode (audioData) {
@ -77,8 +79,7 @@ class ADPCMSoundDecoder {
const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize);
// @todo this line is the only place Tone is used here, should be possible to remove const buffer = this.context.createBuffer(1, samples.length, this.samplesPerSecond);
const buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond);
// @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply? // @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply?
for (let i = 0; i < samples.length; i++) { for (let i = 0; i < samples.length; i++) {

View file

@ -1,14 +1,13 @@
const SoundPlayer = require('./SoundPlayer'); const SoundPlayer = require('./SoundPlayer');
const Tone = require('tone');
class DrumPlayer { class DrumPlayer {
/** /**
* A prototype for the drum sound functionality that can load drum sounds, play, and stop them. * A prototype for the drum sound functionality that can load drum sounds, play, and stop them.
* @param {Tone.Gain} outputNode - a webAudio node that the drum sounds will send their output to * @param {AudioContext} context - a webAudio context
* @constructor * @constructor
*/ */
constructor (outputNode) { constructor (context) {
this.outputNode = outputNode; this.context = context;
const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/';
const fileNames = [ const fileNames = [
@ -35,9 +34,21 @@ class DrumPlayer {
this.drumSounds = []; this.drumSounds = [];
for (let i = 0; i < fileNames.length; i++) { for (let i = 0; i < fileNames.length; i++) {
const url = `${baseUrl + fileNames[i]}_22k.wav`; this.drumSounds[i] = new SoundPlayer(this.context);
this.drumSounds[i] = new SoundPlayer(this.outputNode);
this.drumSounds[i].setBuffer(new Tone.Buffer(url)); // download and decode the drum sounds
// @todo: use scratch-storage to manage these sound files
const url = baseUrl + fileNames[i] + '_22k.wav';
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = () => {
const audioData = request.response;
this.context.decodeAudioData(audioData).then(buffer => {
this.drumSounds[i].setBuffer(buffer);
});
};
request.send();
} }
} }
@ -46,10 +57,10 @@ class DrumPlayer {
* The parameter for output node allows sprites or clones to send the drum sound * The parameter for output node allows sprites or clones to send the drum sound
* to their individual audio effect chains. * to their individual audio effect chains.
* @param {number} drum - the drum number to play (0-indexed) * @param {number} drum - the drum number to play (0-indexed)
* @param {Tone.Gain} outputNode - a node to send the output to * @param {AudioNode} outputNode - a node to send the output to
*/ */
play (drum, outputNode) { play (drum, outputNode) {
this.drumSounds[drum].outputNode = outputNode; this.drumSounds[drum].connect(outputNode);
this.drumSounds[drum].start(); this.drumSounds[drum].start();
} }

View file

@ -1,4 +1,3 @@
const Tone = require('tone');
const Soundfont = require('soundfont-player'); const Soundfont = require('soundfont-player');
class InstrumentPlayer { class InstrumentPlayer {
@ -10,11 +9,12 @@ class InstrumentPlayer {
* play note or set instrument block runs, causing a delay of a few seconds. * play note or set instrument block runs, causing a delay of a few seconds.
* Using this library we don't have a way to set the volume, sustain the note beyond the sample * Using this library we don't have a way to set the volume, sustain the note beyond the sample
* duration, or run it through the sprite-specific audio effects. * duration, or run it through the sprite-specific audio effects.
* @param {Tone.Gain} outputNode - a webAudio node that the instrument will send its output to * @param {AudioNode} outputNode - a webAudio node that the instrument will send its output to
* @constructor * @constructor
*/ */
constructor (outputNode) { constructor (context) {
this.outputNode = outputNode; this.context = context;
this.outputNode = null;
// Instrument names used by Musyng Kite soundfont, in order to // Instrument names used by Musyng Kite soundfont, in order to
// match scratch instruments // match scratch instruments
@ -42,7 +42,7 @@ class InstrumentPlayer {
this.loadInstrument(instrumentNum) this.loadInstrument(instrumentNum)
.then(() => { .then(() => {
this.instruments[instrumentNum].play( this.instruments[instrumentNum].play(
note, Tone.context.currentTime, { note, this.context.currentTime, {
duration: sec, duration: sec,
gain: gain gain: gain
} }
@ -59,7 +59,7 @@ class InstrumentPlayer {
if (this.instruments[instrumentNum]) { if (this.instruments[instrumentNum]) {
return Promise.resolve(); return Promise.resolve();
} }
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) return Soundfont.instrument(this.context, this.instrumentNames[instrumentNum])
.then(inst => { .then(inst => {
inst.connect(this.outputNode); inst.connect(this.outputNode);
this.instruments[instrumentNum] = inst; this.instruments[instrumentNum] = inst;

View file

@ -1,13 +1,13 @@
const Tone = require('tone');
const log = require('./log'); const log = require('./log');
/** /**
* A SoundPlayer stores an audio buffer, and plays it * A SoundPlayer stores an audio buffer, and plays it
*/ */
class SoundPlayer { class SoundPlayer {
constructor () { constructor (context) {
this.context = context;
this.outputNode = null; this.outputNode = null;
this.buffer = new Tone.Buffer(); this.buffer = null;
this.bufferSource = null; this.bufferSource = null;
this.playbackRate = 1; this.playbackRate = 1;
this.isPlaying = false; this.isPlaying = false;
@ -15,7 +15,7 @@ class SoundPlayer {
/** /**
* Connect the SoundPlayer to an output node * Connect the SoundPlayer to an output node
* @param {Tone.Gain} node - an output node to connect to * @param {GainNode} node - an output node to connect to
*/ */
connect (node) { connect (node) {
this.outputNode = node; this.outputNode = node;
@ -23,7 +23,7 @@ class SoundPlayer {
/** /**
* Set an audio buffer * Set an audio buffer
* @param {Tone.Buffer} buffer Buffer to set * @param {AudioBuffer} buffer - Buffer to set
*/ */
setBuffer (buffer) { setBuffer (buffer) {
this.buffer = buffer; this.buffer = buffer;
@ -55,13 +55,13 @@ class SoundPlayer {
* The web audio framework requires a new audio buffer source node for each playback * The web audio framework requires a new audio buffer source node for each playback
*/ */
start () { start () {
if (!this.buffer || !this.buffer.loaded) { if (!this.buffer) {
log.warn('tried to play a sound that was not loaded yet'); log.warn('tried to play a sound that was not loaded yet');
return; return;
} }
this.bufferSource = Tone.context.createBufferSource(); this.bufferSource = this.context.createBufferSource();
this.bufferSource.buffer = this.buffer.get(); this.bufferSource.buffer = this.buffer;
this.bufferSource.playbackRate.value = this.playbackRate; this.bufferSource.playbackRate.value = this.playbackRate;
this.bufferSource.connect(this.outputNode); this.bufferSource.connect(this.outputNode);
this.bufferSource.start(); this.bufferSource.start();

View file

@ -1,17 +1,14 @@
const Tone = require('tone');
/** /**
* A pan effect, which moves the sound to the left or right between the speakers * A pan effect, which moves the sound to the left or right between the speakers
* Effect value of -100 puts the audio entirely on the left channel, * Effect value of -100 puts the audio entirely on the left channel,
* 0 centers it, 100 puts it on the right. * 0 centers it, 100 puts it on the right.
* Clamped -100 to 100 * Clamped -100 to 100
*/ */
class PanEffect extends Tone.Effect { class PanEffect {
constructor () { constructor (context) {
super(); this.context = context;
this.panner = this.context.createStereoPanner();
this.value = 0; this.value = 0;
this.panner = new Tone.Panner();
this.effectSend.chain(this.panner, this.effectReturn);
} }
/** /**
@ -23,6 +20,10 @@ class PanEffect extends Tone.Effect {
this.panner.pan.value = this.value / 100; this.panner.pan.value = this.value / 100;
} }
connect (node) {
this.panner.connect(node);
}
/** /**
* Change the effect value * Change the effect value
* @param {number} val - the value to change the effect by * @param {number} val - the value to change the effect by

View file

@ -1,5 +1,4 @@
const log = require('./log'); const log = require('./log');
const Tone = require('tone');
const PitchEffect = require('./effects/PitchEffect'); const PitchEffect = require('./effects/PitchEffect');
const PanEffect = require('./effects/PanEffect'); const PanEffect = require('./effects/PanEffect');
@ -25,15 +24,15 @@ class AudioPlayer {
constructor (audioEngine) { constructor (audioEngine) {
this.audioEngine = audioEngine; this.audioEngine = audioEngine;
// effects setup // Create the audio effects
this.pitchEffect = new PitchEffect(); this.pitchEffect = new PitchEffect();
this.panEffect = new PanEffect(); this.panEffect = new PanEffect(this.audioEngine.context);
// the effects are chained to an effects node for this player, then to the main audio engine // Chain the audio effects together
// audio is sent from each soundplayer, through the effects in order, then to the global effects // effectsNode -> panEffect -> audioEngine.input -> destination (speakers)
// note that the pitch effect works differently - it sets the playback rate for each soundplayer this.effectsNode = this.audioEngine.context.createGain();
this.effectsNode = new Tone.Gain(); this.effectsNode.connect(this.panEffect.panner);
this.effectsNode.chain(this.panEffect, this.audioEngine.input); this.panEffect.connect(this.audioEngine.input);
// reset effects to their default parameters // reset effects to their default parameters
this.clearEffects(); this.clearEffects();
@ -59,7 +58,7 @@ class AudioPlayer {
} }
// create a new soundplayer to play the sound // create a new soundplayer to play the sound
const player = new SoundPlayer(); const player = new SoundPlayer(this.audioEngine.context);
player.setBuffer(this.audioEngine.audioBuffers[md5]); player.setBuffer(this.audioEngine.audioBuffers[md5]);
player.connect(this.effectsNode); player.connect(this.effectsNode);
this.pitchEffect.updatePlayer(player); this.pitchEffect.updatePlayer(player);
@ -150,18 +149,22 @@ class AudioPlayer {
*/ */
class AudioEngine { class AudioEngine {
constructor () { constructor () {
this.input = new Tone.Gain(); var AudioContext = window.AudioContext || window.webkitAudioContext;
this.input.connect(Tone.Master); this.context = new AudioContext();
this.input = this.context.createGain();
this.input.connect(this.context.destination);
// global tempo in bpm (beats per minute) // global tempo in bpm (beats per minute)
this.currentTempo = 60; this.currentTempo = 60;
// instrument player for play note blocks // instrument player for play note blocks
this.instrumentPlayer = new InstrumentPlayer(this.input); this.instrumentPlayer = new InstrumentPlayer(this.context);
this.instrumentPlayer.outputNode = this.input;
this.numInstruments = this.instrumentPlayer.instrumentNames.length; this.numInstruments = this.instrumentPlayer.instrumentNames.length;
// drum player for play drum blocks // drum player for play drum blocks
this.drumPlayer = new DrumPlayer(this.input); this.drumPlayer = new DrumPlayer(this.context);
this.numDrums = this.drumPlayer.drumSounds.length; this.numDrums = this.drumPlayer.drumSounds.length;
// a map of md5s to audio buffers, holding sounds for all sprites // a map of md5s to audio buffers, holding sounds for all sprites
@ -201,10 +204,10 @@ class AudioEngine {
switch (sound.format) { switch (sound.format) {
case '': case '':
loaderPromise = Tone.context.decodeAudioData(bufferCopy); loaderPromise = this.context.decodeAudioData(bufferCopy);
break; break;
case 'adpcm': case 'adpcm':
loaderPromise = (new ADPCMSoundDecoder()).decode(bufferCopy); loaderPromise = (new ADPCMSoundDecoder(this.context)).decode(bufferCopy);
break; break;
default: default:
return log.warn('unknown sound format', sound.format); return log.warn('unknown sound format', sound.format);
@ -213,7 +216,7 @@ class AudioEngine {
const storedContext = this; const storedContext = this;
return loaderPromise.then( return loaderPromise.then(
decodedAudio => { decodedAudio => {
storedContext.audioBuffers[sound.md5] = new Tone.Buffer(decodedAudio); storedContext.audioBuffers[sound.md5] = decodedAudio;
}, },
error => { error => {
log.warn('audio data could not be decoded', error); log.warn('audio data could not be decoded', error);
@ -289,15 +292,15 @@ class AudioEngine {
* @return {number} loudness scaled 0 to 100 * @return {number} loudness scaled 0 to 100
*/ */
getLoudness () { getLoudness () {
if (!this.mic) { // if (!this.mic) {
this.mic = new Tone.UserMedia(); // this.mic = new Tone.UserMedia();
this.micMeter = new Tone.Meter('level', 0.5); // this.micMeter = new Tone.Meter('level', 0.5);
this.mic.open(); // this.mic.open();
this.mic.connect(this.micMeter); // this.mic.connect(this.micMeter);
} // }
if (this.mic && this.mic.state === 'started') { // if (this.mic && this.mic.state === 'started') {
return this.micMeter.value * 100; // return this.micMeter.value * 100;
} // }
return -1; return -1;
} }