UNPKG

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
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 };