Skip to content

Fix Morse trainer audio on mobile Safari (await AudioContext resume)#2088

Merged
atomantic merged 3 commits into
mainfrom
cos/task-mr5ic6xs/agent-a55ea1ba
Jul 3, 2026
Merged

Fix Morse trainer audio on mobile Safari (await AudioContext resume)#2088
atomantic merged 3 commits into
mainfrom
cos/task-mr5ic6xs/agent-a55ea1ba

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

The Morse code trainer produced no sound on mobile Safari. iOS Safari starts the Web Audio AudioContext suspended and resume() is asynchronous — the trainer fired resume() without awaiting it, so the first tones were scheduled against a still-suspended clock and never sounded. Desktop browsers resume fast enough that the 50 ms scheduling lead hid the bug.

Root cause

useAudioContext().ensureCtx() called ctx.resume() fire-and-forget, unlike every other audio module in the app (metronome.js, scorePlayback.js, songPlayback.js), which all await c.resume() before scheduling.

Changes

  • ensureCtx now awaits resume() before returning the context (Copy / Head Copy / Send all benefit).
  • playPrompt awaits ensureCtx().
  • Send-mode keying: startTone awaits ensureCtx() and uses a generation token so a fast release-then-repress during the first-play unlock can't orphan a droning oscillator; stopTone reads the clock off the live oscillator's context so it stays synchronous.
  • Re-entrancy guard on startRound/playPrompt: the earlier await widened the window where the Start Round button is still live during the iOS unlock, so a double-tap could schedule two overlapping prompts — now ignored.

Tests

Added regression tests in MorseTrainer.test.jsx:

  • asserts no oscillator is created while the context is suspended (mock resolves resume() on a macrotask so the test actually fails if the await regresses).
  • asserts a double Start-Round tap in the unlock window creates only one oscillator.

All 18 MorseTrainer tests + 80 meatspace tests pass. Reviewed locally with claude + codex.

https://claude.ai/code/session_01USGJgLzYBHwis8rn38K3Pn

atomantic added 3 commits July 3, 2026 15:39
iOS Safari starts the Web Audio context suspended and resume() is async;
the trainer fired resume() without awaiting it, so the first tones were
scheduled against a still-suspended clock and never sounded on mobile
Safari. ensureCtx now awaits resume() before returning (matching
metronome.js / scorePlayback.js / songPlayback.js), and the keying
decoder's startTone awaits it too (bailing on a fast release to avoid an
orphan oscillator) while stopTone reads the clock off the live
oscillator's context so it stays synchronous.

Claude-Session: https://claude.ai/code/session_01USGJgLzYBHwis8rn38K3Pn
…phan tone

- Regression test now resolves the mock resume() on a macrotask so the
  test actually fails if the await is removed (a microtask-resolving mock
  passed either way, giving false confidence).
- startTone uses a generation token, not just pressingRef, so a
  release-then-repress during first-press resume latency can't leave two
  overlapping starts both creating an oscillator and orphaning the first.

Claude-Session: https://claude.ai/code/session_01USGJgLzYBHwis8rn38K3Pn
Moving the resume() await earlier means prompt/playing aren't set until
after the first-play iOS unlock, so the Start Round / New Round button
stays live during that window. A second tap started a second overlapping
playPrompt(true), scheduling two Morse prompts over each other with the
UI tracking only the last. Add a synchronous playingRef re-entrancy guard
to startRound/playPrompt, with a regression test asserting a double-tap in
the unlock window creates only one oscillator.

Claude-Session: https://claude.ai/code/session_01USGJgLzYBHwis8rn38K3Pn
@atomantic atomantic merged commit 9703891 into main Jul 3, 2026
2 checks passed
@atomantic atomantic deleted the cos/task-mr5ic6xs/agent-a55ea1ba branch July 3, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant