Skip to main content
Lightning Jar - Web Studio Lightning Jar Wordmark

What the Web Audio API Won't Tell You

A watercolor illustration of a vintage gramophone with butterflies drifting from its horn

We spent the last forty days building Stem Shovel, a browser-based music collaboration tool with a synced multi-stem player, a recorder, and a handful of instruments. The Web Audio API carried all of it, and we would choose it again. But the distance between the specification and shipping audio that works on a phone is wide, poorly documented, and paved with failures that make no sound and raise no errors. This is the field report we wish we had read first. Every item below was hit in production code; none of it is speculation.

The phone is the boss fight

Desktop Web Audio mostly behaves. iOS is where the model breaks.

An AudioContext on iOS starts suspended, resumes slowly, and the promise returned by resume() may simply never settle; one of our test phones sat on a loading state for minutes while awaiting it. Meanwhile, notes scheduled against the context's clock pile up at time zero, so every key the user pressed during the wake-up sounds at once, seconds later. The shape that works: warm the context on the first touch, hold notes until the state reads running, and watch the wake by polling state alongside the state-change event rather than trusting the promise. Give it a deadline, and if the context never wakes, throw it away so the next touch gets a fresh one.

The ring/silent switch is its own trap: it mutes Web Audio but not media elements. Safari 17 added an audio session API where you can declare playback intent; before that, the workaround is starting a silent looping audio element on the same user gesture. Nothing tells you the switch's position.

Then the sessions fight each other. Declare the playback session and WebKit refuses microphone capture outright, with an error about category compatibility. Capture needs the play-and-record session, which in turn quietly attenuates the speaker, may reroute output to the earpiece, and makes a wired headset's microphone the input. Our code shuffles session types as the user moves between playing and recording, and ends capture by handing the session back to automatic.

Asking is not getting

Recording an instrument means turning off the three voice processors: echo cancellation, noise suppression, auto gain. They are built for meetings and they ruin music. The trap is that requesting them off is not the same as getting them off. Chrome on macOS has an open bug that leaves them on; Safari ignores your channel request when cancellation is off and may omit fields from the settings it reports back. The honest pattern is to request, then read the track's actual settings, and tell the user what the capture really is rather than what was asked for.

Device identity is no more reliable than device behavior: Safari rotates device IDs between sessions, so the microphone a user chose last week can fail as not found while sitting right there. Retry with the default input and forget the stale choice.

The codec map is folklore

MediaRecorder will happily record lossless audio, in formats you will regret. Safari gives ALAC in MP4; Chrome gives raw PCM in WebM, which almost nothing opens. We re-encode server-side to FLAC before anyone downloads a take. On the decode side, decodeAudioData accepts different formats in different browsers: AIFF, Ogg/Opus, and WebM each fail somewhere, which is why our uploads are limited to WAV, FLAC, MP3, and AAC, and our playback renditions are AAC-LC, the one codec every browser decodes. Even capability detection is folklore: FLAC support is best decided by probing with a twentieth of a second of silence at load, because version tables lie, and WebCodecs' own isConfigSupported is not to be trusted either; the first failed decode is the real answer.

One more from the decode files: a loudly mastered MP3 can decode to peaks above full scale. Our pipeline refused the values as out of range until we learned to clamp.

The failures that make no sound

The cruelest Web Audio bugs are silent in both senses. A worklet whose output connects to nothing never runs on Safari; the fix is a zero-gain connection to the destination so something pulls it. An effects chain that compares desired wiring to current wiring can conclude an empty chain needs no wiring at all, and every track plays silence while every test passes, because no test asserted rendered sound. That one taught us the house rule that matters most in this domain: render audio in tests and assert on the result. Our drum machine's swing is verified by rendering patterns through the machine's own code and measuring the onset times, because the only trustworthy claim about audio code is a measurement of its output.

And when a tab runs out of memory under the weight of decoded stems, there is no event at all. iOS kills and reloads the tab; Chrome shows its error page. We leave a marker in session storage when a load starts and clear it when the load ends, so finding a fresh marker for the same stems means the last attempt died, and the player can choose a lighter path instead of dying the same way twice.

Take the list, skip the bruises

There is more in this vein than one post should hold: K-weighted loudness measurement through IIR filters, compressors that compress with the knobs at zero, autocorrelation reading half tempo on beats that fall between frames. The pattern underneath is consistent. The Web Audio API is a fine instrument with an unreliable manual, the real behavior lives in the intersection of browser, OS, and hardware, and the only defense is measuring what actually rendered. Browser audio is absolutely good enough to build a studio on now. Just bring a lab notebook.

headshot of Kevin Peckham
Kevin Peckham
Principal, Lightning Jar

Ask Eljay

Ask about the studio's work, research, packages, or writing. A few starters:

Answers come from this site's own content and link their sources. For anything that matters, email hello@lightningjar.com.