UNPKG

audio-manager

Version:

Play sounds using Web Audio, fallback to HTML5 Audio

397 lines (332 loc) 14.8 kB
var inherits = require('util').inherits; var ISound = require('./ISound.js'); // setValueAtTime, exponentialRampToValueAtTime and linearRampToValueAtTime thrown an exception if // provided value is less than or equal to 0. // we use MIN_VALUE instead of 0 when calling these functions // see: // http://webaudio.github.io/web-audio-api/#widl-AudioParam-exponentialRampToValueAtTime-void-float-value-double-endTime // http://stackoverflow.com/questions/29819382/how-does-the-audioparam-exponentialramptovalueattime-work var MIN_VALUE = 0.000001; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ /** Audio wrapper using AudioBufferSourceNode * @author Cedric Stoquer * */ function SoundBuffered() { ISound.call(this); this.buffer = null; this.source = null; this.sourceConnector = null; this.gain = null; this.panNode = null; this.rawAudioData = null; this._playPitch = 0.0; // pitch for each independent play this._fadeTimeout = null; this._onStopCallback = null; this._audioNodeReady = false; this._loopStart = 0; this._loopEnd = 0; this.init(); } inherits(SoundBuffered, ISound); module.exports = SoundBuffered; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype._createAudioNodes = function () { if (this._audioNodeReady) return; if (!this.audioContext) return; // create webAudio nodes // source -> gain -> pan -> destination var audioContext = this.audioContext; var gainNode = audioContext.createGain(); var panNode; if (audioContext.createStereoPanner) { panNode = audioContext.createStereoPanner(); } else { // fallback to 3D PannerNode panNode = audioContext.createPanner(); } gainNode.connect(panNode); panNode.connect(audioContext.destination); gainNode.gain.value = 0; this.sourceConnector = gainNode; this.gain = gainNode.gain; this.panNode = panNode; this._audioNodeReady = true; }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype._destroyAudioNodes = function () { if (!this._audioNodeReady) return; var audioContext = this.audioContext; var panNode = this.panNode; var gainNode = this.sourceConnector; gainNode.disconnect(panNode); panNode.disconnect(audioContext.destination); if (this.source) { this.source.disconnect(gainNode); this.source.onended = null; this.source = null; } this.sourceConnector = null; this.gain = null; this.panNode = null; this.rawAudioData = null; this._audioNodeReady = false; }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype.init = function () { this._createAudioNodes(); if (!this.rawAudioData) return; var self = this; var maxPlayLatency = this.audioManager.settings.maxPlayLatency; var audioContext = this.audioContext; function onAudioDecodeSuccess(buffer) { self.buffer = buffer; self.usedMemory = buffer.duration; self.audioManager.usedMemory += buffer.duration; self.rawAudioData = null; if (self._loaded && self._playTriggered) { if (self.loop || Date.now() - self._playTriggered < maxPlayLatency) { self._play(); } self._playTriggered = 0; } } function onAudioDecodeFail() { console.error('decode audio failed for sound ', self.id); } audioContext.decodeAudioData(this.rawAudioData, onAudioDecodeSuccess, onAudioDecodeFail); }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype.setVolume = function (value) { this.volume = value; if (!this.playing) return; if (!this.fade) { this.gain.value = value; return; } if (value <= 0) value = MIN_VALUE; var currentTime = this.audioContext.currentTime; this.gain.cancelScheduledValues(currentTime); this.gain.setValueAtTime(this.gain.value || MIN_VALUE, currentTime); this.gain.linearRampToValueAtTime(value, currentTime + this.fade); }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype.setPan = function (value) { this.pan = value; if (!this.panNode) return; if (this.panNode.pan) { // stereo panning this.panNode.pan.value = value; } else { // 3D panning this.panNode.setPosition(value, 0, 0.2); } }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype.setLoop = function (value, loopStart, loopEnd) { this.loop = !!value; this._loopStart = loopStart || 0; this._loopEnd = loopEnd || 0; if (!this.source || !this.buffer) return; this.source.loop = value; this._setLoopPoints(); }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype._setLoopPoints = function () { this.source.loopStart = this._loopStart || 0; var loopEnd = this._loopEnd; // When loop end point is negative, we set endPoint from the end of the buffer if (loopEnd < 0) loopEnd = this.buffer.duration + loopEnd; if (loopEnd < 0) loopEnd = 0; this.source.loopEnd = loopEnd || this.buffer.duration; }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ /** Set sound pitch * * @param {number} pitch - pitch in semi-tone * @param {number} [portamento] - duration to slide from previous to new pitch. */ SoundBuffered.prototype.setPitch = function (pitch, portamento) { this.pitch = pitch; this._setPlaybackRate(portamento); }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype._updatePlayPitch = function (pitch) { if ((pitch || pitch === 0) && pitch !== this._playPitch) { this._playPitch = pitch; this._setPlaybackRate(0); } }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype._setPlaybackRate = function (portamento) { if (!this.source) return; var rate = Math.pow(2, (this._playPitch + this.pitch) / 12); if (!portamento) { this.source.playbackRate.value = rate; return; } var currentTime = this.audioContext.currentTime; this.source.playbackRate.cancelScheduledValues(currentTime); this.source.playbackRate.setValueAtTime(this.source.playbackRate.value || MIN_VALUE, currentTime); this.source.playbackRate.linearRampToValueAtTime(rate, currentTime + portamento); }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ /** Load sound * @private */ SoundBuffered.prototype._load = function () { var self = this; this._createAudioNodes(); function loadFail(error) { // TODO: keep track that loading has failed so we don't retry to load it ? self._finalizeLoad(error); } function onAudioLoaded(buffer) { self.buffer = buffer; self.usedMemory = buffer.duration; self.audioManager.usedMemory += buffer.duration; self._finalizeLoad(null); } function loadAudio(uri) { var xobj = new XMLHttpRequest(); xobj.responseType = 'arraybuffer'; xobj.onreadystatechange = function onXhrStateChange() { if (~~xobj.readyState !== 4) return; if (~~xobj.status !== 200 && ~~xobj.status !== 0) { return loadFail('xhrError:' + xobj.status); } if (self.audioContext) { self.audioContext.decodeAudioData(xobj.response, onAudioLoaded, loadFail); } else { self.rawAudioData = xobj.response; self._finalizeLoad(null); } }; xobj.open('GET', uri, true); xobj.send(); } var getFileUri = this.audioManager.settings.getFileUri; var audioPath = this.audioManager.settings.audioPath; if (getFileUri.length > 2) { // asynchronous getFileUri(audioPath, this.id, function onUri(error, uri) { if (error) return loadFail(error); loadAudio(uri); }); } else { // synchronous try { var uri = getFileUri(audioPath, this.id); if (!uri) return loadFail('emptyUri'); loadAudio(uri); } catch (error) { loadFail(error); } } }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ /** Unload sound from memory */ SoundBuffered.prototype.unload = function () { if (ISound.prototype.unload.call(this)) { if (this._fadeTimeout) { this._onStopCallback = null; this._stopAndClear(); } this.buffer = null; if (this.source) { this.source.onended = null; this.source.stop(0); this.source = null; } this._destroyAudioNodes(); } }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ /** Play sound. If sound is not yet loaded, it is loaded in memory and flagged to be played * once loading has finished. If loading take too much time, playback may be cancelled. */ SoundBuffered.prototype._play = function (pitch) { if (!this.buffer) { this._playTriggered = Date.now(); return; } this.playing = true; var currentTime = this.audioContext.currentTime; this.gain.cancelScheduledValues(currentTime); if (this.fade) { this.gain.setValueAtTime(this.gain.value || MIN_VALUE, currentTime); this.gain.linearRampToValueAtTime(this.volume || MIN_VALUE, currentTime + this.fade); } else { this.gain.value = this.volume; } // if sound is still fading out, clear all onStop callback if (this._fadeTimeout) { this._onStopCallback = null; this.source.onended = null; this.stopping = false; window.clearTimeout(this._fadeTimeout); this._fadeTimeout = null; return; } if (this.source) { this.source.disconnect(this.sourceConnector); this.source.onended = null; } var sourceNode = this.source = this.audioContext.createBufferSource(); sourceNode.connect(this.sourceConnector); var self = this; sourceNode.onended = function onPlaybackEnd() { self.playing = false; sourceNode.onended = null; self.source = null; self.onEnd && self.onEnd(); }; this._playPitch = pitch || 0; if (pitch || this.pitch) { this._setPlaybackRate(0); } sourceNode.loop = this.loop; sourceNode.buffer = this.buffer; this._setLoopPoints(); sourceNode.start(0); }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ SoundBuffered.prototype._stopAndClear = function () { this.stopping = false; this.source.onended = null; this.source.stop(0); this.source = null; if (this._fadeTimeout) { window.clearTimeout(this._fadeTimeout); this._fadeTimeout = null; } if (this._onStopCallback) { this._onStopCallback(); this._onStopCallback = null; } }; //▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ /** Stop sound * * @param {Function} [cb] - optional callback function */ SoundBuffered.prototype.stop = function (cb) { if (!this.playing && !this.stopping) return cb && cb(); this._playTriggered = 0; this.stopping = true; this.playing = false; if (!this.source) return cb && cb(); this._onStopCallback = cb; // TODO: do we allow multiple stop cb ? if (this._fadeTimeout) return; if (this.fade) { var self = this; var currentTime = this.audioContext.currentTime; this.gain.cancelScheduledValues(currentTime); this.gain.setValueAtTime(this.gain.value || MIN_VALUE, currentTime); this.gain.linearRampToValueAtTime(MIN_VALUE, currentTime + this.fade); this._fadeTimeout = window.setTimeout(function onFadeEnd() { self._fadeTimeout = null; self._stopAndClear(); }, this.fade * 1000); return; } this._stopAndClear(); };