Disclosure: I’m Giang Pham, Pianory’s maker. AI helped draft this article from Pianory’s source code. It describes the implementation and unit-test coverage, not a Safari or production test.
A browser can interrupt audio without the player receiving a tidy pause button click. A phone may lock, an app may move to the background, or a call may interrupt Web Audio. If the interface keeps claiming that playback is running, the returning learner can see a moving score cursor with no sound—or hear notes that no longer match the page. Pianory handles this as two linked state changes: stop the audio work when the page becomes hidden, then resume only from a visible page and a user gesture.
Give the page one shared audio context
Pianory keeps one AudioContext and one sampled piano instrument per page. The sheet player, lessons and drills share them, so a page does not create a separate context for every widget. The module also conditionally asks iOS’s audio-session API to use the playback category. It checks whether that API exists and catches assignment failures because some WebKit versions expose the session object as read-only. This is a compatibility preference, not proof that every browser will keep audio available.
The shared context also gives the score player a clock. Its notes may already be scheduled when the page starts changing state, so silencing the instrument alone is insufficient: the transport must also stop queuing future events.
Treat a hidden page as a transport pause
When Pianory first creates the shared context, it registers for visibilitychange and pagehide. If the page becomes hidden, the handler notifies audio-halt listeners, stops the sampled instrument, and suspends the context if it is running. The score component subscribes to that halt and pauses its player. A route change also halts shared audio because scheduled notes can otherwise outlive the view that started them.
Pause captures the score event at the current AudioContext position, clears the scheduler interval and animation-frame loop, and silences scheduled notes and clicks. The position is an event index, not a sample-accurate offset within a sustained note. That is a deliberate transport boundary: when playback starts again, the score can return to a recognizable event instead of trying to reconstruct audio that was already cut off.
Resume while the gesture is still available
Browsers commonly require a user action to start or resume Web Audio. The sheet player therefore asks for the shared context synchronously inside its Play handler, before awaiting piano sample loading. If a piano already exists, it calls the wake helper immediately in the click handler. The helper does nothing while the document is hidden and requests resume only when the context is neither running nor closed. It does not await that request; iOS can leave it pending until another tap if code tries to resume outside a gesture.
Once the player is ready, Play starts from its stored score event. Pianory can run the configured count-in again, so this restores the musical location without promising an exact continuation of the interrupted waveform. If the context resumes but required samples are still loading, the player waits for the piece’s notes before starting sound.
Keep the claim inside the evidence
The score-player unit test uses a fake audio clock: after pausing at 1.3 seconds, it checks that the player is paused at event 2 and that the next played note is the one at that event. That verifies the transport’s in-memory pause/resume behavior. It does not exercise real page visibility, iOS audio-session support, a suspended Safari context, or audio after a phone call.
The broader lesson is to model interruption explicitly. A hidden page should stop producing sound and stop advancing its transport; a visible page should request audio recovery during the next user gesture, then resume from state the application actually kept. In Pianory that state is held by the live player instance, not a durable bookmark across page teardown. How do you represent the boundary between an interrupted audio stream and the musical position your UI can safely resume?
Top comments (0)