diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 5be40e9..0000000 --- a/.eslintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "parser": "babel-eslint", - "rules": { - "curly": [2, "multi-line"], - "eol-last": [2], - "indent": [2, 4], - "linebreak-style": [2, "unix"], - "max-len": [2, 120, 4], - "no-trailing-spaces": [2, { "skipBlankLines": true }], - "no-unused-vars": [2, {"args": "after-used", "varsIgnorePattern": "^_"}], - "quotes": [2, "single"], - "semi": [2, "always"], - "space-before-function-paren": [2, "always"], - "strict": [2, "never"] - }, - "env": { - "browser": true, - "es6": true, - "node": true - }, - "extends": ["eslint:recommended"] -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..36ff570 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['scratch', 'scratch/node'] +}; diff --git a/package.json b/package.json index 2f90211..351192c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "babel-loader": "^6.4.1", "babel-preset-es2015": "^6.24.1", "eslint": "^3.19.0", + "eslint-config-scratch": "^3.1.0", "json": "^9.0.6", "minilog": "^3.0.1", "soundfont-player": "0.10.5", diff --git a/src/.eslintrc.js b/src/.eslintrc.js new file mode 100644 index 0000000..bfdc88d --- /dev/null +++ b/src/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + root: true, + extends: ['scratch', 'scratch/es6'], + env: {browser: true} +}; diff --git a/src/ADPCMSoundDecoder.js b/src/ADPCMSoundDecoder.js index 63d4baf..f9e855e 100644 --- a/src/ADPCMSoundDecoder.js +++ b/src/ADPCMSoundDecoder.js @@ -1,6 +1,6 @@ -var ArrayBufferStream = require('./ArrayBufferStream'); -var Tone = require('tone'); -var log = require('./log'); +const ArrayBufferStream = require('./ArrayBufferStream'); +const Tone = require('tone'); +const log = require('./log'); /** * Decode wav audio files that have been compressed with the ADPCM format. @@ -10,38 +10,37 @@ var log = require('./log'); * https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as * @constructor */ -function ADPCMSoundDecoder () { -} +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} + * @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'); 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'); reject(); } - var formatChunk = this.extractChunk('fmt ', stream); + const formatChunk = this.extractChunk('fmt ', stream); this.encoding = formatChunk.readUint16(); this.channels = formatChunk.readUint16(); this.samplesPerSecond = formatChunk.readUint32(); @@ -52,18 +51,18 @@ ADPCMSoundDecoder.prototype.decode = function (audioData) { this.samplesPerBlock = formatChunk.readUint16(); this.adpcmBlockSize = ((this.samplesPerBlock - 1) / 2) + 4; // block size in bytes - 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 { this.instruments[instrumentNum].play( note, Tone.context.currentTime, { - duration : sec, - gain : gain + duration: sec, + gain: gain } ); }); @@ -57,20 +57,20 @@ InstrumentPlayer.prototype.playNoteForSecWithInstAndVol = function (note, sec, i InstrumentPlayer.prototype.loadInstrument = function (instrumentNum) { if (this.instruments[instrumentNum]) { 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; }); - } + }; /** * Stop all notes being played on all instruments */ InstrumentPlayer.prototype.stopAll = function () { - for (var i=0; i { storedContext.bufferSource.onended = function () { this.isPlaying = false; resolve(); diff --git a/src/effects/EchoEffect.js b/src/effects/EchoEffect.js index 2dfd9bd..95b2b81 100644 --- a/src/effects/EchoEffect.js +++ b/src/effects/EchoEffect.js @@ -1,4 +1,4 @@ -var Tone = require('tone'); +const Tone = require('tone'); /** * An echo effect (aka 'delay effect' in audio terms) @@ -8,7 +8,7 @@ var Tone = require('tone'); * Clamped 0-100 * @constructor */ -function EchoEffect () { +const EchoEffect = function () { Tone.Effect.call(this); this.value = 0; @@ -16,7 +16,7 @@ function EchoEffect () { this.delay = new Tone.FeedbackDelay(0.25, 0.5); this.effectSend.chain(this.delay, this.effectReturn); -} +}; Tone.extend(EchoEffect, Tone.Effect); @@ -30,14 +30,14 @@ EchoEffect.prototype.set = function (val) { this.value = this.clamp(this.value, 0, 100); // mute the effect if value is 0 - if (this.value == 0) { + if (this.value === 0) { this.wet.value = 0; } else { this.wet.value = 0.5; } - 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); }; /** @@ -53,10 +53,10 @@ EchoEffect.prototype.changeBy = function (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 */ EchoEffect.prototype.clamp = function (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 9ed24e3..bae55b2 100644 --- a/src/effects/FuzzEffect.js +++ b/src/effects/FuzzEffect.js @@ -1,4 +1,4 @@ -var Tone = require('tone'); +const Tone = require('tone'); /** * A fuzz effect (aka 'distortion effect' in audio terms) @@ -7,7 +7,7 @@ var Tone = require('tone'); * Clamped 0-100 * @constructor */ -function FuzzEffect () { +const FuzzEffect = function () { Tone.Effect.call(this); this.value = 0; @@ -15,7 +15,7 @@ function FuzzEffect () { this.distortion = new Tone.Distortion(1); this.effectSend.chain(this.distortion, this.effectReturn); -} +}; Tone.extend(FuzzEffect, Tone.Effect); @@ -43,10 +43,10 @@ FuzzEffect.prototype.changeBy = function (val) { * @param {number} input - the input to clamp * @param {number} min - the min value to clamp to * @param {number} max - the max value to clamp to +* @return {number} the clamped value */ FuzzEffect.prototype.clamp = function (input, min, max) { return Math.min(Math.max(input, min), max); }; module.exports = FuzzEffect; - diff --git a/src/effects/PanEffect.js b/src/effects/PanEffect.js index a0950c5..03c29a3 100644 --- a/src/effects/PanEffect.js +++ b/src/effects/PanEffect.js @@ -1,4 +1,4 @@ -var Tone = require('tone'); +const Tone = require('tone'); /** * A pan effect, which moves the sound to the left or right between the speakers @@ -7,7 +7,7 @@ var Tone = require('tone'); * Clamped -100 to 100 * @constructor */ -function PanEffect () { +const PanEffect = function () { Tone.Effect.call(this); this.value = 0; @@ -15,7 +15,7 @@ function PanEffect () { this.panner = new Tone.Panner(); this.effectSend.chain(this.panner, this.effectReturn); -} +}; Tone.extend(PanEffect, Tone.Effect); @@ -44,10 +44,10 @@ PanEffect.prototype.changeBy = function (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 */ PanEffect.prototype.clamp = function (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 d93c35a..f48e8f3 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -1,4 +1,4 @@ -var Tone = require('tone'); +const Tone = require('tone'); /** * A pitch change effect, which changes the playback rate of the sound in order @@ -18,12 +18,12 @@ var Tone = require('tone'); * on one SoundPlayer or a group of them. * @constructor */ -function PitchEffect () { +const PitchEffect = function () { this.value = 0; // effect value this.ratio = 1; // the playback rate ratio this.tone = new Tone(); -} +}; /** * Set the effect value @@ -39,7 +39,7 @@ PitchEffect.prototype.set = function (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 +* @param {object} players - a dictionary of SoundPlayer objects indexed by md5 */ PitchEffect.prototype.changeBy = function (val, players) { this.set(this.value + val, players); @@ -58,7 +58,7 @@ PitchEffect.prototype.getRatio = function (val) { /** * 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 */ PitchEffect.prototype.updatePlayer = function (player) { player.setPlaybackRate(this.ratio); @@ -71,7 +71,7 @@ PitchEffect.prototype.updatePlayer = function (player) { PitchEffect.prototype.updatePlayers = function (players) { if (!players) return; - for (var md5 in players) { + for (const md5 in players) { if (players.hasOwnProperty(md5)) { this.updatePlayer(players[md5]); } @@ -79,4 +79,3 @@ PitchEffect.prototype.updatePlayers = function (players) { }; module.exports = PitchEffect; - diff --git a/src/effects/ReverbEffect.js b/src/effects/ReverbEffect.js index 2ad393a..43fa70c 100644 --- a/src/effects/ReverbEffect.js +++ b/src/effects/ReverbEffect.js @@ -1,4 +1,4 @@ -var Tone = require('tone'); +const Tone = require('tone'); /** * A reverb effect, simulating reverberation in a room @@ -7,7 +7,7 @@ var Tone = require('tone'); * Clamped 0 to 100 * @constructor */ -function ReverbEffect () { +const ReverbEffect = function () { Tone.Effect.call(this); this.value = 0; @@ -15,7 +15,7 @@ function ReverbEffect () { this.reverb = new Tone.Freeverb(); this.effectSend.chain(this.reverb, this.effectReturn); -} +}; Tone.extend(ReverbEffect, Tone.Effect); @@ -44,10 +44,10 @@ ReverbEffect.prototype.changeBy = function (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 */ ReverbEffect.prototype.clamp = function (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 d2f9a41..b8ebc9a 100644 --- a/src/effects/RoboticEffect.js +++ b/src/effects/RoboticEffect.js @@ -1,5 +1,4 @@ - -var Tone = require('tone'); +const Tone = require('tone'); /** * A "robotic" effect that adds a low-pitched buzzing to the sound, reminiscent of the @@ -12,16 +11,16 @@ var Tone = require('tone'); * Exterminate. * @constructor */ -function RoboticEffect () { +const RoboticEffect = function () { Tone.Effect.call(this); this.value = 0; - 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); -} +}; Tone.extend(RoboticEffect, Tone.Effect); @@ -33,15 +32,15 @@ RoboticEffect.prototype.set = function (val) { this.value = val; // mute the effect if value is 0 - if (this.value == 0) { + if (this.value === 0) { this.wet.value = 0; } else { this.wet.value = 1; } // 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); }; /** @@ -60,10 +59,9 @@ RoboticEffect.prototype.changeBy = function (val) { * @returns {number} a delay time in seconds */ 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; }; module.exports = RoboticEffect; - diff --git a/src/effects/WobbleEffect.js b/src/effects/WobbleEffect.js index 13f5877..9b6b4d8 100644 --- a/src/effects/WobbleEffect.js +++ b/src/effects/WobbleEffect.js @@ -1,4 +1,4 @@ -var Tone = require('tone'); +const Tone = require('tone'); /** * A wobble effect. In audio terms, it sounds like tremolo. @@ -11,7 +11,7 @@ var Tone = require('tone'); * Clamped 0 to 100 * @constructor */ -function WobbleEffect () { +const WobbleEffect = function () { Tone.Effect.call(this); this.value = 0; @@ -21,7 +21,7 @@ function WobbleEffect () { this.wobbleLFO.connect(this.wobbleGain.gain); this.effectSend.chain(this.wobbleGain, this.effectReturn); -} +}; Tone.extend(WobbleEffect, Tone.Effect); @@ -36,7 +36,7 @@ WobbleEffect.prototype.set = function (val) { this.wet.value = this.value / 100; - this.wobbleLFO.frequency.rampTo(this.value / 10, 1/60); + this.wobbleLFO.frequency.rampTo(this.value / 10, 1 / 60); }; /** @@ -52,10 +52,10 @@ WobbleEffect.prototype.changeBy = function (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 */ WobbleEffect.prototype.clamp = function (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 67ea9ed..7d20af5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,213 +1,32 @@ -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'); /** * @fileOverview Scratch Audio is divided into a single AudioEngine, * that handles global functionality, and AudioPlayers, belonging to individual sprites and clones. */ -/** - * There is a single instance of the AudioEngine. It handles global audio properties and effects, - * loads all the audio buffers for sounds belonging to sprites, and creates a single instrument player - * and a drum player, used by all play note and play drum blocks. - * @constructor - */ -function AudioEngine () { - - // 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 - ); - - // 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; - - // 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 = {}; - - // 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) { - - var 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); - } - - var storedContext = this; - return loaderPromise.then( - function (decodedAudio) { - storedContext.audioBuffers[sound.md5] = new Tone.Buffer(decodedAudio); - }, - function (error) { - 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 () { - 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) { - var 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 - * @return {number} seconds - */ -AudioEngine.prototype.beatsToSec = function (beats) { - return (60 / this.currentTempo) * beats; -}; - -/** - * Wait for some number of beats - * @param {number} beats - * @return {Promise} a Promise that resolves after the duration has elapsed - */ -AudioEngine.prototype.waitForBeats = function (beats) { - var storedContext = this; - return new Promise(function (resolve) { - setTimeout(function () { - resolve(); - }, storedContext.beatsToSec(beats) * 1000); - }); -}; - -/** - * Set the global tempo in bpm (beats per minute) - * @param {number} value - the new tempo to set - */ -AudioEngine.prototype.setTempo = function (value) { - 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); - } - if (this.mic && this.mic.state == 'started') { - return this.micMeter.value * 100; - } else { - return -1; - } -}; - -/** - * Names of the audio effects. - * @readonly - * @enum {string} - */ -AudioEngine.prototype.EFFECT_NAMES = { - pitch: 'pitch', - pan: 'pan', - echo: 'echo', - reverb: 'reverb', - fuzz: 'fuzz', - robot: 'robot' -}; - -/** - * Create an AudioPlayer. Each sprite or clone has an AudioPlayer. - * It includes a reference to the AudioEngine so it can use global - * functionality such as playing notes. - * @return {AudioPlayer} - */ -AudioEngine.prototype.createPlayer = function () { - return new AudioPlayer(this); -}; - - /** * 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} + * @param {AudioEngine} audioEngine AudioEngine for player * @constructor */ -function AudioPlayer (audioEngine) { +const AudioPlayer = function (audioEngine) { this.audioEngine = audioEngine; @@ -226,7 +45,7 @@ function AudioPlayer (audioEngine) { // sound players that are currently playing, indexed by the sound's md5 this.activeSoundPlayers = {}; -} +}; /** * Play a sound @@ -245,7 +64,7 @@ AudioPlayer.prototype.playSound = function (md5) { } // create a new soundplayer to play the sound - var player = new SoundPlayer(); + const player = new SoundPlayer(); player.setBuffer(this.audioEngine.audioBuffers[md5]); player.connect(this.effectsNode); this.pitchEffect.updatePlayer(player); @@ -255,7 +74,7 @@ AudioPlayer.prototype.playSound = function (md5) { this.activeSoundPlayers[md5] = player; // remove sounds that are not playing from the active sound players array - for (var id in this.activeSoundPlayers) { + for (const id in this.activeSoundPlayers) { if (this.activeSoundPlayers.hasOwnProperty(id)) { if (!this.activeSoundPlayers[id].isPlaying) { delete this.activeSoundPlayers[id]; @@ -283,7 +102,7 @@ AudioPlayer.prototype.playDrumForBeats = function (drum, beats) { */ AudioPlayer.prototype.stopAllSounds = function () { // stop all active sound players - for (var md5 in this.activeSoundPlayers) { + for (const md5 in this.activeSoundPlayers) { this.activeSoundPlayers[md5].stop(); } @@ -344,4 +163,185 @@ 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 () { + + // 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 + ); + + // 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; + + // 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 = {}; + + // 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); + } + + 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); + } + ); +}; + +/** + * 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); + } + if (this.mic && this.mic.state === 'started') { + return this.micMeter.value * 100; + } + return -1; + +}; + +/** + * Names of the audio effects. + * @readonly + * @enum {string} + */ +AudioEngine.prototype.EFFECT_NAMES = { + pitch: 'pitch', + pan: 'pan', + echo: 'echo', + reverb: 'reverb', + fuzz: 'fuzz', + robot: 'robot' +}; + +/** + * Create an AudioPlayer. Each sprite or clone has an AudioPlayer. + * It includes a reference to the AudioEngine so it can use global + * functionality such as playing notes. + * @return {AudioPlayer} new AudioPlayer instance + */ +AudioEngine.prototype.createPlayer = function () { + return new AudioPlayer(this); +}; + module.exports = AudioEngine; diff --git a/src/log.js b/src/log.js index 9be8fac..da6ed3c 100644 --- a/src/log.js +++ b/src/log.js @@ -1,4 +1,4 @@ -var minilog = require('minilog'); +const minilog = require('minilog'); minilog.enable(); module.exports = minilog('scratch-audioengine'); diff --git a/webpack.config.js b/webpack.config.js index 4ad8cfe..40ffb73 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,7 +2,7 @@ var path = require('path'); module.exports = { entry: { - 'dist': './src/index.js' + dist: './src/index.js' }, output: { path: __dirname,