From 7514111f5cd01dc003623ffb7a7b5bed726dc222 Mon Sep 17 00:00:00 2001 From: you Date: Sun, 22 Mar 2026 17:31:23 +0000 Subject: [PATCH] Audio: "Tap to enable audio" overlay when context is suspended Instead of silently dropping notes or hoping gesture listeners fire, show a clear overlay on first packet if AudioContext is suspended. One tap resumes context and removes overlay. Standard pattern used by every browser game/music site. --- public/audio.js | 36 ++++++++++++++++++++++-------------- public/index.html | 4 ++-- public/style.css | 28 ++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/public/audio.js b/public/audio.js index e59eed98..fdf0f19c 100644 --- a/public/audio.js +++ b/public/audio.js @@ -88,11 +88,13 @@ function sonifyPacket(pkt) { if (!audioEnabled || !currentVoice) return; - // Lazy-init AudioContext on first actual packet (may not have user gesture, - // but browser will auto-resume when it gets one) if (!audioCtx) initAudio(); if (!audioCtx) return; - if (audioCtx.state === 'suspended') audioCtx.resume(); + if (audioCtx.state === 'suspended') { + // Show unlock overlay if not already showing + _showUnlockOverlay(); + return; // don't schedule notes on suspended context + } if (activeVoices >= MAX_VOICES) return; const parsed = parsePacketBytes(pkt); @@ -175,22 +177,28 @@ const savedVoice = localStorage.getItem('live-audio-voice'); if (savedVoice) setVoice(savedVoice); - // If audio was enabled, create context now and try to resume. - // Browsers with prior engagement will auto-allow; gesture listener is fallback. + // If audio was enabled, create context eagerly. If browser suspends it, + // the unlock overlay will appear when the first packet arrives. if (audioEnabled) { initAudio(); - const unlockAudio = () => { - if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); - document.removeEventListener('click', unlockAudio, true); - document.removeEventListener('touchstart', unlockAudio, true); - document.removeEventListener('keydown', unlockAudio, true); - }; - document.addEventListener('click', unlockAudio, true); - document.addEventListener('touchstart', unlockAudio, true); - document.addEventListener('keydown', unlockAudio, true); } } + let _overlayShown = false; + + function _showUnlockOverlay() { + if (_overlayShown) return; + _overlayShown = true; + const overlay = document.createElement('div'); + overlay.className = 'audio-unlock-overlay'; + overlay.innerHTML = '
🔊 Tap to enable audio
'; + overlay.addEventListener('click', () => { + if (audioCtx) audioCtx.resume(); + overlay.remove(); + }, { once: true }); + document.body.appendChild(overlay); + } + // Export engine + helpers for voice modules window.MeshAudio = { sonifyPacket, diff --git a/public/index.html b/public/index.html index 4af7268d..67f1d178 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,7 @@ - + - + diff --git a/public/style.css b/public/style.css index 7439305c..753e643c 100644 --- a/public/style.css +++ b/public/style.css @@ -1640,3 +1640,31 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); } color: #00ff41 !important; border-color: #00ff4130 !important; } + +/* Audio unlock overlay */ +.audio-unlock-overlay { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0,0,0,0.6); + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} +.audio-unlock-prompt { + background: #1f2937; + color: #e5e7eb; + padding: 24px 40px; + border-radius: 12px; + font-size: 20px; + font-weight: 600; + box-shadow: 0 4px 24px rgba(0,0,0,0.5); + user-select: none; +} +.matrix-theme .audio-unlock-prompt { + background: #001a00; + color: #00ff41; + box-shadow: 0 0 30px rgba(0,255,65,0.2); +}