cd /news/ai-tools/when-a-piano-tab-goes-quiet-keep-the… · home › topics › ai-tools › article
[ARTICLE · art-152162] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

When a Piano Tab Goes Quiet, Keep the Score Position

Giang Pham, the maker of the browser-based piano app Pianory, built an explicit interruption model for Web Audio playback: when a page becomes hidden, the shared AudioContext is suspended, the sampled instrument is silenced, and the score transport stops queuing events, then playback resumes only from a visible page and a user gesture, restarting at a stored score event index rather than a sample-accurate offset. A unit test using a fake audio clock confirms that pausing at 1.3 seconds leaves the player paused at event 2 and resumes from that event, though Pham notes the test does not exercise real page visibility, iOS audio-session support, or audio after a phone call.

by read3 min views2 publishedOct 18, 2026

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 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.

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.

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 s its player. A route change also halts shared audio because scheduled notes can otherwise outlive the view that started them.

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.

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 . 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 , the player waits for the piece’s notes before starting sound.

The score-player unit test uses a fake audio clock: after pausing at 1.3 seconds, it checks that the player is d at event 2 and that the next played note is the one at that event. That verifies the transport’s in-memory /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?

── more in #ai-tools 4 stories · sorted by recency
── more on @pianory 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/when-a-piano-tab-goe…] indexed:0 read:3min 2026-10-18 · —