buzz
Version:
Buzz, a Javascript HTML5 Audio library
1,001 lines (766 loc) • 28.6 kB
JavaScript
// ----------------------------------------------------------------------------
// 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 };