clicktone
Version:
ClickTone is a lightweight helper for UI sound feedback. It wraps the Web Audio API, giving you instant click‑sounds with volume control, throttling, callbacks, and an iOS resume workaround.
94 lines (93 loc) • 3.6 kB
JavaScript
var s = Object.defineProperty;
var c = (r, t, e) => t in r ? s(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
var n = (r, t, e) => c(r, typeof t != "symbol" ? t + "" : t, e);
class h {
constructor({ file: t, volume: e = 1, callback: o = null, throttle: i = 0, debug: a = !1 }) {
n(this, "fileSource");
n(this, "volume");
n(this, "callback");
n(this, "throttle");
n(this, "debug");
n(this, "lastClickTime");
n(this, "audioCache");
n(this, "audioContext");
this.fileSource = t, this.volume = e, this.callback = o, this.throttle = i, this.debug = a, this.lastClickTime = 0, this.audioCache = {}, this.audioContext = null;
}
resolveFileUrl(t) {
if (typeof t == "string") return t;
if (t instanceof HTMLSourceElement) {
if (!t.src) throw new Error('<source> element has no "src" attribute.');
return t.src;
}
if (typeof t == "object" && t !== null && "id" in t) {
const e = document.getElementById(String(t.id));
if (!e) throw new Error(`No element found with id "${t.id}".`);
if (!(e instanceof HTMLSourceElement)) throw new Error(`Element with id "${t.id}" is not a <source> element.`);
if (!e.src) throw new Error(`<source> element with id "${t.id}" has no "src" attribute.`);
return e.src;
}
throw new Error('Invalid "file" value. Expected string, HTMLSourceElement, or { id: string }.');
}
initAudioContext() {
this.audioContext || (this.audioContext = new (window.AudioContext || window.webkitAudioContext)(), this.iOSFixAudioContext());
}
iOSFixAudioContext() {
if (this.audioContext && this.audioContext.state === "suspended" && "ontouchstart" in window) {
const t = () => {
this.audioContext.state === "suspended" && this.audioContext.resume().then(() => {
document.body.removeEventListener("touchstart", t), document.body.removeEventListener("touchend", t);
}).catch((e) => {
this.debug;
});
};
document.body.addEventListener("touchstart", t, !1), document.body.addEventListener("touchend", t, !1);
}
}
async fetchAndDecodeAudio(t) {
try {
if (this.audioCache[t]) return this.audioCache[t];
const e = await fetch(t), o = await e.arrayBuffer(), i = await this.audioContext.decodeAudioData(o);
return this.audioCache[t] = i, i;
} catch (e) {
throw this.debug, new Error(`Something went wrong when loading and decoding the audio: ${e.message}`);
}
}
async playAudio(t) {
this.initAudioContext();
try {
const e = await this.fetchAndDecodeAudio(t), o = this.audioContext.createBufferSource(), i = this.audioContext.createGain();
o.buffer = e, i.gain.value = this.volume, o.connect(i), i.connect(this.audioContext.destination), o.onended = () => {
this.callback && this.callback();
}, o.start(0);
} catch (e) {
throw this.debug, new Error(`Something went wrong while playing audio: ${e.message}`);
}
}
throttleFn(t) {
return () => {
const e = Date.now();
e - this.lastClickTime >= this.throttle && (t().catch((o) => {
this.debug;
}), this.lastClickTime = e);
};
}
play(t) {
let e;
try {
e = this.resolveFileUrl(t ?? this.fileSource);
} catch (i) {
if (this.callback) return void this.callback(i);
throw i;
}
const o = this.throttleFn(() => this.playAudio(e));
try {
o();
} catch (i) {
if (this.debug, !this.callback) throw i;
this.callback(i);
}
}
}
export {
h as default
};