mirror of
https://github.com/scratchfoundation/scratch-audio.git
synced 2025-01-03 11:35:49 -05:00
Remove dependency on Tone.js
This commit is contained in:
parent
865d3cde88
commit
03034dd2f7
7 changed files with 75 additions and 60 deletions
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
53
src/index.js
53
src/index.js
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue