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.
2 lines (1 loc) • 2.85 kB
JavaScript
"use strict";var s=Object.defineProperty;var a=(t,e,i)=>e in t?s(t,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[e]=i;var n=(t,e,i)=>a(t,typeof e!="symbol"?e+"":e,i);module.exports=class{constructor({file:t,volume:e=1,callback:i=null,throttle:o=0,debug:r=!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=i,this.throttle=o,this.debug=r,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),i=await e.arrayBuffer(),o=await this.audioContext.decodeAudioData(i);return this.audioCache[t]=o,o}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),i=this.audioContext.createBufferSource(),o=this.audioContext.createGain();i.buffer=e,o.gain.value=this.volume,i.connect(o),o.connect(this.audioContext.destination),i.onended=()=>{this.callback&&this.callback()},i.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(i=>{this.debug}),this.lastClickTime=e)}}play(t){let e;try{e=this.resolveFileUrl(t??this.fileSource)}catch(o){if(this.callback)return void this.callback(o);throw o}const i=this.throttleFn(()=>this.playAudio(e));try{i()}catch(o){if(this.debug,!this.callback)throw o;this.callback(o)}}};