aboutsummaryrefslogtreecommitdiffstats
path: root/src/zap-sound.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/zap-sound.js')
-rw-r--r--src/zap-sound.js59
1 files changed, 59 insertions, 0 deletions
diff --git a/src/zap-sound.js b/src/zap-sound.js
new file mode 100644
index 0000000..fcf6d1f
--- /dev/null
+++ b/src/zap-sound.js
@@ -0,0 +1,59 @@
+// The supplied zap.mp3, encoded verbatim so hosted and standalone builds
+// both include it. Decode ahead of the tap; no network request is needed.
+import mp3 from './assets/zap.mp3.base64.txt' with { type: 'text' };
+
+let context, buffer, loading, playing, sequence = 0;
+
+function prepareZap() {
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
+ if (!AudioCtx) return null;
+ if (!context || context.state === 'closed') {
+ context = new AudioCtx({ latencyHint: 'interactive' });
+ buffer = null;
+ loading = null;
+ }
+ if (!loading) {
+ const ctx = context;
+ const bytes = Uint8Array.from(atob(mp3.trim()), c => c.charCodeAt(0));
+ loading = ctx.decodeAudioData(bytes.buffer).then(decoded => {
+ if (context === ctx) buffer = decoded;
+ return decoded;
+ });
+ loading.catch(() => { if (context === ctx) loading = null; });
+ }
+ return loading;
+}
+
+// Decoding is allowed while the audio context is suspended; the tap resumes
+// playback. A browser without audio must still be able to load the wallet.
+try { prepareZap(); } catch {}
+
+// tappedAt: when the finger landed — the caller's clock, since this module
+// may still have been loading when it did.
+export function playZapSound(tappedAt = performance.now()) {
+ try {
+ const ready = prepareZap();
+ if (!ready) return;
+ const ctx = context, token = ++sequence;
+ if (ctx.state !== 'running') ctx.resume().catch(() => {});
+ const start = decoded => {
+ // A very early tap may beat decoding. Never queue an old sound behind
+ // a later zap or surprise the user after a stalled decoder recovers.
+ if (token !== sequence || context !== ctx || performance.now() - tappedAt > 250) return;
+ if (playing) { playing.stop(); playing.disconnect(); }
+ const source = ctx.createBufferSource(), gain = ctx.createGain();
+ source.buffer = decoded;
+ gain.gain.value = .65;
+ source.connect(gain);
+ gain.connect(ctx.destination);
+ source.onended = () => {
+ source.disconnect(); gain.disconnect();
+ if (playing === source) playing = null;
+ };
+ playing = source;
+ source.start();
+ };
+ if (buffer) start(buffer);
+ else ready.then(start).catch(() => {});
+ } catch {} // Unavailable audio never interrupts a payment or its animation.
+}