Skip to content

Bump dsptap to the srdif engine; close the convolution engine's IR-load race; gate FFT sizes - #36

Merged
tap merged 2 commits into
mainfrom
ccr-a6b591b6-6psr9k
Oct 1, 2026
Merged

tap merged 2 commits into
mainfrom
ccr-a6b591b6-6psr9k

Conversation

@tap

@tap tap commented Oct 1, 2026

Copy link
Copy Markdown
Owner

What this changes

submodules/dsptap moves from 41215c6 to 2137d86 (DspTap main, the clean-room srdif engine, header-only). conv_engine gets its own scratch buffer for load_ir() plus a reader guard on the IR slot swap, conv_engine::configure and stft::configure (behind spectra and nr) refuse FFT sizes the engine does not support, and the three notebooks that run through the real FFT are re-executed.

Why

Found by an adversarial audit of the FFT and its consumers (2026-10-01):

  • Race. load_ir() on the control thread and process_block() on the audio thread transformed through one scratch buffer, so an IR load during playback corrupted either the partition being built or the audio frame. A standalone repro against the old header measured ~5e5 corrupted samples per run of 3.3e5; the new header measures zero. The double-buffer slot swap also had an ABA window (a block reads the slot it claimed at its start; a second load inside that block wrote it), closed by the audio thread publishing the slot it reads and load_ir waiting for it (bounded; refused on timeout, never written while in use). The FFT object stays shared, which DspTap's k_is_shareable permits and a static_assert pins.
  • Size gates. DspTap's constructor checks nothing in a release build: measured under ASan, N = 12, 24, 96 and 3072 overflow the heap and N = 1000 returns garbage silently. Nothing in TapTools or its C ABI gated the size. configure now returns false and leaves the object unchanged; process() emits silence until a configure succeeds, since it indexed unallocated buffers otherwise. The C ABI returns -1 on refusal (the Python bridge already raises on it).
  • Install rule. install(TARGETS … tap_dsp_fft) failed every top-level configure against the new pin, where that target exists only under TAP_DSP_FFT_CMSIS.

Verification

  • Release GCC 13: 325 / 325 Catch2 cases (three new scenarios: the gate, unconfigured silence, and the concurrent IR load, which fails on the old engine and passes here).
  • scripts/tidy.sh on the changed TUs: no finding in the changed files (the 46 in taptools_capi.cpp are identical on main). clang-format applied.
  • Notebooks re-executed through the C ABI: convolution_reverb exactness against direct convolution tightened from 3.13e-12 to 1.14e-13 and block-size independence from 1.81e-12 to 7.11e-14; every printed number in harmonizer and tune is unchanged. The book cites none of the moved numbers.
  • Not verified here: TapTools-Max. configure now returns a bool its wrappers may want to check, and load_ir returns false on the refusal path; the Max attribute setters that decide whether a non-power-of-two size ever reaches stft are in that repo.

Notes for the reviewer

  • Submodule pin moved to 2137d86, DspTap main. The engine swap moves every float and double output bit (the tests tolerate it; the notebooks are re-executed for it).
  • Notebooks re-executed, because behavior changed.
  • Contract change at the TapTools boundary: configure() on conv_engine, stft, spectra::remapper and nr::reducer returns bool; load_ir() returns bool; a block size of 1 (N = 2, below the engine's minimum) is now refused where it used to run by accident.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QM4N2sNauJqVYyetTcgNcB


Generated by Claude Code

tap and others added 2 commits October 1, 2026 16:20
…ad race; gate FFT sizes

submodules/dsptap moves from 41215c6 to 2137d86 (DspTap main): the floating
real FFT is now the clean-room srdif engine, header-only, with tables built
in the constructor. tap_dsp_fft exists only under TAP_DSP_FFT_CMSIS now, so
the install rule exports it only when the target exists (a top-level
configure failed on the old rule).

conv_engine: load_ir() (control thread) and process_block() (audio thread)
transformed through one scratch buffer, so an IR load during playback
corrupted either the partition being built or the audio frame (a standalone
repro measured ~5e5 corrupted samples per run of 3.3e5). load_ir() now has its
own scratch; the FFT object stays shared, which the engine's k_is_shareable
contract permits and a static_assert pins. The double-buffer slot swap also
had an ABA window (a block reads the slot it claimed at its start; a second
load inside that block wrote it): the audio thread now publishes the slot it
is reading and load_ir waits for it to be released (bounded; refused on
timeout, never written while in use). The new concurrency scenario fails on
the old engine and passes on this one.

Size gates, per DspTap's rule that configuration-driven sizes go through
supports_size (an unsupported size corrupts the heap in a release build):
conv_engine::configure and stft::configure (behind spectra and nr) refuse
unsupported sizes and return false leaving the object unchanged; process()
emits silence until a configure has succeeded. The C ABI returns -1 on a
refusal (the Python bridge already raises on it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QM4N2sNauJqVYyetTcgNcB
…new dsptap pin

convolution_reverb, harmonizer and tune drive conv_engine and pvoc through the
C ABI, so the dsptap bump (the srdif engine replacing the Ooura-derived one)
moves their output bits; the house rule is to re-execute, and the verified
numbers are: convolution exactness against direct convolution tightened from
3.13e-12 to 1.14e-13 (max |engine - direct|, 47 partitions) and the block-size
independence from 1.81e-12 to 7.11e-14; every printed number in harmonizer
and tune is unchanged (figures re-rendered).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QM4N2sNauJqVYyetTcgNcB
@tap
tap merged commit 1c14dff into main Oct 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant