Skip to content

acoustickit carries a frame, and a note the engine drops is counted - #104

Merged
bdbarnett merged 1 commit into
mainfrom
housekeeping/acoustickit-schedulable
Sep 22, 2026
Merged

bdbarnett merged 1 commit into
mainfrom
housekeeping/acoustickit-schedulable

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

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 to Keys.strike_bank, which queues an audiopump.STRIKE when the keyboard is armed and answers False when it is not. The hat's clear() becomes a CHOKE at 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 to b9f1ff42ef405112....

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. Only kick, snare and sidestick carry 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 a STRIKE reached the bank.

ok   kit        loudest sample before block 20: 0 (want 0), after: 6981
ok   kitlevels  tom at 127 peaks 6981, tom at 32 peaks 2072
ok   kitchoke   the open hat peaks 5055, the tail 8 blocks after the closing hat peaks 0

kitlevels is the case a frame-stamped strike is for. Declining strike_bank and 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.py gains STRIKE/CHOKE from audiopump_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_polyphony is the wrong denominator once notes-per-key varies with a macro, and len(synth.pressed) has been measured reading 0 while a fresh press was refused.

Sequencer.health() carries it as unvoiced, beside refused and never added to it. Two bars at 200 BPM at the 64-voice ceiling, on the real pump:

bar refused (queue) unvoiced (engine)
tr808, every circuit on every sixteenth 0 0
juno106, 4-note chords, 4-step gate 0 0
juno106, 8-note chords, 8-step gate 0 488
solina, 8-note chords, 8-step gate 1520 1796

488 is the issue's own number, reproduced exactly. The solina row 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.

None rather than 0 where 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 reads None while refused still reads 0.

What this needs before CI can prove it

AUDIODSP_PIN is 4e58091, which predates both #137 and #138. CI installs audiodsp from that pin and nothing else, so on it audiopump has PRESS/RELEASE/RELEASE_ALL and no STRIKE, and synthio.Synthesizer has no refused. This is the same shape as the cebb7ca move: "acoustickit imports audiomodal.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:

  • Everything in tests/ passes at the current pin, because it runs on fake_pump, which is this repository's own Python and now carries the two ops.
  • _bank_ops() turns the AttributeError into 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

flake8                    clean
validate_api              55 instruments and 45 effects
effects_library_smoke     45 classes, 249 patches, 0 failures
unittest discover         Ran 1546 tests
scheduling_seam_live      7 cases green, 7 faults seen

One trap worth knowing. The venv carries an editable install of audioinstruments pointing at the main checkout, so python -m unittest discover run from a worktree silently tests main's library instead of the branch's. Everything above was run with PYTHONPATH=$PWD/lib.

…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.
@bdbarnett
bdbarnett merged commit d449401 into main Sep 22, 2026
14 checks passed
@bdbarnett
bdbarnett deleted the housekeeping/acoustickit-schedulable branch September 22, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

acoustickit cannot be scheduled

1 participant