DROP/TUNER

FIELD MANUAL 06 / ENGINEERING / WEB AUDIO

A Cancel Button Must Also Handle a Microphone Stream That Arrives Late

Consider a short sequence: a visitor presses Enable microphone, the browser opens a permission prompt, and the visitor presses Cancel in the application. Later, they accept the still-visible browser prompt. The application now receives a stream for a session the visitor already cancelled.

Changing the button label is not cancellation

getUserMedia() returns a promise. MDN notes that a visitor can ignore the permission request, leaving that promise unsettled. A local timeout or Cancel button can stop the application from waiting, but neither makes a later successful result safe to attach to the UI.

The key question at every asynchronous boundary is: does this result still belong to the current session? If it does not, it must release any resources it owns and return without starting analysis.

Give each session a generation

The current implementation uses a monotonically increasing generation number. Stopping increments it. Starting first stops the previous session, then captures the new value. After resuming the audio context and again after receiving a media stream, the code compares the captured value with the current generation.

When a late stream belongs to an obsolete generation, every returned track is stopped immediately. It is never assigned as the active stream and never connected to the analyser. A later start has a different generation, so the old promise cannot take ownership of the new session.

The same ownership check matters in error handling. A rejection from an old request should not stop a newer successful session. In the implementation, cleanup in the catch branch is conditional on that request still being current.

Clean up both capture and processing

A normal stop releases the app’s tracks, disconnects its source node and closes its audio context. References are cleared. The context’s state-change callback is detached before closing so that cleanup does not act like an unexpected live interruption.

Stopping a MediaStreamTrack does not guarantee that the physical microphone becomes unused everywhere: another track or application may still use the source. MDN also documents that calling stop() sets that track’s state to ended without firing its ended event. The application therefore needs its own explicit stop transition rather than depending on that event for local cleanup.

Do not treat stream arrival as usable input

The returned stream is checked for an audio track. An already ended, muted or disabled track is rejected. The audio context must be running before analysis starts; if it was interrupted while permission was pending, that condition needs handling even when an earlier event was missed.

The app also watches track and context changes during use. For a pitch-confirmation interface, preserving the previous green reading after input disappears would be misleading. Losing usable input must clear readiness as well as change a microphone indicator.

Test the ordering, not just the happy path

The cancellation regression test uses a manually resolved promise for getUserMedia(). It starts a session, lets execution reach the pending permission request, stops the session and only then resolves the promise with a fake stream. It asserts that start returns no active input, the late track is stopped and the earlier context is closed.

Related local tests cover permission rejection, external track ending, a suspended context, a muted capture track, unusable tracks returned by capture and interruption while permission is pending. A browser test with synthetic audio can exercise more of the processing path, but it still cannot validate a physical microphone’s capture behavior.

This ownership pattern applies anywhere a resource can arrive after the UI has moved on. Cancellation needs a plan for the eventual result, not just an immediate visual reset.

For a player-facing reading problem, start with the wrong-octave troubleshooting guide.

Sources & method

This is an implementation note from Drop Tuner, a browser guitar-tuning project I maintain. The article was prepared with AI assistance and checked against the local session implementation and tests. The lesson concerns resource ownership, not a claim of real-instrument pitch accuracy.

MDN: getUserMedia(); MDN: MediaStreamTrack.stop(). The implementation description was checked against Drop Tuner’s audio-session code and eight local session tests on November 1, 2026.

First published on Hashnode on November 1, 2026; this edition adds site navigation and reference formatting.

Drop Tuner remains a public beta. Real-guitar microphone checks and physical iOS/Android validation remain pending. See the method and testing limits.