Fix iOS audio on shared play page

- Call unlockAudio() synchronously at start of click handler
- In audioUnlock: fire resume() immediately without await
- Play silent buffer synchronously before any async work
- Then await resume() again to ensure it completes

iOS requires audio interaction in synchronous user gesture context

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
b1rdmania
2025-12-29 15:26:42 +00:00
parent 238dd7c0ea
commit 2d97d5eea6
2 changed files with 20 additions and 10 deletions
+6 -2
View File
@@ -266,6 +266,10 @@ async function main(): Promise<void> {
playBtn.addEventListener('click', async () => {
if (!events) return;
// CRITICAL: On iOS, we must interact with AudioContext synchronously
// in the user gesture before any async work. Fire off unlock immediately.
const unlockPromise = unlockAudio();
// Pause (implemented as stop + remembered position)
if (isPlaying) {
showTimeRow();
@@ -284,8 +288,8 @@ async function main(): Promise<void> {
}
try {
// Unlock audio for iOS - must be called from user gesture
await unlockAudio();
// Wait for unlock to complete
await unlockPromise;
// First play generates the artifact (deterministically from MIDI structure).
if (!isGenerated) {
+14 -8
View File
@@ -60,17 +60,14 @@ async function doUnlock(): Promise<AudioContext> {
return ctx;
}
// Try to resume
// CRITICAL for iOS: Call resume() synchronously in user gesture context.
// Don't await - just fire it off immediately to register the user intent.
if (ctx.state === 'suspended') {
try {
await ctx.resume();
} catch (e) {
console.warn('AudioContext.resume() failed:', e);
}
ctx.resume().catch(() => {});
}
// Play a silent buffer to fully unlock on older iOS
// This is a no-op on modern browsers but essential for iOS < 14
// Play a silent buffer IMMEDIATELY to fully unlock on iOS.
// This must happen synchronously in the user gesture.
try {
const buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
const source = ctx.createBufferSource();
@@ -82,6 +79,15 @@ async function doUnlock(): Promise<AudioContext> {
// Ignore - this is just a fallback unlock
}
// Now try resume again and wait for it
if (ctx.state === 'suspended') {
try {
await ctx.resume();
} catch (e) {
console.warn('AudioContext.resume() failed:', e);
}
}
// Wait briefly for state to update (cast to string to avoid TS narrowing issues)
const getState = () => ctx.state as string;