scratch-audio
Advanced tools
Comparing version
{ | ||
"name": "scratch-audio", | ||
"version": "0.1.0-prerelease.1492531432", | ||
"version": "0.1.0-prerelease.1492554669", | ||
"description": "audio engine for scratch 3.0", | ||
@@ -5,0 +5,0 @@ "main": "dist.js", |
@@ -11,159 +11,165 @@ const ArrayBufferStream = require('./ArrayBufferStream'); | ||
* https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as | ||
* @constructor | ||
*/ | ||
const ADPCMSoundDecoder = function () {}; | ||
class ADPCMSoundDecoder { | ||
/** | ||
* Data used by the decompression algorithm | ||
* @type {Array} | ||
*/ | ||
static get STEP_TABLE () { | ||
return [ | ||
7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, | ||
50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, | ||
253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, | ||
1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, | ||
3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, | ||
12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 | ||
]; | ||
} | ||
/** | ||
* Decode an ADPCM sound stored in an ArrayBuffer and return a promise | ||
* with the decoded audio buffer. | ||
* @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio | ||
* @return {Tone.Buffer} the decoded audio buffer | ||
*/ | ||
ADPCMSoundDecoder.prototype.decode = function (audioData) { | ||
/** | ||
* Data used by the decompression algorithm | ||
* @type {Array} | ||
*/ | ||
static get INDEX_TABLE () { | ||
return [ | ||
-1, -1, -1, -1, 2, 4, 6, 8, | ||
-1, -1, -1, -1, 2, 4, 6, 8 | ||
]; | ||
} | ||
return new Promise((resolve, reject) => { | ||
const stream = new ArrayBufferStream(audioData); | ||
/** | ||
* Decode an ADPCM sound stored in an ArrayBuffer and return a promise | ||
* with the decoded audio buffer. | ||
* @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio | ||
* @return {Tone.Buffer} the decoded audio buffer | ||
*/ | ||
decode (audioData) { | ||
const riffStr = stream.readUint8String(4); | ||
if (riffStr !== 'RIFF') { | ||
log.warn('incorrect adpcm wav header'); | ||
reject(); | ||
} | ||
return new Promise((resolve, reject) => { | ||
const stream = new ArrayBufferStream(audioData); | ||
const lengthInHeader = stream.readInt32(); | ||
if ((lengthInHeader + 8) !== audioData.byteLength) { | ||
log.warn(`adpcm wav length in header: ${lengthInHeader} is incorrect`); | ||
} | ||
const riffStr = stream.readUint8String(4); | ||
if (riffStr !== 'RIFF') { | ||
log.warn('incorrect adpcm wav header'); | ||
reject(); | ||
} | ||
const wavStr = stream.readUint8String(4); | ||
if (wavStr !== 'WAVE') { | ||
log.warn('incorrect adpcm wav header'); | ||
reject(); | ||
} | ||
const lengthInHeader = stream.readInt32(); | ||
if ((lengthInHeader + 8) !== audioData.byteLength) { | ||
log.warn(`adpcm wav length in header: ${lengthInHeader} is incorrect`); | ||
} | ||
const formatChunk = this.extractChunk('fmt ', stream); | ||
this.encoding = formatChunk.readUint16(); | ||
this.channels = formatChunk.readUint16(); | ||
this.samplesPerSecond = formatChunk.readUint32(); | ||
this.bytesPerSecond = formatChunk.readUint32(); | ||
this.blockAlignment = formatChunk.readUint16(); | ||
this.bitsPerSample = formatChunk.readUint16(); | ||
formatChunk.position += 2; // skip extra header byte count | ||
this.samplesPerBlock = formatChunk.readUint16(); | ||
this.adpcmBlockSize = ((this.samplesPerBlock - 1) / 2) + 4; // block size in bytes | ||
const wavStr = stream.readUint8String(4); | ||
if (wavStr !== 'WAVE') { | ||
log.warn('incorrect adpcm wav header'); | ||
reject(); | ||
} | ||
const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); | ||
const formatChunk = this.extractChunk('fmt ', stream); | ||
this.encoding = formatChunk.readUint16(); | ||
this.channels = formatChunk.readUint16(); | ||
this.samplesPerSecond = formatChunk.readUint32(); | ||
this.bytesPerSecond = formatChunk.readUint32(); | ||
this.blockAlignment = formatChunk.readUint16(); | ||
this.bitsPerSample = formatChunk.readUint16(); | ||
formatChunk.position += 2; // skip extra header byte count | ||
this.samplesPerBlock = formatChunk.readUint16(); | ||
this.adpcmBlockSize = ((this.samplesPerBlock - 1) / 2) + 4; // block size in bytes | ||
// @todo this line is the only place Tone is used here, should be possible to remove | ||
const buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond); | ||
const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); | ||
// @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply? | ||
for (let i = 0; i < samples.length; i++) { | ||
buffer.getChannelData(0)[i] = samples[i] / 32768; | ||
} | ||
// @todo this line is the only place Tone is used here, should be possible to remove | ||
const buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond); | ||
resolve(buffer); | ||
}); | ||
}; | ||
// @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply? | ||
for (let i = 0; i < samples.length; i++) { | ||
buffer.getChannelData(0)[i] = samples[i] / 32768; | ||
} | ||
/** | ||
* Data used by the decompression algorithm | ||
* @type {Array} | ||
*/ | ||
ADPCMSoundDecoder.prototype.stepTable = [ | ||
7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, | ||
50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, | ||
253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, | ||
1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, | ||
3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, | ||
12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767]; | ||
resolve(buffer); | ||
}); | ||
} | ||
/** | ||
* Data used by the decompression algorithm | ||
* @type {Array} | ||
*/ | ||
ADPCMSoundDecoder.prototype.indexTable = [ | ||
-1, -1, -1, -1, 2, 4, 6, 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 | ||
*/ | ||
extractChunk (chunkType, stream) { | ||
stream.position = 12; | ||
while (stream.position < (stream.getLength() - 8)) { | ||
const typeStr = stream.readUint8String(4); | ||
const chunkSize = stream.readInt32(); | ||
if (typeStr === chunkType) { | ||
const chunk = stream.extract(chunkSize); | ||
return chunk; | ||
} | ||
stream.position += chunkSize; | ||
/** | ||
* 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 | ||
*/ | ||
ADPCMSoundDecoder.prototype.extractChunk = function (chunkType, stream) { | ||
stream.position = 12; | ||
while (stream.position < (stream.getLength() - 8)) { | ||
const typeStr = stream.readUint8String(4); | ||
const chunkSize = stream.readInt32(); | ||
if (typeStr === chunkType) { | ||
const chunk = stream.extract(chunkSize); | ||
return chunk; | ||
} | ||
stream.position += chunkSize; | ||
} | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
ADPCMSoundDecoder.prototype.imaDecompress = function (compressedData, blockSize) { | ||
let sample; | ||
let step; | ||
let code; | ||
let delta; | ||
let index = 0; | ||
let lastByte = -1; // -1 indicates that there is no saved lastByte | ||
const out = []; | ||
/** | ||
* 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 | ||
*/ | ||
imaDecompress (compressedData, blockSize) { | ||
let sample; | ||
let step; | ||
let code; | ||
let delta; | ||
let index = 0; | ||
let lastByte = -1; // -1 indicates that there is no saved lastByte | ||
const out = []; | ||
// Bail and return no samples if we have no data | ||
if (!compressedData) return out; | ||
// Bail and return no samples if we have no data | ||
if (!compressedData) return out; | ||
compressedData.position = 0; | ||
const a = 0; | ||
while (a === 0) { | ||
if (((compressedData.position % blockSize) === 0) && (lastByte < 0)) { // read block header | ||
if (compressedData.getBytesAvailable() === 0) break; | ||
sample = compressedData.readInt16(); | ||
index = compressedData.readUint8(); | ||
compressedData.position++; // skip extra header byte | ||
if (index > 88) index = 88; | ||
out.push(sample); | ||
} else { | ||
// read 4-bit code and compute delta from previous sample | ||
if (lastByte < 0) { | ||
compressedData.position = 0; | ||
// @todo Update this loop ported from Scratch 2.0 to use a condition or a for loop. | ||
while (true) { // eslint-disable-line no-constant-condition | ||
if (((compressedData.position % blockSize) === 0) && (lastByte < 0)) { // read block header | ||
if (compressedData.getBytesAvailable() === 0) break; | ||
lastByte = compressedData.readUint8(); | ||
code = lastByte & 0xF; | ||
sample = compressedData.readInt16(); | ||
index = compressedData.readUint8(); | ||
compressedData.position++; // skip extra header byte | ||
if (index > 88) index = 88; | ||
out.push(sample); | ||
} else { | ||
code = (lastByte >> 4) & 0xF; | ||
lastByte = -1; | ||
// read 4-bit code and compute delta from previous sample | ||
if (lastByte < 0) { | ||
if (compressedData.getBytesAvailable() === 0) break; | ||
lastByte = compressedData.readUint8(); | ||
code = lastByte & 0xF; | ||
} else { | ||
code = (lastByte >> 4) & 0xF; | ||
lastByte = -1; | ||
} | ||
step = ADPCMSoundDecoder.STEP_TABLE[index]; | ||
delta = 0; | ||
if (code & 4) delta += step; | ||
if (code & 2) delta += step >> 1; | ||
if (code & 1) delta += step >> 2; | ||
delta += step >> 3; | ||
// compute next index | ||
index += ADPCMSoundDecoder.INDEX_TABLE[code]; | ||
if (index > 88) index = 88; | ||
if (index < 0) index = 0; | ||
// compute and output sample | ||
sample += (code & 8) ? -delta : delta; | ||
if (sample > 32767) sample = 32767; | ||
if (sample < -32768) sample = -32768; | ||
out.push(sample); | ||
} | ||
step = this.stepTable[index]; | ||
delta = 0; | ||
if (code & 4) delta += step; | ||
if (code & 2) delta += step >> 1; | ||
if (code & 1) delta += step >> 2; | ||
delta += step >> 3; | ||
// compute next index | ||
index += this.indexTable[code]; | ||
if (index > 88) index = 88; | ||
if (index < 0) index = 0; | ||
// compute and output sample | ||
sample += (code & 8) ? -delta : delta; | ||
if (sample > 32767) sample = 32767; | ||
if (sample < -32768) sample = -32768; | ||
out.push(sample); | ||
} | ||
const samples = Int16Array.from(out); | ||
return samples; | ||
} | ||
const samples = Int16Array.from(out); | ||
return samples; | ||
}; | ||
} | ||
module.exports = ADPCMSoundDecoder; |
@@ -1,107 +0,109 @@ | ||
/** | ||
* 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 | ||
*/ | ||
const ArrayBufferStream = function (arrayBuffer) { | ||
this.arrayBuffer = arrayBuffer; | ||
this.position = 0; | ||
}; | ||
class ArrayBufferStream { | ||
/** | ||
* 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 | ||
*/ | ||
constructor (arrayBuffer) { | ||
this.arrayBuffer = arrayBuffer; | ||
this.position = 0; | ||
} | ||
/** | ||
* 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) { | ||
const slicedArrayBuffer = this.arrayBuffer.slice(this.position, this.position + length); | ||
const newStream = new ArrayBufferStream(slicedArrayBuffer); | ||
return newStream; | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
extract (length) { | ||
const slicedArrayBuffer = this.arrayBuffer.slice(this.position, this.position + length); | ||
const newStream = new ArrayBufferStream(slicedArrayBuffer); | ||
return newStream; | ||
} | ||
/** | ||
* @return {number} the length of the stream in bytes | ||
*/ | ||
ArrayBufferStream.prototype.getLength = function () { | ||
return this.arrayBuffer.byteLength; | ||
}; | ||
/** | ||
* @return {number} the length of the stream in bytes | ||
*/ | ||
getLength () { | ||
return this.arrayBuffer.byteLength; | ||
} | ||
/** | ||
* @return {number} the number of bytes available after the current position in the stream | ||
*/ | ||
ArrayBufferStream.prototype.getBytesAvailable = function () { | ||
return (this.arrayBuffer.byteLength - this.position); | ||
}; | ||
/** | ||
* @return {number} the number of bytes available after the current position in the stream | ||
*/ | ||
getBytesAvailable () { | ||
return (this.arrayBuffer.byteLength - this.position); | ||
} | ||
/** | ||
* Read an unsigned 8 bit integer from the stream | ||
* @return {number} the next 8 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readUint8 = function () { | ||
const val = new Uint8Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 1; | ||
return val; | ||
}; | ||
/** | ||
* Read an unsigned 8 bit integer from the stream | ||
* @return {number} the next 8 bit integer in the stream | ||
*/ | ||
readUint8 () { | ||
const val = new Uint8Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 1; | ||
return val; | ||
} | ||
/** | ||
* 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) { | ||
const arr = new Uint8Array(this.arrayBuffer, this.position, length); | ||
this.position += length; | ||
let str = ''; | ||
for (let i = 0; i < arr.length; i++) { | ||
str += String.fromCharCode(arr[i]); | ||
/** | ||
* 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 | ||
*/ | ||
readUint8String (length) { | ||
const arr = new Uint8Array(this.arrayBuffer, this.position, length); | ||
this.position += length; | ||
let str = ''; | ||
for (let i = 0; i < arr.length; i++) { | ||
str += String.fromCharCode(arr[i]); | ||
} | ||
return str; | ||
} | ||
return str; | ||
}; | ||
/** | ||
* Read a 16 bit integer from the stream | ||
* @return {number} the next 16 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readInt16 = function () { | ||
const val = new Int16Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 2; // one 16 bit int is 2 bytes | ||
return val; | ||
}; | ||
/** | ||
* Read a 16 bit integer from the stream | ||
* @return {number} the next 16 bit integer in the stream | ||
*/ | ||
readInt16 () { | ||
const val = new Int16Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 2; // one 16 bit int is 2 bytes | ||
return val; | ||
} | ||
/** | ||
* Read an unsigned 16 bit integer from the stream | ||
* @return {number} the next unsigned 16 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readUint16 = function () { | ||
const val = new Uint16Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 2; // one 16 bit int is 2 bytes | ||
return val; | ||
}; | ||
/** | ||
* Read an unsigned 16 bit integer from the stream | ||
* @return {number} the next unsigned 16 bit integer in the stream | ||
*/ | ||
readUint16 () { | ||
const val = new Uint16Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 2; // one 16 bit int is 2 bytes | ||
return val; | ||
} | ||
/** | ||
* Read a 32 bit integer from the stream | ||
* @return {number} the next 32 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readInt32 = function () { | ||
const val = new Int32Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 4; // one 32 bit int is 4 bytes | ||
return val; | ||
}; | ||
/** | ||
* Read a 32 bit integer from the stream | ||
* @return {number} the next 32 bit integer in the stream | ||
*/ | ||
readInt32 () { | ||
const val = new Int32Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 4; // one 32 bit int is 4 bytes | ||
return val; | ||
} | ||
/** | ||
* Read an unsigned 32 bit integer from the stream | ||
* @return {number} the next unsigned 32 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readUint32 = function () { | ||
const val = new Uint32Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 4; // one 32 bit int is 4 bytes | ||
return val; | ||
}; | ||
/** | ||
* Read an unsigned 32 bit integer from the stream | ||
* @return {number} the next unsigned 32 bit integer in the stream | ||
*/ | ||
readUint32 () { | ||
const val = new Uint32Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 4; // one 32 bit int is 4 bytes | ||
return val; | ||
} | ||
} | ||
module.exports = ArrayBufferStream; |
const SoundPlayer = require('./SoundPlayer'); | ||
const 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 | ||
*/ | ||
const DrumPlayer = function (outputNode) { | ||
this.outputNode = outputNode; | ||
class DrumPlayer { | ||
/** | ||
* 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 | ||
*/ | ||
constructor (outputNode) { | ||
this.outputNode = outputNode; | ||
const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; | ||
const fileNames = [ | ||
'SnareDrum(1)', | ||
'BassDrum(1b)', | ||
'SideStick(1)', | ||
'Crash(2)', | ||
'HiHatOpen(2)', | ||
'HiHatClosed(1)', | ||
'Tambourine(3)', | ||
'Clap(1)', | ||
'Claves(1)', | ||
'WoodBlock(1)', | ||
'Cowbell(3)', | ||
'Triangle(1)', | ||
'Bongo', | ||
'Conga(1)', | ||
'Cabasa(1)', | ||
'GuiroLong(1)', | ||
'Vibraslap(1)', | ||
'Cuica(2)' | ||
]; | ||
const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; | ||
const fileNames = [ | ||
'SnareDrum(1)', | ||
'BassDrum(1b)', | ||
'SideStick(1)', | ||
'Crash(2)', | ||
'HiHatOpen(2)', | ||
'HiHatClosed(1)', | ||
'Tambourine(3)', | ||
'Clap(1)', | ||
'Claves(1)', | ||
'WoodBlock(1)', | ||
'Cowbell(3)', | ||
'Triangle(1)', | ||
'Bongo', | ||
'Conga(1)', | ||
'Cabasa(1)', | ||
'GuiroLong(1)', | ||
'Vibraslap(1)', | ||
'Cuica(2)' | ||
]; | ||
this.drumSounds = []; | ||
this.drumSounds = []; | ||
for (let i = 0; i < fileNames.length; i++) { | ||
const url = `${baseUrl + fileNames[i]}_22k.wav`; | ||
this.drumSounds[i] = new SoundPlayer(this.outputNode); | ||
this.drumSounds[i].setBuffer(new Tone.Buffer(url)); | ||
for (let i = 0; i < fileNames.length; i++) { | ||
const url = `${baseUrl + fileNames[i]}_22k.wav`; | ||
this.drumSounds[i] = new SoundPlayer(this.outputNode); | ||
this.drumSounds[i].setBuffer(new Tone.Buffer(url)); | ||
} | ||
} | ||
}; | ||
/** | ||
* 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) { | ||
this.drumSounds[drum].outputNode = outputNode; | ||
this.drumSounds[drum].start(); | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
play (drum, outputNode) { | ||
this.drumSounds[drum].outputNode = outputNode; | ||
this.drumSounds[drum].start(); | ||
} | ||
/** | ||
* Stop all drum sounds. | ||
*/ | ||
DrumPlayer.prototype.stopAll = function () { | ||
for (let i = 0; i < this.drumSounds.length; i++) { | ||
this.drumSounds[i].stop(); | ||
/** | ||
* Stop all drum sounds. | ||
*/ | ||
stopAll () { | ||
for (let i = 0; i < this.drumSounds.length; i++) { | ||
this.drumSounds[i].stop(); | ||
} | ||
} | ||
}; | ||
} | ||
module.exports = DrumPlayer; |
@@ -9,55 +9,49 @@ const Tone = require('tone'); | ||
* Clamped 0-100 | ||
* @constructor | ||
*/ | ||
const EchoEffect = function () { | ||
Tone.Effect.call(this); | ||
class EchoEffect extends Tone.Effect { | ||
constructor () { | ||
super(); | ||
this.value = 0; | ||
this.delay = new Tone.FeedbackDelay(0.25, 0.5); | ||
this.effectSend.chain(this.delay, this.effectReturn); | ||
} | ||
this.value = 0; | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
set (val) { | ||
this.value = this.clamp(val, 0, 100); | ||
this.delay = new Tone.FeedbackDelay(0.25, 0.5); | ||
// mute the effect if value is 0 | ||
if (this.value === 0) { | ||
this.wet.value = 0; | ||
} else { | ||
this.wet.value = 0.5; | ||
} | ||
this.effectSend.chain(this.delay, this.effectReturn); | ||
}; | ||
const feedback = (this.value / 100) * 0.75; | ||
this.delay.feedback.rampTo(feedback, 1 / 60); | ||
} | ||
Tone.extend(EchoEffect, Tone.Effect); | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
changeBy (val) { | ||
this.set(this.value + val); | ||
} | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
EchoEffect.prototype.set = function (val) { | ||
this.value = val; | ||
this.value = this.clamp(this.value, 0, 100); | ||
// mute the effect if value is 0 | ||
if (this.value === 0) { | ||
this.wet.value = 0; | ||
} else { | ||
this.wet.value = 0.5; | ||
/** | ||
* 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 | ||
* @return {number} the clamped value | ||
*/ | ||
clamp (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
} | ||
} | ||
const feedback = (this.value / 100) * 0.75; | ||
this.delay.feedback.rampTo(feedback, 1 / 60); | ||
}; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
EchoEffect.prototype.changeBy = function (val) { | ||
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 | ||
* @return {number} the clamped value | ||
*/ | ||
EchoEffect.prototype.clamp = function (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
}; | ||
module.exports = EchoEffect; |
@@ -8,46 +8,39 @@ const Tone = require('tone'); | ||
* Clamped 0-100 | ||
* @constructor | ||
*/ | ||
const FuzzEffect = function () { | ||
Tone.Effect.call(this); | ||
class FuzzEffect extends Tone.Effect { | ||
constructor () { | ||
super(); | ||
this.value = 0; | ||
this.distortion = new Tone.Distortion(1); | ||
this.effectSend.chain(this.distortion, this.effectReturn); | ||
} | ||
this.value = 0; | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
set (val) { | ||
this.value = this.clamp(val, 0, 100); | ||
this.distortion.wet.value = this.value / 100; | ||
} | ||
this.distortion = new Tone.Distortion(1); | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
changeBy (val) { | ||
this.set(this.value + val); | ||
} | ||
this.effectSend.chain(this.distortion, this.effectReturn); | ||
}; | ||
/** | ||
* @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 | ||
* @return {number} the clamped value | ||
*/ | ||
clamp (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
} | ||
} | ||
Tone.extend(FuzzEffect, Tone.Effect); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
FuzzEffect.prototype.set = function (val) { | ||
this.value = val; | ||
this.value = this.clamp(this.value, 0, 100); | ||
this.distortion.wet.value = this.value / 100; | ||
}; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
FuzzEffect.prototype.changeBy = function (val) { | ||
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 | ||
* @return {number} the clamped value | ||
*/ | ||
FuzzEffect.prototype.clamp = function (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
}; | ||
module.exports = FuzzEffect; |
@@ -8,47 +8,40 @@ const Tone = require('tone'); | ||
* Clamped -100 to 100 | ||
* @constructor | ||
*/ | ||
const PanEffect = function () { | ||
Tone.Effect.call(this); | ||
class PanEffect extends Tone.Effect { | ||
constructor () { | ||
super(); | ||
this.value = 0; | ||
this.panner = new Tone.Panner(); | ||
this.effectSend.chain(this.panner, this.effectReturn); | ||
} | ||
this.value = 0; | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
set (val) { | ||
this.value = this.clamp(val, -100, 100); | ||
this.panner.pan.value = this.value / 100; | ||
} | ||
this.panner = new Tone.Panner(); | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
changeBy (val) { | ||
this.set(this.value + val); | ||
} | ||
this.effectSend.chain(this.panner, this.effectReturn); | ||
}; | ||
/** | ||
* 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 | ||
* @return {number} the clamped value | ||
*/ | ||
clamp (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
} | ||
} | ||
Tone.extend(PanEffect, Tone.Effect); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
PanEffect.prototype.set = function (val) { | ||
this.value = val; | ||
this.value = this.clamp(this.value, -100, 100); | ||
this.panner.pan.value = this.value / 100; | ||
}; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
PanEffect.prototype.changeBy = function (val) { | ||
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 | ||
* @return {number} the clamped value | ||
*/ | ||
PanEffect.prototype.clamp = function (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
}; | ||
module.exports = PanEffect; |
@@ -19,64 +19,64 @@ const Tone = require('tone'); | ||
* on one SoundPlayer or a group of them. | ||
* @constructor | ||
*/ | ||
const PitchEffect = function () { | ||
this.value = 0; // effect value | ||
this.ratio = 1; // the playback rate ratio | ||
class PitchEffect { | ||
constructor () { | ||
this.value = 0; // effect value | ||
this.ratio = 1; // the playback rate ratio | ||
this.tone = new Tone(); | ||
} | ||
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 | ||
*/ | ||
set (val, players) { | ||
this.value = val; | ||
this.ratio = this.getRatio(this.value); | ||
this.updatePlayers(players); | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
changeBy (val, players) { | ||
this.set(this.value + val, 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) { | ||
this.set(this.value + val, players); | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
getRatio (val) { | ||
return this.tone.intervalToFrequencyRatio(val / 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 | ||
*/ | ||
updatePlayer (player) { | ||
player.setPlaybackRate(this.ratio); | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
updatePlayers (players) { | ||
if (!players) return; | ||
/** | ||
* 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; | ||
for (const md5 in players) { | ||
if (players.hasOwnProperty(md5)) { | ||
this.updatePlayer(players[md5]); | ||
for (const md5 in players) { | ||
if (players.hasOwnProperty(md5)) { | ||
this.updatePlayer(players[md5]); | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
module.exports = PitchEffect; |
@@ -8,47 +8,40 @@ const Tone = require('tone'); | ||
* Clamped 0 to 100 | ||
* @constructor | ||
*/ | ||
const ReverbEffect = function () { | ||
Tone.Effect.call(this); | ||
class ReverbEffect extends Tone.Effect { | ||
constructor () { | ||
super(); | ||
this.value = 0; | ||
this.reverb = new Tone.Freeverb(); | ||
this.effectSend.chain(this.reverb, this.effectReturn); | ||
} | ||
this.value = 0; | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
set (val) { | ||
this.value = this.clamp(val, 0, 100); | ||
this.reverb.wet.value = this.value / 100; | ||
} | ||
this.reverb = new Tone.Freeverb(); | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
changeBy (val) { | ||
this.set(this.value + val); | ||
} | ||
this.effectSend.chain(this.reverb, this.effectReturn); | ||
}; | ||
/** | ||
* 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 | ||
* @return {number} the clamped value | ||
*/ | ||
clamp (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
} | ||
} | ||
Tone.extend(ReverbEffect, Tone.Effect); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
ReverbEffect.prototype.set = function (val) { | ||
this.value = val; | ||
this.value = this.clamp(this.value, 0, 100); | ||
this.reverb.wet.value = this.value / 100; | ||
}; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
ReverbEffect.prototype.changeBy = function (val) { | ||
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 | ||
* @return {number} the clamped value | ||
*/ | ||
ReverbEffect.prototype.clamp = function (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
}; | ||
module.exports = ReverbEffect; |
@@ -12,57 +12,56 @@ const Tone = require('tone'); | ||
* Exterminate. | ||
* @constructor | ||
*/ | ||
const RoboticEffect = function () { | ||
Tone.Effect.call(this); | ||
class RoboticEffect extends Tone.Effect { | ||
constructor () { | ||
super(); | ||
this.value = 0; | ||
this.value = 0; | ||
const time = this._delayTimeForValue(100); | ||
this.feedbackCombFilter = new Tone.FeedbackCombFilter(time, 0.9); | ||
const time = this._delayTimeForValue(100); | ||
this.feedbackCombFilter = new Tone.FeedbackCombFilter(time, 0.9); | ||
this.effectSend.chain(this.feedbackCombFilter, this.effectReturn); | ||
}; | ||
this.effectSend.chain(this.feedbackCombFilter, this.effectReturn); | ||
} | ||
Tone.extend(RoboticEffect, Tone.Effect); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
set (val) { | ||
this.value = val; | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
RoboticEffect.prototype.set = function (val) { | ||
this.value = val; | ||
// mute the effect if value is 0 | ||
if (this.value === 0) { | ||
this.wet.value = 0; | ||
} else { | ||
this.wet.value = 1; | ||
} | ||
// mute the effect if value is 0 | ||
if (this.value === 0) { | ||
this.wet.value = 0; | ||
} else { | ||
this.wet.value = 1; | ||
// set delay time using the value | ||
const time = this._delayTimeForValue(this.value); | ||
this.feedbackCombFilter.delayTime.rampTo(time, 1 / 60); | ||
} | ||
// set delay time using the value | ||
const time = this._delayTimeForValue(this.value); | ||
this.feedbackCombFilter.delayTime.rampTo(time, 1 / 60); | ||
}; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
changeBy (val) { | ||
this.set(this.value + val); | ||
} | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
RoboticEffect.prototype.changeBy = function (val) { | ||
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 | ||
*/ | ||
_delayTimeForValue (val) { | ||
const midiNote = ((val - 100) / 10) + 36; | ||
const freq = Tone.Frequency(midiNote, 'midi').eval(); | ||
return 1 / freq; | ||
} | ||
} | ||
/** | ||
* 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) { | ||
const midiNote = ((val - 100) / 10) + 36; | ||
const freq = Tone.Frequency(midiNote, 'midi').eval(); | ||
return 1 / freq; | ||
}; | ||
module.exports = RoboticEffect; |
@@ -12,51 +12,50 @@ const Tone = require('tone'); | ||
* Clamped 0 to 100 | ||
* @constructor | ||
*/ | ||
const WobbleEffect = function () { | ||
Tone.Effect.call(this); | ||
class WobbleEffect extends Tone.Effect { | ||
constructor () { | ||
super(); | ||
this.value = 0; | ||
this.value = 0; | ||
this.wobbleLFO = new Tone.LFO(10, 0, 1).start(); | ||
this.wobbleGain = new Tone.Gain(); | ||
this.wobbleLFO.connect(this.wobbleGain.gain); | ||
this.wobbleLFO = new Tone.LFO(10, 0, 1).start(); | ||
this.wobbleGain = new Tone.Gain(); | ||
this.wobbleLFO.connect(this.wobbleGain.gain); | ||
this.effectSend.chain(this.wobbleGain, this.effectReturn); | ||
}; | ||
this.effectSend.chain(this.wobbleGain, this.effectReturn); | ||
} | ||
Tone.extend(WobbleEffect, Tone.Effect); | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
set (val) { | ||
this.value = val; | ||
/** | ||
* Set the effect value | ||
* @param {number} val - the new value to set the effect to | ||
*/ | ||
WobbleEffect.prototype.set = function (val) { | ||
this.value = val; | ||
this.value = this.clamp(this.value, 0, 100); | ||
this.value = this.clamp(this.value, 0, 100); | ||
this.wet.value = this.value / 100; | ||
this.wet.value = this.value / 100; | ||
this.wobbleLFO.frequency.rampTo(this.value / 10, 1 / 60); | ||
} | ||
this.wobbleLFO.frequency.rampTo(this.value / 10, 1 / 60); | ||
}; | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
changeBy (val) { | ||
this.set(this.value + val); | ||
} | ||
/** | ||
* Change the effect value | ||
* @param {number} val - the value to change the effect by | ||
*/ | ||
WobbleEffect.prototype.changeBy = function (val) { | ||
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 | ||
* @return {number} the clamped value | ||
*/ | ||
clamp (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
} | ||
} | ||
/** | ||
* 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 | ||
* @return {number} the clamped value | ||
*/ | ||
WobbleEffect.prototype.clamp = function (input, min, max) { | ||
return Math.min(Math.max(input, min), max); | ||
}; | ||
module.exports = WobbleEffect; |
550
src/index.js
@@ -22,327 +22,329 @@ const log = require('./log'); | ||
/** | ||
* 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} audioEngine AudioEngine for player | ||
* @constructor | ||
*/ | ||
const AudioPlayer = function (audioEngine) { | ||
class AudioPlayer { | ||
/** | ||
* 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} audioEngine AudioEngine for player | ||
* @constructor | ||
*/ | ||
constructor (audioEngine) { | ||
this.audioEngine = audioEngine; | ||
this.audioEngine = audioEngine; | ||
// effects setup | ||
this.pitchEffect = new PitchEffect(); | ||
this.panEffect = new PanEffect(); | ||
// effects setup | ||
this.pitchEffect = new PitchEffect(); | ||
this.panEffect = new PanEffect(); | ||
// the effects are chained to an effects node for this player, then to the main audio engine | ||
// audio is sent from each soundplayer, through the effects in order, then to the global effects | ||
// note that the pitch effect works differently - it sets the playback rate for each soundplayer | ||
this.effectsNode = new Tone.Gain(); | ||
this.effectsNode.chain(this.panEffect, this.audioEngine.input); | ||
// the effects are chained to an effects node for this player, then to the main audio engine | ||
// audio is sent from each soundplayer, through the effects in order, then to the global effects | ||
// note that the pitch effect works differently - it sets the playback rate for each soundplayer | ||
this.effectsNode = new Tone.Gain(); | ||
this.effectsNode.chain(this.panEffect, this.audioEngine.input); | ||
// reset effects to their default parameters | ||
this.clearEffects(); | ||
// reset effects to their default parameters | ||
this.clearEffects(); | ||
// sound players that are currently playing, indexed by the sound's md5 | ||
this.activeSoundPlayers = {}; | ||
} | ||
// sound players that are currently playing, indexed by the sound's md5 | ||
this.activeSoundPlayers = {}; | ||
}; | ||
/** | ||
* Play a sound | ||
* @param {string} md5 - the md5 id of a sound file | ||
* @return {Promise} a Promise that resolves when the sound finishes playing | ||
*/ | ||
playSound (md5) { | ||
// if this sound is not in the audio engine, return | ||
if (!this.audioEngine.audioBuffers[md5]) { | ||
return; | ||
} | ||
/** | ||
* 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; | ||
} | ||
// if this sprite or clone is already playing this sound, stop it first | ||
if (this.activeSoundPlayers[md5]) { | ||
this.activeSoundPlayers[md5].stop(); | ||
} | ||
// 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 | ||
const player = new SoundPlayer(); | ||
player.setBuffer(this.audioEngine.audioBuffers[md5]); | ||
player.connect(this.effectsNode); | ||
this.pitchEffect.updatePlayer(player); | ||
player.start(); | ||
// create a new soundplayer to play the sound | ||
const player = new SoundPlayer(); | ||
player.setBuffer(this.audioEngine.audioBuffers[md5]); | ||
player.connect(this.effectsNode); | ||
this.pitchEffect.updatePlayer(player); | ||
player.start(); | ||
// add it to the list of active sound players | ||
this.activeSoundPlayers[md5] = player; | ||
// add it to the list of active sound players | ||
this.activeSoundPlayers[md5] = player; | ||
// remove sounds that are not playing from the active sound players array | ||
for (const id in this.activeSoundPlayers) { | ||
if (this.activeSoundPlayers.hasOwnProperty(id)) { | ||
if (!this.activeSoundPlayers[id].isPlaying) { | ||
delete this.activeSoundPlayers[id]; | ||
// remove sounds that are not playing from the active sound players array | ||
for (const id in this.activeSoundPlayers) { | ||
if (this.activeSoundPlayers.hasOwnProperty(id)) { | ||
if (!this.activeSoundPlayers[id].isPlaying) { | ||
delete this.activeSoundPlayers[id]; | ||
} | ||
} | ||
} | ||
return player.finished(); | ||
} | ||
return player.finished(); | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
playDrumForBeats (drum, beats) { | ||
this.audioEngine.drumPlayer.play(drum, this.effectsNode); | ||
return this.audioEngine.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.audioEngine.waitForBeats(beats); | ||
}; | ||
/** | ||
* Stop all sounds, notes and drums that are playing | ||
*/ | ||
stopAllSounds () { | ||
// stop all active sound players | ||
for (const md5 in this.activeSoundPlayers) { | ||
this.activeSoundPlayers[md5].stop(); | ||
} | ||
/** | ||
* Stop all sounds, notes and drums that are playing | ||
*/ | ||
AudioPlayer.prototype.stopAllSounds = function () { | ||
// stop all active sound players | ||
for (const md5 in this.activeSoundPlayers) { | ||
this.activeSoundPlayers[md5].stop(); | ||
// stop all instruments | ||
this.audioEngine.instrumentPlayer.stopAll(); | ||
// stop drum notes | ||
this.audioEngine.drumPlayer.stopAll(); | ||
} | ||
// stop all instruments | ||
this.audioEngine.instrumentPlayer.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 | ||
*/ | ||
setEffect (effect, value) { | ||
switch (effect) { | ||
case this.audioEngine.EFFECT_NAMES.pitch: | ||
this.pitchEffect.set(value, this.activeSoundPlayers); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.pan: | ||
this.panEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.echo: | ||
this.audioEngine.echoEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.reverb: | ||
this.audioEngine.reverbEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.fuzz: | ||
this.audioEngine.fuzzEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.robot: | ||
this.audioEngine.roboticEffect.set(value); | ||
break; | ||
} | ||
} | ||
// stop drum notes | ||
this.audioEngine.drumPlayer.stopAll(); | ||
}; | ||
/** | ||
* Clear all audio effects | ||
*/ | ||
clearEffects () { | ||
this.panEffect.set(0); | ||
this.pitchEffect.set(0, this.activeSoundPlayers); | ||
this.effectsNode.gain.value = 1; | ||
/** | ||
* 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 this.audioEngine.EFFECT_NAMES.pitch: | ||
this.pitchEffect.set(value, this.activeSoundPlayers); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.pan: | ||
this.panEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.echo: | ||
this.audioEngine.echoEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.reverb: | ||
this.audioEngine.reverbEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.fuzz: | ||
this.audioEngine.fuzzEffect.set(value); | ||
break; | ||
case this.audioEngine.EFFECT_NAMES.robot: | ||
this.audioEngine.roboticEffect.set(value); | ||
break; | ||
this.audioEngine.echoEffect.set(0); | ||
this.audioEngine.reverbEffect.set(0); | ||
this.audioEngine.fuzzEffect.set(0); | ||
this.audioEngine.roboticEffect.set(0); | ||
} | ||
}; | ||
/** | ||
* Clear all audio effects | ||
*/ | ||
AudioPlayer.prototype.clearEffects = function () { | ||
this.panEffect.set(0); | ||
this.pitchEffect.set(0, this.activeSoundPlayers); | ||
this.effectsNode.gain.value = 1; | ||
/** | ||
* Set the volume for sounds played by this AudioPlayer | ||
* @param {number} value - the volume in range 0-100 | ||
*/ | ||
setVolume (value) { | ||
this.effectsNode.gain.value = value / 100; | ||
} | ||
} | ||
this.audioEngine.echoEffect.set(0); | ||
this.audioEngine.reverbEffect.set(0); | ||
this.audioEngine.fuzzEffect.set(0); | ||
this.audioEngine.roboticEffect.set(0); | ||
}; | ||
/** | ||
* Set the volume for sounds played by this AudioPlayer | ||
* @param {number} value - the volume in range 0-100 | ||
*/ | ||
AudioPlayer.prototype.setVolume = function (value) { | ||
this.effectsNode.gain.value = value / 100; | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
const AudioEngine = function () { | ||
class AudioEngine { | ||
constructor () { | ||
// create the global audio effects | ||
this.roboticEffect = new RoboticEffect(); | ||
this.fuzzEffect = new FuzzEffect(); | ||
this.echoEffect = new EchoEffect(); | ||
this.reverbEffect = new ReverbEffect(); | ||
// create the global audio effects | ||
this.roboticEffect = new RoboticEffect(); | ||
this.fuzzEffect = new FuzzEffect(); | ||
this.echoEffect = new EchoEffect(); | ||
this.reverbEffect = new ReverbEffect(); | ||
// chain the global effects to the output | ||
this.input = new Tone.Gain(); | ||
this.input.chain( | ||
this.roboticEffect, this.fuzzEffect, this.echoEffect, this.reverbEffect, | ||
Tone.Master | ||
); | ||
// chain the global effects to the output | ||
this.input = new Tone.Gain(); | ||
this.input.chain( | ||
this.roboticEffect, this.fuzzEffect, this.echoEffect, this.reverbEffect, | ||
Tone.Master | ||
); | ||
// global tempo in bpm (beats per minute) | ||
this.currentTempo = 60; | ||
// global tempo in bpm (beats per minute) | ||
this.currentTempo = 60; | ||
// instrument player for play note blocks | ||
this.instrumentPlayer = new InstrumentPlayer(this.input); | ||
this.numInstruments = this.instrumentPlayer.instrumentNames.length; | ||
// instrument player for play note blocks | ||
this.instrumentPlayer = new InstrumentPlayer(this.input); | ||
this.numInstruments = this.instrumentPlayer.instrumentNames.length; | ||
// drum player for play drum blocks | ||
this.drumPlayer = new DrumPlayer(this.input); | ||
this.numDrums = this.drumPlayer.drumSounds.length; | ||
// drum player for play drum blocks | ||
this.drumPlayer = new DrumPlayer(this.input); | ||
this.numDrums = this.drumPlayer.drumSounds.length; | ||
// a map of md5s to audio buffers, holding sounds for all sprites | ||
this.audioBuffers = {}; | ||
// a map of md5s to audio buffers, holding sounds for all sprites | ||
this.audioBuffers = {}; | ||
// microphone, for measuring loudness, with a level meter analyzer | ||
this.mic = null; | ||
this.micMeter = null; | ||
} | ||
// microphone, for measuring loudness, with a level meter analyzer | ||
this.mic = null; | ||
this.micMeter = null; | ||
}; | ||
/** | ||
* Names of the audio effects. | ||
* @enum {string} | ||
*/ | ||
get EFFECT_NAMES () { | ||
return { | ||
pitch: 'pitch', | ||
pan: 'pan', | ||
echo: 'echo', | ||
reverb: 'reverb', | ||
fuzz: 'fuzz', | ||
robot: 'robot' | ||
}; | ||
} | ||
/** | ||
* Decode a sound, decompressing it into audio samples. | ||
* Store a reference to it the sound in the audioBuffers dictionary, indexed by md5 | ||
* @param {object} sound - an object containing audio data and metadata for a sound | ||
* @property {Buffer} data - sound data loaded from scratch-storage. | ||
* @property {string} format - format type, either empty or adpcm. | ||
* @property {string} md5 - the MD5 and extension of the sound. | ||
* @returns {?Promise} - a promise which will resolve after the audio buffer is stored, or null on error. | ||
*/ | ||
AudioEngine.prototype.decodeSound = function (sound) { | ||
/** | ||
* Decode a sound, decompressing it into audio samples. | ||
* Store a reference to it the sound in the audioBuffers dictionary, indexed by md5 | ||
* @param {object} sound - an object containing audio data and metadata for a sound | ||
* @property {Buffer} data - sound data loaded from scratch-storage. | ||
* @property {string} format - format type, either empty or adpcm. | ||
* @property {string} md5 - the MD5 and extension of the sound. | ||
* @returns {?Promise} - a promise which will resolve after the audio buffer is stored, or null on error. | ||
*/ | ||
decodeSound (sound) { | ||
let loaderPromise = null; | ||
let loaderPromise = null; | ||
switch (sound.format) { | ||
case '': | ||
loaderPromise = Tone.context.decodeAudioData(sound.data.buffer); | ||
break; | ||
case 'adpcm': | ||
loaderPromise = (new ADPCMSoundDecoder()).decode(sound.data.buffer); | ||
break; | ||
default: | ||
return log.warn('unknown sound format', sound.format); | ||
} | ||
const storedContext = this; | ||
return loaderPromise.then( | ||
decodedAudio => { | ||
storedContext.audioBuffers[sound.md5] = new Tone.Buffer(decodedAudio); | ||
}, | ||
error => { | ||
log.warn('audio data could not be decoded', error); | ||
switch (sound.format) { | ||
case '': | ||
loaderPromise = Tone.context.decodeAudioData(sound.data.buffer); | ||
break; | ||
case 'adpcm': | ||
loaderPromise = (new ADPCMSoundDecoder()).decode(sound.data.buffer); | ||
break; | ||
default: | ||
return log.warn('unknown sound format', sound.format); | ||
} | ||
); | ||
}; | ||
/** | ||
* An older version of the AudioEngine had this function to load all sounds | ||
* This is a stub to provide a warning when it is called | ||
* @todo remove this | ||
*/ | ||
AudioEngine.prototype.loadSounds = function () { | ||
log.warn('The loadSounds function is no longer available. Please use Scratch Storage.'); | ||
}; | ||
const storedContext = this; | ||
return loaderPromise.then( | ||
decodedAudio => { | ||
storedContext.audioBuffers[sound.md5] = new Tone.Buffer(decodedAudio); | ||
}, | ||
error => { | ||
log.warn('audio data could not be decoded', error); | ||
} | ||
); | ||
} | ||
/** | ||
* Play a note for a duration on an instrument with a volume | ||
* @param {number} note - a MIDI note number | ||
* @param {number} beats - a duration in beats | ||
* @param {number} inst - an instrument number (0-indexed) | ||
* @param {number} vol - a volume level (0-100%) | ||
* @return {Promise} a Promise that resolves after the duration has elapsed | ||
*/ | ||
AudioEngine.prototype.playNoteForBeatsWithInstAndVol = function (note, beats, inst, vol) { | ||
const sec = this.beatsToSec(beats); | ||
this.instrumentPlayer.playNoteForSecWithInstAndVol(note, sec, inst, vol); | ||
return this.waitForBeats(beats); | ||
}; | ||
/** | ||
* An older version of the AudioEngine had this function to load all sounds | ||
* This is a stub to provide a warning when it is called | ||
* @todo remove this | ||
*/ | ||
loadSounds () { | ||
log.warn('The loadSounds function is no longer available. Please use Scratch Storage.'); | ||
} | ||
/** | ||
* Convert a number of beats to a number of seconds, using the current tempo | ||
* @param {number} beats number of beats to convert to secs | ||
* @return {number} seconds number of seconds `beats` will last | ||
*/ | ||
AudioEngine.prototype.beatsToSec = function (beats) { | ||
return (60 / this.currentTempo) * beats; | ||
}; | ||
/** | ||
* Play a note for a duration on an instrument with a volume | ||
* @param {number} note - a MIDI note number | ||
* @param {number} beats - a duration in beats | ||
* @param {number} inst - an instrument number (0-indexed) | ||
* @param {number} vol - a volume level (0-100%) | ||
* @return {Promise} a Promise that resolves after the duration has elapsed | ||
*/ | ||
playNoteForBeatsWithInstAndVol (note, beats, inst, vol) { | ||
const sec = this.beatsToSec(beats); | ||
this.instrumentPlayer.playNoteForSecWithInstAndVol(note, sec, inst, vol); | ||
return this.waitForBeats(beats); | ||
} | ||
/** | ||
* Wait for some number of beats | ||
* @param {number} beats number of beats to wait for | ||
* @return {Promise} a Promise that resolves after the duration has elapsed | ||
*/ | ||
AudioEngine.prototype.waitForBeats = function (beats) { | ||
const storedContext = this; | ||
return new Promise(resolve => { | ||
setTimeout(() => { | ||
resolve(); | ||
}, storedContext.beatsToSec(beats) * 1000); | ||
}); | ||
}; | ||
/** | ||
* Convert a number of beats to a number of seconds, using the current tempo | ||
* @param {number} beats number of beats to convert to secs | ||
* @return {number} seconds number of seconds `beats` will last | ||
*/ | ||
beatsToSec (beats) { | ||
return (60 / this.currentTempo) * beats; | ||
} | ||
/** | ||
* Set the global tempo in bpm (beats per minute) | ||
* @param {number} value - the new tempo to set | ||
*/ | ||
AudioEngine.prototype.setTempo = function (value) { | ||
this.currentTempo = value; | ||
}; | ||
/** | ||
* Wait for some number of beats | ||
* @param {number} beats number of beats to wait for | ||
* @return {Promise} a Promise that resolves after the duration has elapsed | ||
*/ | ||
waitForBeats (beats) { | ||
const storedContext = this; | ||
return new Promise(resolve => { | ||
setTimeout(() => { | ||
resolve(); | ||
}, storedContext.beatsToSec(beats) * 1000); | ||
}); | ||
} | ||
/** | ||
* 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) { | ||
this.setTempo(this.currentTempo + value); | ||
}; | ||
/** | ||
* Set the global tempo in bpm (beats per minute) | ||
* @param {number} value - the new tempo to set | ||
*/ | ||
setTempo (value) { | ||
this.currentTempo = value; | ||
} | ||
/** | ||
* Get the current loudness of sound received by the microphone. | ||
* Sound is measured in RMS and smoothed. | ||
* @return {number} loudness scaled 0 to 100 | ||
*/ | ||
AudioEngine.prototype.getLoudness = function () { | ||
if (!this.mic) { | ||
this.mic = new Tone.UserMedia(); | ||
this.micMeter = new Tone.Meter('level', 0.5); | ||
this.mic.open(); | ||
this.mic.connect(this.micMeter); | ||
/** | ||
* Change the tempo by some number of bpm (beats per minute) | ||
* @param {number} value - the number of bpm to change the tempo by | ||
*/ | ||
changeTempo (value) { | ||
this.setTempo(this.currentTempo + value); | ||
} | ||
if (this.mic && this.mic.state === 'started') { | ||
return this.micMeter.value * 100; | ||
} | ||
return -1; | ||
}; | ||
/** | ||
* Get the current loudness of sound received by the microphone. | ||
* Sound is measured in RMS and smoothed. | ||
* @return {number} loudness scaled 0 to 100 | ||
*/ | ||
getLoudness () { | ||
if (!this.mic) { | ||
this.mic = new Tone.UserMedia(); | ||
this.micMeter = new Tone.Meter('level', 0.5); | ||
this.mic.open(); | ||
this.mic.connect(this.micMeter); | ||
} | ||
if (this.mic && this.mic.state === 'started') { | ||
return this.micMeter.value * 100; | ||
} | ||
return -1; | ||
/** | ||
* 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} new AudioPlayer instance | ||
*/ | ||
AudioEngine.prototype.createPlayer = function () { | ||
return new AudioPlayer(this); | ||
}; | ||
/** | ||
* 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} new AudioPlayer instance | ||
*/ | ||
createPlayer () { | ||
return new AudioPlayer(this); | ||
} | ||
} | ||
module.exports = AudioEngine; |
const Tone = require('tone'); | ||
const 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 | ||
*/ | ||
const InstrumentPlayer = function (outputNode) { | ||
this.outputNode = outputNode; | ||
class InstrumentPlayer { | ||
/** | ||
* 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 | ||
*/ | ||
constructor (outputNode) { | ||
this.outputNode = outputNode; | ||
// Instrument names used by Musyng Kite soundfont, in order to | ||
// match scratch instruments | ||
this.instrumentNames = ['acoustic_grand_piano', 'electric_piano_1', | ||
'drawbar_organ', 'acoustic_guitar_nylon', 'electric_guitar_clean', | ||
'acoustic_bass', 'pizzicato_strings', 'cello', 'trombone', 'clarinet', | ||
'tenor_sax', 'flute', 'pan_flute', 'bassoon', 'choir_aahs', 'vibraphone', | ||
'music_box', 'steel_drums', 'marimba', 'lead_1_square', 'fx_4_atmosphere']; | ||
// Instrument names used by Musyng Kite soundfont, in order to | ||
// match scratch instruments | ||
this.instrumentNames = ['acoustic_grand_piano', 'electric_piano_1', | ||
'drawbar_organ', 'acoustic_guitar_nylon', 'electric_guitar_clean', | ||
'acoustic_bass', 'pizzicato_strings', 'cello', 'trombone', 'clarinet', | ||
'tenor_sax', 'flute', 'pan_flute', 'bassoon', 'choir_aahs', 'vibraphone', | ||
'music_box', 'steel_drums', 'marimba', 'lead_1_square', 'fx_4_atmosphere']; | ||
this.instruments = []; | ||
}; | ||
this.instruments = []; | ||
} | ||
/** | ||
* 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) | ||
* @param {number} vol - a volume level (0-100%) | ||
*/ | ||
InstrumentPlayer.prototype.playNoteForSecWithInstAndVol = function (note, sec, instrumentNum, vol) { | ||
const gain = vol / 100; | ||
this.loadInstrument(instrumentNum) | ||
.then(() => { | ||
this.instruments[instrumentNum].play( | ||
note, Tone.context.currentTime, { | ||
duration: sec, | ||
gain: gain | ||
} | ||
); | ||
}); | ||
}; | ||
/** | ||
* 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) | ||
* @param {number} vol - a volume level (0-100%) | ||
*/ | ||
playNoteForSecWithInstAndVol (note, sec, instrumentNum, vol) { | ||
const gain = vol / 100; | ||
this.loadInstrument(instrumentNum) | ||
.then(() => { | ||
this.instruments[instrumentNum].play( | ||
note, Tone.context.currentTime, { | ||
duration: sec, | ||
gain: gain | ||
} | ||
); | ||
}); | ||
} | ||
/** | ||
* 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) { | ||
if (this.instruments[instrumentNum]) { | ||
return Promise.resolve(); | ||
/** | ||
* 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 | ||
*/ | ||
loadInstrument (instrumentNum) { | ||
if (this.instruments[instrumentNum]) { | ||
return Promise.resolve(); | ||
} | ||
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) | ||
.then(inst => { | ||
inst.connect(this.outputNode); | ||
this.instruments[instrumentNum] = inst; | ||
}); | ||
} | ||
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) | ||
.then(inst => { | ||
inst.connect(this.outputNode); | ||
this.instruments[instrumentNum] = inst; | ||
}); | ||
}; | ||
/** | ||
* Stop all notes being played on all instruments | ||
*/ | ||
InstrumentPlayer.prototype.stopAll = function () { | ||
for (let i = 0; i < this.instruments.length; i++) { | ||
if (this.instruments[i]) { | ||
this.instruments[i].stop(); | ||
/** | ||
* Stop all notes being played on all instruments | ||
*/ | ||
stopAll () { | ||
for (let i = 0; i < this.instruments.length; i++) { | ||
if (this.instruments[i]) { | ||
this.instruments[i].stop(); | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
module.exports = InstrumentPlayer; |
@@ -6,82 +6,83 @@ const Tone = require('tone'); | ||
* A SoundPlayer stores an audio buffer, and plays it | ||
* @constructor | ||
*/ | ||
const SoundPlayer = function () { | ||
this.outputNode = null; | ||
this.buffer = new Tone.Buffer(); | ||
this.bufferSource = null; | ||
this.playbackRate = 1; | ||
this.isPlaying = false; | ||
}; | ||
class SoundPlayer { | ||
constructor () { | ||
this.outputNode = null; | ||
this.buffer = new Tone.Buffer(); | ||
this.bufferSource = null; | ||
this.playbackRate = 1; | ||
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; | ||
}; | ||
/** | ||
* Connect the SoundPlayer to an output node | ||
* @param {Tone.Gain} node - an output node to connect to | ||
*/ | ||
connect (node) { | ||
this.outputNode = node; | ||
} | ||
/** | ||
* Set an audio buffer | ||
* @param {Tone.Buffer} buffer Buffer to set | ||
*/ | ||
SoundPlayer.prototype.setBuffer = function (buffer) { | ||
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) { | ||
this.playbackRate = playbackRate; | ||
if (this.bufferSource && this.bufferSource.playbackRate) { | ||
this.bufferSource.playbackRate.value = this.playbackRate; | ||
/** | ||
* Set an audio buffer | ||
* @param {Tone.Buffer} buffer Buffer to set | ||
*/ | ||
setBuffer (buffer) { | ||
this.buffer = buffer; | ||
} | ||
}; | ||
/** | ||
* Stop the sound | ||
*/ | ||
SoundPlayer.prototype.stop = function () { | ||
if (this.bufferSource) { | ||
this.bufferSource.stop(); | ||
/** | ||
* 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. | ||
*/ | ||
setPlaybackRate (playbackRate) { | ||
this.playbackRate = playbackRate; | ||
if (this.bufferSource && this.bufferSource.playbackRate) { | ||
this.bufferSource.playbackRate.value = this.playbackRate; | ||
} | ||
} | ||
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 () { | ||
if (!this.buffer || !this.buffer.loaded) { | ||
log.warn('tried to play a sound that was not loaded yet'); | ||
return; | ||
/** | ||
* Stop the sound | ||
*/ | ||
stop () { | ||
if (this.bufferSource) { | ||
this.bufferSource.stop(); | ||
} | ||
this.isPlaying = false; | ||
} | ||
this.bufferSource = new Tone.BufferSource(this.buffer.get()); | ||
this.bufferSource.playbackRate.value = this.playbackRate; | ||
this.bufferSource.connect(this.outputNode); | ||
this.bufferSource.start(); | ||
/** | ||
* Start playing the sound | ||
* The web audio framework requires a new audio buffer source node for each playback | ||
*/ | ||
start () { | ||
if (!this.buffer || !this.buffer.loaded) { | ||
log.warn('tried to play a sound that was not loaded yet'); | ||
return; | ||
} | ||
this.isPlaying = true; | ||
}; | ||
this.bufferSource = new Tone.BufferSource(this.buffer.get()); | ||
this.bufferSource.playbackRate.value = this.playbackRate; | ||
this.bufferSource.connect(this.outputNode); | ||
this.bufferSource.start(); | ||
/** | ||
* 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 () { | ||
const storedContext = this; | ||
return new Promise(resolve => { | ||
storedContext.bufferSource.onended = function () { | ||
this.isPlaying = false; | ||
resolve(); | ||
}.bind(storedContext); | ||
}); | ||
}; | ||
this.isPlaying = true; | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
finished () { | ||
const storedContext = this; | ||
return new Promise(resolve => { | ||
storedContext.bufferSource.onended = function () { | ||
this.isPlaying = false; | ||
resolve(); | ||
}.bind(storedContext); | ||
}); | ||
} | ||
} | ||
module.exports = SoundPlayer; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
1220841
2.04%26332
1%