Update to use ES6 class syntax

This commit is contained in:
Ray Schamp 2017-04-17 12:55:09 -04:00
parent 5007c43ddf
commit 9874cf006f
13 changed files with 1011 additions and 1026 deletions

View file

@ -8,9 +8,33 @@ 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 () {};
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
];
}
/**
* 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
];
}
/**
* Decode an ADPCM sound stored in an ArrayBuffer and return a promise
@ -18,7 +42,7 @@ const ADPCMSoundDecoder = function () {};
* @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio
* @return {Tone.Buffer} the decoded audio buffer
*/
ADPCMSoundDecoder.prototype.decode = function (audioData) {
decode (audioData) {
return new Promise((resolve, reject) => {
const stream = new ArrayBufferStream(audioData);
@ -63,27 +87,7 @@ ADPCMSoundDecoder.prototype.decode = function (audioData) {
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
@ -91,7 +95,7 @@ ADPCMSoundDecoder.prototype.indexTable = [
* @param {ArrayBufferStream} stream - an stream containing the audio data
* @return {ArrayBufferStream} a stream containing the desired chunk
*/
ADPCMSoundDecoder.prototype.extractChunk = function (chunkType, stream) {
extractChunk (chunkType, stream) {
stream.position = 12;
while (stream.position < (stream.getLength() - 8)) {
const typeStr = stream.readUint8String(4);
@ -103,7 +107,7 @@ ADPCMSoundDecoder.prototype.extractChunk = function (chunkType, stream) {
stream.position += chunkSize;
}
};
}
/**
* Decompress sample data using the IMA ADPCM algorithm.
@ -112,7 +116,7 @@ ADPCMSoundDecoder.prototype.extractChunk = function (chunkType, stream) {
* @param {number} blockSize - the number of bytes in the stream
* @return {Int16Array} the uncompressed audio samples
*/
ADPCMSoundDecoder.prototype.imaDecompress = function (compressedData, blockSize) {
imaDecompress (compressedData, blockSize) {
let sample;
let step;
let code;
@ -163,6 +167,7 @@ ADPCMSoundDecoder.prototype.imaDecompress = function (compressedData, blockSize)
}
const samples = Int16Array.from(out);
return samples;
};
}
}
module.exports = ADPCMSoundDecoder;

View file

@ -1,3 +1,4 @@
class ArrayBufferStream {
/**
* ArrayBufferStream wraps the built-in javascript ArrayBuffer, adding the ability to access
* data in it like a stream, tracking its position.
@ -8,45 +9,45 @@
* @param {ArrayBuffer} arrayBuffer - array to use as a stream
* @constructor
*/
const ArrayBufferStream = function (arrayBuffer) {
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) {
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 () {
getLength () {
return this.arrayBuffer.byteLength;
};
}
/**
* @return {number} the number of bytes available after the current position in the stream
*/
ArrayBufferStream.prototype.getBytesAvailable = function () {
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 () {
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.
@ -54,7 +55,7 @@ ArrayBufferStream.prototype.readUint8 = function () {
* @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) {
readUint8String (length) {
const arr = new Uint8Array(this.arrayBuffer, this.position, length);
this.position += length;
let str = '';
@ -62,46 +63,47 @@ ArrayBufferStream.prototype.readUint8String = function (length) {
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
*/
ArrayBufferStream.prototype.readInt16 = function () {
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 () {
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 () {
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 () {
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;

View file

@ -1,12 +1,13 @@
const SoundPlayer = require('./SoundPlayer');
const Tone = require('tone');
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
*/
const DrumPlayer = function (outputNode) {
constructor (outputNode) {
this.outputNode = outputNode;
const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/';
@ -38,7 +39,7 @@ const DrumPlayer = function (outputNode) {
this.drumSounds[i] = new SoundPlayer(this.outputNode);
this.drumSounds[i].setBuffer(new Tone.Buffer(url));
}
};
}
/**
* Play a drum sound.
@ -47,18 +48,19 @@ const DrumPlayer = function (outputNode) {
* @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) {
play (drum, outputNode) {
this.drumSounds[drum].outputNode = outputNode;
this.drumSounds[drum].start();
};
}
/**
* Stop all drum sounds.
*/
DrumPlayer.prototype.stopAll = function () {
stopAll () {
for (let i = 0; i < this.drumSounds.length; i++) {
this.drumSounds[i].stop();
}
};
}
}
module.exports = DrumPlayer;

View file

@ -1,6 +1,7 @@
const Tone = require('tone');
const Soundfont = require('soundfont-player');
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
@ -12,7 +13,7 @@ const Soundfont = require('soundfont-player');
* @param {Tone.Gain} outputNode - a webAudio node that the instrument will send its output to
* @constructor
*/
const InstrumentPlayer = function (outputNode) {
constructor (outputNode) {
this.outputNode = outputNode;
// Instrument names used by Musyng Kite soundfont, in order to
@ -24,7 +25,7 @@ const InstrumentPlayer = function (outputNode) {
'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.
@ -36,7 +37,7 @@ const InstrumentPlayer = function (outputNode) {
* @param {number} instrumentNum - an instrument number (0-indexed)
* @param {number} vol - a volume level (0-100%)
*/
InstrumentPlayer.prototype.playNoteForSecWithInstAndVol = function (note, sec, instrumentNum, vol) {
playNoteForSecWithInstAndVol (note, sec, instrumentNum, vol) {
const gain = vol / 100;
this.loadInstrument(instrumentNum)
.then(() => {
@ -47,14 +48,14 @@ InstrumentPlayer.prototype.playNoteForSecWithInstAndVol = function (note, sec, i
}
);
});
};
}
/**
* 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) {
loadInstrument (instrumentNum) {
if (this.instruments[instrumentNum]) {
return Promise.resolve();
}
@ -64,17 +65,18 @@ InstrumentPlayer.prototype.loadInstrument = function (instrumentNum) {
this.instruments[instrumentNum] = inst;
});
};
}
/**
* Stop all notes being played on all instruments
*/
InstrumentPlayer.prototype.stopAll = function () {
stopAll () {
for (let i = 0; i < this.instruments.length; i++) {
if (this.instruments[i]) {
this.instruments[i].stop();
}
}
};
}
}
module.exports = InstrumentPlayer;

View file

@ -3,58 +3,58 @@ const log = require('./log');
/**
* A SoundPlayer stores an audio buffer, and plays it
* @constructor
*/
const SoundPlayer = function () {
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) {
connect (node) {
this.outputNode = node;
};
}
/**
* Set an audio buffer
* @param {Tone.Buffer} buffer Buffer to set
*/
SoundPlayer.prototype.setBuffer = function (buffer) {
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) {
setPlaybackRate (playbackRate) {
this.playbackRate = playbackRate;
if (this.bufferSource && this.bufferSource.playbackRate) {
this.bufferSource.playbackRate.value = this.playbackRate;
}
};
}
/**
* Stop the sound
*/
SoundPlayer.prototype.stop = function () {
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
*/
SoundPlayer.prototype.start = function () {
start () {
if (!this.buffer || !this.buffer.loaded) {
log.warn('tried to play a sound that was not loaded yet');
return;
@ -66,14 +66,14 @@ SoundPlayer.prototype.start = function () {
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 () {
finished () {
const storedContext = this;
return new Promise(resolve => {
storedContext.bufferSource.onended = function () {
@ -81,6 +81,7 @@ SoundPlayer.prototype.finished = function () {
resolve();
}.bind(storedContext);
});
};
}
}
module.exports = SoundPlayer;

View file

@ -6,28 +6,21 @@ 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);
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);
};
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);
set (val) {
this.value = this.clamp(val, 0, 100);
// mute the effect if value is 0
if (this.value === 0) {
@ -38,15 +31,15 @@ EchoEffect.prototype.set = function (val) {
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) {
changeBy (val) {
this.set(this.value + val);
};
}
/**
* Clamp the input to a range
@ -55,8 +48,9 @@ EchoEffect.prototype.changeBy = function (val) {
* @param {number} max - the max value to clamp to
* @return {number} the clamped value
*/
EchoEffect.prototype.clamp = function (input, min, max) {
clamp (input, min, max) {
return Math.min(Math.max(input, min), max);
};
}
}
module.exports = EchoEffect;

View file

@ -5,39 +5,31 @@ 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);
};
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);
set (val) {
this.value = this.clamp(val, 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) {
changeBy (val) {
this.set(this.value + val);
};
}
/**
* @param {number} input - the input to clamp
@ -45,8 +37,9 @@ FuzzEffect.prototype.changeBy = function (val) {
* @param {number} max - the max value to clamp to
* @return {number} the clamped value
*/
FuzzEffect.prototype.clamp = function (input, min, max) {
clamp (input, min, max) {
return Math.min(Math.max(input, min), max);
};
}
}
module.exports = FuzzEffect;

View file

@ -5,39 +5,31 @@ 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);
};
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);
set (val) {
this.value = this.clamp(val, -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) {
changeBy (val) {
this.set(this.value + val);
};
}
/**
* Clamp the input to a range
@ -46,8 +38,9 @@ PanEffect.prototype.changeBy = function (val) {
* @param {number} max - the max value to clamp to
* @return {number} the clamped value
*/
PanEffect.prototype.clamp = function (input, min, max) {
clamp (input, min, max) {
return Math.min(Math.max(input, min), max);
};
}
}
module.exports = PanEffect;

View file

@ -16,34 +16,33 @@ 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 () {
class PitchEffect {
constructor () {
this.value = 0; // effect value
this.ratio = 1; // the playback rate ratio
this.tone = new Tone();
};
}
/**
* Set the effect value
* @param {number} val - the new value to set the effect to
* @param {object} players - a dictionary of SoundPlayer objects to apply the effect to, indexed by md5
*/
PitchEffect.prototype.set = function (val, players) {
set (val, players) {
this.value = val;
this.ratio = this.getRatio(this.value);
this.updatePlayers(players);
};
}
/**
* Change the effect value
* @param {number} val - the value to change the effect by
* @param {object} players - a dictionary of SoundPlayer objects indexed by md5
*/
PitchEffect.prototype.changeBy = function (val, players) {
changeBy (val, players) {
this.set(this.value + val, players);
};
}
/**
* Compute the playback ratio for an effect value.
@ -52,23 +51,23 @@ PitchEffect.prototype.changeBy = function (val, players) {
* @param {number} val - an effect value
* @returns {number} a playback ratio
*/
PitchEffect.prototype.getRatio = function (val) {
getRatio (val) {
return this.tone.intervalToFrequencyRatio(val / 10);
};
}
/**
* Update a sound player's playback rate using the current ratio for the effect
* @param {object} player - a SoundPlayer object
*/
PitchEffect.prototype.updatePlayer = function (player) {
updatePlayer (player) {
player.setPlaybackRate(this.ratio);
};
}
/**
* Update a sound player's playback rate using the current ratio for the effect
* @param {object} players - a dictionary of SoundPlayer objects to update, indexed by md5
*/
PitchEffect.prototype.updatePlayers = function (players) {
updatePlayers (players) {
if (!players) return;
for (const md5 in players) {
@ -76,6 +75,7 @@ PitchEffect.prototype.updatePlayers = function (players) {
this.updatePlayer(players[md5]);
}
}
};
}
}
module.exports = PitchEffect;

View file

@ -5,39 +5,31 @@ 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);
};
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);
set (val) {
this.value = this.clamp(val, 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) {
changeBy (val) {
this.set(this.value + val);
};
}
/**
* Clamp the input to a range
@ -46,8 +38,9 @@ ReverbEffect.prototype.changeBy = function (val) {
* @param {number} max - the max value to clamp to
* @return {number} the clamped value
*/
ReverbEffect.prototype.clamp = function (input, min, max) {
clamp (input, min, max) {
return Math.min(Math.max(input, min), max);
};
}
}
module.exports = ReverbEffect;

View file

@ -9,10 +9,10 @@ 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;
@ -20,15 +20,13 @@ const RoboticEffect = function () {
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) {
set (val) {
this.value = val;
// mute the effect if value is 0
@ -41,15 +39,15 @@ RoboticEffect.prototype.set = function (val) {
// 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
*/
RoboticEffect.prototype.changeBy = function (val) {
changeBy (val) {
this.set(this.value + val);
};
}
/**
* Compute the delay time for an effect value.
@ -58,10 +56,11 @@ RoboticEffect.prototype.changeBy = function (val) {
* @param {number} val - the effect value
* @returns {number} a delay time in seconds
*/
RoboticEffect.prototype._delayTimeForValue = function (val) {
_delayTimeForValue (val) {
const midiNote = ((val - 100) / 10) + 36;
const freq = Tone.Frequency(midiNote, 'midi').eval();
return 1 / freq;
};
}
}
module.exports = RoboticEffect;

View file

@ -9,10 +9,10 @@ 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;
@ -21,15 +21,13 @@ const WobbleEffect = function () {
this.wobbleLFO.connect(this.wobbleGain.gain);
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
*/
WobbleEffect.prototype.set = function (val) {
set (val) {
this.value = val;
this.value = this.clamp(this.value, 0, 100);
@ -37,15 +35,15 @@ WobbleEffect.prototype.set = function (val) {
this.wet.value = this.value / 100;
this.wobbleLFO.frequency.rampTo(this.value / 10, 1 / 60);
};
}
/**
* Change the effect value
* @param {number} val - the value to change the effect by
*/
WobbleEffect.prototype.changeBy = function (val) {
changeBy (val) {
this.set(this.value + val);
};
}
/**
* Clamp the input to a range
@ -54,8 +52,9 @@ WobbleEffect.prototype.changeBy = function (val) {
* @param {number} max - the max value to clamp to
* @return {number} the clamped value
*/
WobbleEffect.prototype.clamp = function (input, min, max) {
clamp (input, min, max) {
return Math.min(Math.max(input, min), max);
};
}
}
module.exports = WobbleEffect;

View file

@ -19,6 +19,7 @@ const DrumPlayer = require('./DrumPlayer');
* that handles global functionality, and AudioPlayers, belonging to individual sprites and clones.
*/
class AudioPlayer {
/**
* Each sprite or clone has an audio player
* the audio player handles sound playback, volume, and the sprite-specific audio effects:
@ -26,8 +27,7 @@ const DrumPlayer = require('./DrumPlayer');
* @param {AudioEngine} audioEngine AudioEngine for player
* @constructor
*/
const AudioPlayer = function (audioEngine) {
constructor (audioEngine) {
this.audioEngine = audioEngine;
// effects setup
@ -45,14 +45,14 @@ const AudioPlayer = function (audioEngine) {
// 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) {
playSound (md5) {
// if this sound is not in the audio engine, return
if (!this.audioEngine.audioBuffers[md5]) {
return;
@ -83,7 +83,7 @@ AudioPlayer.prototype.playSound = function (md5) {
}
return player.finished();
};
}
/**
* Play a drum sound. The AudioEngine contains the DrumPlayer, but the AudioPlayer
@ -92,15 +92,15 @@ AudioPlayer.prototype.playSound = function (md5) {
* @param {number} beats - a duration in beats
* @return {Promise} a Promise that resolves after the duration has elapsed
*/
AudioPlayer.prototype.playDrumForBeats = function (drum, beats) {
playDrumForBeats (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 () {
stopAllSounds () {
// stop all active sound players
for (const md5 in this.activeSoundPlayers) {
this.activeSoundPlayers[md5].stop();
@ -111,14 +111,14 @@ AudioPlayer.prototype.stopAllSounds = function () {
// 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) {
setEffect (effect, value) {
switch (effect) {
case this.audioEngine.EFFECT_NAMES.pitch:
this.pitchEffect.set(value, this.activeSoundPlayers);
@ -139,12 +139,12 @@ AudioPlayer.prototype.setEffect = function (effect, value) {
this.audioEngine.roboticEffect.set(value);
break;
}
};
}
/**
* Clear all audio effects
*/
AudioPlayer.prototype.clearEffects = function () {
clearEffects () {
this.panEffect.set(0);
this.pitchEffect.set(0, this.activeSoundPlayers);
this.effectsNode.gain.value = 1;
@ -153,25 +153,25 @@ AudioPlayer.prototype.clearEffects = function () {
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) {
setVolume (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();
@ -202,7 +202,22 @@ const AudioEngine = function () {
// microphone, for measuring loudness, with a level meter analyzer
this.mic = null;
this.micMeter = null;
}
/**
* 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.
@ -213,7 +228,7 @@ const AudioEngine = function () {
* @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) {
decodeSound (sound) {
let loaderPromise = null;
@ -237,16 +252,16 @@ AudioEngine.prototype.decodeSound = function (sound) {
log.warn('audio data could not be decoded', error);
}
);
};
}
/**
* 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 () {
loadSounds () {
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
@ -256,57 +271,57 @@ AudioEngine.prototype.loadSounds = function () {
* @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) {
playNoteForBeatsWithInstAndVol (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) {
beatsToSec (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) {
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
*/
AudioEngine.prototype.setTempo = function (value) {
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
*/
AudioEngine.prototype.changeTempo = function (value) {
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
*/
AudioEngine.prototype.getLoudness = function () {
getLoudness () {
if (!this.mic) {
this.mic = new Tone.UserMedia();
this.micMeter = new Tone.Meter('level', 0.5);
@ -318,21 +333,7 @@ AudioEngine.prototype.getLoudness = function () {
}
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.
@ -340,8 +341,9 @@ AudioEngine.prototype.EFFECT_NAMES = {
* functionality such as playing notes.
* @return {AudioPlayer} new AudioPlayer instance
*/
AudioEngine.prototype.createPlayer = function () {
createPlayer () {
return new AudioPlayer(this);
};
}
}
module.exports = AudioEngine;