2018-06-11 15:24:41 -04:00
|
|
|
const {EventEmitter} = require('events');
|
|
|
|
|
|
|
|
const VolumeEffect = require('./effects/VolumeEffect');
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Name of event that indicates playback has ended.
|
|
|
|
* @const {string}
|
|
|
|
*/
|
2018-06-12 09:18:48 -04:00
|
|
|
const ON_ENDED = 'ended';
|
|
|
|
|
2018-06-11 15:24:41 -04:00
|
|
|
class SoundPlayer extends EventEmitter {
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
constructor (audioEngine, {id, buffer}) {
|
|
|
|
super();
|
|
|
|
|
2018-06-22 14:43:37 -04:00
|
|
|
/**
|
|
|
|
* Unique sound identifier set by AudioEngine.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.id = id;
|
|
|
|
|
2018-06-22 14:43:37 -04:00
|
|
|
/**
|
|
|
|
* AudioEngine creating this sound player.
|
|
|
|
* @type {AudioEngine}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.audioEngine = audioEngine;
|
2018-06-22 14:43:37 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Decoded audio buffer from audio engine for playback.
|
|
|
|
* @type {AudioBuffer}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.buffer = buffer;
|
|
|
|
|
2018-06-22 14:43:37 -04:00
|
|
|
/**
|
|
|
|
* Output audio node.
|
|
|
|
* @type {AudioNode}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.outputNode = null;
|
2018-06-22 14:43:37 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* VolumeEffect used to fade out playing sounds when stopping them.
|
|
|
|
* @type {VolumeEffect}
|
|
|
|
*/
|
2018-06-20 14:02:05 -04:00
|
|
|
this.volumeEffect = null;
|
2018-06-22 14:43:37 -04:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Target engine, effect, or chain this player directly connects to.
|
|
|
|
* @type {AudioEngine|Effect|EffectChain}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.target = null;
|
|
|
|
|
2018-06-22 14:43:37 -04:00
|
|
|
/**
|
|
|
|
* Internally is the SoundPlayer initialized with at least its buffer
|
|
|
|
* source node and output node.
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.initialized = false;
|
2018-06-22 14:43:37 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Is the sound playing or starting to play?
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.isPlaying = false;
|
2018-06-22 14:43:37 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Timestamp sound is expected to be starting playback until. Once the
|
|
|
|
* future timestamp is reached the sound is considered to be playing
|
|
|
|
* through the audio hardware and stopping should fade out instead of
|
|
|
|
* cutting off playback.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2018-06-21 13:44:41 -04:00
|
|
|
this.startingUntil = 0;
|
2018-06-22 14:43:37 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Rate to play back the audio at.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
this.playbackRate = 1;
|
2018-06-19 13:37:00 -04:00
|
|
|
|
2018-06-22 14:43:37 -04:00
|
|
|
// 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
|
2018-06-19 13:37:00 -04:00
|
|
|
this.handleEvent = this.handleEvent.bind(this);
|
2018-06-12 09:18:48 -04:00
|
|
|
}
|
2018-06-11 15:24:41 -04:00
|
|
|
|
2018-06-21 13:44:41 -04:00
|
|
|
/**
|
|
|
|
* Is plaback currently starting?
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
get isStarting () {
|
2018-06-25 14:27:21 -04:00
|
|
|
return this.isPlaying && this.startingUntil > this.audioEngine.currentTime;
|
2018-06-21 13:44:41 -04:00
|
|
|
}
|
|
|
|
|
2018-06-12 09:18:48 -04:00
|
|
|
/**
|
|
|
|
* Handle any event we have told the output node to listen for.
|
2018-06-12 09:40:59 -04:00
|
|
|
* @param {Event} event - dom event to handle
|
2018-06-12 09:18:48 -04:00
|
|
|
*/
|
|
|
|
handleEvent (event) {
|
|
|
|
if (event.type === ON_ENDED) {
|
|
|
|
this.onEnded();
|
|
|
|
}
|
2018-06-11 15:24:41 -04:00
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Event listener for when playback ends.
|
|
|
|
*/
|
2018-06-12 09:18:48 -04:00
|
|
|
onEnded () {
|
2018-06-11 15:24:41 -04:00
|
|
|
this.emit('stop');
|
2018-06-12 09:18:48 -04:00
|
|
|
|
|
|
|
this.isPlaying = false;
|
2018-06-11 15:24:41 -04:00
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Create the buffer source node during initialization or secondary
|
|
|
|
* playback.
|
|
|
|
*/
|
2018-06-12 09:18:48 -04:00
|
|
|
_createSource () {
|
|
|
|
if (this.outputNode !== null) {
|
2018-06-19 13:37:00 -04:00
|
|
|
this.outputNode.removeEventListener(ON_ENDED, this.handleEvent);
|
2018-06-12 09:18:48 -04:00
|
|
|
this.outputNode.disconnect();
|
|
|
|
}
|
|
|
|
|
2018-06-11 15:24:41 -04:00
|
|
|
this.outputNode = this.audioEngine.audioContext.createBufferSource();
|
|
|
|
this.outputNode.playbackRate.value = this.playbackRate;
|
|
|
|
this.outputNode.buffer = this.buffer;
|
|
|
|
|
2018-06-19 13:37:00 -04:00
|
|
|
this.outputNode.addEventListener(ON_ENDED, this.handleEvent);
|
2018-06-11 15:24:41 -04:00
|
|
|
|
|
|
|
if (this.target !== null) {
|
|
|
|
this.connect(this.target);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Initialize the player for first playback.
|
|
|
|
*/
|
2018-06-12 09:18:48 -04:00
|
|
|
initialize () {
|
|
|
|
this.initialized = true;
|
|
|
|
|
|
|
|
this._createSource();
|
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Connect the player to the engine or an effect chain.
|
|
|
|
* @param {object} target - object to connect to
|
|
|
|
* @returns {object} - return this sound player
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
connect (target) {
|
|
|
|
if (target === this.volumeEffect) {
|
|
|
|
this.outputNode.disconnect();
|
|
|
|
this.outputNode.connect(this.volumeEffect.getInputNode());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.target = target;
|
|
|
|
|
|
|
|
if (!this.initialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-21 11:25:27 -04:00
|
|
|
if (this.volumeEffect === null) {
|
2018-06-20 14:02:05 -04:00
|
|
|
this.outputNode.disconnect();
|
|
|
|
this.outputNode.connect(target.getInputNode());
|
2018-06-21 11:25:27 -04:00
|
|
|
} else {
|
|
|
|
this.volumeEffect.connect(target);
|
2018-06-20 14:02:05 -04:00
|
|
|
}
|
2018-06-11 15:24:41 -04:00
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Teardown the player.
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
dispose () {
|
|
|
|
if (!this.initialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.stopImmediately();
|
|
|
|
|
2018-06-20 14:20:11 -04:00
|
|
|
if (this.volumeEffect !== null) {
|
2018-06-20 14:02:05 -04:00
|
|
|
this.volumeEffect.dispose();
|
|
|
|
this.volumeEffect = null;
|
|
|
|
}
|
2018-06-11 15:24:41 -04:00
|
|
|
|
|
|
|
this.outputNode.disconnect();
|
|
|
|
this.outputNode = null;
|
|
|
|
|
|
|
|
this.target = null;
|
|
|
|
|
|
|
|
this.initialized = false;
|
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
take () {
|
|
|
|
if (this.outputNode) {
|
2018-06-19 13:37:00 -04:00
|
|
|
this.outputNode.removeEventListener(ON_ENDED, this.handleEvent);
|
2018-06-11 15:24:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const taken = new SoundPlayer(this.audioEngine, this);
|
2018-06-12 09:20:21 -04:00
|
|
|
taken.playbackRate = this.playbackRate;
|
|
|
|
if (this.isPlaying) {
|
2018-06-21 13:44:41 -04:00
|
|
|
taken.startingUntil = this.startingUntil;
|
2018-06-12 09:20:21 -04:00
|
|
|
taken.isPlaying = this.isPlaying;
|
2018-06-20 14:02:05 -04:00
|
|
|
taken.initialized = this.initialized;
|
2018-06-12 09:20:21 -04:00
|
|
|
taken.outputNode = this.outputNode;
|
2018-06-19 13:37:00 -04:00
|
|
|
taken.outputNode.addEventListener(ON_ENDED, taken.handleEvent);
|
2018-06-20 14:02:05 -04:00
|
|
|
taken.volumeEffect = this.volumeEffect;
|
2018-06-20 14:45:09 -04:00
|
|
|
if (taken.volumeEffect) {
|
|
|
|
taken.volumeEffect.audioPlayer = taken;
|
|
|
|
}
|
2018-06-12 09:20:21 -04:00
|
|
|
if (this.target !== null) {
|
|
|
|
taken.connect(this.target);
|
|
|
|
}
|
2018-06-11 15:24:41 -04:00
|
|
|
|
|
|
|
this.emit('stop');
|
|
|
|
taken.emit('play');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.outputNode = null;
|
|
|
|
this.volumeEffect = null;
|
|
|
|
this.initialized = false;
|
2018-06-21 13:44:41 -04:00
|
|
|
this.startingUntil = 0;
|
2018-06-11 15:24:41 -04:00
|
|
|
this.isPlaying = false;
|
2018-06-12 09:20:21 -04:00
|
|
|
|
|
|
|
return taken;
|
2018-06-11 15:24:41 -04:00
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Start playback for this sound.
|
|
|
|
*
|
|
|
|
* If the sound is already playing it will stop playback with a quick fade
|
|
|
|
* out.
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
play () {
|
2018-06-18 14:47:38 -04:00
|
|
|
if (this.isStarting) {
|
2018-06-21 15:05:08 -04:00
|
|
|
this.emit('stop');
|
|
|
|
this.emit('play');
|
2018-06-18 14:47:38 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-11 15:24:41 -04:00
|
|
|
if (this.isPlaying) {
|
2018-06-20 14:02:05 -04:00
|
|
|
this.stop();
|
2018-06-11 15:24:41 -04:00
|
|
|
}
|
|
|
|
|
2018-06-19 13:37:00 -04:00
|
|
|
if (this.initialized) {
|
2018-06-12 09:18:48 -04:00
|
|
|
this._createSource();
|
2018-06-19 13:37:00 -04:00
|
|
|
} else {
|
|
|
|
this.initialize();
|
2018-06-11 15:24:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
this.outputNode.start();
|
|
|
|
|
|
|
|
this.isPlaying = true;
|
|
|
|
|
2018-06-25 14:27:21 -04:00
|
|
|
const {currentTime, DECAY_DURATION} = this.audioEngine;
|
|
|
|
this.startingUntil = currentTime + DECAY_DURATION;
|
2018-06-18 14:47:38 -04:00
|
|
|
|
2018-06-11 15:24:41 -04:00
|
|
|
this.emit('play');
|
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Stop playback after quickly fading out.
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
stop () {
|
|
|
|
if (!this.isPlaying) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-22 14:43:37 -04:00
|
|
|
// 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
|
2018-06-20 14:02:05 -04:00
|
|
|
const taken = this.take();
|
|
|
|
taken.volumeEffect = new VolumeEffect(taken.audioEngine, taken, null);
|
2018-06-20 14:45:09 -04:00
|
|
|
|
2018-06-20 14:02:05 -04:00
|
|
|
taken.volumeEffect.connect(taken.target);
|
2018-06-22 14:43:37 -04:00
|
|
|
// volumeEffect will recursively connect to us if it needs to, so this
|
|
|
|
// happens too:
|
2018-06-20 14:45:09 -04:00
|
|
|
// taken.connect(taken.volumeEffect);
|
|
|
|
|
2018-06-20 14:20:11 -04:00
|
|
|
taken.finished().then(() => taken.dispose());
|
2018-06-11 15:24:41 -04:00
|
|
|
|
2018-06-20 14:02:05 -04:00
|
|
|
taken.volumeEffect.set(0);
|
2019-09-25 14:14:48 -04:00
|
|
|
const {currentTime, DECAY_DURATION} = this.audioEngine;
|
|
|
|
taken.outputNode.stop(currentTime + DECAY_DURATION);
|
2018-06-11 15:24:41 -04:00
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Stop immediately without fading out. May cause audible clipping.
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
stopImmediately () {
|
|
|
|
if (!this.isPlaying) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.outputNode.stop();
|
|
|
|
|
|
|
|
this.isPlaying = false;
|
2018-06-21 13:44:41 -04:00
|
|
|
this.startingUntil = 0;
|
2018-06-11 15:24:41 -04:00
|
|
|
|
|
|
|
this.emit('stop');
|
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Return a promise that resolves when the sound next finishes.
|
|
|
|
* @returns {Promise} - resolves when the sound finishes
|
|
|
|
*/
|
2018-06-12 09:18:48 -04:00
|
|
|
finished () {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
this.once('stop', resolve);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-06-12 09:40:59 -04:00
|
|
|
/**
|
|
|
|
* Set the sound's playback rate.
|
|
|
|
* @param {number} value - playback rate. Default is 1.
|
|
|
|
*/
|
2018-06-11 15:24:41 -04:00
|
|
|
setPlaybackRate (value) {
|
|
|
|
this.playbackRate = value;
|
|
|
|
|
|
|
|
if (this.initialized) {
|
|
|
|
this.outputNode.playbackRate.value = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = SoundPlayer;
|