UNPKG

buzz

Version:

Buzz, a Javascript HTML5 Audio library

991 lines (757 loc) 27.9 kB
// ---------------------------------------------------------------------------- // Buzz, a Javascript HTML5 Audio library // Licensed under the MIT license. // http://buzz.jaysalvat.com/ // ---------------------------------------------------------------------------- // Copyright (C) Jay Salvat // http://jaysalvat.com/ // ---------------------------------------------------------------------------- const AudioContext = window.AudioContext || window.webkitAudioContext const buzz = { defaults: { autoplay: false, crossOrigin: null, duration: 5000, formats: [], loop: false, placeholder: '--', preload: 'metadata', volume: 80, webAudioApi: false, document: window.document // iframe support }, types: { 'mp3': 'audio/mpeg', 'ogg': 'audio/ogg', 'wav': 'audio/wav', 'aac': 'audio/aac', 'm4a': 'audio/x-m4a' }, sounds: [], el: document.createElement('audio'), getAudioContext() { if (this.audioCtx === undefined) { try { this.audioCtx = AudioContext ? new AudioContext() : null } catch (e) { // There is a limit to how many contexts you can have, so fall back in case of errors constructing it this.audioCtx = null } } return this.audioCtx }, sound: function(src, options) { options = options || {} const doc = options.document || buzz.defaults.document let pid = 0 const events = [] const eventsOnce = {} const supported = buzz.isSupported() // publics this.load = function() { if (!supported) { return this } this.sound.load() return this } this.play = function() { if (!supported) { return this } this.sound.play().catch(() => {}) return this } this.togglePlay = function() { if (!supported) { return this } if (this.sound.paused) { this.sound.play().catch(() => {}) } else { this.sound.pause() } return this } this.pause = function() { if (!supported) { return this } this.sound.pause() return this } this.isPaused = function() { if (!supported) { return null } return this.sound.paused } this.stop = function() { if (!supported ) { return this } this.sound.pause() this.setTime(0) return this } this.isEnded = function() { if (!supported) { return null } return this.sound.ended } this.loop = function() { if (!supported) { return this } this.sound.loop = 'loop' this.bind('ended.buzzloop', function() { this.currentTime = 0 this.play() }) return this } this.unloop = function() { if (!supported) { return this } this.sound.removeAttribute('loop') this.unbind('ended.buzzloop') return this } this.mute = function() { if (!supported) { return this } this.sound.muted = true return this } this.unmute = function() { if (!supported) { return this } this.sound.muted = false return this } this.toggleMute = function() { if (!supported) { return this } this.sound.muted = !this.sound.muted return this } this.isMuted = function() { if (!supported) { return null } return this.sound.muted } this.setVolume = function(volume) { if (!supported) { return this } if (volume < 0) { volume = 0 } if (volume > 100) { volume = 100 } this.volume = volume this.sound.volume = volume / 100 return this } this.getVolume = function() { if (!supported) { return this } return this.volume } this.increaseVolume = function(value) { return this.setVolume(this.volume + (value || 1)) } this.decreaseVolume = function(value) { return this.setVolume(this.volume - (value || 1)) } this.setTime = function(time) { if (!supported) { return this } let set = true this.whenReady(function() { if (set === true) { set = false this.sound.currentTime = time } }) return this } this.getTime = function() { if (!supported) { return null } const time = Math.round(this.sound.currentTime * 100) / 100 return isNaN(time) ? buzz.defaults.placeholder : time } this.setPercent = function(percent) { if (!supported) { return this } return this.setTime(buzz.fromPercent(percent, this.sound.duration)) } this.getPercent = function() { if (!supported) { return null } const percent = Math.round(buzz.toPercent(this.sound.currentTime, this.sound.duration)) return isNaN(percent) ? buzz.defaults.placeholder : percent } this.setSpeed = function(duration) { if (!supported) { return this } this.sound.playbackRate = duration return this } this.getSpeed = function() { if (!supported) { return null } return this.sound.playbackRate } this.getDuration = function() { if (!supported) { return null } const duration = Math.round(this.sound.duration * 100) / 100 return isNaN(duration) ? buzz.defaults.placeholder : duration } this.getPlayed = function() { if (!supported) { return null } return timerangeToArray(this.sound.played) } this.getBuffered = function() { if (!supported) { return null } return timerangeToArray(this.sound.buffered) } this.getSeekable = function() { if (!supported) { return null } return timerangeToArray(this.sound.seekable) } this.getErrorCode = function() { if (supported && this.sound.error) { return this.sound.error.code } return 0 } this.getErrorMessage = function() { if (!supported) { return null } switch(this.getErrorCode()) { case 1: return 'MEDIA_ERR_ABORTED' case 2: return 'MEDIA_ERR_NETWORK' case 3: return 'MEDIA_ERR_DECODE' case 4: return 'MEDIA_ERR_SRC_NOT_SUPPORTED' default: return null } } this.getStateCode = function() { if (!supported) { return null } return this.sound.readyState } this.getStateMessage = function() { if (!supported) { return null } switch(this.getStateCode()) { case 0: return 'HAVE_NOTHING' case 1: return 'HAVE_METADATA' case 2: return 'HAVE_CURRENT_DATA' case 3: return 'HAVE_FUTURE_DATA' case 4: return 'HAVE_ENOUGH_DATA' default: return null } } this.getNetworkStateCode = function() { if (!supported) { return null } return this.sound.networkState } this.getNetworkStateMessage = function() { if (!supported) { return null } switch(this.getNetworkStateCode()) { case 0: return 'NETWORK_EMPTY' case 1: return 'NETWORK_IDLE' case 2: return 'NETWORK_LOADING' case 3: return 'NETWORK_NO_SOURCE' default: return null } } this.set = function(key, value) { if (!supported) { return this } this.sound[key] = value return this } this.get = function(key) { if (!supported) { return null } return key ? this.sound[key] : this.sound } this.bind = function(types, func) { if (!supported) { return this } types = types.split(' ') const self = this const efunc = (e) => { func.call(self, e) } for (let t = 0; t < types.length; t++) { const type = types[t] const idx = type const eventType = idx.split('.')[0] events.push({ idx: idx, func: efunc }) this.sound.addEventListener(eventType, efunc, true) } return this } this.unbind = function(types) { if (!supported) { return this } types = types.split(' ') for (let t = 0; t < types.length; t++) { const idx = types[t] const type = idx.split('.')[0] for (let i = 0; i < events.length; i++) { const namespace = events[i].idx.split('.') if (events[i].idx === idx || (namespace[1] && namespace[1] === idx.replace('.', ''))) { this.sound.removeEventListener(type, events[i].func, true) // remove event events.splice(i, 1) } } } return this } this.bindOnce = function(type, func) { if (!supported) { return this } const self = this eventsOnce[pid++] = false this.bind(`${type}.${pid}`, function() { if (!eventsOnce[pid]) { eventsOnce[pid] = true func.call(self) } self.unbind(`${type}.${pid}`) }) return this } this.trigger = function(types, detail) { if (!supported) { return this } types = types.split(' ') for (let t = 0; t < types.length; t++) { const idx = types[t] for (let i = 0; i < events.length; i++) { const eventType = events[i].idx.split('.') if (events[i].idx === idx || (eventType[0] && eventType[0] === idx.replace('.', ''))) { const evt = doc.createEvent('HTMLEvents') evt.initEvent(eventType[0], false, true) evt.originalEvent = detail this.sound.dispatchEvent(evt) } } } return this } this.fadeTo = function(to, duration, callback) { if (!supported) { return this } // Handle optional duration parameter if (duration instanceof Function) { callback = duration duration = buzz.defaults.duration } else { duration = duration || buzz.defaults.duration } const from = this.volume const delay = duration / Math.abs(from - to) const self = this let fadeToTimeout // Determine if we should return a promise const hasCallback = callback instanceof Function let promiseResolve = null if (!hasCallback) { // No callback provided, prepare for promise mode var promise = new Promise((resolve) => { promiseResolve = resolve }) } this.play() function doFade() { clearTimeout(fadeToTimeout) fadeToTimeout = setTimeout(() => { if (from < to && self.volume < to) { self.setVolume(self.volume += 1) doFade() } else if (from > to && self.volume > to) { self.setVolume(self.volume -= 1) doFade() } else { // Fade complete if (hasCallback) { callback.apply(self) } else if (promiseResolve) { promiseResolve(self) } } }, delay) } this.whenReady(function() { doFade() }) // Return promise if no callback, otherwise return this for chaining return hasCallback ? this : promise } this.fadeIn = function(duration, callback) { if (!supported) { return this } // fadeIn inherits promise/callback behavior from fadeTo const result = this.setVolume(0).fadeTo(100, duration, callback) // If fadeTo returned a promise, return it; otherwise return this return (result && typeof result.then === 'function') ? result : this } this.fadeOut = function(duration, callback) { if (!supported) { return this } // fadeOut inherits promise/callback behavior from fadeTo return this.fadeTo(0, duration, callback) } this.fadeWith = function(sound, duration) { if (!supported) { return this } this.fadeOut(duration, function() { this.stop() }) sound.play().fadeIn(duration) return this } this.whenReady = function(func) { if (!supported) { return null } const self = this const hasCallback = func instanceof Function if (!hasCallback) { // Promise mode return new Promise((resolve) => { if (self.sound.readyState === 0) { self.bind('canplay.buzzwhenready', function() { resolve(self) }) } else { resolve(self) } }) } else { // Callback mode (legacy behavior) if (this.sound.readyState === 0) { this.bind('canplay.buzzwhenready', function() { func.call(self) }) } else { func.call(self) } } } this.addSource = function(src) { const self = this const source = doc.createElement('source') source.src = src if (buzz.types[getExt(src)]) { source.type = buzz.types[getExt(src)] } this.sound.appendChild(source) source.addEventListener('error', (e) => { self.trigger('sourceerror', e) }) return source } // privates function timerangeToArray(timeRange) { const array = [] const length = timeRange.length - 1 for (let i = 0; i <= length; i++) { array.push({ start: timeRange.start(i), end: timeRange.end(i) }) } return array } function getExt(filename) { return filename.split('.').pop() } // init if (supported && src) { for (const i in buzz.defaults) { if (buzz.defaults.hasOwnProperty(i)) { if (options[i] === undefined) { options[i] = buzz.defaults[i] } } } this.sound = doc.createElement('audio') // Shoud we set crossOrigin? if (options.crossOrigin !== null) { this.sound.crossOrigin = options.crossOrigin } // Use web audio if possible to improve performance. if (options.webAudioApi) { const audioCtx = buzz.getAudioContext() if (audioCtx) { this.source = audioCtx.createMediaElementSource(this.sound) this.source.connect(audioCtx.destination) } } if (src instanceof Array) { for (const j in src) { if (src.hasOwnProperty(j)) { this.addSource(src[j]) } } } else if (options.formats.length) { for (const k in options.formats) { if (options.formats.hasOwnProperty(k)) { this.addSource(`${src}.${options.formats[k]}`) } } } else { this.addSource(src) } if (options.loop) { this.loop() } if (options.autoplay) { this.sound.autoplay = 'autoplay' } if (options.preload === true) { this.sound.preload = 'auto' } else if (options.preload === false) { this.sound.preload = 'none' } else { this.sound.preload = options.preload } this.setVolume(options.volume) buzz.sounds.push(this) } }, group: function(sounds) { sounds = argsToArray(sounds, arguments) // publics this.getSounds = () => sounds this.add = function(soundArray) { soundArray = argsToArray(soundArray, arguments) for (let a = 0; a < soundArray.length; a++) { sounds.push(soundArray[a]) } } this.remove = function(soundArray) { soundArray = argsToArray(soundArray, arguments) for (let a = 0; a < soundArray.length; a++) { for (let i = 0; i < sounds.length; i++) { if (sounds[i] === soundArray[a]) { sounds.splice(i, 1) break } } } } this.load = function() { fn('load') return this } this.play = function() { fn('play') return this } this.togglePlay = function() { fn('togglePlay') return this } this.pause = function(time) { fn('pause', time) return this } this.stop = function() { fn('stop') return this } this.mute = function() { fn('mute') return this } this.unmute = function() { fn('unmute') return this } this.toggleMute = function() { fn('toggleMute') return this } this.setVolume = function(volume) { fn('setVolume', volume) return this } this.increaseVolume = function(value) { fn('increaseVolume', value) return this } this.decreaseVolume = function(value) { fn('decreaseVolume', value) return this } this.loop = function() { fn('loop') return this } this.unloop = function() { fn('unloop') return this } this.setSpeed = function(speed) { fn('setSpeed', speed) return this } this.setTime = function(time) { fn('setTime', time) return this } this.set = function(key, value) { fn('set', key, value) return this } this.bind = function(type, func) { fn('bind', type, func) return this } this.unbind = function(type) { fn('unbind', type) return this } this.bindOnce = function(type, func) { fn('bindOnce', type, func) return this } this.trigger = function(type) { fn('trigger', type) return this } this.fade = function(from, to, duration, callback) { fn('fade', from, to, duration, callback) return this } this.fadeIn = function(duration, callback) { fn('fadeIn', duration, callback) return this } this.fadeOut = function(duration, callback) { fn('fadeOut', duration, callback) return this } // privates function fn() { const args = argsToArray(null, arguments) const func = args.shift() for (let i = 0; i < sounds.length; i++) { sounds[i][func].apply(sounds[i], args) } } function argsToArray(array, args) { return (array instanceof Array) ? array : Array.prototype.slice.call(args) } }, all() { return new buzz.group(buzz.sounds) }, isSupported() { return !!buzz.el.canPlayType }, isOGGSupported() { return !!buzz.el.canPlayType && buzz.el.canPlayType('audio/ogg; codecs="vorbis"') }, isWAVSupported() { return !!buzz.el.canPlayType && buzz.el.canPlayType('audio/wav; codecs="1"') }, isMP3Supported() { return !!buzz.el.canPlayType && buzz.el.canPlayType('audio/mpeg;') }, isAACSupported() { return !!buzz.el.canPlayType && (buzz.el.canPlayType('audio/x-m4a;') || buzz.el.canPlayType('audio/aac;')) }, toTimer(time, withHours) { let h, m, s h = Math.floor(time / 3600) h = isNaN(h) ? '--' : (h >= 10) ? h : `0${h}` m = withHours ? Math.floor(time / 60 % 60) : Math.floor(time / 60) m = isNaN(m) ? '--' : (m >= 10) ? m : `0${m}` s = Math.floor(time % 60) s = isNaN(s) ? '--' : (s >= 10) ? s : `0${s}` return withHours ? `${h}:${m}:${s}` : `${m}:${s}` }, fromTimer(time) { const splits = time.toString().split(':') if (splits && splits.length === 3) { time = (parseInt(splits[0], 10) * 3600) + (parseInt(splits[1], 10) * 60) + parseInt(splits[2], 10) } if (splits && splits.length === 2) { time = (parseInt(splits[0], 10) * 60) + parseInt(splits[1], 10) } return time }, toPercent(value, total, decimal) { const r = Math.pow(10, decimal || 0) return Math.round(((value * 100) / total) * r) / r }, fromPercent(percent, total, decimal) { const r = Math.pow(10, decimal || 0) return Math.round(((total / 100) * percent) * r) / r } } export default buzz