Repository navigation
perf(core): polyphase half-band FIR decimation - #5
Conversation
Replace the per-voice Butterworth IIR decimator with cascaded linear-phase half-band FIR stages (eco 35; standard 15 -> 47; high 11 -> 15 -> 47 taps). Substep calls only buffer their input; one hand-unrolled kernel per output frame runs the cascade, so the voice loop inlines the per-substep path. Passband is flat to 0.40 output Fs and products folding below it are rejected by >= 81 dB (63 dB eco). Output gains a constant 13.25-frame linear-phase delay for standard. Test references now evaluate the half-band DTFT directly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HT2ixgjbZRrhGw7uzgGPpz
|
The same No fix exists yet in any open PR, so nothing is ported here. I can't re-run jobs from this session. The other jobs (Linux node / browsers) are the relevant signal for this change. Generated by Claude Code |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 913e30fde9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HT2ixgjbZRrhGw7uzgGPpz
…-fgj3d9 # Conflicts: # CHANGELOG.md # dist/core/synth.js # dist/core/synth.js.map # doc/changelog.html # doc/search-data.js
Requested by 小語 · project thread
Summary
Replaces the per-voice Butterworth IIR decimator (
src/core/decimator.ts, cutoff 0.30 Fs) with cascaded polyphase linear-phase half-band FIR stages:decimateSample(input, state, coefficients)keeps the same signature. Each substep call only buffers its input. The last substep of a frame runs one hand-unrolled kernel for the whole cascade, so outputs are computed only where needed. The first unrolled version was 10–25% slower because V8 would not inline helpers insiderenderVoice. The tiny per-substep path fixed that.synth.tschanges are limited to 3 lines: the import,filterssized byDECIMATOR_STATE_SIZE(158 scalars), and the tail check via the newdecimatorPeak(), which skips counter/position slots.Quality (standard, 48 kHz,
test/decimator-quality.ts, measured = analytic within 2e-9)Passband ripple to .40 Fs is 0.0011 dB. The worst stopband across folds landing below .40 Fs is -81.4 dB.
npm run sound-qualitypasses for all three profiles. At .35 Fs, measured loss is ≤0.006 dB on every profile; it was 7.3–11.5 dB before. The THD is unchanged (0.0208%).Tradeoffs:
CPU (
npm run benchmark, median of 4 interleaved base/new runs, cloud Xeon, Node 22)Eco is flat within noise; this container's run-to-run spread is about ±10%. Arithmetic per output frame is 20 multiplies for standard, down from 80 (16 biquads).
Tests
test/decimator-reference.tsnow evaluates the half-band DTFT directly from the published taps; it does not use the production rings.decimator-quality,engine-v6,spectralandsound-qualityassert a flat passband to .40 Fs and ≥80 dB folded rejection. The Butterworth formula is kept asbutterworthFilterMagnitudefor comparison rows.filters.fill(Infinity), because slot 0 is the frame counter.npm testpasses (440/440).npm run typecheck,npm run build(dist + docs) andnpm run sound-qualityall pass.Security review (SECURITY.md §C,
src/core/change)| 0.npm ls --omit=devis empty).Note
Another thread is moving decimation post-mix in
synth.ts. This PR keeps thedecimateSampleinterface, so that change can call it on the mixed signal.Generated by Claude Code