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:
+6
-2
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user