scratch-audio
Advanced tools
Comparing version 0.1.0-prerelease.1492442591 to 0.1.0-prerelease.1492531432
{ | ||
"name": "scratch-audio", | ||
"version": "0.1.0-prerelease.1492442591", | ||
"version": "0.1.0-prerelease.1492531432", | ||
"description": "audio engine for scratch 3.0", | ||
@@ -29,2 +29,3 @@ "main": "dist.js", | ||
"eslint": "^3.19.0", | ||
"eslint-config-scratch": "^3.1.0", | ||
"json": "^9.0.6", | ||
@@ -31,0 +32,0 @@ "minilog": "^3.0.1", |
@@ -1,4 +0,4 @@ | ||
var ArrayBufferStream = require('./ArrayBufferStream'); | ||
var Tone = require('tone'); | ||
var log = require('./log'); | ||
const ArrayBufferStream = require('./ArrayBufferStream'); | ||
const Tone = require('tone'); | ||
const log = require('./log'); | ||
@@ -13,4 +13,3 @@ /** | ||
*/ | ||
function ADPCMSoundDecoder () { | ||
} | ||
const ADPCMSoundDecoder = function () {}; | ||
@@ -21,11 +20,11 @@ /** | ||
* @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio | ||
* @return {Tone.Buffer} | ||
* @return {Tone.Buffer} the decoded audio buffer | ||
*/ | ||
ADPCMSoundDecoder.prototype.decode = function (audioData) { | ||
return new Promise(function (resolve, reject) { | ||
var stream = new ArrayBufferStream(audioData); | ||
return new Promise((resolve, reject) => { | ||
const stream = new ArrayBufferStream(audioData); | ||
var riffStr = stream.readUint8String(4); | ||
if (riffStr != 'RIFF') { | ||
const riffStr = stream.readUint8String(4); | ||
if (riffStr !== 'RIFF') { | ||
log.warn('incorrect adpcm wav header'); | ||
@@ -35,9 +34,9 @@ reject(); | ||
var lengthInHeader = stream.readInt32(); | ||
if ((lengthInHeader + 8) != audioData.byteLength) { | ||
log.warn('adpcm wav length in header: ' + lengthInHeader + ' is incorrect'); | ||
const lengthInHeader = stream.readInt32(); | ||
if ((lengthInHeader + 8) !== audioData.byteLength) { | ||
log.warn(`adpcm wav length in header: ${lengthInHeader} is incorrect`); | ||
} | ||
var wavStr = stream.readUint8String(4); | ||
if (wavStr != 'WAVE') { | ||
const wavStr = stream.readUint8String(4); | ||
if (wavStr !== 'WAVE') { | ||
log.warn('incorrect adpcm wav header'); | ||
@@ -47,3 +46,3 @@ reject(); | ||
var formatChunk = this.extractChunk('fmt ', stream); | ||
const formatChunk = this.extractChunk('fmt ', stream); | ||
this.encoding = formatChunk.readUint16(); | ||
@@ -59,9 +58,9 @@ this.channels = formatChunk.readUint16(); | ||
var samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); | ||
const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); | ||
// todo: this line is the only place Tone is used here, should be possible to remove | ||
var buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond); | ||
// @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 (var i=0; i<samples.length; i++) { | ||
// @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; | ||
@@ -71,3 +70,3 @@ } | ||
resolve(buffer); | ||
}.bind(this)); | ||
}); | ||
}; | ||
@@ -104,10 +103,10 @@ | ||
while (stream.position < (stream.getLength() - 8)) { | ||
var typeStr = stream.readUint8String(4); | ||
var chunkSize = stream.readInt32(); | ||
if (typeStr == chunkType) { | ||
var chunk = stream.extract(chunkSize); | ||
const typeStr = stream.readUint8String(4); | ||
const chunkSize = stream.readInt32(); | ||
if (typeStr === chunkType) { | ||
const chunk = stream.extract(chunkSize); | ||
return chunk; | ||
} else { | ||
stream.position += chunkSize; | ||
} | ||
stream.position += chunkSize; | ||
} | ||
@@ -124,6 +123,9 @@ }; | ||
ADPCMSoundDecoder.prototype.imaDecompress = function (compressedData, blockSize) { | ||
var sample, step, code, delta; | ||
var index = 0; | ||
var lastByte = -1; // -1 indicates that there is no saved lastByte | ||
var out = []; | ||
let sample; | ||
let step; | ||
let code; | ||
let delta; | ||
let index = 0; | ||
let lastByte = -1; // -1 indicates that there is no saved lastByte | ||
const out = []; | ||
@@ -134,6 +136,6 @@ // Bail and return no samples if we have no data | ||
compressedData.position = 0; | ||
var a = 0; | ||
while (a==0) { | ||
if (((compressedData.position % blockSize) == 0) && (lastByte < 0)) { // read block header | ||
if (compressedData.getBytesAvailable() == 0) break; | ||
const a = 0; | ||
while (a === 0) { | ||
if (((compressedData.position % blockSize) === 0) && (lastByte < 0)) { // read block header | ||
if (compressedData.getBytesAvailable() === 0) break; | ||
sample = compressedData.readInt16(); | ||
@@ -147,3 +149,3 @@ index = compressedData.readUint8(); | ||
if (lastByte < 0) { | ||
if (compressedData.getBytesAvailable() == 0) break; | ||
if (compressedData.getBytesAvailable() === 0) break; | ||
lastByte = compressedData.readUint8(); | ||
@@ -172,3 +174,3 @@ code = lastByte & 0xF; | ||
} | ||
var samples = Int16Array.from(out); | ||
const samples = Int16Array.from(out); | ||
return samples; | ||
@@ -175,0 +177,0 @@ }; |
@@ -11,6 +11,6 @@ /** | ||
*/ | ||
function ArrayBufferStream (arrayBuffer) { | ||
const ArrayBufferStream = function (arrayBuffer) { | ||
this.arrayBuffer = arrayBuffer; | ||
this.position = 0; | ||
} | ||
}; | ||
@@ -23,4 +23,4 @@ /** | ||
ArrayBufferStream.prototype.extract = function (length) { | ||
var slicedArrayBuffer = this.arrayBuffer.slice(this.position, this.position+length); | ||
var newStream = new ArrayBufferStream(slicedArrayBuffer); | ||
const slicedArrayBuffer = this.arrayBuffer.slice(this.position, this.position + length); | ||
const newStream = new ArrayBufferStream(slicedArrayBuffer); | ||
return newStream; | ||
@@ -45,6 +45,6 @@ }; | ||
* Read an unsigned 8 bit integer from the stream | ||
* @return {number} | ||
* @return {number} the next 8 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readUint8 = function () { | ||
var val = new Uint8Array(this.arrayBuffer, this.position, 1)[0]; | ||
const val = new Uint8Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 1; | ||
@@ -58,9 +58,9 @@ return val; | ||
* @param {number} length - the number of bytes to convert | ||
* @return {String} a String made by concatenating the chars in the input | ||
* @return {string} a String made by concatenating the chars in the input | ||
*/ | ||
ArrayBufferStream.prototype.readUint8String = function (length) { | ||
var arr = new Uint8Array(this.arrayBuffer, this.position, length); | ||
const arr = new Uint8Array(this.arrayBuffer, this.position, length); | ||
this.position += length; | ||
var str = ''; | ||
for (var i=0; i<arr.length; i++) { | ||
let str = ''; | ||
for (let i = 0; i < arr.length; i++) { | ||
str += String.fromCharCode(arr[i]); | ||
@@ -73,6 +73,6 @@ } | ||
* Read a 16 bit integer from the stream | ||
* @return {number} | ||
* @return {number} the next 16 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readInt16 = function () { | ||
var val = new Int16Array(this.arrayBuffer, this.position, 1)[0]; | ||
const val = new Int16Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 2; // one 16 bit int is 2 bytes | ||
@@ -84,6 +84,6 @@ return val; | ||
* Read an unsigned 16 bit integer from the stream | ||
* @return {number} | ||
* @return {number} the next unsigned 16 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readUint16 = function () { | ||
var val = new Uint16Array(this.arrayBuffer, this.position, 1)[0]; | ||
const val = new Uint16Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 2; // one 16 bit int is 2 bytes | ||
@@ -95,6 +95,6 @@ return val; | ||
* Read a 32 bit integer from the stream | ||
* @return {number} | ||
* @return {number} the next 32 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readInt32 = function () { | ||
var val = new Int32Array(this.arrayBuffer, this.position, 1)[0]; | ||
const val = new Int32Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 4; // one 32 bit int is 4 bytes | ||
@@ -106,6 +106,6 @@ return val; | ||
* Read an unsigned 32 bit integer from the stream | ||
* @return {number} | ||
* @return {number} the next unsigned 32 bit integer in the stream | ||
*/ | ||
ArrayBufferStream.prototype.readUint32 = function () { | ||
var val = new Uint32Array(this.arrayBuffer, this.position, 1)[0]; | ||
const val = new Uint32Array(this.arrayBuffer, this.position, 1)[0]; | ||
this.position += 4; // one 32 bit int is 4 bytes | ||
@@ -112,0 +112,0 @@ return val; |
@@ -1,3 +0,3 @@ | ||
var SoundPlayer = require('./SoundPlayer'); | ||
var Tone = require('tone'); | ||
const SoundPlayer = require('./SoundPlayer'); | ||
const Tone = require('tone'); | ||
@@ -9,7 +9,7 @@ /** | ||
*/ | ||
function DrumPlayer (outputNode) { | ||
const DrumPlayer = function (outputNode) { | ||
this.outputNode = outputNode; | ||
var baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; | ||
var fileNames = [ | ||
const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; | ||
const fileNames = [ | ||
'SnareDrum(1)', | ||
@@ -37,8 +37,8 @@ 'BassDrum(1b)', | ||
for (var i=0; i<fileNames.length; i++) { | ||
var url = baseUrl + fileNames[i] + '_22k.wav'; | ||
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)); | ||
} | ||
} | ||
}; | ||
@@ -61,3 +61,3 @@ /** | ||
DrumPlayer.prototype.stopAll = function () { | ||
for (var i=0; i<this.drumSounds.length; i++) { | ||
for (let i = 0; i < this.drumSounds.length; i++) { | ||
this.drumSounds[i].stop(); | ||
@@ -64,0 +64,0 @@ } |
@@ -1,2 +0,2 @@ | ||
var Tone = require('tone'); | ||
const Tone = require('tone'); | ||
@@ -11,3 +11,3 @@ /** | ||
*/ | ||
function EchoEffect () { | ||
const EchoEffect = function () { | ||
Tone.Effect.call(this); | ||
@@ -20,3 +20,3 @@ | ||
this.effectSend.chain(this.delay, this.effectReturn); | ||
} | ||
}; | ||
@@ -35,3 +35,3 @@ Tone.extend(EchoEffect, Tone.Effect); | ||
// mute the effect if value is 0 | ||
if (this.value == 0) { | ||
if (this.value === 0) { | ||
this.wet.value = 0; | ||
@@ -42,4 +42,4 @@ } else { | ||
var feedback = (this.value / 100) * 0.75; | ||
this.delay.feedback.rampTo(feedback, 1/60); | ||
const feedback = (this.value / 100) * 0.75; | ||
this.delay.feedback.rampTo(feedback, 1 / 60); | ||
}; | ||
@@ -60,2 +60,3 @@ | ||
* @param {number} max - the max value to clamp to | ||
* @return {number} the clamped value | ||
*/ | ||
@@ -67,2 +68,1 @@ EchoEffect.prototype.clamp = function (input, min, max) { | ||
module.exports = EchoEffect; | ||
@@ -1,2 +0,2 @@ | ||
var Tone = require('tone'); | ||
const Tone = require('tone'); | ||
@@ -10,3 +10,3 @@ /** | ||
*/ | ||
function FuzzEffect () { | ||
const FuzzEffect = function () { | ||
Tone.Effect.call(this); | ||
@@ -19,3 +19,3 @@ | ||
this.effectSend.chain(this.distortion, this.effectReturn); | ||
} | ||
}; | ||
@@ -48,2 +48,3 @@ Tone.extend(FuzzEffect, Tone.Effect); | ||
* @param {number} max - the max value to clamp to | ||
* @return {number} the clamped value | ||
*/ | ||
@@ -55,2 +56,1 @@ FuzzEffect.prototype.clamp = function (input, min, max) { | ||
module.exports = FuzzEffect; | ||
@@ -1,2 +0,2 @@ | ||
var Tone = require('tone'); | ||
const Tone = require('tone'); | ||
@@ -10,3 +10,3 @@ /** | ||
*/ | ||
function PanEffect () { | ||
const PanEffect = function () { | ||
Tone.Effect.call(this); | ||
@@ -19,3 +19,3 @@ | ||
this.effectSend.chain(this.panner, this.effectReturn); | ||
} | ||
}; | ||
@@ -49,2 +49,3 @@ Tone.extend(PanEffect, Tone.Effect); | ||
* @param {number} max - the max value to clamp to | ||
* @return {number} the clamped value | ||
*/ | ||
@@ -56,2 +57,1 @@ PanEffect.prototype.clamp = function (input, min, max) { | ||
module.exports = PanEffect; | ||
@@ -1,2 +0,2 @@ | ||
var Tone = require('tone'); | ||
const Tone = require('tone'); | ||
@@ -21,3 +21,3 @@ /** | ||
*/ | ||
function PitchEffect () { | ||
const PitchEffect = function () { | ||
this.value = 0; // effect value | ||
@@ -27,3 +27,3 @@ this.ratio = 1; // the playback rate ratio | ||
this.tone = new Tone(); | ||
} | ||
}; | ||
@@ -44,3 +44,3 @@ /** | ||
* @param {number} val - the value to change the effect by | ||
* @param {Object} players - a dictionary of SoundPlayer objects indexed by md5 | ||
* @param {object} players - a dictionary of SoundPlayer objects indexed by md5 | ||
*/ | ||
@@ -64,3 +64,3 @@ PitchEffect.prototype.changeBy = function (val, players) { | ||
* Update a sound player's playback rate using the current ratio for the effect | ||
* @param {Object} player - a SoundPlayer object | ||
* @param {object} player - a SoundPlayer object | ||
*/ | ||
@@ -78,3 +78,3 @@ PitchEffect.prototype.updatePlayer = function (player) { | ||
for (var md5 in players) { | ||
for (const md5 in players) { | ||
if (players.hasOwnProperty(md5)) { | ||
@@ -87,2 +87,1 @@ this.updatePlayer(players[md5]); | ||
module.exports = PitchEffect; | ||
@@ -1,2 +0,2 @@ | ||
var Tone = require('tone'); | ||
const Tone = require('tone'); | ||
@@ -10,3 +10,3 @@ /** | ||
*/ | ||
function ReverbEffect () { | ||
const ReverbEffect = function () { | ||
Tone.Effect.call(this); | ||
@@ -19,3 +19,3 @@ | ||
this.effectSend.chain(this.reverb, this.effectReturn); | ||
} | ||
}; | ||
@@ -49,2 +49,3 @@ Tone.extend(ReverbEffect, Tone.Effect); | ||
* @param {number} max - the max value to clamp to | ||
* @return {number} the clamped value | ||
*/ | ||
@@ -56,2 +57,1 @@ ReverbEffect.prototype.clamp = function (input, min, max) { | ||
module.exports = ReverbEffect; | ||
@@ -0,4 +1,3 @@ | ||
const Tone = require('tone'); | ||
var Tone = require('tone'); | ||
/** | ||
@@ -15,3 +14,3 @@ * A "robotic" effect that adds a low-pitched buzzing to the sound, reminiscent of the | ||
*/ | ||
function RoboticEffect () { | ||
const RoboticEffect = function () { | ||
Tone.Effect.call(this); | ||
@@ -21,7 +20,7 @@ | ||
var time = this._delayTimeForValue(100); | ||
const time = this._delayTimeForValue(100); | ||
this.feedbackCombFilter = new Tone.FeedbackCombFilter(time, 0.9); | ||
this.effectSend.chain(this.feedbackCombFilter, this.effectReturn); | ||
} | ||
}; | ||
@@ -38,3 +37,3 @@ Tone.extend(RoboticEffect, Tone.Effect); | ||
// mute the effect if value is 0 | ||
if (this.value == 0) { | ||
if (this.value === 0) { | ||
this.wet.value = 0; | ||
@@ -46,4 +45,4 @@ } else { | ||
// set delay time using the value | ||
var time = this._delayTimeForValue(this.value); | ||
this.feedbackCombFilter.delayTime.rampTo(time, 1/60); | ||
const time = this._delayTimeForValue(this.value); | ||
this.feedbackCombFilter.delayTime.rampTo(time, 1 / 60); | ||
}; | ||
@@ -67,4 +66,4 @@ | ||
RoboticEffect.prototype._delayTimeForValue = function (val) { | ||
var midiNote = ((val - 100) / 10) + 36; | ||
var freq = Tone.Frequency(midiNote, 'midi').eval(); | ||
const midiNote = ((val - 100) / 10) + 36; | ||
const freq = Tone.Frequency(midiNote, 'midi').eval(); | ||
return 1 / freq; | ||
@@ -74,2 +73,1 @@ }; | ||
module.exports = RoboticEffect; | ||
@@ -1,2 +0,2 @@ | ||
var Tone = require('tone'); | ||
const Tone = require('tone'); | ||
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
function WobbleEffect () { | ||
const WobbleEffect = function () { | ||
Tone.Effect.call(this); | ||
@@ -25,3 +25,3 @@ | ||
this.effectSend.chain(this.wobbleGain, this.effectReturn); | ||
} | ||
}; | ||
@@ -41,3 +41,3 @@ Tone.extend(WobbleEffect, Tone.Effect); | ||
this.wobbleLFO.frequency.rampTo(this.value / 10, 1/60); | ||
this.wobbleLFO.frequency.rampTo(this.value / 10, 1 / 60); | ||
}; | ||
@@ -58,2 +58,3 @@ | ||
* @param {number} max - the max value to clamp to | ||
* @return {number} the clamped value | ||
*/ | ||
@@ -65,2 +66,1 @@ WobbleEffect.prototype.clamp = function (input, min, max) { | ||
module.exports = WobbleEffect; | ||
354
src/index.js
@@ -1,16 +0,16 @@ | ||
var log = require('./log'); | ||
var Tone = require('tone'); | ||
const log = require('./log'); | ||
const Tone = require('tone'); | ||
var PitchEffect = require('./effects/PitchEffect'); | ||
var PanEffect = require('./effects/PanEffect'); | ||
const PitchEffect = require('./effects/PitchEffect'); | ||
const PanEffect = require('./effects/PanEffect'); | ||
var RoboticEffect = require('./effects/RoboticEffect'); | ||
var FuzzEffect = require('./effects/FuzzEffect'); | ||
var EchoEffect = require('./effects/EchoEffect'); | ||
var ReverbEffect = require('./effects/ReverbEffect'); | ||
const RoboticEffect = require('./effects/RoboticEffect'); | ||
const FuzzEffect = require('./effects/FuzzEffect'); | ||
const EchoEffect = require('./effects/EchoEffect'); | ||
const ReverbEffect = require('./effects/ReverbEffect'); | ||
var SoundPlayer = require('./SoundPlayer'); | ||
var ADPCMSoundDecoder = require('./ADPCMSoundDecoder'); | ||
var InstrumentPlayer = require('./InstrumentPlayer'); | ||
var DrumPlayer = require('./DrumPlayer'); | ||
const SoundPlayer = require('./SoundPlayer'); | ||
const ADPCMSoundDecoder = require('./ADPCMSoundDecoder'); | ||
const InstrumentPlayer = require('./InstrumentPlayer'); | ||
const DrumPlayer = require('./DrumPlayer'); | ||
@@ -23,2 +23,147 @@ /** | ||
/** | ||
* 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) { | ||
this.audioEngine = audioEngine; | ||
// 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); | ||
// 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; | ||
} | ||
// if this sprite or clone is already playing this sound, stop it first | ||
if (this.activeSoundPlayers[md5]) { | ||
this.activeSoundPlayers[md5].stop(); | ||
} | ||
// create a new soundplayer to play the sound | ||
const player = new SoundPlayer(); | ||
player.setBuffer(this.audioEngine.audioBuffers[md5]); | ||
player.connect(this.effectsNode); | ||
this.pitchEffect.updatePlayer(player); | ||
player.start(); | ||
// 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 | ||
*/ | ||
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(); | ||
} | ||
// 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; | ||
} | ||
}; | ||
/** | ||
* 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, | ||
@@ -29,3 +174,3 @@ * loads all the audio buffers for sounds belonging to sprites, and creates a single instrument player | ||
*/ | ||
function AudioEngine () { | ||
const AudioEngine = function () { | ||
@@ -40,3 +185,3 @@ // create the global audio effects | ||
this.input = new Tone.Gain(); | ||
this.input.chain ( | ||
this.input.chain( | ||
this.roboticEffect, this.fuzzEffect, this.echoEffect, this.reverbEffect, | ||
@@ -63,3 +208,3 @@ Tone.Master | ||
this.micMeter = null; | ||
} | ||
}; | ||
@@ -69,3 +214,3 @@ /** | ||
* 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 | ||
* @param {object} sound - an object containing audio data and metadata for a sound | ||
* @property {Buffer} data - sound data loaded from scratch-storage. | ||
@@ -78,3 +223,3 @@ * @property {string} format - format type, either empty or adpcm. | ||
var loaderPromise = null; | ||
let loaderPromise = null; | ||
@@ -92,8 +237,8 @@ switch (sound.format) { | ||
var storedContext = this; | ||
const storedContext = this; | ||
return loaderPromise.then( | ||
function (decodedAudio) { | ||
decodedAudio => { | ||
storedContext.audioBuffers[sound.md5] = new Tone.Buffer(decodedAudio); | ||
}, | ||
function (error) { | ||
error => { | ||
log.warn('audio data could not be decoded', error); | ||
@@ -122,3 +267,3 @@ } | ||
AudioEngine.prototype.playNoteForBeatsWithInstAndVol = function (note, beats, inst, vol) { | ||
var sec = this.beatsToSec(beats); | ||
const sec = this.beatsToSec(beats); | ||
this.instrumentPlayer.playNoteForSecWithInstAndVol(note, sec, inst, vol); | ||
@@ -130,4 +275,4 @@ return this.waitForBeats(beats); | ||
* Convert a number of beats to a number of seconds, using the current tempo | ||
* @param {number} beats | ||
* @return {number} seconds | ||
* @param {number} beats number of beats to convert to secs | ||
* @return {number} seconds number of seconds `beats` will last | ||
*/ | ||
@@ -140,9 +285,9 @@ AudioEngine.prototype.beatsToSec = function (beats) { | ||
* Wait for some number of beats | ||
* @param {number} 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) { | ||
var storedContext = this; | ||
return new Promise(function (resolve) { | ||
setTimeout(function () { | ||
const storedContext = this; | ||
return new Promise(resolve => { | ||
setTimeout(() => { | ||
resolve(); | ||
@@ -166,3 +311,3 @@ }, storedContext.beatsToSec(beats) * 1000); | ||
AudioEngine.prototype.changeTempo = function (value) { | ||
this.setTempo(this.currentTempo + value); | ||
this.setTempo(this.currentTempo + value); | ||
}; | ||
@@ -182,7 +327,7 @@ | ||
} | ||
if (this.mic && this.mic.state == 'started') { | ||
if (this.mic && this.mic.state === 'started') { | ||
return this.micMeter.value * 100; | ||
} else { | ||
return -1; | ||
} | ||
return -1; | ||
}; | ||
@@ -208,3 +353,3 @@ | ||
* functionality such as playing notes. | ||
* @return {AudioPlayer} | ||
* @return {AudioPlayer} new AudioPlayer instance | ||
*/ | ||
@@ -215,147 +360,2 @@ AudioEngine.prototype.createPlayer = function () { | ||
/** | ||
* Each sprite or clone has an audio player | ||
* the audio player handles sound playback, volume, and the sprite-specific audio effects: | ||
* pitch and pan | ||
* @param {AudioEngine} | ||
* @constructor | ||
*/ | ||
function AudioPlayer (audioEngine) { | ||
this.audioEngine = audioEngine; | ||
// 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); | ||
// 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; | ||
} | ||
// if this sprite or clone is already playing this sound, stop it first | ||
if (this.activeSoundPlayers[md5]) { | ||
this.activeSoundPlayers[md5].stop(); | ||
} | ||
// create a new soundplayer to play the sound | ||
var player = new SoundPlayer(); | ||
player.setBuffer(this.audioEngine.audioBuffers[md5]); | ||
player.connect(this.effectsNode); | ||
this.pitchEffect.updatePlayer(player); | ||
player.start(); | ||
// 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 (var 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 | ||
*/ | ||
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 (var 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 | ||
*/ | ||
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; | ||
} | ||
}; | ||
/** | ||
* 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; | ||
}; | ||
module.exports = AudioEngine; |
@@ -1,3 +0,3 @@ | ||
var Tone = require('tone'); | ||
var Soundfont = require('soundfont-player'); | ||
const Tone = require('tone'); | ||
const Soundfont = require('soundfont-player'); | ||
@@ -15,3 +15,3 @@ /** | ||
*/ | ||
function InstrumentPlayer (outputNode) { | ||
const InstrumentPlayer = function (outputNode) { | ||
this.outputNode = outputNode; | ||
@@ -28,3 +28,3 @@ | ||
this.instruments = []; | ||
} | ||
}; | ||
@@ -42,3 +42,3 @@ /** | ||
InstrumentPlayer.prototype.playNoteForSecWithInstAndVol = function (note, sec, instrumentNum, vol) { | ||
var gain = vol / 100; | ||
const gain = vol / 100; | ||
this.loadInstrument(instrumentNum) | ||
@@ -48,4 +48,4 @@ .then(() => { | ||
note, Tone.context.currentTime, { | ||
duration : sec, | ||
gain : gain | ||
duration: sec, | ||
gain: gain | ||
} | ||
@@ -64,9 +64,9 @@ ); | ||
return Promise.resolve(); | ||
} else { | ||
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) | ||
.then((inst) => { | ||
} | ||
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) | ||
.then(inst => { | ||
inst.connect(this.outputNode); | ||
this.instruments[instrumentNum] = inst; | ||
}); | ||
} | ||
}; | ||
@@ -78,3 +78,3 @@ | ||
InstrumentPlayer.prototype.stopAll = function () { | ||
for (var i=0; i<this.instruments.length; i++) { | ||
for (let i = 0; i < this.instruments.length; i++) { | ||
if (this.instruments[i]) { | ||
@@ -81,0 +81,0 @@ this.instruments[i].stop(); |
@@ -1,4 +0,4 @@ | ||
var minilog = require('minilog'); | ||
const minilog = require('minilog'); | ||
minilog.enable(); | ||
module.exports = minilog('scratch-audioengine'); |
@@ -1,3 +0,3 @@ | ||
var Tone = require('tone'); | ||
var log = require('./log'); | ||
const Tone = require('tone'); | ||
const log = require('./log'); | ||
@@ -8,3 +8,3 @@ /** | ||
*/ | ||
function SoundPlayer () { | ||
const SoundPlayer = function () { | ||
this.outputNode = null; | ||
@@ -15,3 +15,3 @@ this.buffer = new Tone.Buffer(); | ||
this.isPlaying = false; | ||
} | ||
}; | ||
@@ -28,3 +28,3 @@ /** | ||
* Set an audio buffer | ||
* @param {Tone.Buffer} buffer | ||
* @param {Tone.Buffer} buffer Buffer to set | ||
*/ | ||
@@ -80,4 +80,4 @@ SoundPlayer.prototype.setBuffer = function (buffer) { | ||
SoundPlayer.prototype.finished = function () { | ||
var storedContext = this; | ||
return new Promise(function (resolve) { | ||
const storedContext = this; | ||
return new Promise(resolve => { | ||
storedContext.bufferSource.onended = function () { | ||
@@ -84,0 +84,0 @@ this.isPlaying = false; |
@@ -5,3 +5,3 @@ var path = require('path'); | ||
entry: { | ||
'dist': './src/index.js' | ||
dist: './src/index.js' | ||
}, | ||
@@ -8,0 +8,0 @@ output: { |
Sorry, the diff of this file is too big to display
1196440
51
26071
12