UNPKG

buzz

Version:

Buzz, a Javascript HTML5 Audio library

1,001 lines (766 loc) 28.6 kB
// ---------------------------------------------------------------------------- // Buzz, a Javascript HTML5 Audio library // v2.0.0 - Built 2026-02-18 14:00 // Licensed under the MIT license. // http://buzz.jaysalvat.com/ // ---------------------------------------------------------------------------- // Copyright (C) 2010-2026 Jay Salvat // http://jaysalvat.com/ // ---------------------------------------------------------------------------- // ---------------------------------------------------------------------------- // 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 { buzz as default };