From 5e6e33264de460f48f7f24c57e46b11214aee412 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 19 Jun 2017 17:25:11 -0400 Subject: [PATCH 01/14] Remove tone.js dependency --- src/effects/PitchEffect.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/effects/PitchEffect.js b/src/effects/PitchEffect.js index 38f8fd1..f482185 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -1,5 +1,3 @@ -const Tone = require('tone'); - /** * A pitch change effect, which changes the playback rate of the sound in order * to change its pitch: reducing the playback rate lowers the pitch, increasing the rate @@ -21,7 +19,6 @@ class PitchEffect { constructor () { this.value = 0; // effect value this.ratio = 1; // the playback rate ratio - this.tone = new Tone(); } /** @@ -52,9 +49,19 @@ class PitchEffect { * @returns {number} a playback ratio */ getRatio (val) { - return this.tone.intervalToFrequencyRatio(val / 10); + return intervalToFrequencyRatio(val / 10); } + /** + * Convert a musical interval to a frequency ratio. + * With thanks to Tone.js: https://github.com/Tonejs/Tone.js + * @param {number} interval - a musical interval, in semitones + * @returns {number} a frequency ratio + */ + intervalToFrequencyRatio (interval) { + return Math.pow(2, (interval/12)); + }; + /** * Update a sound player's playback rate using the current ratio for the effect * @param {object} player - a SoundPlayer object From e515bf4026a0a306878adae4544a9462f0c44f44 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 19 Jun 2017 17:40:53 -0400 Subject: [PATCH 02/14] Cleanup --- src/effects/PitchEffect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/PitchEffect.js b/src/effects/PitchEffect.js index f482185..2281eba 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -60,7 +60,7 @@ class PitchEffect { */ intervalToFrequencyRatio (interval) { return Math.pow(2, (interval/12)); - }; + } /** * Update a sound player's playback rate using the current ratio for the effect From 865d3cde88fa36d0b15694c87a83e1775f24b105 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 19 Jun 2017 17:41:08 -0400 Subject: [PATCH 03/14] Correctly call this.intervalToFrequencyRatio --- src/effects/PitchEffect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/PitchEffect.js b/src/effects/PitchEffect.js index 2281eba..7cebc44 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -49,7 +49,7 @@ class PitchEffect { * @returns {number} a playback ratio */ getRatio (val) { - return intervalToFrequencyRatio(val / 10); + return this.intervalToFrequencyRatio(val / 10); } /** From 03034dd2f7dcbb602cb01dbbe38edc822b969edc Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 20 Jun 2017 16:50:02 -0400 Subject: [PATCH 04/14] Remove dependency on Tone.js --- package.json | 1 - src/ADPCMSoundDecoder.js | 9 ++++--- src/DrumPlayer.js | 29 +++++++++++++++------- src/InstrumentPlayer.js | 12 ++++----- src/SoundPlayer.js | 16 ++++++------ src/effects/PanEffect.js | 15 ++++++------ src/index.js | 53 +++++++++++++++++++++------------------- 7 files changed, 75 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 351192c..60f5821 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "json": "^9.0.6", "minilog": "^3.0.1", "soundfont-player": "0.10.5", - "tone": "0.9.0", "travis-after-all": "^1.4.4", "webpack": "2.4.0" } diff --git a/src/ADPCMSoundDecoder.js b/src/ADPCMSoundDecoder.js index e9e0b00..3a29d79 100644 --- a/src/ADPCMSoundDecoder.js +++ b/src/ADPCMSoundDecoder.js @@ -1,5 +1,4 @@ const ArrayBufferStream = require('./ArrayBufferStream'); -const Tone = require('tone'); const log = require('./log'); /** @@ -10,6 +9,9 @@ const log = require('./log'); * https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as */ class ADPCMSoundDecoder { + constructor (context) { + this.context = context; + } /** * Data used by the decompression algorithm * @type {Array} @@ -40,7 +42,7 @@ class ADPCMSoundDecoder { * Decode an ADPCM sound stored in an ArrayBuffer and return a promise * with the decoded audio buffer. * @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio - * @return {Tone.Buffer} the decoded audio buffer + * @return {AudioBuffer} the decoded audio buffer */ decode (audioData) { @@ -77,8 +79,7 @@ class ADPCMSoundDecoder { const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); - // @todo this line is the only place Tone is used here, should be possible to remove - const buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond); + const buffer = this.context.createBuffer(1, samples.length, this.samplesPerSecond); // @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply? for (let i = 0; i < samples.length; i++) { diff --git a/src/DrumPlayer.js b/src/DrumPlayer.js index f4d0fde..b020542 100644 --- a/src/DrumPlayer.js +++ b/src/DrumPlayer.js @@ -1,14 +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 + * @param {AudioContext} context - a webAudio context * @constructor */ - constructor (outputNode) { - this.outputNode = outputNode; + constructor (context) { + this.context = context; const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; const fileNames = [ @@ -35,9 +34,21 @@ class DrumPlayer { this.drumSounds = []; for (let i = 0; i < fileNames.length; i++) { - const url = `${baseUrl + fileNames[i]}_22k.wav`; - this.drumSounds[i] = new SoundPlayer(this.outputNode); - this.drumSounds[i].setBuffer(new Tone.Buffer(url)); + this.drumSounds[i] = new SoundPlayer(this.context); + + // download and decode the drum sounds + // @todo: use scratch-storage to manage these sound files + const url = baseUrl + fileNames[i] + '_22k.wav'; + const request = new XMLHttpRequest(); + request.open('GET', url, true); + request.responseType = 'arraybuffer'; + request.onload = () => { + const audioData = request.response; + this.context.decodeAudioData(audioData).then(buffer => { + this.drumSounds[i].setBuffer(buffer); + }); + }; + request.send(); } } @@ -46,10 +57,10 @@ class DrumPlayer { * The parameter for output node allows sprites or clones to send the drum sound * to their individual audio effect chains. * @param {number} drum - the drum number to play (0-indexed) - * @param {Tone.Gain} outputNode - a node to send the output to + * @param {AudioNode} outputNode - a node to send the output to */ play (drum, outputNode) { - this.drumSounds[drum].outputNode = outputNode; + this.drumSounds[drum].connect(outputNode); this.drumSounds[drum].start(); } diff --git a/src/InstrumentPlayer.js b/src/InstrumentPlayer.js index d7e3a77..627e910 100644 --- a/src/InstrumentPlayer.js +++ b/src/InstrumentPlayer.js @@ -1,4 +1,3 @@ -const Tone = require('tone'); const Soundfont = require('soundfont-player'); class InstrumentPlayer { @@ -10,11 +9,12 @@ class InstrumentPlayer { * play note or set instrument block runs, causing a delay of a few seconds. * Using this library we don't have a way to set the volume, sustain the note beyond the sample * duration, or run it through the sprite-specific audio effects. - * @param {Tone.Gain} outputNode - a webAudio node that the instrument will send its output to + * @param {AudioNode} outputNode - a webAudio node that the instrument will send its output to * @constructor */ - constructor (outputNode) { - this.outputNode = outputNode; + constructor (context) { + this.context = context; + this.outputNode = null; // Instrument names used by Musyng Kite soundfont, in order to // match scratch instruments @@ -42,7 +42,7 @@ class InstrumentPlayer { this.loadInstrument(instrumentNum) .then(() => { this.instruments[instrumentNum].play( - note, Tone.context.currentTime, { + note, this.context.currentTime, { duration: sec, gain: gain } @@ -59,7 +59,7 @@ class InstrumentPlayer { if (this.instruments[instrumentNum]) { return Promise.resolve(); } - return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) + return Soundfont.instrument(this.context, this.instrumentNames[instrumentNum]) .then(inst => { inst.connect(this.outputNode); this.instruments[instrumentNum] = inst; diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index debf85e..1a67b7f 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -1,13 +1,13 @@ -const Tone = require('tone'); const log = require('./log'); /** * A SoundPlayer stores an audio buffer, and plays it */ class SoundPlayer { - constructor () { + constructor (context) { + this.context = context; this.outputNode = null; - this.buffer = new Tone.Buffer(); + this.buffer = null; this.bufferSource = null; this.playbackRate = 1; this.isPlaying = false; @@ -15,7 +15,7 @@ class SoundPlayer { /** * Connect the SoundPlayer to an output node - * @param {Tone.Gain} node - an output node to connect to + * @param {GainNode} node - an output node to connect to */ connect (node) { this.outputNode = node; @@ -23,7 +23,7 @@ class SoundPlayer { /** * Set an audio buffer - * @param {Tone.Buffer} buffer Buffer to set + * @param {AudioBuffer} buffer - Buffer to set */ setBuffer (buffer) { this.buffer = buffer; @@ -55,13 +55,13 @@ class SoundPlayer { * The web audio framework requires a new audio buffer source node for each playback */ start () { - if (!this.buffer || !this.buffer.loaded) { + if (!this.buffer) { log.warn('tried to play a sound that was not loaded yet'); return; } - this.bufferSource = Tone.context.createBufferSource(); - this.bufferSource.buffer = this.buffer.get(); + this.bufferSource = this.context.createBufferSource(); + this.bufferSource.buffer = this.buffer; this.bufferSource.playbackRate.value = this.playbackRate; this.bufferSource.connect(this.outputNode); this.bufferSource.start(); diff --git a/src/effects/PanEffect.js b/src/effects/PanEffect.js index b091600..86e55eb 100644 --- a/src/effects/PanEffect.js +++ b/src/effects/PanEffect.js @@ -1,17 +1,14 @@ -const Tone = require('tone'); - /** * A pan effect, which moves the sound to the left or right between the speakers * 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 */ -class PanEffect extends Tone.Effect { - constructor () { - super(); +class PanEffect { + constructor (context) { + this.context = context; + this.panner = this.context.createStereoPanner(); this.value = 0; - this.panner = new Tone.Panner(); - this.effectSend.chain(this.panner, this.effectReturn); } /** @@ -23,6 +20,10 @@ class PanEffect extends Tone.Effect { this.panner.pan.value = this.value / 100; } + connect (node) { + this.panner.connect(node); + } + /** * Change the effect value * @param {number} val - the value to change the effect by diff --git a/src/index.js b/src/index.js index b794c22..149f843 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,4 @@ const log = require('./log'); -const Tone = require('tone'); const PitchEffect = require('./effects/PitchEffect'); const PanEffect = require('./effects/PanEffect'); @@ -25,15 +24,15 @@ class AudioPlayer { constructor (audioEngine) { this.audioEngine = audioEngine; - // effects setup + // Create the audio effects this.pitchEffect = new PitchEffect(); - this.panEffect = new PanEffect(); + this.panEffect = new PanEffect(this.audioEngine.context); - // 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); + // Chain the audio effects together + // effectsNode -> panEffect -> audioEngine.input -> destination (speakers) + this.effectsNode = this.audioEngine.context.createGain(); + this.effectsNode.connect(this.panEffect.panner); + this.panEffect.connect(this.audioEngine.input); // reset effects to their default parameters this.clearEffects(); @@ -59,7 +58,7 @@ class AudioPlayer { } // create a new soundplayer to play the sound - const player = new SoundPlayer(); + const player = new SoundPlayer(this.audioEngine.context); player.setBuffer(this.audioEngine.audioBuffers[md5]); player.connect(this.effectsNode); this.pitchEffect.updatePlayer(player); @@ -150,18 +149,22 @@ class AudioPlayer { */ class AudioEngine { constructor () { - this.input = new Tone.Gain(); - this.input.connect(Tone.Master); + var AudioContext = window.AudioContext || window.webkitAudioContext; + this.context = new AudioContext(); + + this.input = this.context.createGain(); + this.input.connect(this.context.destination); // global tempo in bpm (beats per minute) this.currentTempo = 60; // instrument player for play note blocks - this.instrumentPlayer = new InstrumentPlayer(this.input); + this.instrumentPlayer = new InstrumentPlayer(this.context); + this.instrumentPlayer.outputNode = this.input; this.numInstruments = this.instrumentPlayer.instrumentNames.length; // drum player for play drum blocks - this.drumPlayer = new DrumPlayer(this.input); + this.drumPlayer = new DrumPlayer(this.context); this.numDrums = this.drumPlayer.drumSounds.length; // a map of md5s to audio buffers, holding sounds for all sprites @@ -201,10 +204,10 @@ class AudioEngine { switch (sound.format) { case '': - loaderPromise = Tone.context.decodeAudioData(bufferCopy); + loaderPromise = this.context.decodeAudioData(bufferCopy); break; case 'adpcm': - loaderPromise = (new ADPCMSoundDecoder()).decode(bufferCopy); + loaderPromise = (new ADPCMSoundDecoder(this.context)).decode(bufferCopy); break; default: return log.warn('unknown sound format', sound.format); @@ -213,7 +216,7 @@ class AudioEngine { const storedContext = this; return loaderPromise.then( decodedAudio => { - storedContext.audioBuffers[sound.md5] = new Tone.Buffer(decodedAudio); + storedContext.audioBuffers[sound.md5] = decodedAudio; }, error => { log.warn('audio data could not be decoded', error); @@ -289,15 +292,15 @@ class AudioEngine { * @return {number} loudness scaled 0 to 100 */ getLoudness () { - if (!this.mic) { - this.mic = new Tone.UserMedia(); - this.micMeter = new Tone.Meter('level', 0.5); - this.mic.open(); - this.mic.connect(this.micMeter); - } - if (this.mic && this.mic.state === 'started') { - return this.micMeter.value * 100; - } + // 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; } From 2974a5e65b70a0813d9394e9f11c05259f5258b8 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 20 Jun 2017 18:34:12 -0400 Subject: [PATCH 05/14] Re-implement loudness block --- src/index.js | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index 149f843..ea53ea7 100644 --- a/src/index.js +++ b/src/index.js @@ -289,20 +289,47 @@ class AudioEngine { /** * Get the current loudness of sound received by the microphone. * Sound is measured in RMS and smoothed. + * Some code adapted from Tone.js: https://github.com/Tonejs/Tone.js * @return {number} loudness scaled 0 to 100 */ getLoudness () { - // if (!this.mic) { - // this.mic = new Tone.UserMedia(); - // this.micMeter = new Tone.Meter('level', 0.5); - // this.mic.open(); - // this.mic.connect(this.micMeter); - // } - // if (this.mic && this.mic.state === 'started') { - // return this.micMeter.value * 100; - // } - return -1; + // the microphone has not been set up, try to connect to it + if (!this.mic && !this.connectingToMic) { + this.connectingToMic = true; // prevent multiple connection attempts + navigator.mediaDevices.getUserMedia({audio : true}).then(stream => { + this.mic = this.context.createMediaStreamSource(stream); + this.analyser = this.context.createAnalyser(); + this.mic.connect(this.analyser); + this.micDataArray = new Float32Array(this.analyser.fftSize); + }).catch(err => { + log.warn(err) + }); + } + // if the microphone is set up and active, measure the loudness + if (this.mic && this.mic.mediaStream.active) { + this.analyser.getFloatTimeDomainData(this.micDataArray); + let sum = 0; + // compute the RMS of the sound + for (let i = 0; i < this.micDataArray.length; i++){ + sum += Math.pow(this.micDataArray[i], 2); + } + let rms = Math.sqrt(sum / this.micDataArray.length); + // smooth the value, if it is descending + if (this._lastValue) { + rms = Math.max(rms, this._lastValue * 0.5); + } + this._lastValue = rms; + + // scale it + // @todo figure out why this magic number is needed and remove it! + rms *= 1.63; + // scale and round the output + return Math.round(Math.sqrt(rms) * 100); + } + + // if there is no microphone input, return -1 + return -1; } /** From 43e04084341fb5487b1e6bc1c052bdeaf63f4a70 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 21 Jun 2017 10:46:42 -0400 Subject: [PATCH 06/14] Lint --- src/DrumPlayer.js | 2 +- src/InstrumentPlayer.js | 2 +- src/effects/PitchEffect.js | 2 +- src/index.js | 11 ++++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/DrumPlayer.js b/src/DrumPlayer.js index b020542..3d1aaf4 100644 --- a/src/DrumPlayer.js +++ b/src/DrumPlayer.js @@ -38,7 +38,7 @@ class DrumPlayer { // download and decode the drum sounds // @todo: use scratch-storage to manage these sound files - const url = baseUrl + fileNames[i] + '_22k.wav'; + const url = `${baseUrl}${fileNames[i]}_22k.wav`; const request = new XMLHttpRequest(); request.open('GET', url, true); request.responseType = 'arraybuffer'; diff --git a/src/InstrumentPlayer.js b/src/InstrumentPlayer.js index 627e910..bb3484d 100644 --- a/src/InstrumentPlayer.js +++ b/src/InstrumentPlayer.js @@ -9,7 +9,7 @@ class InstrumentPlayer { * play note or set instrument block runs, causing a delay of a few seconds. * Using this library we don't have a way to set the volume, sustain the note beyond the sample * duration, or run it through the sprite-specific audio effects. - * @param {AudioNode} outputNode - a webAudio node that the instrument will send its output to + * @param {AudioContext} context - a webAudio context * @constructor */ constructor (context) { diff --git a/src/effects/PitchEffect.js b/src/effects/PitchEffect.js index 7cebc44..3de5c5b 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -59,7 +59,7 @@ class PitchEffect { * @returns {number} a frequency ratio */ intervalToFrequencyRatio (interval) { - return Math.pow(2, (interval/12)); + return Math.pow(2, (interval / 12)); } /** diff --git a/src/index.js b/src/index.js index ea53ea7..ff638ba 100644 --- a/src/index.js +++ b/src/index.js @@ -149,7 +149,7 @@ class AudioPlayer { */ class AudioEngine { constructor () { - var AudioContext = window.AudioContext || window.webkitAudioContext; + const AudioContext = window.AudioContext || window.webkitAudioContext; this.context = new AudioContext(); this.input = this.context.createGain(); @@ -200,7 +200,7 @@ class AudioEngine { let loaderPromise = null; // Make a copy of the buffer because decoding detaches the original buffer - var bufferCopy = sound.data.buffer.slice(0); + const bufferCopy = sound.data.buffer.slice(0); switch (sound.format) { case '': @@ -296,13 +296,14 @@ class AudioEngine { // the microphone has not been set up, try to connect to it if (!this.mic && !this.connectingToMic) { this.connectingToMic = true; // prevent multiple connection attempts - navigator.mediaDevices.getUserMedia({audio : true}).then(stream => { + navigator.mediaDevices.getUserMedia({audio: true}).then(stream => { this.mic = this.context.createMediaStreamSource(stream); this.analyser = this.context.createAnalyser(); this.mic.connect(this.analyser); this.micDataArray = new Float32Array(this.analyser.fftSize); - }).catch(err => { - log.warn(err) + }) + .catch(err => { + log.warn(err); }); } From 90af375b0335a43a77dd24f92a426b5a52c9dd0b Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 21 Jun 2017 11:23:32 -0400 Subject: [PATCH 07/14] Use arrow functions --- src/SoundPlayer.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index 1a67b7f..ed73a78 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -75,12 +75,11 @@ class SoundPlayer { * @return {Promise} a Promise that resolves when the sound finishes playing */ finished () { - const storedContext = this; return new Promise(resolve => { - storedContext.bufferSource.onended = function () { + this.bufferSource.onended = () => { this.isPlaying = false; resolve(); - }.bind(storedContext); + }; }); } } From 0bc892f03d05564210b24051f48ed97d9f73adfc Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 22 Jun 2017 10:51:57 -0400 Subject: [PATCH 08/14] =?UTF-8?q?Use=20=E2=80=9CaudioContext=E2=80=9D=20to?= =?UTF-8?q?=20refer=20to=20webAudioContext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ADPCMSoundDecoder.js | 10 +++++++--- src/DrumPlayer.js | 10 +++++----- src/InstrumentPlayer.js | 10 +++++----- src/SoundPlayer.js | 10 +++++++--- src/effects/PanEffect.js | 10 +++++++--- src/index.js | 24 ++++++++++++------------ 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/ADPCMSoundDecoder.js b/src/ADPCMSoundDecoder.js index 3a29d79..b970d76 100644 --- a/src/ADPCMSoundDecoder.js +++ b/src/ADPCMSoundDecoder.js @@ -9,8 +9,12 @@ const log = require('./log'); * https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as */ class ADPCMSoundDecoder { - constructor (context) { - this.context = context; + /** + * @param {AudioContext} audioContext - a webAudio context + * @constructor + */ + constructor (audioContext) { + this.audioContext = audioContext; } /** * Data used by the decompression algorithm @@ -79,7 +83,7 @@ class ADPCMSoundDecoder { const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); - const buffer = this.context.createBuffer(1, samples.length, this.samplesPerSecond); + const buffer = this.audioContext.createBuffer(1, samples.length, this.samplesPerSecond); // @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply? for (let i = 0; i < samples.length; i++) { diff --git a/src/DrumPlayer.js b/src/DrumPlayer.js index 3d1aaf4..8592321 100644 --- a/src/DrumPlayer.js +++ b/src/DrumPlayer.js @@ -3,11 +3,11 @@ const SoundPlayer = require('./SoundPlayer'); class DrumPlayer { /** * A prototype for the drum sound functionality that can load drum sounds, play, and stop them. - * @param {AudioContext} context - a webAudio context + * @param {AudioContext} audioContext - a webAudio context * @constructor */ - constructor (context) { - this.context = context; + constructor (audioContext) { + this.audioContext = audioContext; const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; const fileNames = [ @@ -34,7 +34,7 @@ class DrumPlayer { this.drumSounds = []; for (let i = 0; i < fileNames.length; i++) { - this.drumSounds[i] = new SoundPlayer(this.context); + this.drumSounds[i] = new SoundPlayer(this.audioContext); // download and decode the drum sounds // @todo: use scratch-storage to manage these sound files @@ -44,7 +44,7 @@ class DrumPlayer { request.responseType = 'arraybuffer'; request.onload = () => { const audioData = request.response; - this.context.decodeAudioData(audioData).then(buffer => { + this.audioContext.decodeAudioData(audioData).then(buffer => { this.drumSounds[i].setBuffer(buffer); }); }; diff --git a/src/InstrumentPlayer.js b/src/InstrumentPlayer.js index bb3484d..87c900f 100644 --- a/src/InstrumentPlayer.js +++ b/src/InstrumentPlayer.js @@ -9,11 +9,11 @@ class InstrumentPlayer { * play note or set instrument block runs, causing a delay of a few seconds. * Using this library we don't have a way to set the volume, sustain the note beyond the sample * duration, or run it through the sprite-specific audio effects. - * @param {AudioContext} context - a webAudio context + * @param {AudioContext} audioContext - a webAudio context * @constructor */ - constructor (context) { - this.context = context; + constructor (audioContext) { + this.audioContext = audioContext; this.outputNode = null; // Instrument names used by Musyng Kite soundfont, in order to @@ -42,7 +42,7 @@ class InstrumentPlayer { this.loadInstrument(instrumentNum) .then(() => { this.instruments[instrumentNum].play( - note, this.context.currentTime, { + note, this.audioContext.currentTime, { duration: sec, gain: gain } @@ -59,7 +59,7 @@ class InstrumentPlayer { if (this.instruments[instrumentNum]) { return Promise.resolve(); } - return Soundfont.instrument(this.context, this.instrumentNames[instrumentNum]) + return Soundfont.instrument(this.audioContext, this.instrumentNames[instrumentNum]) .then(inst => { inst.connect(this.outputNode); this.instruments[instrumentNum] = inst; diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index ed73a78..35e9e67 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -4,8 +4,12 @@ const log = require('./log'); * A SoundPlayer stores an audio buffer, and plays it */ class SoundPlayer { - constructor (context) { - this.context = context; + /** + * @param {AudioContext} audioContext - a webAudio context + * @constructor + */ + constructor (audioContext) { + this.audioContext = audioContext; this.outputNode = null; this.buffer = null; this.bufferSource = null; @@ -60,7 +64,7 @@ class SoundPlayer { return; } - this.bufferSource = this.context.createBufferSource(); + this.bufferSource = this.audioContext.createBufferSource(); this.bufferSource.buffer = this.buffer; this.bufferSource.playbackRate.value = this.playbackRate; this.bufferSource.connect(this.outputNode); diff --git a/src/effects/PanEffect.js b/src/effects/PanEffect.js index 86e55eb..8374243 100644 --- a/src/effects/PanEffect.js +++ b/src/effects/PanEffect.js @@ -5,9 +5,13 @@ * Clamped -100 to 100 */ class PanEffect { - constructor (context) { - this.context = context; - this.panner = this.context.createStereoPanner(); + /** + * @param {AudioContext} audioContext - a webAudio context + * @constructor + */ + constructor (audioContext) { + this.audioContext = audioContext; + this.panner = this.audioContext.createStereoPanner(); this.value = 0; } diff --git a/src/index.js b/src/index.js index ff638ba..49dfdf2 100644 --- a/src/index.js +++ b/src/index.js @@ -26,11 +26,11 @@ class AudioPlayer { // Create the audio effects this.pitchEffect = new PitchEffect(); - this.panEffect = new PanEffect(this.audioEngine.context); + this.panEffect = new PanEffect(this.audioEngine.audioContext); // Chain the audio effects together // effectsNode -> panEffect -> audioEngine.input -> destination (speakers) - this.effectsNode = this.audioEngine.context.createGain(); + this.effectsNode = this.audioEngine.audioContext.createGain(); this.effectsNode.connect(this.panEffect.panner); this.panEffect.connect(this.audioEngine.input); @@ -58,7 +58,7 @@ class AudioPlayer { } // create a new soundplayer to play the sound - const player = new SoundPlayer(this.audioEngine.context); + const player = new SoundPlayer(this.audioEngine.audioContext); player.setBuffer(this.audioEngine.audioBuffers[md5]); player.connect(this.effectsNode); this.pitchEffect.updatePlayer(player); @@ -150,21 +150,21 @@ class AudioPlayer { class AudioEngine { constructor () { const AudioContext = window.AudioContext || window.webkitAudioContext; - this.context = new AudioContext(); + this.audioContext = new AudioContext(); - this.input = this.context.createGain(); - this.input.connect(this.context.destination); + this.input = this.audioContext.createGain(); + this.input.connect(this.audioContext.destination); // global tempo in bpm (beats per minute) this.currentTempo = 60; // instrument player for play note blocks - this.instrumentPlayer = new InstrumentPlayer(this.context); + this.instrumentPlayer = new InstrumentPlayer(this.audioContext); this.instrumentPlayer.outputNode = this.input; this.numInstruments = this.instrumentPlayer.instrumentNames.length; // drum player for play drum blocks - this.drumPlayer = new DrumPlayer(this.context); + this.drumPlayer = new DrumPlayer(this.audioContext); this.numDrums = this.drumPlayer.drumSounds.length; // a map of md5s to audio buffers, holding sounds for all sprites @@ -204,10 +204,10 @@ class AudioEngine { switch (sound.format) { case '': - loaderPromise = this.context.decodeAudioData(bufferCopy); + loaderPromise = this.audioContext.decodeAudioData(bufferCopy); break; case 'adpcm': - loaderPromise = (new ADPCMSoundDecoder(this.context)).decode(bufferCopy); + loaderPromise = (new ADPCMSoundDecoder(this.audioContext)).decode(bufferCopy); break; default: return log.warn('unknown sound format', sound.format); @@ -297,8 +297,8 @@ class AudioEngine { if (!this.mic && !this.connectingToMic) { this.connectingToMic = true; // prevent multiple connection attempts navigator.mediaDevices.getUserMedia({audio: true}).then(stream => { - this.mic = this.context.createMediaStreamSource(stream); - this.analyser = this.context.createAnalyser(); + this.mic = this.audioContext.createMediaStreamSource(stream); + this.analyser = this.audioContext.createAnalyser(); this.mic.connect(this.analyser); this.micDataArray = new Float32Array(this.analyser.fftSize); }) From 66b4ae379568340c4caf1f5e3eaf5fe3b216e7e6 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 22 Jun 2017 10:54:52 -0400 Subject: [PATCH 09/14] Remove unused variable --- src/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.js b/src/index.js index 49dfdf2..c8a4488 100644 --- a/src/index.js +++ b/src/index.js @@ -172,7 +172,6 @@ class AudioEngine { // microphone, for measuring loudness, with a level meter analyzer this.mic = null; - this.micMeter = null; } /** From b7ff586ca1459ba8351dfd766bcb944e085e0fd5 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 22 Jun 2017 11:06:12 -0400 Subject: [PATCH 10/14] Inline the interval to frequency function --- src/effects/PitchEffect.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/effects/PitchEffect.js b/src/effects/PitchEffect.js index 3de5c5b..4d04736 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -49,16 +49,8 @@ class PitchEffect { * @returns {number} a playback ratio */ getRatio (val) { - return this.intervalToFrequencyRatio(val / 10); - } - - /** - * Convert a musical interval to a frequency ratio. - * With thanks to Tone.js: https://github.com/Tonejs/Tone.js - * @param {number} interval - a musical interval, in semitones - * @returns {number} a frequency ratio - */ - intervalToFrequencyRatio (interval) { + const interval = val / 10; + // Convert the musical interval in semitones to a frequency ratio return Math.pow(2, (interval / 12)); } From a081333ddd19994bb24095becefc276f21376cb8 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 22 Jun 2017 11:07:24 -0400 Subject: [PATCH 11/14] Fix comment --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index c8a4488..7b5f191 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,7 @@ class AudioPlayer { this.panEffect = new PanEffect(this.audioEngine.audioContext); // Chain the audio effects together - // effectsNode -> panEffect -> audioEngine.input -> destination (speakers) + // effectsNode -> panEffect -> audioEngine.input this.effectsNode = this.audioEngine.audioContext.createGain(); this.effectsNode.connect(this.panEffect.panner); this.panEffect.connect(this.audioEngine.input); From dba8c6f91ddbad2331f40d3d352018a0d3e18562 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Fri, 23 Jun 2017 10:59:10 -0400 Subject: [PATCH 12/14] Adjust loudness smoothing --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 7b5f191..3cc959d 100644 --- a/src/index.js +++ b/src/index.js @@ -317,7 +317,7 @@ class AudioEngine { let rms = Math.sqrt(sum / this.micDataArray.length); // smooth the value, if it is descending if (this._lastValue) { - rms = Math.max(rms, this._lastValue * 0.5); + rms = Math.max(rms, this._lastValue * 0.6); } this._lastValue = rms; From 5add85bd60f6e4c82996666d21560dca5d4e5e60 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Fri, 23 Jun 2017 11:58:04 -0400 Subject: [PATCH 13/14] Fix comments --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 3cc959d..c5fc058 100644 --- a/src/index.js +++ b/src/index.js @@ -292,7 +292,7 @@ class AudioEngine { * @return {number} loudness scaled 0 to 100 */ getLoudness () { - // the microphone has not been set up, try to connect to it + // The microphone has not been set up, so try to connect to it if (!this.mic && !this.connectingToMic) { this.connectingToMic = true; // prevent multiple connection attempts navigator.mediaDevices.getUserMedia({audio: true}).then(stream => { @@ -306,7 +306,7 @@ class AudioEngine { }); } - // if the microphone is set up and active, measure the loudness + // If the microphone is set up and active, measure the loudness if (this.mic && this.mic.mediaStream.active) { this.analyser.getFloatTimeDomainData(this.micDataArray); let sum = 0; From dd0d9556ed53783e4f19a62387c026b7fb6b8767 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Fri, 23 Jun 2017 13:13:30 -0400 Subject: [PATCH 14/14] Explain loudness scaling, and clamp to 100 --- src/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index c5fc058..a0841d9 100644 --- a/src/index.js +++ b/src/index.js @@ -321,11 +321,14 @@ class AudioEngine { } this._lastValue = rms; - // scale it - // @todo figure out why this magic number is needed and remove it! + // Scale the measurement so it's more sensitive to quieter sounds rms *= 1.63; - // scale and round the output - return Math.round(Math.sqrt(rms) * 100); + rms = Math.sqrt(rms); + // Scale it up to 0-100 and round + rms = Math.round(rms * 100); + // Prevent it from going above 100 + rms = Math.min(rms, 100); + return rms; } // if there is no microphone input, return -1