scratch-audio
Advanced tools
Comparing version
{ | ||
"name": "scratch-audio", | ||
"version": "0.1.0-prerelease.20180621200816", | ||
"version": "0.1.0-prerelease.20180621210133", | ||
"description": "audio engine for scratch 3.0", | ||
@@ -5,0 +5,0 @@ "main": "dist.js", |
@@ -8,6 +8,12 @@ const StartAudioContext = require('./StartAudioContext'); | ||
const ADPCMSoundDecoder = require('./ADPCMSoundDecoder'); | ||
const AudioPlayer = require('./AudioPlayer'); | ||
const Loudness = require('./Loudness'); | ||
const SoundPlayer = require('./GreenPlayer'); | ||
const SoundPlayer = require('./SoundPlayer'); | ||
const EffectChain = require('./effects/EffectChain'); | ||
const PanEffect = require('./effects/PanEffect'); | ||
const PitchEffect = require('./effects/PitchEffect'); | ||
const VolumeEffect = require('./effects/VolumeEffect'); | ||
const SoundBank = require('./SoundBank'); | ||
/** | ||
@@ -67,2 +73,8 @@ * Wrapper to ensure that audioContext.decodeAudioData is a promise | ||
this.loudness = null; | ||
/** | ||
* Array of effects applied in order, left to right, | ||
* Left is closest to input, Right is closest to output | ||
*/ | ||
this.effects = [PanEffect, PitchEffect, VolumeEffect]; | ||
} | ||
@@ -191,6 +203,5 @@ | ||
* Retrieve the audio buffer as held in memory for a given sound id. | ||
* @param {!string} soundId - the id of the sound buffer to get | ||
* @return {AudioBuffer} the buffer corresponding to the given sound id. | ||
* @todo remove this | ||
*/ | ||
getSoundBuffer (soundId) { | ||
getSoundBuffer () { | ||
// todo: Deprecate audioBuffers. If something wants to hold onto the | ||
@@ -201,3 +212,3 @@ // buffer, it should. Otherwise buffers need to be able to release their | ||
// each audio channel. | ||
return this.audioBuffers[soundId]; | ||
log.warn('The getSoundBuffer function is no longer available. Use soundBank.getSoundPlayer().buffer.'); | ||
} | ||
@@ -207,9 +218,6 @@ | ||
* Add or update the in-memory audio buffer to a new one by soundId. | ||
* @param {!string} soundId - the id of the sound buffer to update. | ||
* @param {AudioBuffer} newBuffer - the new buffer to swap in. | ||
* @return {string} The uid of the sound that was updated or added | ||
* @todo remove this | ||
*/ | ||
updateSoundBuffer (soundId, newBuffer) { | ||
this.audioBuffers[soundId] = newBuffer; | ||
return soundId; | ||
updateSoundBuffer () { | ||
log.warn('The updateSoundBuffer function is no longer available. Use soundBank.getSoundPlayer().buffer.'); | ||
} | ||
@@ -241,12 +249,20 @@ | ||
/** | ||
* 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 | ||
* Deprecated way to create an AudioPlayer | ||
* @todo remove this | ||
*/ | ||
createPlayer () { | ||
return new AudioPlayer(this); | ||
log.warn('the createPlayer method is no longer available, please use createBank'); | ||
} | ||
createEffectChain () { | ||
const effects = new EffectChain(this, this.effects); | ||
effects.connect(this); | ||
return effects; | ||
} | ||
createBank () { | ||
return new SoundBank(this, this.createEffectChain()); | ||
} | ||
} | ||
module.exports = AudioEngine; |
@@ -27,2 +27,10 @@ /** | ||
/** | ||
* Return the name of the effect. | ||
* @type {string} | ||
*/ | ||
get name () { | ||
throw new Error(`${this.constructor.name}.name is not implemented`); | ||
} | ||
/** | ||
* Default value to set the Effect to when constructed and when clear'ed. | ||
@@ -29,0 +37,0 @@ * @const {number} |
@@ -23,2 +23,6 @@ const Effect = require('./Effect'); | ||
get name () { | ||
return 'pan'; | ||
} | ||
/** | ||
@@ -25,0 +29,0 @@ * Initialize the Effect. |
@@ -38,2 +38,6 @@ const Effect = require('./Effect'); | ||
get name () { | ||
return 'pitch'; | ||
} | ||
/** | ||
@@ -40,0 +44,0 @@ * Should the effect be connected to the audio graph? |
@@ -15,2 +15,6 @@ const Effect = require('./Effect'); | ||
get name () { | ||
return 'volume'; | ||
} | ||
/** | ||
@@ -17,0 +21,0 @@ * Initialize the Effect. |
@@ -1,17 +0,68 @@ | ||
const log = require('./log'); | ||
const {EventEmitter} = require('events'); | ||
const VolumeEffect = require('./effects/VolumeEffect'); | ||
/** | ||
* A SoundPlayer stores an audio buffer, and plays it | ||
* Name of event that indicates playback has ended. | ||
* @const {string} | ||
*/ | ||
class SoundPlayer { | ||
const ON_ENDED = 'ended'; | ||
class SoundPlayer extends EventEmitter { | ||
/** | ||
* @param {AudioContext} audioContext - a webAudio context | ||
* Play sounds that stop without audible clipping. | ||
* | ||
* @param {AudioEngine} audioEngine - engine to play sounds on | ||
* @param {object} data - required data for sound playback | ||
* @param {string} data.id - a unique id for this sound | ||
* @param {ArrayBuffer} data.buffer - buffer of the sound's waveform to play | ||
* @constructor | ||
*/ | ||
constructor (audioContext) { | ||
this.audioContext = audioContext; | ||
constructor (audioEngine, {id, buffer}) { | ||
super(); | ||
this.id = id; | ||
this.audioEngine = audioEngine; | ||
this.buffer = buffer; | ||
this.outputNode = null; | ||
this.buffer = null; | ||
this.bufferSource = null; | ||
this.volumeEffect = null; | ||
this.target = null; | ||
this.initialized = false; | ||
this.isPlaying = false; | ||
this.startingUntil = 0; | ||
this.playbackRate = 1; | ||
// handleEvent is a EventTarget api for the DOM, however the web-audio-test-api we use | ||
// uses an addEventListener that isn't compatable with object and requires us to pass | ||
// this bound function instead | ||
this.handleEvent = this.handleEvent.bind(this); | ||
} | ||
/** | ||
* Is plaback currently starting? | ||
* @type {boolean} | ||
*/ | ||
get isStarting () { | ||
return this.isPlaying && this.startingUntil > this.audioEngine.audioContext.currentTime; | ||
} | ||
/** | ||
* Handle any event we have told the output node to listen for. | ||
* @param {Event} event - dom event to handle | ||
*/ | ||
handleEvent (event) { | ||
if (event.type === ON_ENDED) { | ||
this.onEnded(); | ||
} | ||
} | ||
/** | ||
* Event listener for when playback ends. | ||
*/ | ||
onEnded () { | ||
this.emit('stop'); | ||
this.isPlaying = false; | ||
@@ -21,72 +72,220 @@ } | ||
/** | ||
* Connect the SoundPlayer to an output node | ||
* @param {GainNode} node - an output node to connect to | ||
* Create the buffer source node during initialization or secondary | ||
* playback. | ||
*/ | ||
connect (node) { | ||
this.outputNode = node; | ||
_createSource () { | ||
if (this.outputNode !== null) { | ||
this.outputNode.removeEventListener(ON_ENDED, this.handleEvent); | ||
this.outputNode.disconnect(); | ||
} | ||
this.outputNode = this.audioEngine.audioContext.createBufferSource(); | ||
this.outputNode.playbackRate.value = this.playbackRate; | ||
this.outputNode.buffer = this.buffer; | ||
this.outputNode.addEventListener(ON_ENDED, this.handleEvent); | ||
if (this.target !== null) { | ||
this.connect(this.target); | ||
} | ||
} | ||
/** | ||
* Set an audio buffer | ||
* @param {AudioBuffer} buffer - Buffer to set | ||
* Initialize the player for first playback. | ||
*/ | ||
setBuffer (buffer) { | ||
this.buffer = buffer; | ||
initialize () { | ||
this.initialized = true; | ||
this._createSource(); | ||
} | ||
/** | ||
* Set the playback rate for the sound | ||
* @param {number} playbackRate - a ratio where 1 is normal playback, 0.5 is half speed, 2 is double speed, etc. | ||
* Connect the player to the engine or an effect chain. | ||
* @param {object} target - object to connect to | ||
* @returns {object} - return this sound player | ||
*/ | ||
setPlaybackRate (playbackRate) { | ||
this.playbackRate = playbackRate; | ||
if (this.bufferSource && this.bufferSource.playbackRate) { | ||
this.bufferSource.playbackRate.value = this.playbackRate; | ||
connect (target) { | ||
if (target === this.volumeEffect) { | ||
this.outputNode.disconnect(); | ||
this.outputNode.connect(this.volumeEffect.getInputNode()); | ||
return; | ||
} | ||
this.target = target; | ||
if (!this.initialized) { | ||
return; | ||
} | ||
if (this.volumeEffect === null) { | ||
this.outputNode.disconnect(); | ||
this.outputNode.connect(target.getInputNode()); | ||
} else { | ||
this.volumeEffect.connect(target); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Stop the sound | ||
* Teardown the player. | ||
*/ | ||
stop () { | ||
if (this.bufferSource && this.isPlaying) { | ||
this.bufferSource.stop(); | ||
dispose () { | ||
if (!this.initialized) { | ||
return; | ||
} | ||
this.stopImmediately(); | ||
if (this.volumeEffect !== null) { | ||
this.volumeEffect.dispose(); | ||
this.volumeEffect = null; | ||
} | ||
this.outputNode.disconnect(); | ||
this.outputNode = null; | ||
this.target = null; | ||
this.initialized = false; | ||
} | ||
/** | ||
* Take the internal state of this player and create a new player from | ||
* that. Restore the state of this player to that before its first playback. | ||
* | ||
* The returned player can be used to stop the original playback or | ||
* continue it without manipulation from the original player. | ||
* | ||
* @returns {SoundPlayer} - new SoundPlayer with old state | ||
*/ | ||
take () { | ||
if (this.outputNode) { | ||
this.outputNode.removeEventListener(ON_ENDED, this.handleEvent); | ||
} | ||
const taken = new SoundPlayer(this.audioEngine, this); | ||
taken.playbackRate = this.playbackRate; | ||
if (this.isPlaying) { | ||
taken.startingUntil = this.startingUntil; | ||
taken.isPlaying = this.isPlaying; | ||
taken.initialized = this.initialized; | ||
taken.outputNode = this.outputNode; | ||
taken.outputNode.addEventListener(ON_ENDED, taken.handleEvent); | ||
taken.volumeEffect = this.volumeEffect; | ||
if (taken.volumeEffect) { | ||
taken.volumeEffect.audioPlayer = taken; | ||
} | ||
if (this.target !== null) { | ||
taken.connect(this.target); | ||
} | ||
this.emit('stop'); | ||
taken.emit('play'); | ||
} | ||
this.outputNode = null; | ||
this.volumeEffect = null; | ||
this.initialized = false; | ||
this.startingUntil = 0; | ||
this.isPlaying = false; | ||
return taken; | ||
} | ||
/** | ||
* Start playing the sound | ||
* The web audio framework requires a new audio buffer source node for each playback | ||
* Start playback for this sound. | ||
* | ||
* If the sound is already playing it will stop playback with a quick fade | ||
* out. | ||
*/ | ||
start () { | ||
if (!this.buffer) { | ||
log.warn('tried to play a sound that was not loaded yet'); | ||
play () { | ||
if (this.isStarting) { | ||
this.emit('stop'); | ||
this.emit('play'); | ||
return; | ||
} | ||
this.bufferSource = this.audioContext.createBufferSource(); | ||
this.bufferSource.buffer = this.buffer; | ||
this.bufferSource.playbackRate.value = this.playbackRate; | ||
this.bufferSource.connect(this.outputNode); | ||
this.bufferSource.start(); | ||
if (this.isPlaying) { | ||
this.stop(); | ||
} | ||
if (this.initialized) { | ||
this._createSource(); | ||
} else { | ||
this.initialize(); | ||
} | ||
this.outputNode.start(); | ||
this.isPlaying = true; | ||
this.startingUntil = this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME; | ||
this.emit('play'); | ||
} | ||
/** | ||
* The sound has finished playing. This is called at the correct time even if the playback rate | ||
* has been changed | ||
* @return {Promise} a Promise that resolves when the sound finishes playing | ||
* Stop playback after quickly fading out. | ||
*/ | ||
stop () { | ||
if (!this.isPlaying) { | ||
return; | ||
} | ||
// always do a manual stop on a taken / volume effect fade out sound player | ||
// take will emit "stop" as well as reset all of our playing statuses / remove our | ||
// nodes / etc | ||
const taken = this.take(); | ||
taken.volumeEffect = new VolumeEffect(taken.audioEngine, taken, null); | ||
taken.volumeEffect.connect(taken.target); | ||
// volumeEffect will recursively connect to us if it needs to, so this happens too: | ||
// taken.connect(taken.volumeEffect); | ||
taken.finished().then(() => taken.dispose()); | ||
taken.volumeEffect.set(0); | ||
taken.outputNode.stop(this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME); | ||
} | ||
/** | ||
* Stop immediately without fading out. May cause audible clipping. | ||
*/ | ||
stopImmediately () { | ||
if (!this.isPlaying) { | ||
return; | ||
} | ||
this.outputNode.stop(); | ||
this.isPlaying = false; | ||
this.startingUntil = 0; | ||
this.emit('stop'); | ||
} | ||
/** | ||
* Return a promise that resolves when the sound next finishes. | ||
* @returns {Promise} - resolves when the sound finishes | ||
*/ | ||
finished () { | ||
return new Promise(resolve => { | ||
this.bufferSource.onended = () => { | ||
this.isPlaying = false; | ||
resolve(); | ||
}; | ||
this.once('stop', resolve); | ||
}); | ||
} | ||
/** | ||
* Set the sound's playback rate. | ||
* @param {number} value - playback rate. Default is 1. | ||
*/ | ||
setPlaybackRate (value) { | ||
this.playbackRate = value; | ||
if (this.initialized) { | ||
this.outputNode.playbackRate.value = value; | ||
} | ||
} | ||
} | ||
module.exports = SoundPlayer; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
571927
3.62%4249
9.51%7
16.67%