Port a Tone.js patch

Move an existing Tone.js chain onto kernels, node by node, checking parity as you go.

If you already have a working Tone.js chain, you do not have to rewrite it in one sitting. Port it one node at a time and check your work after each, using the Tone version as the reference you are trying to match.

Set the Tone version as your baseline

@codex-music/audio-tone exists exactly for this: a Tone.js/Web Audio adapter backend, reachable as EngineBackend "tone". It is the migration baseline — useful to compare against, and explicitly never the parity target.

That distinction matters. Web Audio nodes are not bit-defined: a BiquadFilter is whatever the browser implements, and a WaveShaper interpolates a 4096-point curve and may oversample. You are aiming for behavioural equivalence with Tone — it should sound like the same patch — and for bit equivalence only between your own WASM and native builds. Chasing sample-exact agreement with the browser's own nodes is a trap.

Take the measurements first

Before writing any kernel code, render a fixed input through your Tone chain and save it. Same source file, same parameter values, same duration, offline render rather than realtime so the result is reproducible.

You now have a target waveform. Every step below is judged against it, and "judged" means both looking at the difference and listening to it — a 40 dB noise floor of difference is inaudible on a distortion stage and glaring on a clean gain stage.

Map nodes to arithmetic

Most Tone nodes are a few lines once the graph is gone.

Tone nodeIn a kernel
Gainmultiply, one line
WaveShaperevaluate the transfer function per sample
Filter (lowpass)one-pole or biquad, memory in state
FeedbackDelaypreallocated Float32Array plus a write cursor
Envelopeper-voice stage integer plus a level float
Oscillatorphase accumulator, wrap by subtraction

Two nodes have no direct translation and should be resolved rather than ported: anything doing its own resampling, and anything whose behaviour depends on the audio graph's own scheduling. Both are graph features, and the kernel has no graph — it has a process() call and the samples in front of it.

Port one stage, keep the rest in Tone

Do not swap the whole chain. Replace the first node with a kernel, leave the remaining Tone nodes downstream, and re-render. The difference against your baseline is now attributable to exactly one stage.

The transfer function in renderFx1Variation (@codex-music/audio-kernels) is a worked example of this for Tone 15's Distortion curve — the formula is ported directly, with the note that the browser's WaveShaper interpolation and oversampling still need to be quantified. That comment is the honest state of the art on this specific comparison, and it is the kind of thing you will find in your own chain too.

Add fround as you go

Tone runs in f64 in places where your kernel runs in f32. Every stateful accumulation you port — filter memory, delay feedback, envelope level — needs Math.fround at the accumulation, and adding it will change the output slightly against your Tone baseline. That change is correct. It is what makes the WASM and native builds agree with each other, which is the guarantee that actually matters.

Finish, then compare backends

Once the chain is fully ported, drop the Tone baseline and switch to the real check: render the same input through the WASM and native builds and compare the output hashes. Those must match exactly. See Parity and determinism for what the harness does and what is still open.

When to stop

You have finished when the kernel version sounds like the patch you had and the two backends agree bit-for-bit. You have not failed if the null test against Tone leaves residue — you should expect it to, and the residue is usually where you fixed a precision bug the browser was hiding.