scratch-audio
Advanced tools
Comparing version 0.1.0-prerelease.1497907229 to 0.1.0-prerelease.1498238771
{ | ||
"name": "scratch-audio", | ||
"version": "0.1.0-prerelease.1497907229", | ||
"version": "0.1.0-prerelease.1498238771", | ||
"description": "audio engine for scratch 3.0", | ||
@@ -33,3 +33,2 @@ "main": "dist.js", | ||
"soundfont-player": "0.10.5", | ||
"tone": "0.9.0", | ||
"travis-after-all": "^1.4.4", | ||
@@ -36,0 +35,0 @@ "webpack": "2.4.0" |
const ArrayBufferStream = require('./ArrayBufferStream'); | ||
const Tone = require('tone'); | ||
const log = require('./log'); | ||
@@ -14,2 +13,9 @@ | ||
/** | ||
* @param {AudioContext} audioContext - a webAudio context | ||
* @constructor | ||
*/ | ||
constructor (audioContext) { | ||
this.audioContext = audioContext; | ||
} | ||
/** | ||
* Data used by the decompression algorithm | ||
@@ -44,3 +50,3 @@ * @type {Array} | ||
* @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio | ||
* @return {Tone.Buffer} the decoded audio buffer | ||
* @return {AudioBuffer} the decoded audio buffer | ||
*/ | ||
@@ -82,4 +88,3 @@ decode (audioData) { | ||
// @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.audioContext.createBuffer(1, samples.length, this.samplesPerSecond); | ||
@@ -86,0 +91,0 @@ // @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply? |
const SoundPlayer = require('./SoundPlayer'); | ||
const Tone = require('tone'); | ||
@@ -7,7 +6,7 @@ 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} audioContext - a webAudio context | ||
* @constructor | ||
*/ | ||
constructor (outputNode) { | ||
this.outputNode = outputNode; | ||
constructor (audioContext) { | ||
this.audioContext = audioContext; | ||
@@ -39,5 +38,17 @@ const baseUrl = 'https://raw.githubusercontent.com/LLK/scratch-audio/develop/sound-files/drums/'; | ||
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.audioContext); | ||
// 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.audioContext.decodeAudioData(audioData).then(buffer => { | ||
this.drumSounds[i].setBuffer(buffer); | ||
}); | ||
}; | ||
request.send(); | ||
} | ||
@@ -51,6 +62,6 @@ } | ||
* @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(); | ||
@@ -57,0 +68,0 @@ } |
@@ -1,3 +0,1 @@ | ||
const Tone = require('tone'); | ||
/** | ||
@@ -9,8 +7,11 @@ * A pan effect, which moves the sound to the left or right between the speakers | ||
*/ | ||
class PanEffect extends Tone.Effect { | ||
constructor () { | ||
super(); | ||
class PanEffect { | ||
/** | ||
* @param {AudioContext} audioContext - a webAudio context | ||
* @constructor | ||
*/ | ||
constructor (audioContext) { | ||
this.audioContext = audioContext; | ||
this.panner = this.audioContext.createStereoPanner(); | ||
this.value = 0; | ||
this.panner = new Tone.Panner(); | ||
this.effectSend.chain(this.panner, this.effectReturn); | ||
} | ||
@@ -27,2 +28,6 @@ | ||
connect (node) { | ||
this.panner.connect(node); | ||
} | ||
/** | ||
@@ -29,0 +34,0 @@ * Change the effect value |
@@ -1,3 +0,1 @@ | ||
const Tone = require('tone'); | ||
/** | ||
@@ -24,3 +22,2 @@ * A pitch change effect, which changes the playback rate of the sound in order | ||
this.ratio = 1; // the playback rate ratio | ||
this.tone = new Tone(); | ||
} | ||
@@ -56,3 +53,5 @@ | ||
getRatio (val) { | ||
return this.tone.intervalToFrequencyRatio(val / 10); | ||
const interval = val / 10; | ||
// Convert the musical interval in semitones to a frequency ratio | ||
return Math.pow(2, (interval / 12)); | ||
} | ||
@@ -59,0 +58,0 @@ |
const log = require('./log'); | ||
const Tone = require('tone'); | ||
@@ -28,11 +27,11 @@ const PitchEffect = require('./effects/PitchEffect'); | ||
// effects setup | ||
// Create the audio effects | ||
this.pitchEffect = new PitchEffect(); | ||
this.panEffect = new PanEffect(); | ||
this.panEffect = new PanEffect(this.audioEngine.audioContext); | ||
// 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 | ||
this.effectsNode = this.audioEngine.audioContext.createGain(); | ||
this.effectsNode.connect(this.panEffect.panner); | ||
this.panEffect.connect(this.audioEngine.input); | ||
@@ -63,3 +62,3 @@ // reset effects to their default parameters | ||
// create a new soundplayer to play the sound | ||
const player = new SoundPlayer(); | ||
const player = new SoundPlayer(this.audioEngine.audioContext); | ||
player.setBuffer(this.audioEngine.audioBuffers[md5]); | ||
@@ -155,5 +154,8 @@ player.connect(this.effectsNode); | ||
constructor () { | ||
this.input = new Tone.Gain(); | ||
this.input.connect(Tone.Master); | ||
const AudioContext = window.AudioContext || window.webkitAudioContext; | ||
this.audioContext = new AudioContext(); | ||
this.input = this.audioContext.createGain(); | ||
this.input.connect(this.audioContext.destination); | ||
// global tempo in bpm (beats per minute) | ||
@@ -163,7 +165,8 @@ this.currentTempo = 60; | ||
// instrument player for play note blocks | ||
this.instrumentPlayer = new InstrumentPlayer(this.input); | ||
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.input); | ||
this.drumPlayer = new DrumPlayer(this.audioContext); | ||
this.numDrums = this.drumPlayer.drumSounds.length; | ||
@@ -176,3 +179,2 @@ | ||
this.mic = null; | ||
this.micMeter = null; | ||
} | ||
@@ -205,10 +207,10 @@ | ||
// 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 '': | ||
loaderPromise = Tone.context.decodeAudioData(bufferCopy); | ||
loaderPromise = this.audioContext.decodeAudioData(bufferCopy); | ||
break; | ||
case 'adpcm': | ||
loaderPromise = (new ADPCMSoundDecoder()).decode(bufferCopy); | ||
loaderPromise = (new ADPCMSoundDecoder(this.audioContext)).decode(bufferCopy); | ||
break; | ||
@@ -222,3 +224,3 @@ default: | ||
decodedAudio => { | ||
storedContext.audioBuffers[sound.md5] = new Tone.Buffer(decodedAudio); | ||
storedContext.audioBuffers[sound.md5] = decodedAudio; | ||
}, | ||
@@ -296,16 +298,47 @@ error => { | ||
* 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); | ||
// 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 => { | ||
this.mic = this.audioContext.createMediaStreamSource(stream); | ||
this.analyser = this.audioContext.createAnalyser(); | ||
this.mic.connect(this.analyser); | ||
this.micDataArray = new Float32Array(this.analyser.fftSize); | ||
}) | ||
.catch(err => { | ||
log.warn(err); | ||
}); | ||
} | ||
if (this.mic && this.mic.state === 'started') { | ||
return this.micMeter.value * 100; | ||
// 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.6); | ||
} | ||
this._lastValue = rms; | ||
// Scale the measurement so it's more sensitive to quieter sounds | ||
rms *= 1.63; | ||
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 | ||
return -1; | ||
} | ||
@@ -312,0 +345,0 @@ |
@@ -1,2 +0,1 @@ | ||
const Tone = require('tone'); | ||
const Soundfont = require('soundfont-player'); | ||
@@ -13,7 +12,8 @@ | ||
* 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 {AudioContext} audioContext - a webAudio context | ||
* @constructor | ||
*/ | ||
constructor (outputNode) { | ||
this.outputNode = outputNode; | ||
constructor (audioContext) { | ||
this.audioContext = audioContext; | ||
this.outputNode = null; | ||
@@ -46,3 +46,3 @@ // Instrument names used by Musyng Kite soundfont, in order to | ||
this.instruments[instrumentNum].play( | ||
note, Tone.context.currentTime, { | ||
note, this.audioContext.currentTime, { | ||
duration: sec, | ||
@@ -64,3 +64,3 @@ gain: gain | ||
} | ||
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]) | ||
return Soundfont.instrument(this.audioContext, this.instrumentNames[instrumentNum]) | ||
.then(inst => { | ||
@@ -67,0 +67,0 @@ inst.connect(this.outputNode); |
@@ -1,2 +0,1 @@ | ||
const Tone = require('tone'); | ||
const log = require('./log'); | ||
@@ -8,5 +7,10 @@ | ||
class SoundPlayer { | ||
constructor () { | ||
/** | ||
* @param {AudioContext} audioContext - a webAudio context | ||
* @constructor | ||
*/ | ||
constructor (audioContext) { | ||
this.audioContext = audioContext; | ||
this.outputNode = null; | ||
this.buffer = new Tone.Buffer(); | ||
this.buffer = null; | ||
this.bufferSource = null; | ||
@@ -19,3 +23,3 @@ this.playbackRate = 1; | ||
* 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 | ||
*/ | ||
@@ -28,3 +32,3 @@ connect (node) { | ||
* Set an audio buffer | ||
* @param {Tone.Buffer} buffer Buffer to set | ||
* @param {AudioBuffer} buffer - Buffer to set | ||
*/ | ||
@@ -61,3 +65,3 @@ setBuffer (buffer) { | ||
start () { | ||
if (!this.buffer || !this.buffer.loaded) { | ||
if (!this.buffer) { | ||
log.warn('tried to play a sound that was not loaded yet'); | ||
@@ -67,4 +71,4 @@ return; | ||
this.bufferSource = Tone.context.createBufferSource(); | ||
this.bufferSource.buffer = this.buffer.get(); | ||
this.bufferSource = this.audioContext.createBufferSource(); | ||
this.bufferSource.buffer = this.buffer; | ||
this.bufferSource.playbackRate.value = this.playbackRate; | ||
@@ -83,8 +87,7 @@ this.bufferSource.connect(this.outputNode); | ||
finished () { | ||
const storedContext = this; | ||
return new Promise(resolve => { | ||
storedContext.bufferSource.onended = function () { | ||
this.bufferSource.onended = () => { | ||
this.isPlaying = false; | ||
resolve(); | ||
}.bind(storedContext); | ||
}; | ||
}); | ||
@@ -91,0 +94,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
11
592715
3780
24