Parity and determinism
What sample-identical means, how it is tested, the f32 rules you inherit, and where the gaps currently are.
The claim that the browser version and the plugin are the same tool is only worth something if it is checkable. This page says what is guaranteed, what is measured, and what is still open.
What "identical" means here
Two different claims get made about audio, and only one of them is a bit-level promise.
Bit-identical applies between our own backends: the same kernel source compiled to freestanding WASM and to a native library must produce byte-for-byte identical PCM from the same input and the same event stream. This is a hard guarantee and it is hash-checked.
Behaviourally equivalent is the weaker claim, and it is all that is offered
against Tone.js and Web Audio. Browser nodes are not bit-defined — a
BiquadFilter is whatever the vendor implemented, and a WaveShaper
interpolates its curve and may oversample. @codex-music/audio-tone is the
migration baseline, deliberately never the parity target.
The three preconditions
Identical output requires all three of these, and they are the reason the architecture looks the way it does.
One kernel source. Not two implementations kept in sync by discipline — one restricted-TypeScript file compiled twice, with matching state layout and metadata.
One event vocabulary. AudioEvent is a three-member union with integer
frameOffset, never float seconds. sortEvents orders by frame and puts
param before noteOn/noteOff at the same frame, and both hosts apply that
same ordering. A note that lands on the same frame as a filter change resolves
identically in a browser and in a DAW.
One parameter math. normalize/denormalize in
@codex-music/audio-contracts are the single implementation, and the generated
C parameter table reproduces them exactly — same clamping order, same rounding
convention (Math.round as floor(x + 0.5)). A knob position denormalizes to
the same float everywhere.
The f32 rules you inherit
The rule is short: Math.fround at every stateful accumulation. Filter
memory, delay feedback, envelope followers — anywhere a value survives to the
next sample.
The reason is that JavaScript arithmetic is f64 while the compiled kernel's state is f32. A value that lives only inside one sample's computation can round differently without consequence. A value that feeds back into itself compounds its rounding difference on every pass, and a delay line with feedback will diverge audibly long before anything else does.
Denormals, NaN and infinity behaviour, signed zero, and the exact ULP tolerance
of each math function are still being specified (GAP-003 in the compiler gap
register). Until that lands, stay inside the documented surface: sin, cos,
sqrt, exp, log, pow, Math.fround, Math.PI, Math.E, and typed
arrays you allocated up front.
How it is measured
@codex-music/audio-devtools is the internal harness. Its hash is FNV-1a 32-bit
over the PCM bytes — cheap, order-sensitive, and adequate for "are these the
same bytes". Three checks are defined on top of it:
render:golden— factory presets plus a fixed MIDI sequence render to checked-in golden WAVs.verify:cross-target-hash— native LLVM, native C, ASan, and WASM-in-worklet lanes render the same kernel and compare hashes.verify:plugin-vs-offline— the built VST3 is replayed against the golden offline render. This is the acceptance criterion for the plugin shell, and it passes today.
The vertical-slice app is the standing proof: one restricted-TS DSP source compiled through three native lanes, matched exactly against a real Chromium AudioWorklet.
Where the gaps are
Three compiler-side items are open and they bound what can be promised.
GAP-002 — borrowed planar f32 spans. The library ABI is bytes-based today,
so hosts copy input into a managed array rather than handing the kernel a
float* view. It renders correctly and it is not zero-copy; the real-time path
needs the span ABI.
GAP-003 — the numeric surface. tanh has no lowering yet, and the
tolerance, NaN, and denormal policy is unspecified.
GAP-005 and the transitive effect checker. The compiler does not yet walk the full call graph to reject allocation, exceptions, recursion, locks, and I/O transitively. The rules in Rules of the kernel describe the target profile; today part of that enforcement is convention rather than a compile error.
Treat the first two as bounds on performance and on exotic math, and the third
as a reason to actually read your own process() before you publish.