Stakes. Every audioeffects class that fans a source out for a wet/dry
mix builds an audioroute.Splitter, and none of them can release it. The
class gate's Tier 1 asks that deinit() release every node the class built;
on Compressor the answer is 5 of 6, every time, and the sixth is the
Splitter. A host that builds and tears down effects in a session leaks one
Splitter — 34 KB by the Phase 2 cost table's RAM column — per instance.
What the surface is. audioroute.Splitter's Python surface is tap
alone: no deinit, no reset. Its taps carry both, and a tap's
reset_buffer is deliberately a no-op ("the cursors belong to the Splitter
and the other taps are still reading from them",
audioif/src/audioroute/SplitterTap.c:47-55), which is correct — the ring
belongs to the Splitter, so only the Splitter can free it.
Measured, audiocomponents at AUDIOIF_PIN 2f6cbc3, 2026-09-07:
$ .venv/bin/python tools/compressor_evidence.py deinit
node owned for deinit received the call _deinited
0 Splitter no (no deinit) no n/a
1 SplitterTap yes yes True
...
5 of 6 nodes owned, 5 of 5 calls received
Compressor owns the Splitter deinit=False on purpose, because calling a
deinit it does not have would be a build failure; the evidence pack
records it as a palette gap rather than a class choice
(audiocomponents/docs/effects/Compressor-evidence.md §2, §11).
Ask. audioroute.Splitter gets deinit(), freeing the ring, with the
usual deinitialised guard on tap() afterwards. Every Splitter-headed class
in audioeffects then closes this Tier 1 row.
Stakes. Every
audioeffectsclass that fans a source out for a wet/drymix builds an
audioroute.Splitter, and none of them can release it. Theclass gate's Tier 1 asks that
deinit()release every node the class built;on
Compressorthe answer is 5 of 6, every time, and the sixth is theSplitter. A host that builds and tears down effects in a session leaks one
Splitter — 34 KB by the Phase 2 cost table's RAM column — per instance.
What the surface is.
audioroute.Splitter's Python surface istapalone: no
deinit, noreset. Its taps carry both, and a tap'sreset_bufferis deliberately a no-op ("the cursors belong to the Splitterand the other taps are still reading from them",
audioif/src/audioroute/SplitterTap.c:47-55), which is correct — the ringbelongs to the Splitter, so only the Splitter can free it.
Measured,
audiocomponentsatAUDIOIF_PIN2f6cbc3, 2026-09-07:Compressorowns the Splitterdeinit=Falseon purpose, because calling adeinitit does not have would be a build failure; the evidence packrecords it as a palette gap rather than a class choice
(
audiocomponents/docs/effects/Compressor-evidence.md§2, §11).Ask.
audioroute.Splittergetsdeinit(), freeing the ring, with theusual deinitialised guard on
tap()afterwards. Every Splitter-headed classin
audioeffectsthen closes this Tier 1 row.