buzz
Version:
Buzz, a Javascript HTML5 Audio library
991 lines (757 loc) • 27.9 kB
JavaScript
// ----------------------------------------------------------------------------
// 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