Events, not audio: garden.h

garden::bed recirculates events where its siblings recirculate samples, which makes it the family's odd one out mechanically and its purest member conceptually: the wear-as-stabilizer inversion survives the abstraction jump intact, as arithmetic. This appendix walks the machinery — the ring, the split between planting and firing, the bell, the quantizer, the gardener — and the two contracts that had to be designed before they could be tested.

The event ring

Sixty-four fixed seats (std::array, nothing allocated at prepare() — this kernel buys no tape at all), each event a pitch, a velocity, a brightness, a position on the loop, and a plant-order sequence number. The sequence number exists for one policy: when the garden is full, the oldest live bloom yields to a new plant. The musical argument is stated in the header — a touch must always speak (rejecting input makes an instrument feel dead), and the oldest bloom has survived the most decay passes, so it is the quietest thing on the table; retiring it is the least audible edit available. The pinned scenario plants a distinctive high note, floods the ring with sixty-four more, and requires the first note's pitch measurably gone from the following pass.

Fire is not plant

note() does not sound a voice. It quantizes, seats the event at the loop's current position, and returns; the next process() sample finds the event's position under the playhead and fires it. The first draft did both — plant-and-fire in note() — and the loop fired it again one sample later, a double-trigger that fell out of the design the moment firing became the loop's exclusive job. One mechanism, two consequences: a plant sounds one sample late (inaudible, documented), and every sounding of every event goes through a single code path, which is what makes the return grid a testable promise. After each fire the event blooms: velocity times decay, brightness times soften, retire below floor — so a bloom lives exactly ceil(log(floor/velocity)/log(decay)) passes and the population converges no matter the planting rate. That is the stability theorem, and it is three lines of arithmetic instead of a saturator.

The bell

Two-operator FM at a fixed ratio of 3 (Chowning 1973), amplitude from the shared tr808::decay_env, modulation index velocity · brightness · k_index_max. The ratio was a test requirement before it was an aesthetic: an integer ratio keeps the spectrum harmonic, harmonic means the YIN oracle reads the fundamental, and the scale-contract scenario — plant off-scale pitches, require every sounded note on the scale within 20 cents — only exists because the voice is honest to a pitch detector. Softening maps to the index, so "purer every pass" is measurable as a Goertzel trajectory: the 4f sideband fades return over return while the fundamental holds. Steals re-aim: the pool's quietest bell gets trigger()ed with new targets while its envelope and phases free-run, so a steal glides where a reset would click; the decay_env was built for exactly this non-resetting retrigger, one family over.

Quantize at entry

The scale machinery is tune.h's 12-bit pitch-class mask idiom — the make_mask builder, the nearest-allowed search that never travels more than a tritone — copied with citation, not included, because tune.h reaches into tap::dsp for its detector and a garden should not link a pitch tracker to hold five scale presets. The masks themselves are plain public-domain scale theory, deliberately not any app's preset list. Quantizing at entry (rather than at fire) is the semantic choice: a scale change re-pitches nothing already planted, which keeps running gardens stable under live tinkering and makes the contract easy to state.

The gardener and the seed

Idle planting consumes the family RNG (tr808::white_noise, xorshift64*, the seed-folding and clear-reseeds contract) — and only idle planting does. That consumption discipline is load-bearing: the third leg of the seeded triad, "with the gardener disabled the seed cannot matter at all", is only true because a disabled gardener never touches the generator, so two beds with different seeds run bit-identical until the first idle draw. The suite pins all three legs, the way the tr808 voices taught: same seed bit-exact, different seed audibly different, seed irrelevant when the random feature is off. step_seq.h promises "no randomness anywhere"; this kernel is the deliberate counterpoint, and the triad is the bridge back to a reproducible test suite.

A finding: envelopes never reach zero

The return-grid scenario was first written the obvious way — the percussive test bell surely dies between returns, so the first nonzero sample after silence is the onset. It failed, instructively: decay_env's exponential tail crosses the 1e−12 hard-zero more than half a second after a "20 ms" decay, so there is no silence between returns, only −200 dB of not-quite. The fix was to stop pretending: an instant-attack bell, an amplitude threshold scaled to the expected return velocity, and a grid claim of "within 8 samples" — a sixth of a millisecond — with the comment explaining that a threshold on a sine sits a few samples into the cycle. The lesson is general for this library: exponential envelopes make "silence" a tolerance, and tests that assume literal zeros between notes are wrong even when they pass.

The engineering ledger

The suite measures the output, never the internals: peak-per-window ratios for the decay staircase (0.5 ± 0.075 across four returns, then active_events() == 0 and the render below 1e−6), a strictly-decreasing Goertzel sideband for softening, YIN for the scale contract, the seeded triad rendered three times over, and structural bounds exercised at their edges — sixty-five plants against sixty-four seats, thirty-two notes against sixteen bells, finiteness and the k_voices amplitude bound under sustained stealing. The two introspection counts (active_events, active_voices) exist, as phase() does next door, so those scenarios could be written against public surface.

Checkpoint

A fixed ring of events fired by a loop counter into a fixed pool of FM bells: plant and fire kept strictly apart, wear as per-pass arithmetic (decay, soften, floor) with convergence as its theorem, scale masks copied from tune.h and applied at entry, and a gardener whose RNG discipline makes generative behavior compatible with a bit-exact test suite. Third costume, same inversion: the system stays bounded because everything in it is always fading. Every claim lives twice — garden.ipynb executed, garden_test.cpp pinned.