The loudness meter started as TypeScript, and to be fair to it, the TypeScript was correct. It implemented R128 momentary and short-term loudness, passed the EBU conformance material, and drew a needle you could trust. It was also unusable, because correctness on the audio path is a latency budget, not a truth table — and roughly once a minute, the garbage collector took a bite out of a 2.6 millisecond deadline that does not tolerate bites.
You can fight that in JavaScript, and the ingest work I wrote up last summer fights it well: preallocate everything, reuse buffers, treat allocation as a bug. But audio has a property telemetry does not — the deadline is hard, the failure is audible, and the K-weighting filter chain is exactly the kind of tight numeric kernel that compilers love and JIT warmup ruins. This was the corner of the codebase where reaching for Rust stopped being fashion and started being engineering.
The shape that worked: the DSP is a Rust crate compiled to WASM, loaded inside an
AudioWorklet. The worklet owns a pair of ring buffers in linear memory; the audio thread
writes 128-sample blocks in, reads gated loudness values out, and neither operation allocates,
locks, or crosses into JavaScript mid-block. The crate has no idea the browser exists — it is
pure no_std arithmetic over slices — which means the same crate runs in the native ingest
tool, and the browser meter and the rack meter can no longer disagree. One implementation,
two deployments, zero drift.
The real design work was the boundary. Every gram of pain in a WASM integration lives at the FFI line, and the rule that kept it small: values cross, structures do not. The JS side never holds a Rust object; it holds an index into a buffer it was told about once. Serialization appears nowhere. If you find yourself encoding JSON to talk to your own WASM, the boundary is in the wrong place.
Was it worth it? The meter’s worst frame went from eleven milliseconds to under one, and the needle stopped flinching when the tab compiled a chart. But the honest summary is narrower than the headline: Rust did not make the product better, it made one hot loop boring. That is the job. Everything around the loop — the UI, the routing, the config — is still TypeScript, and should be.
