acoustickit carries a frame, and a note the engine drops is counted - #104
Merged
Merged
Conversation
…on the queue (#94, #96) A strike on a modal bank is `Bank.set_mode()` on a node that is already running -- the energy goes in on the line that writes the gains. That is a C state change rather than a press, so no queue could hold it back, and `acoustickit` was the one instrument here that played live only. audiodsp#138 gave the pump both halves with a frame on them. `load()` now fills a mode table for the WHOLE bank -- the struck voice from the kit's tables, every other row back to its resting pole at gain zero -- and hands it to `Keys.strike_bank`, which queues an `audiopump.STRIKE` when the keyboard is armed and answers False when it is not. Live, the same table goes in a `set_mode()` at a time, exactly as before: the live render is byte-identical over a sixteen-hit bar covering every family and the hat choke, proven against a planted 0.01 % decay change that moves the digest. The table always covers the bank, and that is load-bearing. The engine silences a mode a short table does not reach by writing frequency 0, which takes the recursion with it; this package's own 'off' keeps the pole and zeroes the gain. A table that stopped short would be a kit where one strike cuts the ringing crash. `Keys.refused` and `Instrument.refused` surface what the engine drops for want of a channel, and `Sequencer.health()` carries it as `unvoiced` -- beside `refused`, never added to it. They are different failures: the queue turning an event away at the door, and an event that applied perfectly and had nowhere to put its note. Stealing at schedule time is not the fix and the reasoning is at the property: the occupancy at a future frame is unknowable and every number available to guess with lies. None rather than 0 where the engine cannot count, because an engine that cannot say is not one that lost nothing. Measured on the real pump, two bars at 200 BPM at the 64-voice ceiling: tr808 and a four-note juno106 lose nothing, an eight-note juno106 loses 488 and solina loses 1796 while the queue separately refuses 1520 -- which is why the two are never one number.
This was referenced Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #94 and #96. Both are the instrument-side half of engine work that already merged (audiodsp#138 and #137), and they land together because both run through
Keys.#94 — the kit can be scheduled
load()fills a mode table for the whole bank and hands it toKeys.strike_bank, which queues anaudiopump.STRIKEwhen the keyboard is armed and answersFalsewhen it is not. The hat'sclear()becomes aCHOKEat the same frame, scheduled first, because events at one frame apply in schedule order.The live path is byte-identical. A sixteen-hit bar covering every family and the hat choke renders
2e43097240fe281c...before and after. Proven sensitive by a planted 0.01 % decay change, which moves it tob9f1ff42ef405112....The table always covers the bank, and that is not tidiness. The engine silences a mode a short table does not reach by writing frequency 0, which takes the recursion with it; this package's own "off" keeps the pole and zeroes the gain. A table that stopped short would be a kit where one strike cuts the ringing crash — the exact defect
_silence()'s docstring records costing 7584 down to 61.Three new cases in
tests/parity/scheduling_seam_live.py, each striking a tom — the measurement, not a taste in drums. Onlykick,snareandsidestickcarry layer notes played into the mixer; a tom is the bank and nothing else, and the excitation burst is not in the mixer at all. So a tom sounds if and only if aSTRIKEreached the bank.kitlevelsis the case a frame-stamped strike is for. Decliningstrike_bankand leaving everything else scheduled — the old behaviour exactly — the loud hit came out at 666 and the soft one at 808: the bar inside out, and both ten times down.All seven cases green, and all seven planted faults (
early,flat,held,armed,unstruck,even,ringing) fail their own case with a clean control passing.tests/support/fake_pump.pygainsSTRIKE/CHOKEfromaudiopump_events.c, including the silence-what-the-table-misses loop, so the CPython suite can exercise the kit at all.#96 — a dropped note is counted
The decision is refuse and surface it, not steal at schedule time, and the reasoning is written at
Instrument.refused: the occupancy at a future frame is unknowable, and every number available to guess with lies —max_polyphonyis the wrong denominator once notes-per-key varies with a macro, andlen(synth.pressed)has been measured reading 0 while a fresh press was refused.Sequencer.health()carries it asunvoiced, besiderefusedand never added to it. Two bars at 200 BPM at the 64-voice ceiling, on the real pump:refused(queue)unvoiced(engine)tr808, every circuit on every sixteenthjuno106, 4-note chords, 4-step gatejuno106, 8-note chords, 8-step gatesolina, 8-note chords, 8-step gate488 is the issue's own number, reproduced exactly. The
solinarow is why the two are never one number: 1520 events never reached the engine at all, and 1796 that did had nowhere to put their note. The first two rows are the controls that must read zero.Nonerather than0where the engine cannot count — an engine that cannot say is not one that lost nothing. Verified as a natural experiment: against the installed audiodsp 0.4.0, which predates audiodsp#137, every level readsNonewhilerefusedstill reads0.What this needs before CI can prove it
AUDIODSP_PINis4e58091, which predates both #137 and #138. CI installs audiodsp from that pin and nothing else, so on itaudiopumphasPRESS/RELEASE/RELEASE_ALLand noSTRIKE, andsynthio.Synthesizerhas norefused. This is the same shape as thecebb7camove: "acoustickitimportsaudiomodal.Bank, so the floor had to carry it or the kit could not be built at all."Moving the pin is its own change and not mine to make as a side effect, so this PR does not. Two things follow:
tests/passes at the current pin, because it runs onfake_pump, which is this repository's own Python and now carries the two ops._bank_ops()turns theAttributeErrorinto a sentence rather than letting a bare one out of a timer callback: "this audiopump has no STRIKE/CHOKE, so a modal instrument cannot be put on a frame (audiodsp#138). It still plays live." On an older engine the kit behaves exactly as it did before this PR.Gates
One trap worth knowing. The venv carries an editable install of
audioinstrumentspointing at the main checkout, sopython -m unittest discoverrun from a worktree silently testsmain's library instead of the branch's. Everything above was run withPYTHONPATH=$PWD/lib.