From 9874cf006f1d5d5b29933e68f4dd88a700839a73 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 17 Apr 2017 12:55:09 -0400 Subject: [PATCH 1/4] Update to use ES6 class syntax --- src/ADPCMSoundDecoder.js | 301 +++++++++--------- src/ArrayBufferStream.js | 202 +++++++------ src/DrumPlayer.js | 108 +++---- src/InstrumentPlayer.js | 136 +++++---- src/SoundPlayer.js | 143 ++++----- src/effects/EchoEffect.js | 88 +++--- src/effects/FuzzEffect.js | 71 ++--- src/effects/PanEffect.js | 73 ++--- src/effects/PitchEffect.js | 104 +++---- src/effects/ReverbEffect.js | 73 ++--- src/effects/RoboticEffect.js | 91 +++--- src/effects/WobbleEffect.js | 75 +++-- src/index.js | 572 ++++++++++++++++++----------------- 13 files changed, 1011 insertions(+), 1026 deletions(-) diff --git a/src/ADPCMSoundDecoder.js b/src/ADPCMSoundDecoder.js index f9e855e..7c39a28 100644 --- a/src/ADPCMSoundDecoder.js +++ b/src/ADPCMSoundDecoder.js @@ -8,161 +8,166 @@ const log = require('./log'); * 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 */ -const ADPCMSoundDecoder = function () {}; - -/** - * 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) { - - return new Promise((resolve, reject) => { - const stream = new ArrayBufferStream(audioData); - - const riffStr = stream.readUint8String(4); - if (riffStr !== 'RIFF') { - 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 wavStr = stream.readUint8String(4); - if (wavStr !== 'WAVE') { - log.warn('incorrect adpcm wav header'); - reject(); - } - - 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 samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); - - // @todo this line is the only place Tone is used here, should be possible to remove - const buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond); - - // @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; - } - - resolve(buffer); - }); -}; - -/** - * 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]; - -/** - * 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 - */ -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; - +class ADPCMSoundDecoder { + /** + * Data used by the decompression algorithm + * @type {Array} + */ + static get stepTable () { + 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 + ]; } -}; -/** - * 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 = []; + /** + * Data used by the decompression algorithm + * @type {Array} + */ + static get indexTable () { + return [ + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8 + ]; + } - // Bail and return no samples if we have no data - if (!compressedData) return out; + /** + * 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) { - 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) { - if (compressedData.getBytesAvailable() === 0) break; - lastByte = compressedData.readUint8(); - code = lastByte & 0xF; - } else { - code = (lastByte >> 4) & 0xF; - lastByte = -1; + return new Promise((resolve, reject) => { + const stream = new ArrayBufferStream(audioData); + + const riffStr = stream.readUint8String(4); + if (riffStr !== 'RIFF') { + log.warn('incorrect adpcm wav header'); + reject(); } - 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 lengthInHeader = stream.readInt32(); + if ((lengthInHeader + 8) !== audioData.byteLength) { + log.warn(`adpcm wav length in header: ${lengthInHeader} is incorrect`); + } + + const wavStr = stream.readUint8String(4); + if (wavStr !== 'WAVE') { + log.warn('incorrect adpcm wav header'); + reject(); + } + + 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 samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); + + // @todo this line is the only place Tone is used here, should be possible to remove + const buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond); + + // @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; + } + + resolve(buffer); + }); + } + + /** + * 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; + } } - const samples = Int16Array.from(out); - return samples; -}; + + /** + * 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; + + 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) { + if (compressedData.getBytesAvailable() === 0) break; + lastByte = compressedData.readUint8(); + code = lastByte & 0xF; + } else { + code = (lastByte >> 4) & 0xF; + lastByte = -1; + } + 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; + } +} module.exports = ADPCMSoundDecoder; diff --git a/src/ArrayBufferStream.js b/src/ArrayBufferStream.js index 1eb13d8..e96387e 100644 --- a/src/ArrayBufferStream.js +++ b/src/ArrayBufferStream.js @@ -1,107 +1,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; -}; - -/** - * 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 {number} the length of the stream in bytes - */ -ArrayBufferStream.prototype.getLength = function () { - 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); -}; - -/** - * 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 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]); +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 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; -}; + /** + * 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; + } -/** - * 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; -}; + /** + * @return {number} the length of the stream in bytes + */ + getLength () { + return this.arrayBuffer.byteLength; + } -/** - * 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; -}; + /** + * @return {number} the number of bytes available after the current position in the stream + */ + getBytesAvailable () { + return (this.arrayBuffer.byteLength - this.position); + } -/** - * 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 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 + */ + 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; + } + + /** + * 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 + */ + 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 + */ + 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 + */ + 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; diff --git a/src/DrumPlayer.js b/src/DrumPlayer.js index 1532c16..f4d0fde 100644 --- a/src/DrumPlayer.js +++ b/src/DrumPlayer.js @@ -1,64 +1,66 @@ 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(); -}; - -/** - * Stop all drum sounds. - */ -DrumPlayer.prototype.stopAll = function () { - for (let i = 0; i < this.drumSounds.length; i++) { - this.drumSounds[i].stop(); + /** + * 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. + */ + stopAll () { + for (let i = 0; i < this.drumSounds.length; i++) { + this.drumSounds[i].stop(); + } + } +} module.exports = DrumPlayer; diff --git a/src/InstrumentPlayer.js b/src/InstrumentPlayer.js index 95cb8a8..d7e3a77 100644 --- a/src/InstrumentPlayer.js +++ b/src/InstrumentPlayer.js @@ -1,80 +1,82 @@ 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 = []; -}; - -/** - * 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 - } - ); - }); -}; - -/** - * 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(); + this.instruments = []; } - return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) - .then(inst => { - inst.connect(this.outputNode); - this.instruments[instrumentNum] = inst; + + /** + * 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 + */ + 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; + }); -/** - * 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; diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index ed6e4c2..0c93ce0 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -3,84 +3,85 @@ const log = require('./log'); /** * 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 an audio buffer + * @param {Tone.Buffer} buffer Buffer to set + */ + setBuffer (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) { + /** + * 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; + } + } + + /** + * Stop the sound + */ + stop () { + 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 + */ + start () { + if (!this.buffer || !this.buffer.loaded) { + log.warn('tried to play a sound that was not loaded yet'); + return; + } + + this.bufferSource = new Tone.BufferSource(this.buffer.get()); this.bufferSource.playbackRate.value = this.playbackRate; - } -}; + this.bufferSource.connect(this.outputNode); + this.bufferSource.start(); -/** - * Stop the sound - */ -SoundPlayer.prototype.stop = function () { - 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 () { - 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(); - - 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 - */ -SoundPlayer.prototype.finished = function () { - const storedContext = this; - return new Promise(resolve => { - storedContext.bufferSource.onended = function () { - this.isPlaying = false; - resolve(); - }.bind(storedContext); - }); -}; + /** + * 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; diff --git a/src/effects/EchoEffect.js b/src/effects/EchoEffect.js index 95b2b81..7d6b106 100644 --- a/src/effects/EchoEffect.js +++ b/src/effects/EchoEffect.js @@ -6,57 +6,51 @@ const Tone = require('tone'); * Values up to 100 set the echo feedback amount, * increasing the time it takes the echo to fade away * Clamped 0-100 -* @constructor */ -const EchoEffect = function () { - Tone.Effect.call(this); - - this.value = 0; - - this.delay = new Tone.FeedbackDelay(0.25, 0.5); - - this.effectSend.chain(this.delay, this.effectReturn); -}; - -Tone.extend(EchoEffect, Tone.Effect); - -/** -* 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; +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); } - const feedback = (this.value / 100) * 0.75; - this.delay.feedback.rampTo(feedback, 1 / 60); -}; + /** + * Set the effect value + * @param {number} val - the new value to set the effect to + */ + set (val) { + this.value = this.clamp(val, 0, 100); -/** -* Change the effect value -* @param {number} val - the value to change the effect by -*/ -EchoEffect.prototype.changeBy = function (val) { - this.set(this.value + val); -}; + // 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 -*/ -EchoEffect.prototype.clamp = function (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 + */ + changeBy (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); + } +} module.exports = EchoEffect; diff --git a/src/effects/FuzzEffect.js b/src/effects/FuzzEffect.js index bae55b2..92ca148 100644 --- a/src/effects/FuzzEffect.js +++ b/src/effects/FuzzEffect.js @@ -5,48 +5,41 @@ const Tone = require('tone'); * Effect value controls the wet/dry amount: * 0 passes through none of the effect, 100 passes through all effect * 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); -}; - -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); -}; + /** + * @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); + } +} module.exports = FuzzEffect; diff --git a/src/effects/PanEffect.js b/src/effects/PanEffect.js index 03c29a3..b091600 100644 --- a/src/effects/PanEffect.js +++ b/src/effects/PanEffect.js @@ -5,49 +5,42 @@ const Tone = require('tone'); * 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 */ -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); -}; - -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); -}; + /** + * 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); + } +} module.exports = PanEffect; diff --git a/src/effects/PitchEffect.js b/src/effects/PitchEffect.js index f48e8f3..38f8fd1 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -16,66 +16,66 @@ const Tone = require('tone'); * 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 */ -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; diff --git a/src/effects/ReverbEffect.js b/src/effects/ReverbEffect.js index 43fa70c..d535d27 100644 --- a/src/effects/ReverbEffect.js +++ b/src/effects/ReverbEffect.js @@ -5,49 +5,42 @@ const Tone = require('tone'); * Effect value controls the wet/dry amount: * 0 passes through none of the effect, 100 passes through all effect * 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); -}; - -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); -}; + /** + * 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); + } +} module.exports = ReverbEffect; diff --git a/src/effects/RoboticEffect.js b/src/effects/RoboticEffect.js index b8ebc9a..3d467a4 100644 --- a/src/effects/RoboticEffect.js +++ b/src/effects/RoboticEffect.js @@ -9,59 +9,58 @@ const Tone = require('tone'); * 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 */ -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); -}; - -Tone.extend(RoboticEffect, Tone.Effect); - -/** -* 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; + this.effectSend.chain(this.feedbackCombFilter, this.effectReturn); } - // set delay time using the value - const time = this._delayTimeForValue(this.value); - this.feedbackCombFilter.delayTime.rampTo(time, 1 / 60); -}; + /** + * Set the effect value + * @param {number} val - the new value to set the effect to + */ + set (val) { + 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); -}; + // mute the effect if value is 0 + if (this.value === 0) { + this.wet.value = 0; + } else { + this.wet.value = 1; + } -/** -* 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; -}; + // 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); + } + + /** + * 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; + } +} module.exports = RoboticEffect; diff --git a/src/effects/WobbleEffect.js b/src/effects/WobbleEffect.js index 9b6b4d8..0738016 100644 --- a/src/effects/WobbleEffect.js +++ b/src/effects/WobbleEffect.js @@ -9,53 +9,52 @@ const Tone = require('tone'); * 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 */ -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 -*/ -WobbleEffect.prototype.clamp = function (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 + */ + clamp (input, min, max) { + return Math.min(Math.max(input, min), max); + } +} module.exports = WobbleEffect; diff --git a/src/index.js b/src/index.js index 7d20af5..3b440ac 100644 --- a/src/index.js +++ b/src/index.js @@ -19,329 +19,331 @@ const DrumPlayer = require('./DrumPlayer'); * that handles global functionality, and AudioPlayers, belonging to individual sprites and clones. */ -/** - * 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 = {}; -}; - -/** - * 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; + // sound players that are currently playing, indexed by the sound's md5 + this.activeSoundPlayers = {}; } - // if this sprite or clone is already playing this sound, stop it first - if (this.activeSoundPlayers[md5]) { - this.activeSoundPlayers[md5].stop(); - } + /** + * 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; + } - // 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(); + // if this sprite or clone is already playing this sound, stop it first + if (this.activeSoundPlayers[md5]) { + this.activeSoundPlayers[md5].stop(); + } - // add it to the list of active sound players - this.activeSoundPlayers[md5] = player; + // 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(); - // 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]; + // 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]; + } } } + + 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); + } + + /** + * 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 instruments + this.audioEngine.instrumentPlayer.stopAll(); + + // stop drum notes + 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 + */ + 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; + } } - return player.finished(); -}; + /** + * Clear all audio effects + */ + clearEffects () { + this.panEffect.set(0); + this.pitchEffect.set(0, this.activeSoundPlayers); + this.effectsNode.gain.value = 1; -/** - * 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 - */ -AudioPlayer.prototype.stopAllSounds = function () { - // stop all active sound players - for (const md5 in this.activeSoundPlayers) { - this.activeSoundPlayers[md5].stop(); + this.audioEngine.echoEffect.set(0); + this.audioEngine.reverbEffect.set(0); + this.audioEngine.fuzzEffect.set(0); + this.audioEngine.roboticEffect.set(0); } - // stop all instruments - this.audioEngine.instrumentPlayer.stopAll(); - - // stop drum notes - 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 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; + /** + * 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; } -}; - -/** - * Clear all audio effects - */ -AudioPlayer.prototype.clearEffects = function () { - this.panEffect.set(0); - this.pitchEffect.set(0, this.activeSoundPlayers); - this.effectsNode.gain.value = 1; - - 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; -}; - -/** - * 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) { - - 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); + // microphone, for measuring loudness, with a level meter analyzer + this.mic = null; + this.micMeter = null; } - 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); + /** + * Names of the audio effects. + * @enum {string} + */ + static 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. + */ + decodeSound (sound) { + + 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); } - ); -}; -/** - * 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.'); -}; - -/** - * 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); -}; - -/** - * 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; -}; - -/** - * 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); - }); -}; - -/** - * 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; -}; - -/** - * 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); -}; - -/** - * 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); + 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); + } + ); } - if (this.mic && this.mic.state === 'started') { - return this.micMeter.value * 100; + + /** + * 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.'); } - return -1; -}; + /** + * 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); + } -/** - * Names of the audio effects. - * @readonly - * @enum {string} - */ -AudioEngine.prototype.EFFECT_NAMES = { - pitch: 'pitch', - pan: 'pan', - echo: 'echo', - reverb: 'reverb', - fuzz: 'fuzz', - robot: 'robot' -}; + /** + * 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; + } -/** - * 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); -}; + /** + * 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); + }); + } + + /** + * Set the global tempo in bpm (beats per minute) + * @param {number} value - the new tempo to set + */ + setTempo (value) { + 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 + */ + changeTempo (value) { + this.setTempo(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 + */ + 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; + + } + + /** + * 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; From bdd6f55dcc5d2a10756a60ae660169c068701cbb Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 17 Apr 2017 20:25:05 -0400 Subject: [PATCH 2/4] Update Travis to Node 6 LTS For compatibility with ES6 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 77142b3..d8690a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js node_js: -- '4.2' -- 'stable' +- 6 +- node cache: directories: - node_modules From fa879cee9d2337e3d139e7016fb75d8b4bf1545b Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Tue, 18 Apr 2017 17:59:37 -0400 Subject: [PATCH 3/4] Fix `static` getters Thanks @ericrosenbaum! --- src/ADPCMSoundDecoder.js | 8 ++++---- src/index.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ADPCMSoundDecoder.js b/src/ADPCMSoundDecoder.js index 7c39a28..b8198e5 100644 --- a/src/ADPCMSoundDecoder.js +++ b/src/ADPCMSoundDecoder.js @@ -14,7 +14,7 @@ class ADPCMSoundDecoder { * Data used by the decompression algorithm * @type {Array} */ - static get stepTable () { + 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, @@ -29,7 +29,7 @@ class ADPCMSoundDecoder { * Data used by the decompression algorithm * @type {Array} */ - static get indexTable () { + static get INDEX_TABLE () { return [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 @@ -148,14 +148,14 @@ class ADPCMSoundDecoder { code = (lastByte >> 4) & 0xF; lastByte = -1; } - step = this.stepTable[index]; + 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 += this.indexTable[code]; + index += ADPCMSoundDecoder.INDEX_TABLE[code]; if (index > 88) index = 88; if (index < 0) index = 0; // compute and output sample diff --git a/src/index.js b/src/index.js index 3b440ac..bb544b0 100644 --- a/src/index.js +++ b/src/index.js @@ -208,7 +208,7 @@ class AudioEngine { * Names of the audio effects. * @enum {string} */ - static get EFFECT_NAMES () { + get EFFECT_NAMES () { return { pitch: 'pitch', pan: 'pan', From ea3c4a47f227010ae2d69c1d0bb2ba5877c068f3 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Tue, 18 Apr 2017 18:05:36 -0400 Subject: [PATCH 4/4] Fix `while` condition, disable eslint complaint --- src/ADPCMSoundDecoder.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ADPCMSoundDecoder.js b/src/ADPCMSoundDecoder.js index b8198e5..e9e0b00 100644 --- a/src/ADPCMSoundDecoder.js +++ b/src/ADPCMSoundDecoder.js @@ -129,8 +129,9 @@ class ADPCMSoundDecoder { if (!compressedData) return out; compressedData.position = 0; - const a = 0; - while (a === 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; sample = compressedData.readInt16();