Plato on Github
Report Home
boombox.js
Maintainability
65.18
Lines of code
3116
Difficulty
171.41
Estimated Errors
20.69
Function weight
By Complexity
By SLOC
/** * Browser sound library which blended HTMLVideo and HTMLAudio and WebAudio * * The MIT License (MIT) * * Copyright (c) 2014-2015 CyberAgent, Inc. * * 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. * * * @license MIT * @author Kei Funagayama <funagayama_kei@cyberagent.co.jp> */ (function (w) { 'use strict'; var none = function () {}; var isRequire = !!(typeof define === 'function' && define.amd); var SPRITE_SEPARATOR = '-'; var LOGGER_DEFAULT_SEPARATOR = '-'; var getSpriteName = function (name) { return { prefix: name.substring(0, name.indexOf(SPRITE_SEPARATOR)), suffix: name.substring(name.indexOf(SPRITE_SEPARATOR) + 1), name: name }; }; /** * trace: 1 * debug: 2 * info: 3 * warn: 4 * error: 5 * */ var LOG_LEVEL = 3; // default: info var Logger = (function () { var ArrayProto = Array.prototype; var slice = ArrayProto.slice; function Logger(prefix) { this.prefix = prefix || LOGGER_DEFAULT_SEPARATOR; this.prefix = '[' + this.prefix + ']'; } /** * Log output (trace) * @memberof Logger * @name trace */ Logger.prototype.trace = function () { if (LOG_LEVEL <= 1) { if (!w.console) { } else if (w.console.trace) { w.console.trace('[TRACE]', this.prefix, slice.call(arguments).join(' ')); } else if (w.console.debug) { w.console.debug('[TRACE]', this.prefix, slice.call(arguments).join(' ')); } else { w.console.log('[TRACE]', this.prefix, slice.call(arguments).join(' ')); } } }; /** * Log output (debug) * @memberof Logger * @name debug */ Logger.prototype.debug = function () { if (LOG_LEVEL <= 2) { if (!w.console) { } else if (w.console.debug) { w.console.debug('[DEBUG]', this.prefix, slice.call(arguments).join(' ')); } else { w.console.log('[DEBUG]', this.prefix, slice.call(arguments).join(' ')); } } }; /** * Log output (info) * @memberof Logger * @name info */ Logger.prototype.info = function () { if (LOG_LEVEL <= 3) { w.console && w.console.info('[INFO]', this.prefix, slice.call(arguments).join(' ')); } }; /** * Log output (warn) * @memberof Logger * @name warn */ Logger.prototype.warn = function () { if (LOG_LEVEL <= 4) { w.console && w.console.warn('[WARN]', this.prefix, slice.call(arguments).join(' ')); } }; /** * Log output (error) * @memberof Logger * @name error */ Logger.prototype.error = function () { if (LOG_LEVEL <= 5) { w.console && w.console.error('[ERROR]', this.prefix, slice.call(arguments).join(' ')); } }; return Logger; })(); ////////////////////////////////// // Boombox Class var Boombox = (function () { function Boombox() { /** * Version * @memberof Boombox * @name VERSION */ this.VERSION = '1.0.9'; /** * Loop off * * @memberof Boombox * @name LOOP_NOT * @constant * @type {Interger} */ this.LOOP_NOT = 0; /** * orignal loop * * @memberof Boombox * @name LOOP_ORIGINAL * @constant * @type {Interger} */ this.LOOP_ORIGINAL = 1; /** * Native loop * * @memberof Boombox * @name LOOP_NATIVE * @constant * @type {Interger} */ this.LOOP_NATIVE = 2; /** * Turn off the power. * * @memberof Boombox * @name POWER_OFF * @constant * @type {Boolean} */ this.POWER_OFF = false; /** * Turn on the power. * * @memberof Boombox * @name POWER_ON * @constant * @type {Boolean} */ this.POWER_ON = true; /** * It does not support the media type. * * @memberof Boombox * @name ERROR_MEDIA_TYPE * @constant * @type {Interger} */ this.ERROR_MEDIA_TYPE = 0; /** * Hit the filter * * @memberof Boombox * @name ERROR_HIT_FILTER * @constant * @type {Interger} */ this.ERROR_HIT_FILTER = 1; /** * Threshold to determine whether sound source is finished or not * * @memberof Boombox * @name THRESHOLD * @type {Interger} */ this.THRESHOLD = 0.2; /** * flag setup * @memberof Boombox * @name setuped * @type {Boolean} */ this.setuped = false; /** * AudioContext * @memberof Boombox * @name AudioContext * @type {AudioContext} */ this.AudioContext = w.AudioContext || w.webkitAudioContext; /** * Environmental support information * * @memberof Boombox * @name support * @type {Object} */ this.support = { mimes: [], webaudio: { use: !!this.AudioContext }, htmlaudio: { use: false }, htmlvideo: { use: false } }; if (this.support.webaudio.use) { /** * WebAudioContext instance (singleton) * * @memberof Boombox * @name WEB_AUDIO_CONTEXT * @type {AudioContext} */ this.WEB_AUDIO_CONTEXT = new this.AudioContext(); if (!this.WEB_AUDIO_CONTEXT.createGain) { this.WEB_AUDIO_CONTEXT.createGain = this.WEB_AUDIO_CONTEXT.createGainNode; } } // Check HTML Audio support. try { /** * Test local HTMLAudio * @memberof Boombox * @name _audio * @type {HTMLAudioElement} */ if (new w.Audio().canPlayType) { this.support.htmlaudio.use = true; } else { this.support.htmlaudio.use = false; } } catch (e) { this.support.htmlaudio.use = false; } // Check HTML Video support. try { /** * Test local HTMLVideo * @memberof Boombox * @name _video * @type {HTMLVideoElement} */ if (document.createElement('video').canPlayType) { this.support.htmlvideo.use = true; } else { this.support.htmlvideo.use = false; } } catch (e) { this.support.htmlvideo.use = false; } /** * Audio instance pool * * @memberof Boombox * @name pool */ this.pool = {}; /** * Audio instance of waiting * * @memberof Boombox * @name waits */ this.waits = []; /** * Visibility of browser * * @memberof Boombox * @name visibility */ this.visibility = { hidden: undefined, visibilityChange: undefined }; /** * State of boombox * * @memberof Boombox * @name state * @type {Object} */ this.state = { power: this.POWER_ON }; /** * Filtering function * * @memberof Boombox * @name filter * @type {Object} */ this.filter = {}; } //// prototype /** * The availability of the WebLAudio * * @memberof Boombox * @name isWebAudio * @return {Boolean} */ Boombox.prototype.isWebAudio = function () { return this.support.webaudio.use; }; /** * The availability of the HTMLAudio * * @memberof Boombox * @name isHTMLAudio * @return {Boolean} */ Boombox.prototype.isHTMLAudio = function () { return this.support.htmlaudio.use; }; /** * The availability of the HTMLVideo * * @memberof Boombox * @name isHTMLVideo * @return {Boolean} */ Boombox.prototype.isHTMLVideo = function () { return this.support.htmlvideo.use; }; /** * boombox to manage, Audio is playing * * @memberof Boombox * @name isPlayback * @return {Boolean} */ Boombox.prototype.isPlayback = function () { var self = this; var res = false; for (var name in this.pool) { if (this.pool[name].isPlayback()) { res = true; break; } } return res; }; /** * Setup processing * * @memberof Boombox * @name setup * @param {Object} options * @return {Boombox} * @example * var options = { * webaudio: {use: Boolean}, * htmlaudio: {use: Boolean}, * htmlvideo: {use: Boolean}, * loglevel: Number, ) trace:1, debug:2, info:3, warn:4, error:5 * } * */ Boombox.prototype.setup = function setup(options) { var self = this; options = options || {}; if (typeof options.threshold !== 'undefined') { this.THRESHOLD = options.threshold; } if (typeof options.loglevel !== 'undefined') { LOG_LEVEL = options.loglevel; } this.logger = new Logger('Boombox '); // log if (this.setuped) { this.logger.warn('"setup" already, are running.'); return this; } if (options.webaudio) { if (typeof options.webaudio.use !== 'undefined') { this.support.webaudio.use = options.webaudio.use; this.logger.info('options.webaudio.use:', this.support.webaudio.use); } } if (options.htmlaudio) { if (typeof options.htmlaudio.use !== 'undefined') { this.support.htmlaudio.use = options.htmlaudio.use; this.logger.info('options.htmlaudio.use: ', this.support.htmlaudio.use); } } if (options.htmlvideo) { if (typeof options.htmlvideo.use !== 'undefined') { this.support.htmlvideo.use = options.htmlvideo.use; this.logger.info('options htmlvideo', this.support.htmlvideo); } } // scan browser this._browserControl(); // Log: WebAudio support. if (this.support.webaudio.use) { this.logger.debug('WebAudio use support.'); } else { this.logger.debug('WebAudio use not support.'); } // Log: HTMLAudio support. if (this.support.htmlaudio.use) { this.logger.debug('HTMLAudio use support.'); } else { this.logger.debug('HTMLAudio use not support.'); } // Log: HTMLVideo support. if (this.support.htmlvideo.use) { this.logger.debug('HTMLVideo use support.'); } else { this.logger.debug('HTMLVideo use not support.'); } //this.logger.debug('support:', JSON.stringify(this.support)); this.setuped = true; return this; }; /** * Get Audio instance * * @memberof Boombox * @name get * @param {String} name audio name * @return {WebAudio|HTMLAudio|HTMLVideo} */ Boombox.prototype.get = function (name) { return this.pool[name]; }; /** * Loading audio * * @memberof Boombox * @name load * @param {String} name audio name * @param {Object} options Audio options * @param {Boolean} useHTMLVideo forced use HTMLVideo * @param {Function} callback * @return {Boombox} * @example * var options = { * src: [ * { * media: 'audio/mp4', * path: 'http://example.com/sample.m4a', * } * ] * filter: ['android2', 'android4', 'ios5', 'ios6', 'ios7' ] * } * */ Boombox.prototype.load = function (name, options, useHTMLVideo, callback) { if (typeof arguments[2] === 'function') { callback = useHTMLVideo; useHTMLVideo = null; } if (!this.setuped) { callback && callback(new Error('setup incomplete boombox. run: boombox.setup(options)')); return this; } if (this.pool[name]) { this.logger.trace('audio pool cache hit!!', name); return callback && callback(null, this.pool[name]); } // check support media type var src = this.useMediaType(options.src); if (src) { options.media = src.media; options.src = src.path; } // forced use HTMLVideo if (useHTMLVideo && this.isHTMLVideo()) { var htmlvideo = new boombox.HTMLVideo(name); if (!src) { htmlvideo.state.error = this.ERROR_MEDIA_TYPE; } if (typeof htmlvideo.state.error === 'undefined' && !this.runfilter(htmlvideo, options)) { htmlvideo.load(options, callback); } else { return callback && callback(new Error(htmlvideo.state.error), htmlvideo); } this.setPool(name, htmlvideo, boombox.HTMLVideo); return this; } ////////////////////// if (this.isWebAudio()) { this.logger.debug("use web audio"); var webaudio = new boombox.WebAudio(name); if (!src) { webaudio.state.error = this.ERROR_MEDIA_TYPE; } if (typeof webaudio.state.error === 'undefined' && !this.runfilter(webaudio, options)) { webaudio.load(options, callback); } else { return callback && callback(new Error(webaudio.state.error), webaudio); } this.setPool(name, webaudio, boombox.WebAudio); } else if (this.isHTMLAudio()) { this.logger.debug("use html audio"); var htmlaudio = new boombox.HTMLAudio(name); if (!src) { htmlaudio.state.error = this.ERROR_MEDIA_TYPE; } if (typeof htmlaudio.state.error === 'undefined' && !this.runfilter(htmlaudio, options)) { htmlaudio.load(options, callback); } else { return callback && callback(new Error(htmlaudio.state.error), htmlaudio); } this.setPool(name, htmlaudio, boombox.HTMLAudio); } else { callback && callback(new Error('This environment does not support HTMLAudio, both WebAudio both.'), this); } return this; }; /** * remove audio * * @memberof Boombox * @name remove * @param {String} name * @return {Boombox} */ Boombox.prototype.remove = function (name) { if (this.pool[name]) { // change object this.logger.trace('Remove Audio that is pooled. name', name); this.pool[name].dispose && this.pool[name].dispose(); this.pool[name] = undefined; delete this.pool[name]; } return this; }; /** * Set pool * * @memberof Boombox * @name setPool * @param {String} name * @param {AudioContext|HTMLAudioElement|HTMLVideoElement} obj Browser audio instance * @param {WebAudio|HTMLAudio|HTMLVideo} Obj Boombox audio class * @return {Boombox} */ Boombox.prototype.setPool = function (name, obj, Obj) { if (obj.isParentSprite()) { for (var r in this.pool) { if (!!~r.indexOf(name + SPRITE_SEPARATOR)) { delete this.pool[r]; } } for (var n in obj.sprite.options) { var cname = name + SPRITE_SEPARATOR + n; var audio = new Obj(cname, obj); this.pool[audio.name] = audio; } } this.remove(name); this.pool[name] = obj; return this; }; /** * Run filter * * @memberof Boombox * @name runfilter * @param {WebAudio|HTMLAudio|HTMLVideo} audio Boombox audio instance * @param {Object} options * @return {Boolean} */ Boombox.prototype.runfilter = function (audio, options) { var hit; var list = options.filter || []; for (var i = 0; i < list.length; i++) { var name = list[i]; var fn = this.filter[name]; if (!fn) { this.logger.warn('filter not found. name:', name); return; } this.logger.debug('filter run. name:', name); if (fn(name, audio, options)) { hit = name; break; } } if (hit) { audio.state.error = this.ERROR_HIT_FILTER; // set error this.logger.warn('Hit the filter of', hit); return true; } return false; }; /** * check support media type * * @memberof Boombox * @name useMediaType * @param {Array} src audio file data * @return {Object|undefined} */ Boombox.prototype.useMediaType = function (src) { for (var i = 0; i < src.length; i++) { var t = src[i]; if (new w.Audio().canPlayType(t.media)) { return t; } else { this.logger.warn('skip audio type.', t.media); } } return undefined; }; /** * pause sound playback in managing boombox * * @memberof Boombox * @name pause * @return {boombox} */ Boombox.prototype.pause = function () { var self = this; this.logger.trace('pause'); for (var name in this.pool) { var audio = this.pool[name]; audio.pause(); self.waits.push(name); } return this; }; /** * resume the paused, to manage the boombox * * @memberof Boombox * @name resume * @return {Boombox} */ Boombox.prototype.resume = function () { this.logger.trace('resume'); var name = this.waits.shift(); if (name && this.pool[name]) { this.pool[name].resume(); } if (0 < this.waits.length) { this.resume(); } return this; }; /** * Change all audio power on/off * * @memberof Boombox * @name power * @param {Boolean} p power on/off. boombox.(POWER_ON|POWER_OFF) * @return {Boombox} */ Boombox.prototype.power = function (p) { var self = this; this.logger.trace('power:', this.name, 'flag:', p); for (var name in this.pool) { var audio = this.pool[name]; audio.power(p); } this.state.power = p; return this; }; /** * audio change volume. * * @memberof Boombox * @method * @name volume * @param {Interger} v volume * @return {Boombox} */ Boombox.prototype.volume = function (v) { var self = this; this.logger.trace('volume:', this.name, 'volume:', v); for (var name in this.pool) { var audio = this.pool[name]; audio.volume(v); } return this; }; /** * Firing in the occurrence of events VisibilityChange * * @memberof Boombox * @name onVisibilityChange * @param {Event} e event */ Boombox.prototype.onVisibilityChange = function (e) { this.logger.trace('onVisibilityChange'); if (document[this.visibility.hidden]) { this.pause(); } else { this.resume(); } }; /** * Firing in the occurrence of events window.onfocus * * @memberof Boombox * @name onFocus * @param {Event} e event */ Boombox.prototype.onFocus = function (e) { this.logger.trace('onFocus'); this.resume(); }; /** * Firing in the occurrence of events window.onblur * * @memberof Boombox * @name onBlur * @param {Event} e event */ Boombox.prototype.onBlur = function (e) { this.logger.trace('onBlur'); this.pause(); }; /** * Firing in the occurrence of events window.onpageshow * * @memberof Boombox * @name onPageShow * @param {Event} e event */ Boombox.prototype.onPageShow = function (e) { this.logger.trace('onPageShow'); this.resume(); }; /** * Firing in the occurrence of events window.onpagehide * * @memberof Boombox * @name onPageHide * @param {Event} e event */ Boombox.prototype.onPageHide = function (e) { this.logger.trace('onPageHide'); this.pause(); }; /** * Scan browser differences * * @memberof Boombox * @name _browserControl * @return {Boombox} */ Boombox.prototype._browserControl = function () { var self = this; if (typeof document.hidden !== "undefined") { this.visibility.hidden = "hidden"; this.visibility.visibilityChange = "visibilitychange"; } else if (typeof document.webkitHidden !== "undefined") { this.visibility.hidden = "webkitHidden"; this.visibility.visibilityChange = "webkitvisibilitychange"; } else if (typeof document.mozHidden !== "undefined") { this.visibility.hidden = "mozHidden"; this.visibility.visibilityChange = "mozvisibilitychange"; } else if (typeof document.msHidden !== "undefined") { this.visibility.hidden = "msHidden"; this.visibility.visibilityChange = "msvisibilitychange"; } // Visibility.hidden if (this.visibility.hidden) { document.addEventListener(this.visibility.visibilityChange, function (e) { self.onVisibilityChange(e); }, false); } // focus/blur if (typeof window.addEventListener !== "undefined") { window.addEventListener('focus', function (e) { self.onFocus(e); }, false); window.addEventListener('blur', function (e) { self.onBlur(e); }, false); } else { window.attachEvent('onfocusin', function (e) { self.onFocus(e); }, false); window.attachEvent('onfocusout', function (e) { self.onBlur(e); }, false); } // onpage show/hide window.onpageshow = function (e) { self.onPageShow(e); }; window.onpagehide = function (e) { self.onPageHide(e); }; // return this; }; /** * Adding filtering * * @memberof Boombox * @name addFilter * @param {String} filter name * @param {Function} filter function * @return {Boombox} */ Boombox.prototype.addFilter = function (name, fn) { this.filter[name] = fn; return this; }; /** * dispose * * @memberof Boombox * @name dispose */ Boombox.prototype.dispose = function () { for (var name in this.pool) { var audio = this.pool[name]; audio.dispose && audio.dispose(); } delete this.VERSION; delete this.setuped; delete this.support.mimes; delete this.support.webaudio.use; this.WEB_AUDIO_CONTEXT && delete this.WEB_AUDIO_CONTEXT; delete this.support.webaudio; delete this.support.htmlaudio.use; delete this.support.htmlaudio; delete this.support.htmlvideo.use; delete this.support.htmlvideo; delete this.support; delete this.pool; delete this.waits; delete this.visibility; delete this.state.power; delete this.state; delete this.filter; }; return Boombox; })(); ////////////////////////////////// // New!!!! var boombox = new Boombox(); ////////////////////////////////// // HTMLAudio Class var HTMLAudio = (function () { function HTMLAudio(name, parent) { /** * logger * @memberof HTMLAudio * @name logger */ this.logger = new Logger('HTMLAudio'); /** * unique name * @memberof HTMLAudio * @name name */ this.name = name; /** * SetTimeout#id pool running * @memberof HTMLAudio * @name _timer */ this._timer = {}; // setTimeout#id /** * AudioSprite option data * @memberof HTMLAudio * @name sprite */ this.sprite = undefined; if (parent) { var sprite_n = getSpriteName(name); // change Sprite var current = parent.sprite.options[sprite_n.suffix]; /** * Reference of the parent instance HTMLAudio * @memberof HTMLAudio * @name parent */ this.parent = parent; // ref /** * Reference of the parent state instance HTMLAudio * @memberof HTMLAudio * @name state */ this.state = this.parent.state; // ref /** * Reference of the parent HTMLAudioElement instance HTMLAudio * @memberof HTMLAudio * @name state */ this.$el = this.parent.$el; // ref //this.onEnded = this.parent.onEnded; // TODO: ref this.sprite = new Sprite(undefined, current); // new } else { this.state = { time: { playback: undefined, // Playback start time (unixtime) pause: undefined // // Seek time paused }, loop: boombox.LOOP_NOT, // Loop playback flags power: boombox.POWER_ON, // Power flag loaded: false, // Audio file is loaded error: undefined // error state }; this.$el = new w.Audio(); } } //// /** * Loading html audio * * @memberof HTMLAudio * @name load * @method * @param {Object} options * @param {Function} callback * @return {HTMLAudio} * @example * .load({ * src: '', // required * type: '', // optional * media: '', // optional * preload: 'auto', // optional * autoplay: false, // optional * mediagroup: 'boombox', // optional * loop: false, // optional * muted: false, // optional * crossorigin: "anonymous", // optional * controls: false // optional * timeout: 15 * 1000 // optional default) 15s * }, function callback() {}); * */ HTMLAudio.prototype.load = function (options, callback) { var cb = callback || none; if (this.parent) { // skip audiosprite children cb(null, this); return this; } options = options || { //src: '', //type: '', //media: '', preload: 'auto', // auto, metadata, none autoplay: false, // no-auto //mediagroup: 'boombox', loop: false, muted: false, //crossorigin: "anonymous", controls: false }; var timeout = options.timeout || 15 * 1000; delete options.timeout; if (options.spritemap) { // Sprite ON this.sprite = new Sprite(options.spritemap); delete options.spritemap; } for (var k in options) { var v = options[k]; this.logger.trace('HTMLAudioElement attribute:', k, v); this.$el[k] = v; } var self = this; // Debug log /** ["loadstart", "progress", "suspend", "load", "abort", "error", "emptied", "stalled", "play", "pause", "loadedmetadata", "loadeddata", "waiting", "playing", "canplay", "canplaythrough", "seeking", "seeked", "timeupdate", "ended", "ratechange", "durationchange", "volumechange"].forEach(function (eventName) { self.$el.addEventListener(eventName, function () { console.log('audio: ' + eventName); }, true); }); */ var hookEventName = 'canplay'; var ua_ios = window.navigator.userAgent.match(/(iPhone\sOS)\s([\d_]+)/); if (ua_ios && 0 < ua_ios.length) { // IOS Safari hookEventName = 'suspend'; } this.logger.trace('hook event name:', hookEventName); this.$el.addEventListener(hookEventName, function _canplay(e) { self.logger.trace('processEvent ' + e.target.id + ' : ' + e.type, 'event'); self.state.loaded = true; self.$el.removeEventListener(hookEventName, _canplay, false); return cb(null, self); }); this.$el.addEventListener( 'ended', function (e) { self._onEnded(e); }, false); // communication time-out setTimeout(function () { if (self.$el && self.$el.readyState !== 4) { self.$el.src = ''; cb(new Error('load of html audio file has timed out. timeout:' + timeout), self); cb = function () {}; } }, timeout); this.$el.load(); // setInterval(function () { // console.error(self.name, // 'playback:', self.isPlayback(), // 'stop:', self.isStop(), // 'pause:', self.isPause(), // 'power:', self.state.power // ); // }, 100); return this; }; //HTMLAudio.prototype.addTextTrack = function () { /**...*/ }; //HTMLAudio.prototype.canPlayType = function () { /**...*/ }; ////////// /** * Is use. * * @memberof HTMLAudio * @method * @name isUse * @return {Boolean} */ HTMLAudio.prototype.isUse = function () { if (this.state.power === boombox.POWER_OFF || boombox.state.power === boombox.POWER_OFF) { return false; } if (!this.state.loaded || typeof this.state.error !== 'undefined') { return false; } return true; }; /** * Is playing. * * @memberof HTMLAudio * @method * @name isPlayback * @return {Boolean} */ HTMLAudio.prototype.isPlayback = function () { return !!this.state.time.playback; }; /** * Is stoped. * * @memberof HTMLAudio * @method * @name isStop * @return {Boolean} */ HTMLAudio.prototype.isStop = function () { return !this.state.time.playback; }; /** * Is paused. * * @memberof HTMLAudio * @method * @name isPause * @return {boolean} */ HTMLAudio.prototype.isPause = function () { return !!this.state.time.pause; }; /** * Loop flag * * @memberof HTMLAudio * @method * @name isLoop * @return {Interger} */ HTMLAudio.prototype.isLoop = function () { return (0 < this.state.loop); }; /** * Is sprite of the parent * * @memberof HTMLAudio * @method * @name isParentSprite * @return {Boolean} */ HTMLAudio.prototype.isParentSprite = function () { return !!(!this.parent && this.sprite && !this.sprite.current); }; /** * Is sprite * * @memberof HTMLAudio * @method * @name isSprite * @return {Boolean} */ HTMLAudio.prototype.isSprite = function () { return !!(this.parent && this.sprite && this.sprite.current); }; /** * Clear all the setTimeout * * @memberof HTMLAudio * @method * @name clearTimerAll * @return {HTMLAudio} */ HTMLAudio.prototype.clearTimerAll = function () { for (var k in this._timer) { var id = this._timer[k]; this.clearTimer(k); } return this; }; /** * Clear specified setTimeout * * @memberof HTMLAudio * @method * @name clearTimer * @param {String} name * @return {Interger} */ HTMLAudio.prototype.clearTimer = function (name) { var id = this._timer[name]; if (id) { this.logger.debug('remove setTimetout:', id); clearTimeout(id); delete this._timer[name]; } return id; }; /** * Save the specified setTimeout * * @memberof HTMLAudio * @method * @name setTimer * @param {String} name * @param {Interger} id setTimeout#id * @return {Interger} */ HTMLAudio.prototype.setTimer = function (name, id) { if (this._timer[name]) { this.logger.warn('Access that is not expected:', name, id); } this._timer[name] = id; return this._timer[name]; }; ////////// /** * audio play. * * @memberof HTMLAudio * @method * @name play * @param {Boolean} resume resume flag * @return {HTMLAudio} */ HTMLAudio.prototype.play = function (resume) { if (!this.isUse()) { this.logger.debug('skip play:', this.name, 'state can not be used'); return this; } // skip!! if (this.isPlayback()) { this.logger.debug('skip play:', this.name, 'is playing'); return this; } var self = this; var type = 'play'; var fn = none; this.state.time.playback = Date.now(); if (resume && this.state.time.pause) { // resume this.setCurrentTime(this.state.time.pause); if (this.isSprite()) { var _pause = this.state.time.pause; fn = function () { var interval = Math.ceil((self.sprite.current.end - _pause) * 1000); // (ms) self.setTimer('play', setTimeout(function () { self.stop(); self._onEnded(); // fire onended evnet }, interval)); }; } this.state.time.pause = undefined; type = 'resume:'; } else { // zero-play this.setCurrentTime(0); if (this.isSprite()) { var start = this.sprite.current.start; this.setCurrentTime(start); fn = function () { var interval = Math.ceil(self.sprite.current.term * 1000); // (ms) self.setTimer('play', setTimeout(function () { self.stop(); self._onEnded(); // fire onended evnet }, interval)); }; } } this.logger.debug(type, this.name); fn(); this.state.time.name = this.name; this.$el.play(); return this; }; /** * audio stop. * * @memberof HTMLAudio * @method * @name stop * @return {HTMLAudio} */ HTMLAudio.prototype.stop = function () { if (!this.state.loaded || typeof this.state.error !== 'undefined') { this.logger.debug('skip stop:', this.name, 'state can not be used'); return this; } // skip!! if (this.state.time.name && this.state.time.name !== this.name) { this.logger.debug('skip stop: It is used in other sources', this.name, this.state.time.name); return this; } // skip!! this.logger.debug('stop:', this.name); this.clearTimer('play'); this.$el.pause(); this.setCurrentTime(0); this.state.time.playback = undefined; this.state.time.name = undefined; return this; }; /** * audio pause. * * @memberof HTMLAudio * @method * @name pause * @return {HTMLAudio} */ HTMLAudio.prototype.pause = function () { if (!this.isUse()) { this.logger.debug('skip pause:', this.name, 'state can not be used'); return this; } // skip!! if (this.state.time.name && this.state.time.name !== this.name) { this.logger.debug('skip pause: It is used in other sources', this.name, this.state.time.name); return this; } // skip!! this.logger.debug('pause:', this.name); this.clearTimer('play'); this.$el.pause(); this.state.time.pause = this.$el.currentTime; this.state.time.playback = undefined; //this.state.time.name = undefined; return this; }; /** * audio resume. * * @memberof HTMLAudio * @method * @name resume * @return {HTMLAudio} */ HTMLAudio.prototype.resume = function () { if (!this.isUse()) { this.logger.debug('skip resume:', this.name, 'state can not be used'); return this; } // skip!! if (this.state.time.name && this.state.time.name !== this.name) { this.logger.debug('skip resume: It is used in other sources', this.name, this.state.time.name); return this; } // skip!! if (this.state.time.pause) { this.play(true); } return this; }; /** * audio re-play. * * @memberof HTMLAudio * @method * @name replay * @return {HTMLAudio} */ HTMLAudio.prototype.replay = function () { if (!this.isUse()) { this.logger.debug('skip replay:', this.name, 'state can not be used'); return this; } // skip!! this.logger.debug('replay:', this.name); this.clearTimer('play'); this.pause(); this.setCurrentTime(0); this.play(); return this; }; /** * audio change volume. * * @memberof HTMLAudio * @method * @name volume * @param {Interger} v volume * @return {HTMLAudio} */ HTMLAudio.prototype.volume = function (v) { this.logger.trace('volume:', this.name, 'volume:', v); this.$el.volume = Math.max(0, Math.min(1, v)); }; ////////// /** * Audio.ended events * * @memberof HTMLAudio * @method * @name _onEnded * @param {Event} e event */ HTMLAudio.prototype._onEnded = function (e) { if (this.isDisposed()) { // check dispose return; } this.logger.trace('onended fire! name:', this.name); this.state.time.playback = undefined; this.state.time.name = undefined; this.onEnded(e); // fire user ended event!! if (this.isDisposed()) { // check dispose return; } if (this.state.loop === boombox.LOOP_ORIGINAL && typeof this.state.time.pause === 'undefined') { this.logger.trace('onended original loop play.', this.name); this.play(); } }; /** * Override Audio.ended events * * @memberof HTMLAudio * @method * @name onEnded * @param {Event} e event */ HTMLAudio.prototype.onEnded = none; /** * Set loop flag * * @memberof HTMLAudio * @method * @name setLoop * @param {Interger} loop loop flag (Boombox.LOOP_XXX) * @return {HTMLAudio} */ HTMLAudio.prototype.setLoop = function (loop) { if (!this.isUse()) { return this; } // skip!! this.state.loop = loop; if (loop === boombox.LOOP_NOT) { this.$el.loop = boombox.LOOP_NOT; //} else if (loop === boombox.LOOP_ORIGINAL) { } else if (loop === boombox.LOOP_NATIVE) { if (this.isSprite()) { this.logger.warn('audiosprite does not support the native. please use the original loop.'); return this; } if (this.$el) { this.$el.loop = loop; } } return this; }; /** * Change power on/off * * @memberof HTMLAudio * @method * @name power * @param {Boolean} p power on/off. Boombox.(POWER_ON|POWER_OFF) * @return {HTMLAudio} */ HTMLAudio.prototype.power = function (p) { this.logger.trace('power:', this.name, 'flag:', p); if (p === boombox.POWER_OFF) { this.stop(); // force pause } this.state.power = p; return this; }; /** * Set audio.currentTime * * @memberof HTMLAudio * @method * @name setCurrentTime * @param {Interger} t set value(HTMLAudioElement.currentTime) * @return {HTMLAudio} */ HTMLAudio.prototype.setCurrentTime = function (t) { try { this.$el.currentTime = t; } catch (e) { this.logger.error('Set currentTime.', e.message); } return this; }; /** * Check disposed * * @memberof HTMLAudio * @method * @name isDisposed */ HTMLAudio.prototype.isDisposed = function () { return WebAudio.prototype.isDisposed.apply(this, arguments); }; ////////// /** * Dispose * * @memberof HTMLAudio * @method * @name dispose */ HTMLAudio.prototype.dispose = function () { delete this.name; delete this.state.time.playback; delete this.state.time.pause; delete this.state.time.name; delete this.state.time; delete this.state.loop; delete this.state.power; delete this.state.loaded; delete this.state.error; delete this.state; this.$el.src = undefined; delete this.$el; this.clearTimerAll(); delete this._timer; delete this.parent; if (this.sprite && this.sprite.dispose) { this.sprite.dispose(); } delete this.sprite; }; return HTMLAudio; })(); ////////////////////////////////// // HTMLVideo Class var HTMLVideo = (function () { function HTMLVideo(name, parent) { /** * logger * @memberof HTMLVideo * @name logger */ this.logger = new Logger('HTMLVideo'); /** * unique name * @memberof HTMLVideo * @name name */ this.name = name; /** * SetTimeout#id pool running * @memberof HTMLVideo * @name _timer */ this._timer = {}; // setTimeout#id /** * AudioSprite option data * @memberof HTMLVideo * @name sprite */ this.sprite = undefined; if (parent) { var sprite_n = getSpriteName(name); // change Sprite var current = parent.sprite.options[sprite_n.suffix]; /** * Reference of the parent instance HTMLVideo * @memberof HTMLVideo * @name parent */ this.parent = parent; // ref /** * Reference of the parent state instance HTMLVideo * @memberof HTMLVideo * @name state */ this.state = this.parent.state; // ref /** * Reference of the parent HTMLVideoElement instance HTMLVideo * @memberof HTMLAudio * @name state */ this.$el = this.parent.$el; // ref //this.onEnded = this.parent.onEnded; // TODO: ref this.sprite = new Sprite(undefined, current); // new } else { this.state = { time: { playback: undefined, // Playback start time (unixtime) pause: undefined // // Seek time paused }, loop: boombox.LOOP_NOT, // Loop playback flags power: boombox.POWER_ON, // Power flag loaded: false, // Video file is loaded error: undefined // error state }; this.$el = document.createElement('video'); } } /////// /** * Loading html video * * @memberof HTMLVideo * @name load * @method * @param {Object} options * @param {Function} callback * @return {HTMLVideo} * @example * .load({ * src: '', // required * type: '', // optional * media: '', // optional * preload: 'auto', // optional * autoplay: false, // optional * mediagroup: 'boombox', // optional * loop: false, // optional * muted: false, // optional * crossorigin: "anonymous", // optional * controls: false // optional * timeout: 15 * 1000 // optional default) 15s * }, function callback() {}); * */ HTMLVideo.prototype.load = function (options, callback) { var self = this; var cb = callback || none; if (this.parent) { // skip audiosprite children cb(null, this); return this; } options = options || { //src: '', //type: '', //media: '', preload: 'auto', // auto, metadata, none autoplay: false, // no-auto //mediagroup: 'boombox', loop: false, muted: false, //crossorigin: "anonymous", controls: false }; var timeout = options.timeout || 15 * 1000; delete options.timeout; if (options.spritemap) { // Sprite ON this.sprite = new Sprite(options.spritemap); delete options.spritemap; } for (var k in options) { var v = options[k]; self.logger.trace('HTMLVideoElement attribute:', k, v); self.$el[k] = v; } /// Debug log /** ["loadstart", "progress", "suspend", "load", "abort", "error", "emptied", "stalled", "play", "pause", "loadedmetadata", "loadeddata", "waiting", "playing", "canplay", "canplaythrough", "seeking", "seeked", "timeupdate", "ended", "ratechange", "durationchange", "volumechange"].forEach(function (eventName) { self.$el.addEventListener(eventName, function () { console.log('audio: ' + eventName); }, true); }); */ var hookEventName = 'canplay'; var ua_ios = window.navigator.userAgent.match(/(iPhone\sOS)\s([\d_]+)/); if (ua_ios && 0 < ua_ios.length) { // IOS Safari hookEventName = 'suspend'; } else if (!!window.navigator.userAgent.match(/(Android)\s+(4)([\d.]+)/)) { // Android 4 basic hookEventName = 'loadeddata'; } else if (!!window.navigator.userAgent.match(/(Android)\s+(2)([\d.]+)/)) { // Android 2 basic hookEventName = 'stalled'; } self.logger.trace('hook event name:', hookEventName); this.$el.addEventListener(hookEventName, function _canplay(e) { self.logger.trace('processEvent ' + e.target.id + ' : ' + e.type, 'event'); self.state.loaded = true; self.$el.removeEventListener(hookEventName, _canplay, false); return cb(null, self); }); this.$el.addEventListener( 'ended', function (e) { self._onEnded(e); }, false); // communication time-out setTimeout(function () { if (self.$el && self.$el.readyState !== 4) { self.$el.src = ''; cb(new Error('load of html video file has timed out. timeout:' + timeout), self); cb = function () {}; } }, timeout); this.$el.load(); // setInterval(function () { // console.error(self.name, // 'playback:', self.isPlayback(), // 'stop:', self.isStop(), // 'pause:', self.isPause(), // 'power:', self.state.power // ); // }, 100); return this; }; //HTMLVideo.prototype.addTextTrack = function () { /**...*/ }; //HTMLVideo.prototype.canPlayType = function () { /**...*/ }; ////////// /** * Is use. (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name isUse * @return {Boolean} */ HTMLVideo.prototype.isUse = function () { return boombox.HTMLAudio.prototype.isUse.apply(this, arguments); }; /** * Is playing. (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name isPlayback * @return {Boolean} */ HTMLVideo.prototype.isPlayback = function () { return boombox.HTMLAudio.prototype.isPlayback.apply(this, arguments); }; /** * Is stoped. * * @memberof HTMLVideo (apply HTMLAudio) * @method * @name isStop * @return {Boolean} */ HTMLVideo.prototype.isStop = function () { return boombox.HTMLAudio.prototype.isStop.apply(this, arguments); }; /** * Is paused. * * @memberof HTMLVideo (apply HTMLAudio) * @method * @name isPause * @return {Boolean} */ HTMLVideo.prototype.isPause = function () { return boombox.HTMLAudio.prototype.isPause.apply(this, arguments); }; /** * Loop flag (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name isLoop * @return {Interger} */ HTMLVideo.prototype.isLoop = function () { return boombox.HTMLAudio.prototype.isLoop.apply(this, arguments); }; /** * Is sprite of the parent (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name isParentSprite * @return {Boolean} */ HTMLVideo.prototype.isParentSprite = function () { return boombox.HTMLAudio.prototype.isParentSprite.apply(this, arguments); }; /** * Is sprite (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name isSprite * @return {Boolean} */ HTMLVideo.prototype.isSprite = function () { return boombox.HTMLAudio.prototype.isSprite.apply(this, arguments); }; /** * Clear all the setTimeout (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name clearTimerAll * @return {HTMLAudio} */ HTMLVideo.prototype.clearTimerAll = function () { return boombox.HTMLAudio.prototype.clearTimerAll.apply(this, arguments); }; /** * Clear specified setTimeout (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name clearTimer * @param {String} name * @return {Interger} */ HTMLVideo.prototype.clearTimer = function (name) { return boombox.HTMLAudio.prototype.clearTimer.apply(this, arguments); }; /** * Save the specified setTimeout (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name setTimer * @param {String} name * @param {Interger} id setTimeout#id * @return {Interger} */ HTMLVideo.prototype.setTimer = function (name, id) { return boombox.HTMLAudio.prototype.setTimer.apply(this, arguments); }; ////////// /** * video play. * * @memberof HTMLVideo * @method * @name play * @param {Boolean} resume resume flag * @return {HTMLVideo} */ HTMLVideo.prototype.play = function (resume) { if (!this.isUse()) { this.logger.debug('skip play:', this.name, 'state can not be used'); return this; } // skip!! if (this.isPlayback()) { this.logger.debug('skip play:', this.name, 'is playing'); return this; } var self = this; var type = 'play'; var fn = none; this.state.time.playback = Date.now(); if (resume && this.state.time.pause) { // resume this.setCurrentTime(this.state.time.pause); if (this.isSprite()) { var _pause = this.state.time.pause; fn = function () { var interval = Math.ceil((self.sprite.current.end - _pause) * 1000); // (ms) self.setTimer('play', setTimeout(function () { self.stop(); self._onEnded(); // fire onended evnet }, interval)); }; } this.state.time.pause = undefined; type = 'resume:'; } else { // zero-play this.setCurrentTime(0); if (this.isSprite()) { var start = this.sprite.current.start; this.setCurrentTime(start); fn = function () { var interval = Math.ceil(self.sprite.current.term * 1000); // (ms) self.setTimer('play', setTimeout(function () { self.stop(); self._onEnded(); // fire onended evnet }, interval)); }; } } this.logger.debug(type, this.name); fn(); this.state.time.name = this.name; this.$el.play(); return this; }; /** * video stop. (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name stop * @return {HTMLVideo} */ HTMLVideo.prototype.stop = function () { return boombox.HTMLAudio.prototype.stop.apply(this, arguments); }; /** * video pause. (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name pause * @return {HTMLVideo} */ HTMLVideo.prototype.pause = function () { return boombox.HTMLAudio.prototype.pause.apply(this, arguments); }; /** * video resume. (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name resume * @return {HTMLVideo} */ HTMLVideo.prototype.resume = function () { return boombox.HTMLAudio.prototype.resume.apply(this, arguments); }; /** * video re-play. (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name replay * @return {HTMLVideo} */ HTMLVideo.prototype.replay = function () { return boombox.HTMLAudio.prototype.replay.apply(this, arguments); }; /** * audio change volume. (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name volume * @return {HTMLVideo} */ HTMLVideo.prototype.volume = function (v) { return boombox.HTMLAudio.prototype.volume.apply(this, arguments); }; ////////// /** * Video.ended events (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name _onEnded * @param {Event} e event */ HTMLVideo.prototype._onEnded = function (e) { return boombox.HTMLAudio.prototype._onEnded.apply(this, arguments); }; /** * Override Video.ended events (apply HTMLAudio) * * @OVERRIDE ME * @memberof HTMLVideo * @method * @name onEnded * @param {Event} e event */ HTMLVideo.prototype.onEnded = none; /** * Set loop flag (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name setLoop * @param {Interger} loop loop flag (Boombox.LOOP_XXX) * @return {HTMLVideo} */ HTMLVideo.prototype.setLoop = function (loop) { return boombox.HTMLAudio.prototype.setLoop.apply(this, arguments); }; /** * Change power on/off (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name power * @param {Boolean} p power on/off. boombox.(POWER_ON|POWER_OFF) * @return {HTMLVideo} */ HTMLVideo.prototype.power = function (p) { return boombox.HTMLAudio.prototype.power.apply(this, arguments); }; /** * Set video.currentTime (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name setCurrentTime * @param {Interger} t set value(Video.currentTime) * @return {HTMLVideo} */ HTMLVideo.prototype.setCurrentTime = function (t) { return boombox.HTMLAudio.prototype.setCurrentTime.apply(this, arguments); }; /** * Check disposed * * @memberof HTMLVideo * @method * @name isDisposed */ HTMLVideo.prototype.isDisposed = function () { return boombox.HTMLAudio.prototype.isDisposed.apply(this, arguments); }; ////////// /** * Dispose (apply HTMLAudio) * * @memberof HTMLVideo * @method * @name dispose */ HTMLVideo.prototype.dispose = function () { return boombox.HTMLAudio.prototype.dispose.apply(this, arguments); }; return HTMLVideo; })(); ////////////////////////////////// // WebAudio Class var WebAudio = (function () { function WebAudio(name, parent) { /** * logger * @memberof WebAudio * @name logger */ this.logger = new Logger('WebAudio'); /** * Audio name * * @memberof WebAudio * @name name * @type {String} */ this.name = name; /** * SetTimeout#id pool running * @memberof WebAudio * @name _timer */ this._timer = {}; /** * AudioSprite option data * @memberof WebAudio * @name sprite */ this.sprite = undefined; /** * AudioBuffer in use * * @memberof WebAudio * @name buffer * @type {AudioBuffer} */ this.buffer = undefined; /** * AudioBufferSourceNode in use * * @memberof WebAudio * @name source * @type {AudioBufferSourceNode} */ this.source = undefined; /** * WebAudioContext in use * shortcut: boombox.WEB_AUDIO_CONTEXT * * @memberof WebAudio * @name ctx * @type {AudioContext} */ this.ctx = boombox.WEB_AUDIO_CONTEXT; /** * WebAudioContext.GainNode instance * @memberof WebAudio * @name gainNode * @type {GainNode} */ this.gainNode = this.ctx.createGain(); /** * State of Audio * * @memberof WebAudio * @name state * @type {Object} */ this.state = { time: { playback: undefined, // Playback start time (unixtime) pause: undefined, // Seek time paused progress: 0 // Sound progress time }, loop: boombox.LOOP_NOT, // Loop playback flags power: boombox.POWER_ON, // power flag loaded: false, // Audio file is loaded error: undefined // error state }; if (parent) { var sprite_n = getSpriteName(name); this.parent = parent; // ref //this.state = this.parent.state; // ref this.state.loaded = this.parent.state.loaded; // not ref copy this.state.error = this.parent.state.error; // not ref copy //this.$el = this.parent.$el; // ref //this.onEnded = this.parent.onEnded; // TODO: ref // change Sprite var current = parent.sprite.options[sprite_n.suffix]; this.sprite = new Sprite(undefined, current); // new } } ////// /** * Loading web audio * * @memberof WebAudio * @name load * @method * @param {Object} options * @param {Function} callback * @return {WebAudio} * @example * .load({ * src: 'http://example.com/audio.m4a', * timeout: 15 * 1000 * }, function callback() {}); * */ WebAudio.prototype.load = function (options, callback) { var self = this; options = options || {}; var cb = callback || none; if (this.parent) { //this.buffer = this.parent.buffer; // ref cb(null, this); return this; } if (options.spritemap) { // Sprite ON this.sprite = new Sprite(options.spritemap); delete options.spritemap; } for (var k in options) { var v = options[k]; this.logger.trace('WebAudio attribute:', k, v); } var http = new XMLHttpRequest(); http.onload = function (e) { if (e.target.status.toString().charAt(0) === '2') { self.ctx.decodeAudioData( http.response, function (buffer) { if (!buffer) { self.logger.error('error decode file data: ', options.url); return cb(new Error('error decode file data'), self); } self.buffer = buffer; self.state.loaded = true; ///// // audiosprite propagation if (self.isParentSprite()) { for (var k in boombox.pool) { if (!!~k.indexOf(self.name + SPRITE_SEPARATOR)) { boombox.pool[k].buffer = buffer; // ref buffer boombox.pool[k].state.loaded = self.state.loaded; // not ref copy } } } return cb(null, self); }, function () { return cb(new Error('fail to decode file data'), self); } ); } else { self.logger.error('fail to load resource: ', options.url); return cb(new Error('fail to load resource'), self); } }; //http.timeout = 1; var timeout = options.timeout || 15 * 1000; // communication time-out setTimeout(function () { if (http.readyState !== 4) { http.abort(); cb(new Error('load of web audio file has timed out. timeout:' + timeout), self); cb = none; } }, timeout); http.open('GET', options.src, true); http.responseType = 'arraybuffer'; // setInterval(function () { // console.error(self.name, // 'playback', self.isPlayback(), // 'stop:', self.isStop(), // 'pause:', self.isPause(), // 'power:', self.state.power // ); // }, 100); ///////////////////// /// XHR send!! http.send(); return this; }; ////////// /** * Is use. (apply HTMLAudio) * * @memberof WebAudio * @method * @name isUse * @return {Boolean} */ WebAudio.prototype.isUse = function () { return boombox.HTMLAudio.prototype.isUse.apply(this, arguments); }; /** * Is playing. (apply HTMLAudio) * * @memberof WebAudio * @method * @name isPlayback * @return {Boolean} */ WebAudio.prototype.isPlayback = function () { return !!this.source && !!this.state.time.playback && !this.state.time.pause && (this.source.playbackState === 1 || this.source.playbackState === 2); }; /** * Is stoped. (apply HTMLAudio) * * @memberof WebAudio * @method * @name isStop * @return {Boolean} */ WebAudio.prototype.isStop = function () { return !this.source; }; /** * Is paused. (apply HTMLAudio) * * @memberof WebAudio * @method * @name isPause * @return {Boolean} */ WebAudio.prototype.isPause = function () { return boombox.HTMLAudio.prototype.isPause.apply(this, arguments); }; /** * Loop flag (apply HTMLAudio) * * @memberof WebAudio * @method * @name isLoop * @return {Interger} */ WebAudio.prototype.isLoop = function () { return boombox.HTMLAudio.prototype.isLoop.apply(this, arguments); }; /** * Is sprite of the parent (apply HTMLAudio) * * @memberof WebAudio * @method * @name isParentSprite * @return {Boolean} */ WebAudio.prototype.isParentSprite = function () { return boombox.HTMLAudio.prototype.isParentSprite.apply(this, arguments); }; /** * Is sprite (apply HTMLAudio) * * @memberof WebAudio * @method * @name isSprite * @return {Boolean} */ WebAudio.prototype.isSprite = function () { return boombox.HTMLAudio.prototype.isSprite.apply(this, arguments); }; /** * Clear all the setTimeout (apply HTMLAudio) * * @memberof WebAudio * @method * @name clearTimerAll * @return {WebAudio} */ WebAudio.prototype.clearTimerAll = function () { return boombox.HTMLAudio.prototype.clearTimerAll.apply(this, arguments); }; /** * Clear specified setTimeout (apply HTMLAudio) * * @memberof WebAudio * @method * @name clearTimer * @param {String} name * @return {Interger} */ WebAudio.prototype.clearTimer = function (name) { return boombox.HTMLAudio.prototype.clearTimer.apply(this, arguments); }; /** * Save the specified setTimeout (apply HTMLAudio) * * @memberof WebAudio * @method * @name setTimer * @param {String} name * @param {Interger} id setTimeout#id * @return {Interger} */ WebAudio.prototype.setTimer = function (name, id) { return boombox.HTMLAudio.prototype.setTimer.apply(this, arguments); }; ////////// /** * audio play. * * @memberof WebAudio * @method * @name play * @return {WebAudio} */ WebAudio.prototype.play = function (resume) { var self = this; if (!this.isUse()) { this.logger.debug('skip play:', this.name, 'state can not be used'); return this; } // skip!! if (this.isPlayback()) { this.logger.debug('skip play:', this.name, 'is playing'); return this; } if (!resume) { this.sourceDispose(); } this.source = this.ctx.createBufferSource(); if (this.state.loop === boombox.LOOP_NATIVE) { this.source.loop = this.state.loop; } this.source.buffer = this.buffer; this.source.connect(this.gainNode); this.gainNode.connect(this.ctx.destination); var type = 'play'; var fn = none; var start = 0; this.state.time.playback = Date.now(); // Playback start time (ms) if (resume && this.state.time.pause) { // resume start = this.state.time.pause / 1000; // Start position (sec) //this.logger.trace('start:', start); //this.logger.warn('interval:', interval); if (this.isSprite()) { var pause_sec = this.state.time.pause / 1000; // (sec) start = this.sprite.current.start + pause_sec; // Start position (sec) var interval = Math.ceil((self.sprite.current.term - pause_sec) * 1000); // (ms) fn = function () { self.setTimer('play', setTimeout(function () { self.stop(); self._onEnded(); // fire onended evnet }, interval)); }; } this.state.time.pause = undefined; } else { // zero if (this.isSprite()) { start = this.sprite.current.start; fn = function () { var interval = Math.ceil(self.sprite.current.term * 1000); self.setTimer('play', setTimeout(function () { self.stop(); self._onEnded(); // fire onended evnet }, interval)); }; } } this.logger.debug(type, this.name, 'offset:', start); fn(); var duration = this.buffer.duration - start; if (!this.isSprite()) { if (this.source.hasOwnProperty('onended')) { this.source.onended = function (e) { self._onEnded(e); }; } else { var interval = Math.ceil(duration * 1000); this.setTimer('play', setTimeout(function () { self.stop(); self._onEnded(); }, interval)); } } if (this.source.start) { this.logger.debug('use source.start()', this.name, start, duration); this.source.start(0, start, this.buffer.duration); } else { if (this.isSprite()) { duration = self.sprite.current.term; } this.logger.debug('use source.noteGrainOn()', this.name, start, duration); this.source.noteGrainOn(0, start, duration); } return this; }; /** * audio stop. * * @memberof WebAudio * @method * @name stop * @return {WebAudio} */ WebAudio.prototype.stop = function () { if (!this.state.loaded || typeof this.state.error !== 'undefined') { this.logger.debug('skip stop:', this.name, 'state can not be used'); return this; } // skip!! this.logger.debug('stop:', this.name); this.clearTimer('play'); if (this.source) { if (this.source.stop) { this.logger.debug('stop: use source.stop()', this.name); this.source.stop(0); } else { this.logger.debug('stop: use source.noteOff()', this.name); this.source.noteOff(0); } } this.sourceDispose(); return this; }; /** * audio pause. * * @memberof WebAudio * @method * @name pause * @return {WebAudio} */ WebAudio.prototype.pause = function () { if (!this.isUse()) { this.logger.debug('skip pause:', this.name, 'state can not be used'); return this; } // skip!! if (!this.source) { this.logger.debug('skip pause, Not playing.'); return this; } if (this.state.time.pause) { this.logger.debug('skip pause, It is already paused.'); return this; } var now = Date.now(); var offset = now - this.state.time.playback; this.state.time.pause = this.state.time.progress + offset; // Pause time(ms) this.state.time.progress += offset; this.logger.trace('state.time.pause:', this.state.time.pause, 'now:', now, 'state.time.playback', this.state.time.playback); this.logger.debug('pause:', this.name); this.clearTimer('play'); if (this.source) { if (this.source.stop) { this.logger.debug('pause: use source.stop()', this.name); this.source.stop(0); } else { this.logger.debug('pause: use source.noteOff()', this.name); this.source.noteOff(0); } } return this; }; /** * audio resume. * * @memberof WebAudio * @method * @name resume * @return {WebAudio} */ WebAudio.prototype.resume = function () { if (!this.isUse()) { this.logger.debug('skip resume:', this.name, 'state can not be used'); return this; } // skip!! if (this.state.time.pause) { this.play(true); } return this; }; /** * audio re-play. * * @memberof WebAudio * @method * @name replay * @return {WebAudio} */ WebAudio.prototype.replay = function () { if (!this.isUse()) { this.logger.debug('skip replay:', this.name, 'state can not be used'); return this; } // skip!! this.logger.debug('replay:', this.name); this.clearTimer('play'); this.sourceDispose(); this.play(); return this; }; /** * audio change volume. * * @memberof WebAudio * @method * @name volume * @return {WebAudio} */ WebAudio.prototype.volume = function (v) { this.logger.trace('volume:', this.name, 'volume:', v); this.gainNode.gain.value = v; }; ////////// /** * Audio.ended events * * @memberof WebAudio * @method * @name _onEnded * @param {Event} e event */ WebAudio.prototype._onEnded = function (e) { // check dispose if (this.isDisposed()) { return; } var self = this; var now = Date.now(); // skip if sounds is not ended if (self.source && Math.abs((now - self.state.time.playback + self.state.time.progress) / 1000 - self.source.buffer.duration) >= boombox.THRESHOLD) { self.logger.debug('skip if sounds is not ended', self.name); return; } this.logger.trace('onended fire!', this.name); this.state.time.playback = undefined; this.onEnded(e); // fire user ended event!! // check dispose if (this.isDisposed()) { return; } if (this.state.loop && typeof this.state.time.pause === 'undefined') { this.logger.trace('onended loop play.'); this.play(); } else { this.sourceDispose(); } }; /** * Override Audio.ended events * * @memberof WebAudio * @method * @name onEnded * @param {Event} e event */ WebAudio.prototype.onEnded = none; ////////// /** * Set loop flag * * @memberof WebAudio * @method * @name setLoop * @param {Interger} loop loop flag (boombox.LOOP_XXX) * @return {WebAudio} */ WebAudio.prototype.setLoop = function (loop) { if (!this.isUse()) { return this; } // skip!! this.state.loop = loop; if (loop === boombox.LOOP_NOT) { if (this.source) { this.source.loop = boombox.LOOP_NOT; } //} else if (loop === boombox.LOOP_ORIGINAL) { } else if (loop === boombox.LOOP_NATIVE) { if (this.source) { this.source.loop = loop; } } return this; }; /** * Change power on/off (apply HTMLAudio) * * @memberof WebAudio * @method * @name power * @param {Boolean} p power on/off. boombox.(POWER_ON|POWER_OFF) * @return {WebAudio} */ WebAudio.prototype.power = function (p) { return boombox.HTMLAudio.prototype.power.apply(this, arguments); }; ////////// /** * Dispose AudioBufferSourceNode * * @memberof WebAudio * @method * @name sourceDispose */ WebAudio.prototype.sourceDispose = function () { this.logger.trace('source dispose', this.name); this.source && this.source.disconnect(); this.source = undefined; this.state.time.playback = undefined; this.state.time.pause = undefined; this.state.time.progress = 0; }; /** * Check disposed * @memberof WebAudio * @method * @name isDisposed */ WebAudio.prototype.isDisposed = function () { this.logger.trace('check dispose flag', !!this.state); return !this.state; }; /** * Dispose * * @memberof WebAudio * @method * @name dispose */ WebAudio.prototype.dispose = function () { this.logger.trace('WebAudio dispose', this.name); delete this.buffer; //delete this.state.time.playback; //delete this.state.time.pause; //delete this.source; this.sourceDispose(); delete this.source; this.clearTimerAll(); delete this._timer; delete this.state.time; delete this.state.loop; delete this.state.power; delete this.state.loaded; delete this.state.error; delete this.state; this.parent = null; delete this.parent; if (this.sprite && this.sprite.dispose) { this.sprite.dispose(); } delete this.sprite; delete this.name; this.gainNode && this.gainNode.disconnect && delete this.gainNode; this.ctx = null; }; return WebAudio; })(); var Sprite = (function () { function Sprite(options, current) { /** * logger * @memberof Sprite * @type {Logger} * @name logger */ this.logger = new Logger('Sprite '); /** * current options * @memberof Sprite * @type {Object} * @name current */ this.current = current; // target sprite /** * options * @memberof Sprite * @name options * @type {Object} */ this.options = options; if (!current) { // parent for (var k in this.options) { this.options[k].term = this.options[k].end - this.options[k].start; } } } ////////// /** * Dispose * @memberof Sprite * @method * @name dispose */ Sprite.prototype.dispose = function () { this.options = null; delete this.options; delete this.current; }; return Sprite; })(); // Building boombox.HTMLAudio = HTMLAudio; boombox.HTMLVideo = HTMLVideo; boombox.WebAudio = WebAudio; if (isRequire) { define([], function () { return boombox; }); } else { w.boombox = boombox; } })(window);