// Zero external audio assets — synthesized tones via Web Audio, matching // this app's existing "generate what we need with code" approach (the PWA // icons are a hand-rolled PNG encoder for the same reason). One lazily // created AudioContext, unlocked by the first real tap (the kiosk's start // button) so later milestone sounds triggered by a setInterval — with no // gesture of their own — are still allowed to play for the rest of the // page's lifetime on iOS. let audioCtx = null; function getAudioContext() { if (!audioCtx) { const Ctor = window.AudioContext || window.webkitAudioContext; audioCtx = new Ctor(); } if (audioCtx.state === 'suspended') { audioCtx.resume(); } return audioCtx; } function playNote(ctx, { frequency, startTime, duration, peakGain, type }) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(frequency, startTime); gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(peakGain, startTime + 0.015); gain.gain.exponentialRampToValueAtTime(0.001, startTime + duration); osc.connect(gain); gain.connect(ctx.destination); osc.start(startTime); osc.stop(startTime + duration + 0.02); } const CHIMES = { started: [{ frequency: 660, type: 'sine', duration: 0.12, peakGain: 0.15 }], halfway: [ { frequency: 523, type: 'sine', duration: 0.11, peakGain: 0.2 }, { frequency: 659, type: 'sine', duration: 0.11, peakGain: 0.2 }, ], almostDone: [ { frequency: 784, type: 'triangle', duration: 0.11, peakGain: 0.22 }, { frequency: 659, type: 'triangle', duration: 0.11, peakGain: 0.22 }, ], done: [ { frequency: 523, type: 'sine', duration: 0.14, peakGain: 0.25 }, { frequency: 659, type: 'sine', duration: 0.14, peakGain: 0.25 }, { frequency: 784, type: 'sine', duration: 0.14, peakGain: 0.25 }, ], }; function playChime(name) { const notes = CHIMES[name]; if (!notes) return; try { const ctx = getAudioContext(); let t = ctx.currentTime; const gap = 0.03; notes.forEach((note) => { playNote(ctx, { ...note, startTime: t }); t += note.duration + gap; }); } catch { // Audio unavailable/blocked — timers still work visually without it. } }