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 chime, 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 chime
Four decaying mode doublets at the transverse-vibration ratios of the
selected material — the free-free tube's 1 : 2.756 : 5.404 : 8.933 from
the bars-and-tubular-chimes chapter of Fletcher & Rossing's The Physics of
Musical Instruments (f_n grows as (2n+1)²), or the tuned bar's
double-octave 1 : 4 : 10 : 20 from the mallet-percussion chapter — each
mode a pair of sines split a fixed few cents, the doublet splitting of a
real tube's degenerate mode pairs (same source), so the tail beats slowly
instead of decaying like a lab sine. The ratio and haste tables are indexed
[material][mode] and read at strike time, which is the whole
implementation of the material switch: instant, allocation-free, and every
live bloom re-voices at its next return. Each mode rides
its own tr808::decay_env; decay times divide by ~ratio² (radiation
damping grows with frequency), which makes the fourth mode a
tens-of-milliseconds contact tick, and scale by √(440/f) per strike, so
small high tubes ring shorter than long low ones. The upper modes scale
with per-event brightness times strike hardness (a soft strike is a dull
strike) and progressively steeply (b, b², b³), so soften strips the tick
first and mode two by exactly its ratio. Mode levels sum to at most 1, so a
chime is bounded by its velocity and the pool bound stays arithmetic; modes
above 0.45·sr stay silent rather than aliasing.
The phase rule earned a refinement when the doublets arrived: a strike on a silent tube zeroes its phases — fresh initial conditions, so the pair starts aligned and its beat blooms identically at every return, which is what keeps per-return spectral measurements deterministic — while an audible steal keeps free-running phases and glides instead of clicking. The inharmonicity moved the pitch contract rather than breaking it: the upper modes clear quickly, so the YIN oracle reads each strike in its ring-down, and the scale-contract scenario still lands every off-scale plant on the scale within 20 cents.
The tube is the identity
Two more properties hang off each pitch, and neither touches the rng. A
tube's upper modes sit up to ±3 cents off the ideal ratios — the
fundamental stays true, because a maker tunes the fundamental — and the
tube keeps a fixed seat on the stereo rack, spread scaling how far off
center. Both are drawn by tube_unit, a stateless xorshift64* hash keyed
by (fundamental-in-centihertz, index) — the metal_bank.h per-index idiom,
index 0 the seat, 1..3 the mode scatter. Stateless is the load-bearing
word: the gardener's seeded generator is never consumed, so the seed-triad
contract survives intact, the rack is identical in every instance, and
every return of a bloom rings from the same place with the same flaws.
The pinned scenarios measure the scatter by scanning a Goertzel probe
across the second mode (±0.25-cent steps resolve it), and the seat by
left/right energy share: deterministic per pitch, different across pitches,
bounded by the constants. The seat itself follows the phase rule — pan
gains snap on a silent tube and slew ~10 ms through an audible steal — and
the equal-power law is the √((1∓p)/2) form, so spread 0 makes the busses
bitwise identical (also pinned).
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
The gardener is a wind model: once the idle threshold passes, strikes
arrive on a calm/gust cycle driven by a small state machine — a gust
catches 1 to 5 neighboring tubes (gust sizes it) with 30–280 ms between
strikes, the clapper walking a few semitones per swing, and the following
calm stretches with the gust just spent so the average rate stays near one
strike per pass at any setting. 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. The suite pins all three legs, plus
the wind itself: at gust 1 some strikes tumble inside a gust, at gust 0
single strikes never come closer than the minimum calm (the scenario sets
decay 0 so only the gardener's own strikes are counted — planted seeds
recirculate, and returns are not wind). 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: fundamental ratios
for the decay staircase (0.5 ± 0.05 across four returns — the fundamental,
because hardness makes whole-strike peaks fade faster than velocity, 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 modal
wind chimes: 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, tube identity (material voicing, mode
scatter, stereo seat) as stateless hashes so nothing generative leaks into
the audio path, 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.