scratch-audio
Advanced tools
Comparing version 0.1.0-prerelease.20180621200816 to 0.1.0-prerelease.20180621210133
{ | ||
"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
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
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
571927
4249