scratch-audio
Advanced tools
Comparing version
{ | ||
"name": "scratch-audio", | ||
"version": "0.1.0-prerelease.1485809602", | ||
"version": "0.1.0-prerelease.1486075498", | ||
"description": "audio engine for scratch 3.0", | ||
@@ -5,0 +5,0 @@ "main": "dist.js", |
@@ -1,10 +0,1 @@ | ||
/* | ||
ADPCMSoundLoader loads wav files that have been compressed with the ADPCM format | ||
based on code from Scratch-Flash: | ||
https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as | ||
*/ | ||
var ArrayBufferStream = require('./ArrayBufferStream'); | ||
@@ -14,5 +5,19 @@ var Tone = require('tone'); | ||
/** | ||
* Load wav audio files that have been compressed with the ADPCM format. | ||
* This is necessary because, while web browsers have native decoders for many audio | ||
* formats, ADPCM is a non-standard format used by Scratch since its early days. | ||
* This decoder is based on code from Scratch-Flash: | ||
* https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as | ||
* @constructor | ||
*/ | ||
function ADPCMSoundLoader () { | ||
} | ||
/** | ||
* Load an ADPCM sound file from a URL, decode it, and return a promise | ||
* with the audio buffer. | ||
* @param {string} url - a url pointing to the ADPCM wav file | ||
* @return {Tone.Buffer} | ||
*/ | ||
ADPCMSoundLoader.prototype.load = function (url) { | ||
@@ -60,3 +65,3 @@ | ||
// this line is the only place Tone is used here, should be possible to remove | ||
// todo: this line is the only place Tone is used here, should be possible to remove | ||
var buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond); | ||
@@ -78,3 +83,6 @@ | ||
/** | ||
* Data used by the decompression algorithm | ||
* @type {Array} | ||
*/ | ||
ADPCMSoundLoader.prototype.stepTable = [ | ||
@@ -88,2 +96,6 @@ 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, | ||
/** | ||
* Data used by the decompression algorithm | ||
* @type {Array} | ||
*/ | ||
ADPCMSoundLoader.prototype.indexTable = [ | ||
@@ -93,2 +105,8 @@ -1, -1, -1, -1, 2, 4, 6, 8, | ||
/** | ||
* Extract a chunk of audio data from the stream, consisting of a set of audio data bytes | ||
* @param {string} chunkType - the type of chunk to extract. 'data' or 'fmt' (format) | ||
* @param {ArrayBufferStream} stream - an stream containing the audio data | ||
* @return {ArrayBufferStream} a stream containing the desired chunk | ||
*/ | ||
ADPCMSoundLoader.prototype.extractChunk = function (chunkType, stream) { | ||
@@ -108,5 +126,10 @@ stream.position = 12; | ||
/** | ||
* Decompress sample data using the IMA ADPCM algorithm. | ||
* Note: Handles only one channel, 4-bits per sample. | ||
* @param {ArrayBufferStream} compressedData - a stream of compressed audio samples | ||
* @param {number} blockSize - the number of bytes in the stream | ||
* @return {Int16Array} the uncompressed audio samples | ||
*/ | ||
ADPCMSoundLoader.prototype.imaDecompress = function (compressedData, blockSize) { | ||
// Decompress sample data using the IMA ADPCM algorithm. | ||
// Note: Handles only one channel, 4-bits/sample. | ||
var sample, step, code, delta; | ||
@@ -113,0 +136,0 @@ var index = 0; |
@@ -1,9 +0,11 @@ | ||
/* | ||
ArrayBufferStream wraps the built-in javascript ArrayBuffer, adding the ability to access | ||
data in it like a stream. You can request to read a value from the front of the array, | ||
such as an 8 bit unsigned int, a 16 bit int, etc, and it will keep track of the position | ||
within the byte array, so that successive reads are consecutive. | ||
*/ | ||
/** | ||
* ArrayBufferStream wraps the built-in javascript ArrayBuffer, adding the ability to access | ||
* data in it like a stream, tracking its position. | ||
* You can request to read a value from the front of the array, and it will keep track of the position | ||
* within the byte array, so that successive reads are consecutive. | ||
* The available types to read include: | ||
* Uint8, Uint8String, Int16, Uint16, Int32, Uint32 | ||
* @param {ArrayBuffer} arrayBuffer - array to use as a stream | ||
* @constructor | ||
*/ | ||
function ArrayBufferStream (arrayBuffer) { | ||
@@ -14,3 +16,7 @@ this.arrayBuffer = arrayBuffer; | ||
// return a new ArrayBufferStream that is a slice of the existing one | ||
/** | ||
* Return a new ArrayBufferStream that is a slice of the existing one | ||
* @param {number} length - the number of bytes of extract | ||
* @return {ArrayBufferStream} the extracted stream | ||
*/ | ||
ArrayBufferStream.prototype.extract = function (length) { | ||
@@ -22,2 +28,5 @@ var slicedArrayBuffer = this.arrayBuffer.slice(this.position, this.position+length); | ||
/** | ||
* @return {number} the length of the stream in bytes | ||
*/ | ||
ArrayBufferStream.prototype.getLength = function () { | ||
@@ -27,2 +36,5 @@ return this.arrayBuffer.byteLength; | ||
/** | ||
* @return {number} the number of bytes available after the current position in the stream | ||
*/ | ||
ArrayBufferStream.prototype.getBytesAvailable = function () { | ||
@@ -32,2 +44,6 @@ return (this.arrayBuffer.byteLength - this.position); | ||
/** | ||
* Read an unsigned 8 bit integer from the stream | ||
* @return {number} | ||
*/ | ||
ArrayBufferStream.prototype.readUint8 = function () { | ||
@@ -39,4 +55,8 @@ var val = new Uint8Array(this.arrayBuffer, this.position, 1)[0]; | ||
// convert a sequence of bytes of the given length to a string | ||
// for small length strings only | ||
/** | ||
* Read a sequence of bytes of the given length and convert to a string. | ||
* This is a convenience method for use with short strings. | ||
* @param {number} length - the number of bytes to convert | ||
* @return {String} a String made by concatenating the chars in the input | ||
*/ | ||
ArrayBufferStream.prototype.readUint8String = function (length) { | ||
@@ -52,2 +72,6 @@ var arr = new Uint8Array(this.arrayBuffer, this.position, length); | ||
/** | ||
* Read a 16 bit integer from the stream | ||
* @return {number} | ||
*/ | ||
ArrayBufferStream.prototype.readInt16 = function () { | ||
@@ -59,2 +83,6 @@ var val = new Int16Array(this.arrayBuffer, this.position, 1)[0]; | ||
/** | ||
* Read an unsigned 16 bit integer from the stream | ||
* @return {number} | ||
*/ | ||
ArrayBufferStream.prototype.readUint16 = function () { | ||
@@ -66,2 +94,6 @@ var val = new Uint16Array(this.arrayBuffer, this.position, 1)[0]; | ||
/** | ||
* Read a 32 bit integer from the stream | ||
* @return {number} | ||
*/ | ||
ArrayBufferStream.prototype.readInt32 = function () { | ||
@@ -73,2 +105,6 @@ var val = new Int32Array(this.arrayBuffer, this.position, 1)[0]; | ||
/** | ||
* Read an unsigned 32 bit integer from the stream | ||
* @return {number} | ||
*/ | ||
ArrayBufferStream.prototype.readUint32 = function () { | ||
@@ -75,0 +111,0 @@ var val = new Uint32Array(this.arrayBuffer, this.position, 1)[0]; |
var SoundPlayer = require('./SoundPlayer'); | ||
var Tone = require('tone'); | ||
/** | ||
* 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 | ||
* @constructor | ||
*/ | ||
function DrumPlayer (outputNode) { | ||
@@ -38,2 +43,9 @@ this.outputNode = outputNode; | ||
/** | ||
* Play a drum sound. | ||
* The parameter for output node allows sprites or clones to send the drum sound | ||
* to their individual audio effect chains. | ||
* @param {number} drum - the drum number to play (0-indexed) | ||
* @param {Tone.Gain} outputNode - a node to send the output to | ||
*/ | ||
DrumPlayer.prototype.play = function (drum, outputNode) { | ||
@@ -44,2 +56,5 @@ this.drumSounds[drum].outputNode = outputNode; | ||
/** | ||
* Stop all drum sounds. | ||
*/ | ||
DrumPlayer.prototype.stopAll = function () { | ||
@@ -46,0 +61,0 @@ for (var i=0; i<this.drumSounds.length; i++) { |
@@ -1,15 +0,11 @@ | ||
/* | ||
var Tone = require('tone'); | ||
An echo effect | ||
0 mutes the effect | ||
Values up to 100 set the echo feedback amount, | ||
increasing the time it takes the echo to fade away | ||
Clamped 0-100 | ||
/** | ||
* An echo effect (aka 'delay effect' in audio terms) | ||
* Effect value of 0 mutes the effect | ||
* Values up to 100 set the echo feedback amount, | ||
* increasing the time it takes the echo to fade away | ||
* Clamped 0-100 | ||
* @constructor | ||
*/ | ||
var Tone = require('tone'); | ||
function EchoEffect () { | ||
@@ -27,2 +23,6 @@ Tone.Effect.call(this); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
EchoEffect.prototype.set = function (val) { | ||
@@ -44,2 +44,6 @@ this.value = val; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
EchoEffect.prototype.changeBy = function (val) { | ||
@@ -49,2 +53,8 @@ this.set(this.value + val); | ||
/** | ||
* Clamp the input to a range | ||
* @param {number} input - the input to clamp | ||
* @param {number} min - the min value to clamp to | ||
* @param {number} max - the max value to clamp to | ||
*/ | ||
EchoEffect.prototype.clamp = function (input, min, max) { | ||
@@ -51,0 +61,0 @@ return Math.min(Math.max(input, min), max); |
@@ -1,15 +0,10 @@ | ||
/* | ||
var Tone = require('tone'); | ||
A fuzz effect | ||
Distortion | ||
the value controls the wet/dry amount | ||
Clamped 0-100 | ||
/** | ||
* A fuzz effect (aka 'distortion effect' in audio terms) | ||
* Effect value controls the wet/dry amount: | ||
* 0 passes through none of the effect, 100 passes through all effect | ||
* Clamped 0-100 | ||
* @constructor | ||
*/ | ||
var Tone = require('tone'); | ||
function FuzzEffect () { | ||
@@ -27,2 +22,6 @@ Tone.Effect.call(this); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
FuzzEffect.prototype.set = function (val) { | ||
@@ -36,2 +35,6 @@ this.value = val; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
FuzzEffect.prototype.changeBy = function (val) { | ||
@@ -41,2 +44,7 @@ this.set(this.value + val); | ||
/** | ||
* @param {number} input - the input to clamp | ||
* @param {number} min - the min value to clamp to | ||
* @param {number} max - the max value to clamp to | ||
*/ | ||
FuzzEffect.prototype.clamp = function (input, min, max) { | ||
@@ -43,0 +51,0 @@ return Math.min(Math.max(input, min), max); |
@@ -1,13 +0,10 @@ | ||
/* | ||
var Tone = require('tone'); | ||
A Pan effect | ||
-100 puts the audio on the left channel, 0 centers it, 100 puts it on the right. | ||
Clamped -100 to 100 | ||
/** | ||
* 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, | ||
* 0 centers it, 100 puts it on the right. | ||
* Clamped -100 to 100 | ||
* @constructor | ||
*/ | ||
var Tone = require('tone'); | ||
function PanEffect () { | ||
@@ -25,2 +22,6 @@ Tone.Effect.call(this); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
PanEffect.prototype.set = function (val) { | ||
@@ -34,2 +35,6 @@ this.value = val; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
PanEffect.prototype.changeBy = function (val) { | ||
@@ -39,2 +44,8 @@ this.set(this.value + val); | ||
/** | ||
* Clamp the input to a range | ||
* @param {number} input - the input to clamp | ||
* @param {number} min - the min value to clamp to | ||
* @param {number} max - the max value to clamp to | ||
*/ | ||
PanEffect.prototype.clamp = function (input, min, max) { | ||
@@ -41,0 +52,0 @@ return Math.min(Math.max(input, min), max); |
@@ -1,11 +0,24 @@ | ||
/* | ||
var Tone = require('tone'); | ||
A Pitch effect | ||
/** | ||
* A pitch change effect, which changes the playback rate of the sound in order | ||
* to change its pitch: reducing the playback rate lowers the pitch, increasing the rate | ||
* raises the pitch. The duration of the sound is also changed. | ||
* | ||
* Changing the value of the pitch effect by 10 causes a change in pitch by 1 semitone | ||
* (i.e. a musical half-step, such as the difference between C and C#) | ||
* Changing the pitch effect by 120 changes the pitch by one octave (12 semitones) | ||
* | ||
* The value of this effect is not clamped (i.e. it is typically between -120 and 120, | ||
* but can be set much higher or much lower, with weird and fun results). | ||
* We should consider what extreme values to use for clamping it. | ||
* | ||
* Note that this effect functions differently from the other audio effects. It is | ||
* not part of a chain of audio nodes. Instead, it provides a way to set the playback | ||
* on one SoundPlayer or a group of them. | ||
* @constructor | ||
*/ | ||
var Tone = require('tone'); | ||
function PitchEffect () { | ||
this.value = 0; | ||
this.value = 0; // effect value | ||
this.ratio = 1; // the playback rate ratio | ||
@@ -15,7 +28,18 @@ this.tone = new Tone(); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
* @param {object} players - a dictionary of SoundPlayer objects to apply the effect to, indexed by md5 | ||
*/ | ||
PitchEffect.prototype.set = function (val, players) { | ||
this.value = val; | ||
this.ratio = this.getRatio(this.value); | ||
this.updatePlayers(players); | ||
}; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
* @param {Object} players - a dictionary of SoundPlayer objects indexed by md5 | ||
*/ | ||
PitchEffect.prototype.changeBy = function (val, players) { | ||
@@ -25,14 +49,33 @@ this.set(this.value + val, players); | ||
PitchEffect.prototype.getRatio = function () { | ||
return this.tone.intervalToFrequencyRatio(this.value / 10); | ||
/** | ||
* Compute the playback ratio for an effect value. | ||
* The playback ratio is scaled so that a change of 10 in the effect value | ||
* gives a change of 1 semitone in the ratio. | ||
* @param {number} val - an effect value | ||
* @returns {number} a playback ratio | ||
*/ | ||
PitchEffect.prototype.getRatio = function (val) { | ||
return this.tone.intervalToFrequencyRatio(val / 10); | ||
}; | ||
/** | ||
* Update a sound player's playback rate using the current ratio for the effect | ||
* @param {Object} player - a SoundPlayer object | ||
*/ | ||
PitchEffect.prototype.updatePlayer = function (player) { | ||
player.setPlaybackRate(this.ratio); | ||
}; | ||
/** | ||
* Update a sound player's playback rate using the current ratio for the effect | ||
* @param {object} players - a dictionary of SoundPlayer objects to update, indexed by md5 | ||
*/ | ||
PitchEffect.prototype.updatePlayers = function (players) { | ||
if (!players) return; | ||
var ratio = this.getRatio(); | ||
for (var i=0; i<players.length; i++) { | ||
players[i].setPlaybackRate(ratio); | ||
for (var md5 in players) { | ||
if (players.hasOwnProperty(md5)) { | ||
this.updatePlayer(players[md5]); | ||
} | ||
} | ||
}; | ||
@@ -39,0 +82,0 @@ |
@@ -1,13 +0,10 @@ | ||
/* | ||
var Tone = require('tone'); | ||
A Reverb effect | ||
The value controls the wet/dry amount of the effect | ||
Clamped 0 to 100 | ||
/** | ||
* A reverb effect, simulating reverberation in a room | ||
* Effect value controls the wet/dry amount: | ||
* 0 passes through none of the effect, 100 passes through all effect | ||
* Clamped 0 to 100 | ||
* @constructor | ||
*/ | ||
var Tone = require('tone'); | ||
function ReverbEffect () { | ||
@@ -25,2 +22,6 @@ Tone.Effect.call(this); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
ReverbEffect.prototype.set = function (val) { | ||
@@ -34,2 +35,6 @@ this.value = val; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
ReverbEffect.prototype.changeBy = function (val) { | ||
@@ -39,2 +44,8 @@ this.set(this.value + val); | ||
/** | ||
* Clamp the input to a range | ||
* @param {number} input - the input to clamp | ||
* @param {number} min - the min value to clamp to | ||
* @param {number} max - the max value to clamp to | ||
*/ | ||
ReverbEffect.prototype.clamp = function (input, min, max) { | ||
@@ -41,0 +52,0 @@ return Math.min(Math.max(input, min), max); |
@@ -1,18 +0,15 @@ | ||
/* | ||
A robot-voice effect | ||
var Tone = require('tone'); | ||
A feedback comb filter with a short delay time creates a low-pitched buzzing | ||
The effect value controls the length of this delay time, changing the pitch | ||
0 mutes the effect | ||
Other values changes the pitch of the effect, in units of 10 steps per semitone | ||
Not clamped | ||
/** | ||
* A "robotic" effect that adds a low-pitched buzzing to the sound, reminiscent of the | ||
* voice of the daleks from Dr. Who. | ||
* In audio terms it is a feedback comb filter with a short delay time. | ||
* The effect value controls the length of this delay time, changing the pitch of the buzz | ||
* A value of 0 mutes the effect. | ||
* Other values change the pitch of the effect, in units of 10 steps per semitone. | ||
* The effect value is not clamped (but probably should be). | ||
* Exterminate. | ||
* @constructor | ||
*/ | ||
var Tone = require('tone'); | ||
function RoboticEffect () { | ||
@@ -31,2 +28,6 @@ Tone.Effect.call(this); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
RoboticEffect.prototype.set = function (val) { | ||
@@ -47,2 +48,6 @@ this.value = val; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
RoboticEffect.prototype.changeBy = function (val) { | ||
@@ -52,5 +57,10 @@ this.set(this.value + val); | ||
/** | ||
* Compute the delay time for an effect value. | ||
* Convert the effect value to a musical note (in units of 10 per semitone), | ||
* and return the period (single cycle duration) of the frequency of that note. | ||
* @param {number} val - the effect value | ||
* @returns {number} a delay time in seconds | ||
*/ | ||
RoboticEffect.prototype._delayTimeForValue = function (val) { | ||
// convert effect setting range, typically 0-100 but can be outside that, | ||
// to a musical note, and return the period of the frequency of that note | ||
var midiNote = ((val - 100) / 10) + 36; | ||
@@ -57,0 +67,0 @@ var freq = Tone.Frequency(midiNote, 'midi').eval(); |
@@ -1,14 +0,14 @@ | ||
/* | ||
var Tone = require('tone'); | ||
A wobble effect | ||
A low frequency oscillator (LFO) controls a gain node | ||
This creates an effect like tremolo | ||
Clamped 0 to 100 | ||
/** | ||
* A wobble effect. In audio terms, it sounds like tremolo. | ||
* It is implemented using a low frequency oscillator (LFO) controlling | ||
* a gain node, which causes the loudness of the signal passing through | ||
* to increase and decrease rapidly. | ||
* Effect value controls the wet/dry amount: | ||
* 0 passes through none of the effect, 100 passes through all effect | ||
* Effect value also controls the frequency of the LFO. | ||
* Clamped 0 to 100 | ||
* @constructor | ||
*/ | ||
var Tone = require('tone'); | ||
function WobbleEffect () { | ||
@@ -28,2 +28,6 @@ Tone.Effect.call(this); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
WobbleEffect.prototype.set = function (val) { | ||
@@ -39,2 +43,6 @@ this.value = val; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
WobbleEffect.prototype.changeBy = function (val) { | ||
@@ -44,2 +52,8 @@ this.set(this.value + val); | ||
/** | ||
* Clamp the input to a range | ||
* @param {number} input - the input to clamp | ||
* @param {number} min - the min value to clamp to | ||
* @param {number} max - the max value to clamp to | ||
*/ | ||
WobbleEffect.prototype.clamp = function (input, min, max) { | ||
@@ -46,0 +60,0 @@ return Math.min(Math.max(input, min), max); |
316
src/index.js
@@ -17,9 +17,13 @@ var log = require('./log'); | ||
/* Audio Engine | ||
/** | ||
* @fileOverview Scratch Audio is divided into a single AudioEngine, | ||
* that handles global functionality, and AudioPlayers, belonging to individual sprites and clones. | ||
*/ | ||
The Scratch runtime has a single audio engine that handles global audio properties and effects, | ||
and creates the instrument player and a drum player, used by all play note and play drum blocks | ||
*/ | ||
/** | ||
* There is a single instance of the AudioEngine. It handles global audio properties and effects, | ||
* loads all the audio buffers for sounds belonging to sprites, and creates a single instrument player | ||
* and a drum player, used by all play note and play drum blocks. | ||
* @constructor | ||
*/ | ||
function AudioEngine () { | ||
@@ -42,4 +46,2 @@ | ||
this.currentTempo = 60; | ||
this.minTempo = 10; | ||
this.maxTempo = 1000; | ||
@@ -53,9 +55,90 @@ // instrument player for play note blocks | ||
this.numDrums = this.drumPlayer.drumSounds.length; | ||
// a map of md5s to audio buffers, holding sounds for all sprites | ||
this.audioBuffers = {}; | ||
} | ||
/** | ||
* Load all sounds for a sprite and store them in the audioBuffers dictionary, indexed by md5 | ||
* @param {Object} sounds - an array of objects containing metadata for sound files of a sprite | ||
*/ | ||
AudioEngine.prototype.loadSounds = function (sounds) { | ||
var storedContext = this; | ||
for (var i=0; i<sounds.length; i++) { | ||
var md5 = sounds[i].md5; | ||
var buffer = new Tone.Buffer(); | ||
this.audioBuffers[md5] = buffer; | ||
// Squeak sound format (not implemented yet) | ||
if (sounds[i].format == 'squeak') { | ||
log.warn('unable to load sound in squeak format'); | ||
continue; | ||
} | ||
// most sounds decode natively, but for adpcm sounds we use our own decoder | ||
if (sounds[i].format == 'adpcm') { | ||
log.warn('loading sound in adpcm format'); | ||
// create a closure to store the sound md5, to use when the | ||
// decoder completes and resolves the promise | ||
(function () { | ||
var storedMd5 = sounds[i].md5; | ||
var loader = new ADPCMSoundLoader(); | ||
loader.load(sounds[i].fileUrl).then(function (audioBuffer) { | ||
storedContext.audioBuffers[storedMd5] = new Tone.Buffer(audioBuffer); | ||
}); | ||
}()); | ||
} else { | ||
this.audioBuffers[md5] = new Tone.Buffer(sounds[i].fileUrl); | ||
} | ||
} | ||
}; | ||
/** | ||
* Play a note for a duration on an instrument | ||
* @param {number} note - a MIDI note number | ||
* @param {number} beats - a duration in beats | ||
* @param {number} inst - an instrument number (0-indexed) | ||
* @return {Promise} a Promise that resolves after the duration has elapsed | ||
*/ | ||
AudioEngine.prototype.playNoteForBeatsWithInst = function (note, beats, inst) { | ||
var sec = this.beatsToSec(beats); | ||
this.instrumentPlayer.playNoteForSecWithInst(note, sec, inst); | ||
return this.waitForBeats(beats); | ||
}; | ||
/** | ||
* Convert a number of beats to a number of seconds, using the current tempo | ||
* @param {number} beats | ||
* @return {number} seconds | ||
*/ | ||
AudioEngine.prototype.beatsToSec = function (beats) { | ||
return (60 / this.currentTempo) * beats; | ||
}; | ||
/** | ||
* Wait for some number of beats | ||
* @param {number} beats | ||
* @return {Promise} a Promise that resolves after the duration has elapsed | ||
*/ | ||
AudioEngine.prototype.waitForBeats = function (beats) { | ||
var storedContext = this; | ||
return new Promise(function (resolve) { | ||
setTimeout(function () { | ||
resolve(); | ||
}, storedContext.beatsToSec(beats) * 1000); | ||
}); | ||
}; | ||
/** | ||
* Set the global tempo in bpm (beats per minute) | ||
* @param {number} value - the new tempo to set | ||
*/ | ||
AudioEngine.prototype.setTempo = function (value) { | ||
// var newTempo = this._clamp(value, this.minTempo, this.maxTempo); | ||
this.currentTempo = value; | ||
}; | ||
/** | ||
* Change the tempo by some number of bpm (beats per minute) | ||
* @param {number} value - the number of bpm to change the tempo by | ||
*/ | ||
AudioEngine.prototype.changeTempo = function (value) { | ||
@@ -65,2 +148,22 @@ this.setTempo(this.currentTempo + value); | ||
/** | ||
* Names of the audio effects. | ||
* @readonly | ||
* @enum {string} | ||
*/ | ||
AudioEngine.prototype.EFFECT_NAMES = { | ||
pitch: 'pitch', | ||
pan: 'pan', | ||
echo: 'echo', | ||
reverb: 'reverb', | ||
fuzz: 'fuzz', | ||
robot: 'robot' | ||
}; | ||
/** | ||
* Create an AudioPlayer. Each sprite or clone has an AudioPlayer. | ||
* It includes a reference to the AudioEngine so it can use global | ||
* functionality such as playing notes. | ||
* @return {AudioPlayer} | ||
*/ | ||
AudioEngine.prototype.createPlayer = function () { | ||
@@ -70,11 +173,10 @@ return new AudioPlayer(this); | ||
/* Audio Player | ||
Each sprite has an audio player | ||
Clones receive a reference to their parent's audio player | ||
the audio player currently handles sound loading and playback, sprite-specific effects | ||
(pitch and pan) and volume | ||
*/ | ||
/** | ||
* Each sprite or clone has an audio player | ||
* the audio player handles sound playback, volume, and the sprite-specific audio effects: | ||
* pitch and pan | ||
* @param {AudioEngine} | ||
* @constructor | ||
*/ | ||
function AudioPlayer (audioEngine) { | ||
@@ -97,84 +199,63 @@ | ||
this.effectNames = ['PITCH', 'PAN', 'ECHO', 'REVERB', 'FUZZ', 'ROBOT']; | ||
this.currentVolume = 100; | ||
this.currentInstrument = 0; | ||
// sound players that are currently playing, indexed by the sound's md5 | ||
this.activeSoundPlayers = {}; | ||
} | ||
AudioPlayer.prototype.loadSounds = function (sounds) { | ||
this.soundPlayers = []; | ||
// create a set of empty sound player objects | ||
// the sound buffers will be added asynchronously as they load | ||
for (var i=0; i<sounds.length; i++){ | ||
this.soundPlayers[i] = new SoundPlayer(this.effectsNode); | ||
/** | ||
* Play a sound | ||
* @param {string} md5 - the md5 id of a sound file | ||
* @return {Promise} a Promise that resolves when the sound finishes playing | ||
*/ | ||
AudioPlayer.prototype.playSound = function (md5) { | ||
// if this sound is not in the audio engine, return | ||
if (!this.audioEngine.audioBuffers[md5]) { | ||
return; | ||
} | ||
// load the sounds | ||
// most sounds decode natively, but for adpcm sounds we use our own decoder | ||
var storedContext = this; | ||
for (var index=0; index<sounds.length; index++) { | ||
if (sounds[index].format == 'squeak') { | ||
log.warn('unable to load sound in squeak format'); | ||
continue; | ||
} | ||
if (sounds[index].format == 'adpcm') { | ||
log.warn('loading sound in adpcm format'); | ||
// create a closure to store the sound index, to use when the | ||
// decoder completes and resolves the promise | ||
(function () { | ||
var storedIndex = index; | ||
var loader = new ADPCMSoundLoader(); | ||
loader.load(sounds[storedIndex].fileUrl).then(function (audioBuffer) { | ||
storedContext.soundPlayers[storedIndex].setBuffer(new Tone.Buffer(audioBuffer)); | ||
}); | ||
}()); | ||
} else { | ||
this.soundPlayers[index].setBuffer(new Tone.Buffer(sounds[index].fileUrl)); | ||
} | ||
// if this sprite or clone is already playing this sound, stop it first | ||
if (this.activeSoundPlayers[md5]) { | ||
this.activeSoundPlayers[md5].stop(); | ||
} | ||
}; | ||
// create a new soundplayer to play the sound | ||
var player = new SoundPlayer(); | ||
player.setBuffer(this.audioEngine.audioBuffers[md5]); | ||
player.connect(this.effectsNode); | ||
this.pitchEffect.updatePlayer(player); | ||
player.start(); | ||
AudioPlayer.prototype.playSound = function (index) { | ||
if (!this.soundPlayers[index]) return; | ||
// add it to the list of active sound players | ||
this.activeSoundPlayers[md5] = player; | ||
this.soundPlayers[index].start(); | ||
// remove sounds that are not playing from the active sound players array | ||
for (var id in this.activeSoundPlayers) { | ||
if (this.activeSoundPlayers.hasOwnProperty(id)) { | ||
if (!this.activeSoundPlayers[id].isPlaying) { | ||
delete this.activeSoundPlayers[id]; | ||
} | ||
} | ||
} | ||
var storedContext = this; | ||
return new Promise(function (resolve) { | ||
storedContext.soundPlayers[index].onEnded(resolve); | ||
}); | ||
return player.finished(); | ||
}; | ||
AudioPlayer.prototype.playNoteForBeats = function (note, beats) { | ||
var sec = this.beatsToSec(beats); | ||
this.audioEngine.instrumentPlayer.playNoteForSecWithInst(note, sec, this.currentInstrument); | ||
return this.waitForBeats(beats); | ||
}; | ||
/** | ||
* Play a drum sound. The AudioEngine contains the DrumPlayer, but the AudioPlayer | ||
* calls this function so that it can pass a reference to its own effects node. | ||
* @param {number} drum - a drum number (0-indexed) | ||
* @param {number} beats - a duration in beats | ||
* @return {Promise} a Promise that resolves after the duration has elapsed | ||
*/ | ||
AudioPlayer.prototype.playDrumForBeats = function (drum, beats) { | ||
this.audioEngine.drumPlayer.play(drum, this.effectsNode); | ||
return this.waitForBeats(beats); | ||
return this.audioEngine.waitForBeats(beats); | ||
}; | ||
AudioPlayer.prototype.waitForBeats = function (beats) { | ||
var storedContext = this; | ||
return new Promise(function (resolve) { | ||
setTimeout(function () { | ||
resolve(); | ||
}, storedContext.beatsToSec(beats) * 1000); | ||
}); | ||
}; | ||
AudioPlayer.prototype.beatsToSec = function (beats) { | ||
return (60 / this.audioEngine.currentTempo) * beats; | ||
}; | ||
/** | ||
* Stop all sounds, notes and drums that are playing | ||
*/ | ||
AudioPlayer.prototype.stopAllSounds = function () { | ||
// stop all sound players | ||
for (var i=0; i<this.soundPlayers.length; i++) { | ||
this.soundPlayers[i].stop(); | ||
// stop all active sound players | ||
for (var md5 in this.activeSoundPlayers) { | ||
this.activeSoundPlayers[md5].stop(); | ||
} | ||
@@ -187,23 +268,27 @@ | ||
this.audioEngine.drumPlayer.stopAll(); | ||
}; | ||
/** | ||
* Set an audio effect to a value | ||
* @param {string} effect - the name of the effect | ||
* @param {number} value - the value to set the effect to | ||
*/ | ||
AudioPlayer.prototype.setEffect = function (effect, value) { | ||
switch (effect) { | ||
case 'PITCH': | ||
this.pitchEffect.set(value, this.soundPlayers); | ||
case this.audioEngine.EFFECT_NAMES.pitch: | ||
this.pitchEffect.set(value, this.activeSoundPlayers); | ||
break; | ||
case 'PAN': | ||
case this.audioEngine.EFFECT_NAMES.pan: | ||
this.panEffect.set(value); | ||
break; | ||
case 'ECHO': | ||
case this.audioEngine.EFFECT_NAMES.echo: | ||
this.audioEngine.echoEffect.set(value); | ||
break; | ||
case 'REVERB': | ||
case this.audioEngine.EFFECT_NAMES.reverb: | ||
this.audioEngine.reverbEffect.set(value); | ||
break; | ||
case 'FUZZ' : | ||
case this.audioEngine.EFFECT_NAMES.fuzz: | ||
this.audioEngine.fuzzEffect.set(value); | ||
break; | ||
case 'ROBOT' : | ||
case this.audioEngine.EFFECT_NAMES.robot: | ||
this.audioEngine.roboticEffect.set(value); | ||
@@ -214,29 +299,8 @@ break; | ||
AudioPlayer.prototype.changeEffect = function (effect, value) { | ||
switch (effect) { | ||
case 'PITCH': | ||
this.pitchEffect.changeBy(value, this.soundPlayers); | ||
break; | ||
case 'PAN': | ||
this.panEffect.changeBy(value); | ||
break; | ||
case 'ECHO': | ||
this.audioEngine.echoEffect.changeBy(value); | ||
break; | ||
case 'REVERB': | ||
this.audioEngine.reverbEffect.changeBy(value); | ||
break; | ||
case 'FUZZ' : | ||
this.audioEngine.fuzzEffect.changeBy(value); | ||
break; | ||
case 'ROBOT' : | ||
this.audioEngine.roboticEffect.changeBy(value); | ||
break; | ||
} | ||
}; | ||
/** | ||
* Clear all audio effects | ||
*/ | ||
AudioPlayer.prototype.clearEffects = function () { | ||
this.panEffect.set(0); | ||
this.pitchEffect.set(0, this.soundPlayers); | ||
this.pitchEffect.set(0, this.activeSoundPlayers); | ||
this.effectsNode.gain.value = 1; | ||
@@ -250,21 +314,11 @@ | ||
AudioPlayer.prototype.setInstrument = function (instrumentNum) { | ||
this.currentInstrument = instrumentNum; | ||
return this.audioEngine.instrumentPlayer.loadInstrument(this.currentInstrument); | ||
}; | ||
/** | ||
* Set the volume for sounds played by this AudioPlayer | ||
* @param {number} value - the volume in range 0-100 | ||
*/ | ||
AudioPlayer.prototype.setVolume = function (value) { | ||
this.currentVolume = this._clamp(value, 0, 100); | ||
this.effectsNode.gain.value = this.currentVolume / 100; | ||
this.effectsNode.gain.value = value / 100; | ||
}; | ||
AudioPlayer.prototype.changeVolume = function (value) { | ||
this.setVolume(this.currentVolume + value); | ||
}; | ||
AudioPlayer.prototype._clamp = function (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
}; | ||
module.exports = AudioEngine; | ||
var Tone = require('tone'); | ||
var Soundfont = require('soundfont-player'); | ||
/** | ||
* A prototype for the instrument sound functionality that can play notes. | ||
* This prototype version (which will be replaced at some point) uses an | ||
* existing soundfont library that creates several limitations: | ||
* The sound files are high quality but large, so they are loaded 'on demand,' at the time the | ||
* 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 | ||
* 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 | ||
* @constructor | ||
*/ | ||
function InstrumentPlayer (outputNode) { | ||
this.outputNode = outputNode; | ||
// instrument names used by Musyng Kite soundfont, in order to | ||
// Instrument names used by Musyng Kite soundfont, in order to | ||
// match scratch instruments | ||
@@ -18,2 +29,11 @@ this.instrumentNames = ['acoustic_grand_piano', 'electric_piano_1', | ||
/** | ||
* Play a note for some number of seconds with a particular instrument. | ||
* Load the instrument first, if it has not already been loaded. | ||
* The duration is in seconds because the AudioEngine manages the tempo, | ||
* and converts beats to seconds. | ||
* @param {number} note - a MIDI note number | ||
* @param {number} sec - a duration in seconds | ||
* @param {number} instrumentNum - an instrument number (0-indexed) | ||
*/ | ||
InstrumentPlayer.prototype.playNoteForSecWithInst = function (note, sec, instrumentNum) { | ||
@@ -28,2 +48,7 @@ this.loadInstrument(instrumentNum) | ||
/** | ||
* Load an instrument by number | ||
* @param {number} instrumentNum - an instrument number (0-indexed) | ||
* @return {Promise} a Promise that resolves once the instrument audio data has been loaded | ||
*/ | ||
InstrumentPlayer.prototype.loadInstrument = function (instrumentNum) { | ||
@@ -41,2 +66,5 @@ if (this.instruments[instrumentNum]) { | ||
/** | ||
* Stop all notes being played on all instruments | ||
*/ | ||
InstrumentPlayer.prototype.stopAll = function () { | ||
@@ -43,0 +71,0 @@ for (var i=0; i<this.instruments.length; i++) { |
var Tone = require('tone'); | ||
var log = require('./log'); | ||
function SoundPlayer (outputNode) { | ||
this.outputNode = outputNode; | ||
this.buffer; // a Tone.Buffer | ||
this.bufferSource; | ||
/** | ||
* A SoundPlayer stores an audio buffer, and plays it | ||
* @constructor | ||
*/ | ||
function SoundPlayer () { | ||
this.outputNode = null; | ||
this.buffer = new Tone.Buffer(); | ||
this.bufferSource = null; | ||
this.playbackRate = 1; | ||
@@ -12,2 +16,14 @@ this.isPlaying = false; | ||
/** | ||
* Connect the SoundPlayer to an output node | ||
* @param {Tone.Gain} node - an output node to connect to | ||
*/ | ||
SoundPlayer.prototype.connect = function (node) { | ||
this.outputNode = node; | ||
}; | ||
/** | ||
* Set an audio buffer | ||
* @param {Tone.Buffer} buffer | ||
*/ | ||
SoundPlayer.prototype.setBuffer = function (buffer) { | ||
@@ -17,2 +33,6 @@ this.buffer = buffer; | ||
/** | ||
* Set the playback rate for the sound | ||
* @param {number} playbackRate - a ratio where 1 is normal playback, 0.5 is half speed, 2 is double speed, etc. | ||
*/ | ||
SoundPlayer.prototype.setPlaybackRate = function (playbackRate) { | ||
@@ -25,8 +45,16 @@ this.playbackRate = playbackRate; | ||
/** | ||
* Stop the sound | ||
*/ | ||
SoundPlayer.prototype.stop = function () { | ||
if (this.isPlaying){ | ||
if (this.bufferSource) { | ||
this.bufferSource.stop(); | ||
} | ||
this.isPlaying = false; | ||
}; | ||
/** | ||
* Start playing the sound | ||
* The web audio framework requires a new audio buffer source node for each playback | ||
*/ | ||
SoundPlayer.prototype.start = function () { | ||
@@ -38,4 +66,2 @@ if (!this.buffer || !this.buffer.loaded) { | ||
this.stop(); | ||
this.bufferSource = new Tone.BufferSource(this.buffer.get()); | ||
@@ -45,12 +71,21 @@ this.bufferSource.playbackRate.value = this.playbackRate; | ||
this.bufferSource.start(); | ||
this.isPlaying = true; | ||
}; | ||
SoundPlayer.prototype.onEnded = function (callback) { | ||
this.bufferSource.onended = function () { | ||
this.isPlaying = false; | ||
callback(); | ||
}; | ||
/** | ||
* The sound has finished playing. This is called at the correct time even if the playback rate | ||
* has been changed | ||
* @return {Promise} a Promise that resolves when the sound finishes playing | ||
*/ | ||
SoundPlayer.prototype.finished = function () { | ||
var storedContext = this; | ||
return new Promise(function (resolve) { | ||
storedContext.bufferSource.onended = function () { | ||
this.isPlaying = false; | ||
resolve(); | ||
}.bind(storedContext); | ||
}); | ||
}; | ||
module.exports = SoundPlayer; |
Sorry, the diff of this file is too big to display
1206873
2.02%25668
2.72%