X-Git-Url: http://id.pley.net/sound.git/blobdiff_plain/1c34429baed75a88bb01e077086cd59201cb2a9a..82e3a8a15e29c81d92cdb6432d9dad531c03063a:/sound.js diff --git a/sound.js b/sound.js index f823be3..4d29025 100644 --- a/sound.js +++ b/sound.js @@ -1,11 +1,33 @@ -function Sound() { +/* Copyright (c) 2014 Jer Noble + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +function Sound(src) { + + if (Sound.audioContext === undefined) { + var AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext; + Sound.audioContext = new AudioContext(); + } - if (Sound.audioContext === undefined) - Sound.audioContext = new webkitAudioContext(); - - this._src = null; this._networkState = this.NETWORK.EMPTY; - this._preload = true; + this._preload = this.PRELOAD.AUTO; this._buffered = {}; this._readyState = this.READY.NOTHING; this._seeking = false; @@ -15,22 +37,29 @@ function Sound() { this._played = {}; this._seekable = {}; this._ended = false; - this._autoplay = false; + this._autoplay = true; this._loop = false; this._volume = 1; this._muted = false; this._defaultMuted = false; + + this.selectResourceTimer = null; + this.fetchResourceTimer = null; this.buffer = null; this.node = null; this.gainNode = null; this.ajax = null; - this.eventListeners = { }; - this.shouldBePlaying = 0; + this.eventListeners = { }; this.startTime = 0; this.nextStartTime = 0; - this.load(); + + this.autoplaying = false; + this.delayingTheLoadEvent = false; + this.sentLoadedData = false; + + this.setSrc(src); } Sound.prototype = { @@ -58,15 +87,38 @@ Sound.prototype = { ENOUGH_DATA: 4, }, + PRELOAD: { + NONE: 0, + METADATA: 1, + AUTO: 2, + }, + load: function() { - if (this.ajax) - this.ajax.abort(); + if (this.networkState === this.NETWORK.LOADING || this.networkState === this.NETWORK.IDLE) + this.dispatchEventAsync(new CustomEvent('abort')); - if (this.networkState === this.NETWORK.LOADING || this.networkState === this.NETWORK.IDLE) { + if (this.networkState !== this.NETWORK.EMPTY) { this.dispatchEventAsync(new CustomEvent('emptied')); - this.setReadyState(this.READY.NOTHING); + + if (this.ajax) + this.ajax.abort(); + + if (this.selectResourceTimer) { + clearTimeout(this.selectResourceTimer); + this.selectResourceTimer = null; + } + + if (this.fetchResourceTimer) { + clearTimeout(this.fetchResourceTimer); + this.fetchResourceTimer = null; + } + + if (this._readyState != this.READY.NOTHING) + this.setReadyState(this.READY.NOTHING); + if (!this._paused) this.pause(); + if (!this._seeking) this._seeking = false; this.setCurrentTime(0); @@ -75,51 +127,111 @@ Sound.prototype = { this.setPlaybackRate(this.defaultPlaybackRate); this._error = null; - this._autoplay = true; + this.autoplaying = true; this.stopInternal(); + this.sentLoadedData = false; + + this.selectResource(); + }, + + selectResource: function() { + this.setNetworkState(this.NETWORK.NO_SOURCE); + this.delayingTheLoadEvent = true; + + this.selectResourceTimer = setTimeout(this.selectResourceAsync.bind(this), 0); + }, + + selectResourceAsync: function() { + this.selectResourceTimer = null; if (!this._src) { - this._networkState = this.NETWORK.EMPTY; + this.setNetworkState(this.NETWORK.EMPTY); return; } - this._networkState = this.NETWORK.LOADING; + this.setNetworkState(this.NETWORK.LOADING); this.dispatchEventAsync(new CustomEvent('loadstart')); + this.fetchResourceTimer = setTimeout(this.fetchResource(), 0); + }, + + fetchResource: function() { + this.fetchResourceTimer = null; + + if (this._preload === this.PRELOAD.NONE) { + this.setNetworkState(this.NETWORK.IDLE); + this.dispatchEventAsync(new CustomEvent('suspend')); + this.delayingTheLoadEvent = false; + return; + } + this.ajax = new XMLHttpRequest(); this.ajax.open("GET", this._src, true); this.ajax.responseType = "arraybuffer"; - this.ajax.onload = function() { - if (!this.ajax.response) - return; - - this.setReadyState(this.READY.FUTURE_DATA); - - try { - Sound.audioContext.decodeAudioData( - this.ajax.response, - function(buffer) { - this.buffer = buffer; - if (this.shouldBePlaying) - this.play(); - }.bind(this), - function(error) { - console.log("Error in creating buffer for sound '" + this._src + "': " + error); - }.bind(this) - ); - } catch(exception) { - console.log(exception); - } - }.bind(this); - this.ajax.onprogress = function() { - this.dispatchEventAsync(new CustomEvent('progress')); - }.bind(this); + this.ajax.onprogress = this.resourceFetchingProgressed.bind(this); + this.ajax.onload = this.resourceFetchingSucceeded.bind(this); + this.ajax.onerror = this.resourceFetchingFailed.bind(this); this.ajax.send(); }, + resourceFetchingProgressed: function() { + this.dispatchEventAsync(new CustomEvent('progress')); + }, + + resourceFetchingSucceeded: function() { + if (!this.ajax.response) + return; + + this.setNetworkState(this.NETWORK.IDLE); + this.dispatchEventAsync(new CustomEvent('suspend')); + this.setReadyState(this.READY.FUTURE_DATA); + + try { + Sound.audioContext.decodeAudioData( + this.ajax.response, + this.resourceDecodingSucceeded.bind(this), + this.resourceDecodingFailed.bind(this) + ); + } catch(exception) { + console.log(exception); + } + }, + + resourceFetchingFailed: function() { + this.error = { code: MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED }; + this.setNetworkState(this.NETWORK.NO_SOURCE); + this.dispatchEventAsync(new CustomEvent('error')); + this.delayingTheLoadEvent = false; + }, + + resourceDecodingSucceeded: function(buffer) { + this.buffer = buffer; + + this.setCurrentTime(0); + this.dispatchEventAsync(new CustomEvent('durationchange')); + this.setReadyState(this.READY.METADATA); + + if (this.autoplaying && this._paused && this._autoplay) + this.play(); + this.dispatchEventAsync(new CustomEvent('canplaythrough')); + }, + + resourceDecodingFailed: function(error) { + this._error = { code: HTMLMediaElement.MEDIA_ERR_DECODE }; + this.dispatchEventAsync(new CustomEvent('error')); + if (this._readyState === this.READY.NOTHING) { + this.setNetworkState(this.NETWORK.EMPTY); + this.dispatchEventAsync('emptied'); + } else + this.setNetworkState(this.NETWORK.IDLE); + }, + play: function() { + if (this._networkState === this.NETWORK.EMPTY) + this.loadResource(); + if (!this.buffer) { - this.shouldBePlaying = true; + this.autoplaying = true; return; } @@ -129,8 +241,9 @@ Sound.prototype = { if (this._ended && this._playbackRate > 0) this.setCurrentTime(0); - if (this._paused) { + if (this._paused || this._ended) { this._paused = false; + this._ended = false; this.dispatchEventAsync(new CustomEvent('play')); if (this._readyState < this.READY.FUTURE_DATA) @@ -146,9 +259,11 @@ Sound.prototype = { playInternal: function() { this.gainNode = Sound.audioContext.createGainNode(); - this.gainNode.gain.value = this._volume; + this.gainNode.gain.value = this._muted ? 0 : this._volume; this.gainNode.connect(Sound.audioContext.destination); + this.startTime = Sound.audioContext.currentTime; + this.node = Sound.audioContext.createBufferSource(); this.node.connect(this.gainNode); this.node.buffer = this.buffer; @@ -159,10 +274,13 @@ Sound.prototype = { pause: function() { + if (this._networkState === this.NETWORK.EMPTY) + this.loadResource(); + this._autoplay = false; if (!this._paused) { - this._paused = false; + this._paused = true; this.dispatchEventAsync(new CustomEvent('timeupdate')); this.dispatchEventAsync(new CustomEvent('pause')); } @@ -186,6 +304,13 @@ Sound.prototype = { }, onended: function() { + if (this._loop) { + this.stopInternal(); + this.setCurrentTime(0); + this.playInternal(); + return; + } + this._ended = true; this.nextStartTime = 0; this.stopInternal(); @@ -236,7 +361,8 @@ Sound.prototype = { setSrc: function(src) { this._src = src; - this.load(); + if (this._src) + this.load(); }, getCurrentSrc: function() { @@ -247,40 +373,91 @@ Sound.prototype = { return this._networkState; }, + setNetworkState: function(value) { + this._networkState = value; + }, + getReadyState: function() { return this._readyState; }, setReadyState: function(value) { - this._readyState = value; + var oldState = this._readyState; + var newState = this._readyState = value; + + if (this._networkState === this.NETWORK.EMPTY) + return; + + if (oldState === this.READY.NOTHING && newState === this.READY.METADATA) + this.dispatchEventAsync('loadedmetadata'); + + if (oldState === this.READY.METADATA && newState >= this.READY.CURRENT_DATA) { + if (!this.sentLoadedData) + this.dispatchEventAsync('loadeddata'); + } + + if (oldState >= this.READY.FUTURE_DATA && newState <= this.READY.CURRENT_DATA) { + if (this.autoplaying && this._paused && this._autoplay && !this._ended && !this._error) { + this.dispatchEventAsync('timeupdate'); + this.dispatchEventAsync('waiting'); + this.nextStartTime = Sound.audioContext.currentTime - this.startTime; + this.stopInternal(); + } + } + + if (oldState <= this.READY.CURRENT_DATA && newState === this.READY.FUTURE_DATA) { + this.dispatchEventAsync('canplay'); + if (!this._paused) + this.dispatchEventAsync('playing'); + } + + if (oldState <= this.READY.CURRENT_DATA && newState === this.READY.FUTURE_DATA) { + this.dispatchEventAsync('canplay'); + if (!this._paused) { + this.dispatchEventAsync('playing'); + this.playInternal(); + } + + if (this.autoplaying && this._paused && this._autoplay) + this.play(); + } }, getPreload: function() { - if (!this._preload) - return 'none'; - return 'auto'; + switch (this._preload) { + case this.PRELOAD.NONE: return 'none'; + case this.PRELOAD.METADATA: return 'metadata'; + case this.PRELOAD.AUTO: return 'auto'; + default: return ''; + } }, setPreload: function(preload) { switch (preload) { - case 'none': - this._preload = false; - break; - default: - this._preload = true; - if (!this.buffer) - load(); - break; + default: + case 'none': + this._preload = this.PRELOAD.NONE; + break; + case 'metadata': + this._preload = this.PRELOAD.METADATA; + if (this._networkState === this.NETWORK.EMPTY) + this.load(); + break; + case 'auto': + this._preload = this.PRELOAD.auto; + if (this._networkState === this.NETWORK.EMPTY) + this.load(); + break; } }, getCurrentTime: function() { if (!this.node) return this.nextStartTime; - return this.nextStartTime + Sound.audioContext.currentTIme - this.startTime; + return this.nextStartTime + Sound.audioContext.currentTime - this.startTime; }, - setCurrentTime: function(time) { + setCurrentTime: function(time) { this.nextStartTime = time; this.dispatchEventAsync(new CustomEvent('timeupdate')); if (!this.node) @@ -319,12 +496,50 @@ Sound.prototype = { }, setVolume: function(volume) { + if (this._volume === volume) + return; + this._volume = volume; - if (!this.gainNode) + this.dispatchEventAsync(new CustomEvent('volumechange')); + + if (this.gainNode) + this.gainNode.gain.value = this._muted ? 0 : this._volume; + }, + + getMuted: function() { + return this._muted; + }, + + setMuted: function(muted) { + if (this._muted === muted) return; - this.gainNode.gain.value = volume; + this._muted = muted; this.dispatchEventAsync(new CustomEvent('volumechange')); + + if (this.gainNode) + this.gainNode.gain.value = this._muted ? 0 : this._volume; + }, + + getAutoplay: function() { + return this._autoplay; + }, + + setAutoplay: function(autoplay) { + if (this._autoplay === autoplay) + return; + + this._autoplay = autoplay; + if (this._autoplay && this._networkState === this.NETWORK.EMPTY) + this.load(); + }, + + getLoop: function() { + return this._loop; + }, + + setLoop: function(loop) { + this._loop = loop; }, }; @@ -423,8 +638,17 @@ Object.defineProperty(Sound.prototype, 'defaultMuted', { set: Sound.prototype.setDefaultMuted, }); +Object.defineProperty(Sound.prototype, 'preload', { + get: Sound.prototype.getPreload, + set: Sound.prototype.setPreload, +}); + document.createElement = function(elementName) { if (elementName === "Audio" || elementName === "audio") return new Sound(); return Document.prototype.createElement.call(this, elementName); +}; + +window.Audio = function(src) { + return new Sound(src); }; \ No newline at end of file