Design notes — Part XIV — v3.0, Phase 15: Voxo DS 2.0, the Decent Sampler coverage's second generation (steps 87–91: the sustain pedal v2, chorus and phaser, the filter family, the modulator matrix, the instrument slot and the bounce — opened 2026-10-25 at step 87)
Opened 2026-10-25, the day Phase 14 was folded (Part XIII); merged into DECISIONS.md as Part XIV at the phase’s fold. The spec is PROJECT_SPEC.md (v5) §12.2–§12.3 and voxo/COMPAT_REPORT.md (the ledger of what is not played) with TO_PROJECT_SPEC.md §12.7–§12.12 as the phase’s draft (the proposals, corrected by each step as it ships); the Pianobook libraries the author keeps are the acceptance material. THE CORE IS FROZEN (step 78 to step 92): every step here is Voxo’s, libvoxo additive — “DS 2.0” names the format coverage, the library’s version follows its ABI (DECISIONS_10 #1). The instruments: tests/voxo_preset_tests.cpp (the headless bounces against the constant-level fixtures of tests/fixtures/dspresets/), tests/voxo_tests.cpp (the contract: zero allocation in voxo_render), tests/voxo_fuzz.cpp (the mutated presets, now driven through the pedal), the storm from the Release build, the web and lab gates. Numbered entries follow; DECISIONS_14 #n means this part.
#1 — Step 87: the sustain pedal v2 — the half-pedal, re-pedalling, the pedal noises, the CC filter and the choke groups (2026-10-25; Voxo 0.27.0, the ABI unchanged; evidence docs/evidence/step87)
Section titled “#1 — Step 87: the sustain pedal v2 — the half-pedal, re-pedalling, the pedal noises, the CC filter and the choke groups (2026-10-25; Voxo 0.27.0, the ABI unchanged; evidence docs/evidence/step87)”What the spec asked (ROADMAP_6.md step 87; TO_PROJECT_SPEC §12.7): four mechanisms the format expects — (1) pedal noise and release-sample triggers, “<group trigger="cc"> with onLoCC/onHiCC, evaluated on CC 64 crossing 64 upward or downward”; (2) half-pedalling, a note released at 0 < CC 64 < 127 entering a damped-sustain release whose rate interpolates between the preset’s release and sustainDecay; (3) re-pedalling, the pedal pressed after note-off catching an audible tail at its level and slowing its decay; (4) choke groups — silencedByTags with silencingMode normal / fast — and “group-level seqMode gated by CC 64”. The gates: a headless bounce of pedal-down, half-pedal, re-pedal and a hi-hat choke, each asserted on its envelope; zero audio-thread allocations; the compat report’s sentences for these features retired. What shipped is §12.7 as corrected below. The decisions:
- The format’s attributes, read from the format, not from the proposal (2026-10-25). The Decent Sampler developer guide (
decentsampler-developers-guide.readthedocs.io, the<groups>element) was fetched before the parser was written. There is NOtrigger="cc"—triggertakes attack, release, first, legato, continuous. A CC-triggered sample is written with the CC NUMBER IN THE ATTRIBUTE NAME:onLoCC64="64" onHiCC64="127"on the<sample>(or the group; “if you use onLoCCN, you must also use a corresponding onHiCCN for the same CC number”), the pedal-noise idiom being<group name="Pedal Noise" pitchKeyTrack="0"><sample path="…DW.wav" onLoCC64="64" onHiCC64="127"/><sample path="…UP.wav" onLoCC64="0" onHiCC64="63"/></group>(the maker’s own answer in the Q&A thread “Sustain Pedal Noise”, 2021: no rootNote, loNote or hiNote on such samples). The note-on filter isloCCN/hiCCN(“when a note-on is received the engine decides, from the last received value of that CC, whether the sample plays”). The choke issilencedByTags,silencingModefast | normal — THE DEFAULT IS FAST (“immediately”) — andsilencingDecay(seconds, overriding the mode).sustainDecayappears nowhere: it is VOXO’S OWN attribute, kept under the proposal’s name (the format ignores what it does not know). The proposal’s “group-levelseqModegated by CC 64” has no attribute behind it; the pedal-down and pedal-up groups the author meant areloCC64/hiCC64on the groups, which is what ships. - What the parser reads (
voxo/src/ds_preset.cpp,ds_preset.h):cc_rangefinds the firstonLoCC<N>/loCC<N>attribute on the sample, the group or<groups>(the cascade), composes thehipartner’s name from N and reads it through the same cascade (hi < lo swapped; a missing hi = 127; N 0..127, three digits at most);silencedByTags,silencingMode,silencingDecay,pitchKeyTrackandsustainDecaycascade the same way; a sample’stagsADD to its group’s.trigger="continuous"is parsed and plays as an attack zone (the always-on sample is not built — recorded). One CC pair of each kind per element: the first found. - The compiled zone and the dictionary (
instrument.h/.cpp): a zone carrieson_cc/on_lo/on_hi(255 = a note zone),cond_cc/cond_lo/cond_hi(255 = unconditional),key_track,tag_maskandsilenced_by— the instrument’s tag DICTIONARY gives every distinct tag a bit, 64 at most (a 65th tag neither silences nor is silenced; a library with more is beyond the format’s drum kits) — andsilence_s: silencingDecay as given, else 5 ms for fast (the engine’s release floor: “immediately” clicks), −1 for normal (the group’s release). The instrument lists its CC-triggered zones (cc_zones) and the CCs they watch (cc_trigger_mask), and flagschokewhen any zone is silenced by anything, so the dispatch pays for the pass only in presets that use it. A CC-triggered zone ANSWERS NO NOTE (it leaves the reach mask too): the Q&A’s samples carry no note range, and the format’s defaults would otherwise fire a thump on every key. - THE PEDAL IS A POSITION, THE RATES INTERPOLATE (
voxo.cpp:Channel::pedal,pedal_damp_coef,pedal_set,pedal_apply). CC 64 / 127 is the pedal’s position p on the channel (the master’s reaches the zone, as before). A note-off at p > 0 is held (pedalled), and every note layer takes a second envelope factor,damp, decaying toward silence at the rate r(p) = (1 − p)·r_release + p·r_down — r_release = 4.6 / the group’s release (5 ms at least), r_down = 4.6 /sustainDecay, or 0 without it. So: a switch pedal at 127 on a preset without the attribute gives r = 0 — the hold, BIT FOR BIT the old pedal (damp stays 1.0: x + (−x·0) and gg·1.0 are exact; the two pedal gates ofvoxo_testsand the MPE-zone gate pass unchanged); a preset that declaressustainDecay="1.0"decays under the full pedal at 4.6 / s (measured: the ratio across a quarter second 0.317, the formula’s 0.317); a continuous pedal half way (64) on the same preset decays at 25.1 / s (measured 0.081 over a tenth of a second, the formula’s 0.081). The pedal moving re-damps every pedalled voice (lifting slowly speeds the decay progressively); p = 0 releases what it held, the release samples firing as before. Known and accepted: a switch pedal that sends 64 for “down” (rare; most send 127) now holds a note at half the release rate instead of forever on presets withoutsustainDecay; the option of a threshold waits for a library that shows it. A Suzu voice under the pedal takes the same rule through its declared contraction (its T60 stretched by 1/(1 − p); the full pedal a hold, as before); the sine holds. - RE-PEDALLING CATCHES A TAIL WHERE IT IS (
pedal_apply,ENV_HOLD). The pedal coming down (p > 0) over a voice that is releasing with a note layer still audible (env × damp > 10⁻³, not a release sample, not a choked layer) moves that layer to ENV_HOLD — the envelope frozen at its value — and the voice back topedalled; its decay from there is the pedal’s damping, its next lift the normal release. No attack, no jump: the layer’s output continues from the same sample. Measured: a note lifted and caught 30 ms into its 0.1 s release sits at 0.0356 (0.5 × UNIT × e^{−1.38}) and reads 0.0356 half a second later; the lift empties it in the release. A pedal noise, a choked layer and a release sample are never caught; the CC-triggered voices are never pedalled. - THE PEDAL NOISES FIRE ONCE PER CROSSING (
cc_fire,Voice::cc_voice). Every CC’s last value is kept per channel (Channel::cc_last[128], the master’s written to the zone); a zone withonLoCCN/onHiCCNfires when CC N’s value ENTERS its range — the previous value outside, the new one inside — as a voice of its own on the event’s channel: no note (note0xFF, so no note-off finds it), full velocity, the pitch of the zone’s root throughpitchKeyTrack(0 the root exactly:freqraised to the tracking’s power, 1 bit for bit the note), never held, never pedalled, its layers playing out through the group’s envelope to the sample’s end; its tags choke what they choke. The format, by the maker’s note on a Pianobook piano, triggers on EVERY message inside the range (“the PEDALDOWN.wav will be triggered repeatedly while holding the pedal down”) — Voxo fires on the crossing, the proposal’s reading, so a continuous pedal does not machine-gun the thump; recorded as a deliberate difference. Measured: the pedal going to 127 fires the 0.25 zone once (one voice, one layer; 127 and 100 again add nothing; it ends), to 0 the 0.125 zone once (30 again adds nothing). The CC filter: note 60’s resonance zone,loCC64="64" hiCC64="127", silent with the pedal up, playing with it down — the pedal-down resonance group of the proposal, through the format’s own mechanism. - THE CHOKE RUNS BEFORE THE NEW LAYERS START (
choke_layers). A dispatch (a note-on, a release trigger, a CC trigger) first ORs the tag masks of the zones about to start and silences every playing layer anywhere whosesilenced_bymeets them — so a group silenced by its own tag (a mono hi-hat) chokes its predecessor and never itself — then starts the new layers. A choked layer goes to its release phase with its coefficient fromsilence_s(5 ms, silencingDecay’s, or the group’s release) and is marked, so a later note-off changes nothing and a re-pedal does not catch it. Measured on two open hats (looped 0.5) and a closed hat (looped 0.25): both opens 1.0 × UNIT; 20 ms after the closed hat strikes 0.1789 against the formula’s 0.1788 (the fast one gone within its 5 ms, the normal one at e^{−4.6·0.0175/0.3} of its level in its 0.3 s release); after it the closed hat alone, one layer. The union over a round robin’s candidates (one tag set for every position) can over-silence by a tag only some positions carry — a kit that does that is not known; recorded. - The contract and the fuzz. Nothing above allocates in the callback: the CC voices come from the pool, the tag dictionary and the CC-zone list are compiled on the shell thread, the per-channel CC memory is 2 KB of plain bytes. The preset suite’s blocks 19 and 20 arm the counting allocator around the pedal noises, the filter, the catch and the choke (the suite’s own measurement buffer allocated before arming — the first run counted its own
std::vectors, eleven and three, and was corrected).voxo_fuzzseeds the two pedal fixtures and their samples and, after every load that succeeds, plays five notes through a random pedal position, a lift, a catch and a panic, asserting finite output: 280 271 loads in 60 s, 108 776 loaded, no crash, no non-finite sample. - The compat report retires no sentence, because none covered these features: a pedal-noise sample was silently a note zone and a choke tag was ignored — the report had no line for an attribute.
voxo/COMPAT_REPORT.mdgains a paragraph on what the pedal mechanics play since this step; the eleven canonical sentences stand and the test that pins them passes. - Found in the web gate: step 86’s matrix section was never bit for bit, and its record did not show it. The gate read RED on the Duffing’s matrix section — the audio bit-identical, the trace, the inspection and the recent trace within −100.9 dB from the section’s slide event on. Not the pedal (no CC 64 in the script) and not the chaotic modulator (the section without it reads the same): the slide-to-pitch route of step 86 multiplies the frequency by
exp2f(m/12), an ulp apart between the libraries, and the cell’s state walks from there (the plain Duffing section stays bit for bit). The class is the TOLERANT list’s (the trumpet’s pow, the sax’s exp2f); kind 4 joins it with the measured figure, the gate GREEN again. Step 86’s evidence file (web_and_lab_gates.txt) held the gate’s tail only, so the line never appeared: an evidence file is the whole output, never a tail — the rule from here. - The tablets, both plugged in, and the spike’s pedal. The shells’ spike storms (
ios/Sources/VoxoSpike.swift,android/cpp/sumi_jni.cpp: a test hook, not a feature) now press the pedal on the master channel across their forty pushed notes — down for ten (their note-offs held, the damping), half way for five, up for five (the catch on the next press) — so a device run exercises the pedal’s paths under the storm. On the sampler’s Dan Tranh demo both are clean: the iPad 0 XRuns, render at most 0.105 ms of its 2.67 ms burst; the Tab (the release APK signed with the debug key) 0 XRuns, 0.801 ms of its 4 ms. On Suzu patch 0 the iPad reads 0.210 ms and the Tab 21 proxy XRuns at 4.424 ms — the carried Tab item ofDECISIONS_13 #1(its audio thread some forty times the Mac’s cost; a shell thread-placement item, ADPF/affinity), now with the pedal holding fifteen voices under the storm; AAudio reports 0 device XRuns on both runs. Not this step’s code: the sampler’s run is the one the pedal touches, and it is clean. - What stays with the proposal and what does not. Kept: the four mechanisms, the rate interpolation, the catch,
sustainDecay’s name and meaning, the gates. Corrected: the attribute names (above), the choke’s default (fast, the format’s), “a 5 ms linear fade” is a 5 ms one-pole (the engine’s release floor, no click), the “seqMode gated by CC 64” (it isloCC64/hiCC64). Not built, recorded:trigger="continuous",previousNotes/legatoInterval(step 90’s legato),releaseTriggerDecay, tag polyphony limits, accvoice’s velocity from the CC’s value (full velocity ships), more than one CC pair per element, the switch-pedal-at-64 threshold.
Evidence (docs/evidence/step87/SUMMARY.md): the Release suites (the preset suite’s blocks 16–20, the sampler suite, the synth suite), the Debug ctest, the fuzz minute, the storm on the Dan Tranh library, the web and lab gates, both tablets under the spike with the pedal (tablets.txt, tablets/).
#2 — Step 88: the chorus and the phaser, and the CC map’s chorus targets (2026-10-25; Voxo 0.28.0, additive — VOXO_CTL_CHORUS_MIX / _RATE / _DEPTH; evidence docs/evidence/step88)
Section titled “#2 — Step 88: the chorus and the phaser, and the CC map’s chorus targets (2026-10-25; Voxo 0.28.0, additive — VOXO_CTL_CHORUS_MIX / _RATE / _DEPTH; evidence docs/evidence/step88)”What the spec asked (ROADMAP_6.md step 88; TO_PROJECT_SPEC §12.8): <effect type="chorus"> at bus and group level — a stereo quadrature-LFO delay line pre-allocated at load (mix, modDepth, modRate, the 90° stereo offset), 4-point Hermite reads; <effect type="phaser"> — 4 or 6 cascaded all-passes under a stereo LFO (mix, modDepth, modRate, feedback, centerFrequency); the CC map’s 1006–1008. The gates: the effects bit-identical across the native builds at a fixed seed; the storm with chorus on every voice at 0 XRuns; the report’s chorus sentence retired. What shipped is §12.8’s chorus and phaser as corrected (the EQ, the gain and the wave shaper of the same subsection are step 89’s by the roadmap). The decisions:
- The format, read first (the developer guide’s
<effects>element and Appendix B). The chorus takesmix(0..1, default 0.5),modDepth(0..1, 0.2),modRate(Hz, 0..10, 0.2) and nothing else — no delay, feedback or width attribute. The phaser takes those pluscenterFrequency(Hz, 0..22000, 400) andfeedback(−1..1, 0.7); it has nostagesattribute (six it is, the proposal’s “4 or 6” decided by the format’s silence). Group-level effects are PER VOICE in the format (“initialized every time a note is started … two notes, two instances”), and only the filters, the gain and the chorus may sit at the group level — the phaser is the bus’s alone. Every effect takesenabled(default true). The generic binding names —FX_MIX,FX_MOD_DEPTH,FX_MOD_RATE,FX_CENTER_FREQUENCY,FX_FEEDBACK(from the appendix’s summary, the page’s verbatim half ending before the effect rows) — name the effect by its position, so the parser resolves them by the KIND of the effect the binding points at (binding_kind: the instrument’s byposition, a group’s byeffectIndex); the unambiguous names stay as they were. - The chorus (
bus.h/.cppChorus): two delay lines at a 7 ms centre swung by ±4 ms × depth (CENTRE_MS,SWING_MS—[ITERATE]by ear against the author’s chorus library), the LFO a magic-circle cell — the leapfrog rotation ofsuzu.h, exact and drift-free, its (x, y) the quadrature pair, so the right channel is 90° from the left for free and the prewarp2 sin(π f/fs)is the one libm call, once a block — the fractional read the §12.2 Hermite (hermite4, the same tangents as the sampler’s), the output (1 − mix)·dry + mix·wet with mix 0 leaving the samples untouched. THE BUFFERS ARE FIXED (1536 frames a channel, 16 ms at 96 kHz; the swing never exceeds 11 ms), so an instance sits inside everyVoice(12 KB, 786 KB over the pool of 64) and one on the bus: no allocation anywhere, the “pre-allocated at load” of the proposal made structural. The group-level chorus follows the format: a chorused voice’s layers are summed into a scratch block (voice_scratch, 4096 frames; a larger block skips the group chorus, as the plate’s does), its own chorus runs on the sum, the result is added to the bus — one instance a note, the settings of the first group carrying a layer when a stack spans groups (recorded), the live values from the group’s state (itsFX_MIX/FX_MOD_DEPTH/FX_MOD_RATEbindings). A fresh note clears its chorus (the fixed seed: the LFO at phase 0); a retrigger keeps its line. - The phaser (
Phaser): six first-order all-pass stages a channel, H = (a + z⁻¹)/(1 + a z⁻¹) with a = (tan(πf/fs) − 1)/(tan(πf/fs) + 1), swept f = centerFrequency × 2^(2·depth·lfo) — ±2 octaves at full depth, the coefficient refreshed every sixteen samples (a tan a channel, a staircase 60 dB under the sweep) — the feedback from the chain’s last output into its input, held within ±0.95 (the format’s ±1 is the loop’s margin), the output (1 − mix)·dry + mix·chain. Mix 0 returns before touching the block. At the centre the chain’s phase is −3π exactly, so mix ½ is a true notch there: the saw’s 440 Hz harmonic 132 dB down with the phaser frozen at 440 (gate 24), 220 Hz within 0.6 dB (the chain’s −318.8° there), and at mix 1 the chain alone passes every harmonic at the dry’s level. - Bit-identical across the native builds. The chorus and the phaser are compiled without fused multiply-add contraction (
bus.cpp’s#pragma clang fp contract(off)/ GCC’soptimize/ MSVC’sfp_contract, placed before them so the reverb and the delay keep the compiler’s default), and their only libm calls are the prewarp’ssinand the phaser’stan/exp2— the same bits in the Debug (-O0 still contracts: the frontend’s fmuladd intrinsic lowers to fmadd on arm64 whatever the level, found below), the Release and the no-FMA reference builds on one platform. Measured: two fresh instances of the sweep identical in every sample, the output’s FNV hash66a88a01in the Debug run — the Release run’s hash is in the evidence beside it. - Found: the Debug build fuses too. The group-chorus gate first asked for two chorused notes to equal the dry pair delayed, bit for bit; they sat a last bit apart in a third of the samples. Not the chorus (one note is bit for bit against the dry delayed, through the bus and through the group): the dry path accumulates each layer with
out += s·g·pan, which clang contracts into one fused rounding even at -O0, while the chorused path rounds the product into the scratch and adds afterwards. The gate is a tolerance now (−143.6 dB of the peak measured, the bar −120) and the class is recorded: the sampler’s own mix is FMA-dependent wherever two voices add, which is why the chorus and the phaser carry the pragma and the sampler does not. - The gates (
tests/voxo_preset_tests.cpp21–25 ontests/fixtures/dspresets/fx/: a 220 Hz sine and a 110 Hz saw looped on whole periods). 21: the bus chorus at mix 0 is the dry bit for bit; mix 1 at depth 0 throughVOXO_CTL_CHORUS_MIXis the dry delayed by exactly 336 samples at 48 k (7 ms), bit for bit, the first 336 samples silent. 22: full depth at 2 Hz throughVOXO_CTL_CHORUS_DEPTH: the left’s pitch swings 209.2–231.0 Hz, 21.8 Hz peak to peak against the formula’s 22.1 (±4 ms × 2π × 2 Hz on 220 Hz); where the left’s swing peaks (+11.0 Hz) the right reads −0.8 Hz — the quadrature. 23: the group chorus on one note is the bus chorus bit for bit; two notes the dry pair delayed within −143.6 dB; nothing allocated. 24: the phaser’s notch (above) and itsFX_MIXbinding resolved by position. 25: the sweep with feedback 0.7 bounded (peak 0.368 against the dry’s 0.258), finite, two fresh instances identical, the 440 Hz harmonic moving 20.4 dB across 50 ms windows of the period (a quarter-second window averaged the sweep away: 0.4 dB — the window follows the notch’s dwell); a disabled chorus beside it passed over. The fuzz seeds the three effects presets and their waves: 256 925 loads in 60 s, 100 383 loaded, no crash. - The CC map and the shells.
VOXO_CTL_CHORUS_MIX1006 (0..1),_RATE1007 (0..10 Hz: the CC × 10),_DEPTH1008 (0..1),VOXO_CTL_COUNT9; a route turns the bus chorus on, as the reverb’s do.presets/SCHEMA.mdnames them; the desktop’s target table (app_ctl_target_count+ 9, the names), the iPad’s reload range (1000…1008) and the Tab’s name list carry them; the settings reference lists the three. The report’s chorus sentence is retired —VOXO_NOTE_CHORUSstays in the ABI, never raised, its copy empty — and the phaser no longer appears among the unknown effects;features.dspresetgained abit_crusherso the unknown-effect note keeps a witness; the copy test skips a retired bit. - Not built, recorded: the bus EQ,
<effect type="gain">as a bus stage and<effect type="wave_shaper">(step 89); the listed order of the bus effects (chorus, phaser, reverb, delay is Voxo’s); a stack spanning groups with different choruses (the first group’s rules);stereo_simulator,pitch_shift,wave_folder,bit_crusher,compressor,stutter(the unknown effects, named in the report);effectTagson a binding; the phaser’sstages(none in the format).
Evidence (docs/evidence/step88/SUMMARY.md): the Release suites, the Debug ctest, the fuzz minute, the storm on the Dan Tranh library with a chorus on every voice and the phaser on the bus, the web and lab gates, the tablets’ compiles.
#3 — Step 89: the filter family, the filter envelope, the bus EQ, the gain and the wave shaper (2026-10-25; Voxo 0.29.0, no new entry point; evidence docs/evidence/step89)
Section titled “#3 — Step 89: the filter family, the filter envelope, the bus EQ, the gain and the wave shaper (2026-10-25; Voxo 0.29.0, no new entry point; evidence docs/evidence/step89)”What the spec asked (ROADMAP_6.md step 89; TO_PROJECT_SPEC §12.9 and the tail of §12.8): the per-voice SVF’s highpass, bandpass, notch, peak and lowpass_1pl outputs from the same loop; <modulators><envelope> to cutoff and resonance; <effect type="eq"> (low shelf, peaking mid, high shelf), <effect type="gain">, <effect type="wave_shaper">. The gates: each filter’s response against its analytic curve within 0.5 dB; the report’s “other filters” sentence retired. What shipped is §12.9 as corrected (the subsection rewritten as shipped). The decisions:
- The format, read first (the developer guide’s
<effects>element and the modulators page). The voice filters arelowpass(lowpass_4plthe legacy spelling),lowpass_1pl(no resonance),bandpass,highpass, all withfrequencyandresonance— andresonanceis the Q (0.001..5, default 0.7), not an amount: step 51 read it asclamp(resonance − 0.7, 0, 1)and mapped that to k = 2 − 1.9·amount, so a preset at the default sat at Q 0.5 (−6 dB at the cutoff) instead of 0.7 (−3 dB). Corrected: k = 1/Q, Q held within 0.1..18 (Group::resonance,GroupState::resonanceand theFX_FILTER_RESONANCE/FX_FILTER_Qbindings carry the Q itself); every lowpass preset is 3 dB brighter at its cutoff than at 2.0, and the step-51 CC 74 gate’s margin shrank from a factor 2 to 1.49 between the open and the centre readings (the bar lowered to 1.3, the ordering intact).notchtakesQ/q(0.01..18, 0.7) at 10 kHz by default;peakaddsgain(linear 0.0001..10, or decibels undergainUnit="decibels") — there is no<effect type="eq">: the EQ ispeak,low_shelf(200 Hz) andhigh_shelf(5 kHz) withq,gain,gainUnit.gaintakeslevelin dB by default ("-6dB"allowed; the default −6 dB) or linear underlevelUnit="linear".wave_shapertakesdrive(1..1000),driveBoost(0..1),outputLevel(0..1, default 0.1) andhighQuality(read, ignored). The group level admits the filters, the gain, the chorus and (in the guide’s examples) the wave shaper; the peak, the shelves and the notch are the bus’s. The bindings:FX_FILTER_FREQUENCY,FX_FILTER_Q,FX_FILTER_GAIN,FX_DRIVE,FX_DRIVE_BOOST,FX_OUTPUT_LEVEL;<envelope>carriesattack,decay,sustain,release,modAmount,scope,modBehavior(set | add | multiply | modulate). - The family on the one SVF (
voxo.cpprender_layer,Group::filter_mode): the Zavalishin loop’s low (v₂), high (v₀ − k·v₁ − v₂), band at unity (k·v₁) and notch (v₀ − k·v₁) outputs selected by the mode, the state and the per-sample coefficient ramp untouched;lowpass_1plthe TPT one-pole on the same state. The voice’s filter is the group’s first enabled lowpass / lowpass_1pl / highpass / bandpass / notch, else the instrument’s first enabled lowpass / highpass / bandpass (an instrument-level notch is the bus EQ’s, where the format puts it); none = a lowpass at 22 kHz so CC 74 moves something, as before. A lowpass at the top with the default Q skips the loop — the dry, bit for bit (the step-88 gates unchanged); the other modes always run. The proposal’s “peak” output is not an SVF output: the peaking EQ is a biquad on the bus (below). Measured (gate 26, the Release run): every mode at 1 kHz at Q 0.7 and 2, and the one-pole, within 0.001 dB of its analytic curve over the saw’s thirty harmonics — the TPT SVF is the exact bilinear transform of the analog prototype prewarped at the cutoff, |H(f)| the prototype’s at Ω = tan(πf/fs)/tan(πfc/fs) — against the 0.5 dB bar;lowpass_4plat the instrument level is the group’s lowpass bit for bit;enabled="false"the dry bit for bit; the lowpass at Q 0.7 reads −3.01 dB on 990 Hz. - The filter envelope (
Layer::fenv/fenv_stage,fenv_advance,fenv_apply): the first<envelope>carrying a binding on the voice filter’s frequency or Q (instrument level: every group; group level: that group; one a group, the first wins) plays PER VOICE whatever itsscope(“most envelopes should be per note” — the guide), stepped ONCE A BLOCK: a linear attack, an exponential decay (4.6/T) to the sustain, an exponential release that follows the amplitude envelope’s (a note-off, a choke, a retrigger; the pedal’s catch freezes it with the amplitude). Its value at the block’s start and end is folded into the two cutoff coefficients that already ramp per sample, so the sweep costs the block’s two tangents and nothing per sample; the Q is read at the block’s start.modAmountscales the envelope before the binding’s translation;modBehavior(the binding’s, else the element’s; default set): set replaces the cutoff — under the default timbre’s multiplier, so CC 74 still plays — add adds, multiply scales,modulateis read as multiply (its definition is not on the page; recorded). Measured (gate 31): set 200..8000 Hz, add 500 + 0..6000, multiply 500 × 1..16 at modAmount ½ — the 3300 Hz harmonic over the fundamental −33.8 / −34.5 / −37.5 dB after the attack, 10–12 dB lower in the sustain, 24–35 dB lower in the release; the Q from 5 to 0.7 over half a second: 990 Hz −8.8 dB at the start, −22.1 settled; an envelope onAMP_VOLUMEraises no note and plays. - The bus EQ (
bus.h/.cppBiquad,EqChain;Params::eq[8]): every enabledpeak,low_shelf,high_shelfand instrument-levelnotchin its listed order, eight stages at most, RBJ’s cookbook in direct form I (the shelves by Q), the coefficients recomputed every block at the device’s rate (a rate change reallocates nothing; the state clears with the reverb’s).FX_FILTER_FREQUENCY/FX_FILTER_Q/FX_FILTER_GAINbound to an EQ effect’s position resolve to its stage (Target::Eq*, the curve’s group the stage index,eq_stage_ofcounting the enabled EQ-kind effects before it as the chain does; the gain binding’s value linear); bound to a voice filter’s position, to the voice filter (target_oftakes the effect’s kind and level). Measured (gate 28): the peak (+6 dB at 1 kHz, Q 1, in dB or linear), the shelves (200 Hz +6, 2 kHz −6) and the notch within 0.002 dB of RBJ’s response evaluated on the unit circle (the cookbook transcribed in the test, not bus.cpp’s); a four-effect chain with a disabled stage within 0.000 dB of the three enabled stages’ product;FX_FILTER_GAINon CC 20 (1..2 linear) lifting 990 Hz by 6.02 dB, RBJ’s 6.02. - The gain and the wave shaper. The bus’s enabled gains fold into one factor applied after the EQ (
Params::gain_lin); a group’s into the group’s gain (its zones’). The shaper (shaper_tanh,shape): y = outputLevel · tanh(x · drive · (½ + ½·driveBoost)) through Eigen’s (13,6) rational tanh — within 3e-7 of libm over the range (measured; the (7,6) Padé first tried was 1e-4 off near 4 and the gate caught it), no libm call, the same bits everywhere — evaluated AT THE FORMAT’S SCALE: the engine’s samples sit at 0.40 of full scale, so the shaper divides by that going in and multiplies going out, and a preset’sdrivemeans what it means in Decent Sampler (without this a drive of 1 was a drive of 0.4). On the bus the first enabled one; at the group level per voice through the scratch path the chorus uses (chorusednow means chorus or shaper), before the chorus.FX_DRIVE,FX_DRIVE_BOOST,FX_OUTPUT_LEVELbound live, group or bus by the binding’s level as the chorus’s. The bus order from this step: the wave shaper, the EQ’s stages, the gain, the chorus, the phaser, the reverb, the delay — the tone before the modulations and the space. Measured (gates 29–30): the dry × 0.5011872 within 2e-8 sample for sample forlevel="-6","-6dB", the default and a group’s;levelUnit="linear"0.5 exactly; the bus shaper within 7e-8 of 0.16·tanh(50x); the group shaper on one voice the bus shaper bit for bit; the drive on CC 20 from 1 to 100 squaring the saw (its second harmonic from −6.1 to −46.0 dB); the level on CC 21 at 64 scaling by 0.5039. - Found on the way: the loop’s edges clicked (step 51’s
read_frame). The notch gate’s second half-second read 1.2 dB off its first; the cause was not the filter: a loop without a crossfade read its outer interpolation taps from the sample around the loop — the padding’s zeros when the loop spans the whole sample, as the fixtures’ and many libraries’ do — a click once a pass (two frames of a 4-point read against zeros at the end, one at the start; eighteen output samples at a sixfold upsampling). The last two frames beforeloopEndand, once the loop has wrapped, the first frame afterloopStartread their taps wrapped into the loop (read_frame_wrapped,Layer::looped); the first version indexedi0 − 1in the unsigned frame index and crashed on the loop’s first frame (the Release suite’s run caught it at block 11, the backtrace named it): the index is signed. Gate 27 asserts the dry saw repeats its 4800-sample period across the loop point within 1e-4 (2.7e-5 measured; 1.6e-2 before). The filter gates measure 110 cycles ending a tenth of a second before the loop point. - The report and the fixtures. The “EQ and other filters” sentence is RETIRED (
VOXO_NOTE_OTHER_FILTERSstays in the ABI, never raised, its copy empty; the copy test counts nine sentences now); the modulators sentence reads “Modulators (LFOs, sequences): …” — an<envelope>raises nothing. A group-levelpeakor shelf (outside the format) is passed over silently.voxo/COMPAT_REPORT.mdgains the step’s paragraph and loses the row. The fixtures (tests/fixtures/dspresets/fx/, twenty-seven presets on the looped 110 Hz saw):saw_dry,filter_<mode>_q07/_q2,filter_1pole,filter_legacy,filter_disabled,eq_peak/_lin/_lowshelf/_highshelf/_notch/_chain,gain_db/_suffix/_linear/_group/_default,shaper/_group/_cc,fenv_set/_add/_mult/_q/_only. The suite’s blocks 26–32; nothing allocates in the callback (gate 32). - Not built, recorded:
modulateas its own behaviour; an envelope on the bus EQ or on anything but the voice filter (the amp envelope through<envelope>is step 90’s matrix);scope="global"as one envelope shared by the notes (per voice always); a stack spanning groups with different shapers (the first group’s rules, as the chorus);highQualityon the shaper (no oversampling); a group-level peak or shelf; the listed order of the bus effects across kinds (within the EQ it is kept); the filter’s Q ramped within a block.
Evidence (docs/evidence/step89/SUMMARY.md): the Release suites (the preset suite’s blocks 26–32, the sampler suite, the synth suite), the Debug ctest, the fuzz minute, the storm on the Dan Tranh library with a filter envelope, a lowpass and a shaper on every voice and the EQ, the gain and a shaper on the bus, the web and lab gates, the tablets’ compiles.
#4 — Step 90: the modulator matrix, legato and glide, the compat report pruned (2026-10-26; Voxo 0.30.0, additive — voxo_set_tempo; evidence docs/evidence/step90)
Section titled “#4 — Step 90: the modulator matrix, legato and glide, the compat report pruned (2026-10-26; Voxo 0.30.0, additive — voxo_set_tempo; evidence docs/evidence/step90)”What the spec asked (ROADMAP_6.md step 90; TO_PROJECT_SPEC §12.10–§12.11): <lfo> (frequency, waveform sine | triangle | square | saw, offset, phase, sync free | tempo from the host’s BPM); multi-target <binding> modulation of amp, pan, pitch, cutoff and effect parameters; group legato / monophonic mode with glideTime and glideMode; the compat report’s table pruned to the genuinely out-of-scope. The gates: the preset suite’s goldens for every modulator; a long voxo_fuzz run; the compat test green. What shipped is §12.10–§12.11 as corrected (both subsections rewritten as shipped). The decisions:
- The format, read first (the guide’s modulators page, groups page, true-legato topic, appendix B — the site is
decentsampler-developers-guide.readthedocs.io, “developers” plural; the singular host 404s). The<lfo>hasshape(sine, square, saw on the attribute’s line; the SHAPE binding’s list adds triangle — all four play),frequency(Hz, or underfrequencyFormat="musical_time"“an integer index into the same musical subdivision list used by musical_time controls (for example, 10 = 1/8, 13 = 1/4, 19 = 1, 24 = 5/1)”),modAmount(0..1, default 1),delayTime(seconds, 0),scope(global the default for LFOs; voice “starts a new LFO on each note-on”),modBehavior(set the default; add “the legacy behavior”; multiply; modulate “adds a zero-centered modulation delta around the target parameter’s current/base value, so a neutral envelope value contributes no offset”),enabled. Nooffset,phaseorsyncattribute exists — the proposal’s three are not the format’s; the tempo sync is the musical-time format on the host’s tempo. The<envelope>addsdelayandholdto step 89’s ADSR, and the curvesattackCurve(−100, logarithmic),decayCurve/releaseCurve(100, exponential). The groups page:glideTime(“the glide/portamento time in seconds. A value of 0.0 would mean no portamento”, default 0, inherited),glideMode(“always, legato (… only when transitioning from one note to another), and off”, default legato),polyphony(the innermost wins; 1 = monophonic),triggerattack | release | first (“only be played if no other notes are playing”) | legato (“only be played if some other notes are already playing”) | continuous,previousNotes(a comma list; the legato page writespreviousNote),legatoInterval(“only play the sample if the previously triggered note is exactly this semitone distance”). The modulatable parameters: “almost all effect parameters”, the group’s and the global volume, pan and tuning; the appendix’sAMP_VOLUMEat the instrument level is marked not modulatable (Voxo modulates it anyway: every group),PAN−100..100,GROUP_TUNING/GLOBAL_TUNING−36..36 semitones,GLIDE_TIME0..10,GLIDE_MODE. - The matrix (
instrument.hModulator/ModTarget,MOD_MAX8 ofMOD_TARGETS_MAX8;instrument.cpp’s compile from every enabled<lfo>/<envelope>source;voxo.cpplfo_raw,lfo_phase,menv_advance,menv_value,mod_read,mod_out,mod_apply): a modulator is a function of time — an LFO of the rendered clock (clock_seconds), or of the voice’s own clock since its note-on under scope=“voice” (Voice::start_s), itsdelayTimeholding the waveform at its start (the sine and the triangle at their centre, the square at its top, the saw at zero); an envelope per voice whatever its scope (Voice::menv[k]: delay, a linear attack, hold, an exponential decay at 4.6/T to the sustain, the release following the voice’s — a note-off, a lift of the pedal, a retrigger — “most envelopes should be per note”, the guide). Both are READ ONCE A BLOCK, at the block’s start and end; every target scales the raw 0..1 by its amount (the element’smodAmount× the binding’s, since the appendix leaves the binding’s undefined and the features fixture carries one), reads its curve (the binding’s translation, 33 points) and folds the result by its behaviour — set, add, multiply, or MODULATE = base + (curve − the curve at the neutral input: the LFO’s 0.5, the envelope’s 0). The targets fold into the ramps the voice already runs:AMP_VOLUMEon the pressure gain’s start and end,GROUP_TUNINGas semitones on the block’s pitch line (exp2at the start and the end),PANonce a block onto the zone’s pan (render_layer’span_add, the equal-power law),FX_FILTER_FREQUENCY/_Qas step 89’s envelope did (set under the default timbre’s multiplier, so CC 74 still plays), the group chorus’s and shaper’s live values for the scratch path, and — by position — the bus effects’ parameters and the EQ stages’ into a LIVE COPY ofvoxo_bus::Paramstaken every block (the CC bindings’ and the shell’s values stay the base; an envelope on the bus has no voice and contributes its idle value: recorded). Step 89’sGroup::fenvis gone: the filter envelope is one slot with one target, and gate 31’s figures read exactly as before. Per layer per block the cost is the targets’ curve reads; per sample nothing. - Musical time (
musical_whole_notes): the guide prints four anchors and not the list; the reconstruction — a triplet, the plain value and the dotted value for every subdivision from 1/64 to the whole note, then whole bars — matches all four (index 10 = 1/8, 13 = 1/4, 19 = 1, 24 = 5/1) and is recorded as a reconstruction. The LFO’s rate is bpm / (240 × the subdivision in whole notes): a quarter at 120 BPM is 2 Hz.voxo_set_tempo(20..300, else 120) is the additive entry point; the desktop’ssound_apply, the iPad’sSumiCanvas.swift(besidesumi_set_params) and the Tab’ssumi_jni.cpppass the session’s BPM — the same number the roll layouts scroll by. - Legato and glide (
voice_dispatch,GroupState::last_hz/glide_time,Voice::glide_s/glide_t/glide_from,voxo_t::prev_note): the dispatch counts the other held notes (others_held: any other active voice held or pedalled with a note layer) and reads the previously triggered note;trigger="first"and"legato",previousNotes(a 128-bit mask) andlegatoIntervalare conditions inmatches, beside step 87’s CC filter. A group withpolyphonyN at N sounding voices releases its oldest’s layers as the new one starts (N = 1 the monophonic mode). A group with a glide time (and a mode other than off) starts the new voice at the previous note’s CURRENT frequency — the newest voice sounding in the group, mid-glide or bent as it may be — and walks to its own target along a straight line in semitones over glideTime (the block’s start and end on the line; a bend during the glide moves the line’s end), under legato only while that note is still held or pedalled, under always from the group’s last note-on frequency whatever became of it. The first gliding group a stack touches sets the voice’s glide. The Suzu source keeps its own pitch path (no glide; recorded). Measured (gate 39): 222.2 Hz fifteen milliseconds into a 0.2 s glide from 220, 233.0 at a hundred (the line’s 233.1), 246.9 settled; glideMode off 247.1 at forty milliseconds; always 233.0 after a released note, legato 247.1. - The gates (
tests/voxo_preset_tests.cpp33–41 ontests/fixtures/dspresets/mod/: eighteen presets on the looped 220 Hz sine and 110 Hz saw, the instants chosen where the waveform makes the expectation exact). 33: the tremolo (sine 2 Hz × 0.5..1) peak over dip 1.994; a note started a quarter second into the instrument’s clock reads 1.994 (its peak) 0.375 s later under the global LFO and 0.501 (its dip) under the voice LFO. 34: the square’s second half 0.0, the saw’s level at 0.8 s over 0.2 s 3.958, the triangle’s quarter over its half 1.996. 35: the vibrato 226.4 / 214.0 Hz (226.4 / 213.7); the pan 0.0010 left over right at the top, 956 at the bottom. 36: a quarter note 1.994 at 120 BPM at the 2 Hz instants, 1.998 at 60 BPM at the 1 Hz instants (1.006 at the 2 Hz ones — the same level on a 1 Hz sine); delayTime 0.5 flat within 0.01 dB then 1.994. 37: the cutoff modulated ±200 Hz about 300 on the sine 13.6 dB between the LFO’s top and bottom (the analytic 13.7); the bus peak’s gain under a global LFO 5.99 dB (6). 38: the envelope silent through its delay, 0.1604 in its hold, the sustain 0.499 of it, the release −29.9 dB a tenth of a second after the note-off (the modulator’s e^{−2.3} from 0.5 under the amplitude envelope’s e^{−0.46}: −30.0). 39: the glide (above). 40: the triggers and the previous-note attributes, told by the saw’s second harmonic — both fixtures are pitched to the played note, so their fundamentals coincide (the first version of the gate measured the saw’s fundamental and read the sine). 41: nothing allocated. The compat checks: the features preset’s seven notes, the eight sentences and the three retired bits’ silence. - Found on the way: a saw through a swept filter is not a witness through 50 ms windows. The cutoff gate first ran the saw: the implied cutoff zigzagged ±5 dB between consecutive windows (five and a half saw periods a window: the neighbouring harmonics’ leakage into the measured bin). A sine through the same sweep traced the analytic curve within half a decibel at every 40 ms (the probe’s table in the evidence): the engine was right, the measurement was not; the fixture is the sine.
- Found on the way: a scripted region cut anchored 1500 lines away emptied
voxo.cpp. The edit replacing step 89’s envelope functions took its end anchor from the next function-level comment it could name —render_layer’s — and deleted the dispatch, the voice start and the pedal between. The file came back from HEAD: the author had committed step 89 minutes before. The rule recorded: the end anchor is the text that immediately follows the region, with an assert on the cut’s size and a backup in the scratchpad before a large scripted edit. - The report (§12.11): the modulators sentence RETIRED (
VOXO_NOTE_MODULATORSstays in the ABI, never raised, its copy empty); an unknown modulator element is named with the unknown bindings; the table is the eight sentences that remain true.voxo/COMPAT_REPORT.mdgains the step’s paragraph. - Not built, recorded: the LFO’s and the envelope’s curves (
attackCurveand its kin: the linear attack and the 4.6/T exponentials stand in);GLIDE_MODEas a binding; the tag-levelpolyphony(<tag polyphony>);scope="global"as one envelope shared by the notes (per voice always); an envelope on the bus (no voice: its idle value); a stack spanning groups with different glides (the first gliding group rules); the glide on the Suzu source;TRIGGER/MOD_DELAY_TIME/FREQUENCY/SHAPEas modulator-level binding targets; the oscillator’spolyphony.
Evidence (docs/evidence/step90/SUMMARY.md): the Release suites (the preset suite’s blocks 33–41, the sampler suite, the synth suite), the Debug ctest, the five-minute fuzz seeded with the matrix fixtures, the storm on the Dan Tranh library with a per-note vibrato, a tempo-synced tremolo and pan, a slow LFO on the bus reverb and EQ and a legato glide on every group, the web and lab gates, both tablets under the spike.
#5 — Step 91: the instrument slot, the offline bounce and the output device (2026-10-26; Voxo 0.31.0, additive — voxo_set_device / voxo_get_device / voxo_device_count / voxo_device_name, voxo_preset_hash / voxo_preset_name / voxo_preset_file_hash; the preset schema 3; evidence docs/evidence/step91)
Section titled “#5 — Step 91: the instrument slot, the offline bounce and the output device (2026-10-26; Voxo 0.31.0, additive — voxo_set_device / voxo_get_device / voxo_device_count / voxo_device_name, voxo_preset_hash / voxo_preset_name / voxo_preset_file_hash; the preset schema 3; evidence docs/evidence/step91)”What the spec asked (ROADMAP_6.md step 91; TO_PROJECT_SPEC §12.12; PROJECT_SPEC.md §16’s sound items): a library referenced by name and hash in a preset, never embedded; the offline bounce of a replay (--replay-wav productized on every shell, Voxo deviceless); the desktop’s output-device row (miniaudio’s enumeration, the name in the INI, “System default” first, the fallback when the name is gone — DECISIONS_6 #42). The gates: a preset with a missing library loads with the sentence, not a crash; the bounce byte-identical to the live render at the same seed on the desktop; the device switch without a stop. What shipped is §12.12 as corrected (the subsection rewritten as shipped). The decisions:
- The instrument slot is an identity, and the identity is the preset’s XML (
sumi_preset_t::library_name/library_hash/library_path/library_present,presets/SCHEMA.mdlibrary, the schema 3). A session preset names the library the sound played by its name (the.dspreset’s stem) and the FNV-1a 64 of its preset XML —voxo_ds::Instrument::xml_hash, computed as the file is read, before the samples — with the path where the writer found it as a hint; the samples are not hashed (a gigabyte zip would make every save a minute; the XML names the instrument).voxo_preset_hash/voxo_preset_nameanswer for the loaded instrument from fields set ON THE SHELL THREAD at load (voxo_t::preset_hash,preset_name): the first version read the current instrument’s model and answered for the PREVIOUS one until the callback’s swap — the preset suite’s block 42 caught it (an empty name, the old hash).voxo_preset_file_hashreads a.dspreset’s or a.dslibrary’s XML off the disk without a sample (read_preset_xml: the first preset inside the archive), so a shell can test a candidate in milliseconds. A recording’s session header carries the slot too (the serializer writes what the session holds): a replay names its instrument. - The resolution is the shell’s, by name then by hash, and the sentence is the result. The desktop (
app_resolve_library): the hint path if it exists and hashes the same, then — over the hint’s folder, the current instrument’s folder,~/Music/midi-sinkand~/Music/Decent Sampler—<name>.dslibrary,<name>.dspreset,<name>/<name>.dspreset, then any.dspreset/.dslibrary(and<dir>/<dir>.dspreset) one level down whose XML hashes the same;app_settings_from_presetsetssound_presetto the find or clears it and setssound_library_note— “Library ’…’ is not on this computer: the preset plays without it.” — which the Sound section’s report shows while no instrument is loaded. The iPad (SoundController.resolveLibrary) and the Tab (Sound.resolveLibrary,nativeVoxoFileHash) search their Instruments folder the same way (the relative names they already list), load the find, else clear the instrument and show the sentence in the Sound page’s report; the desktop writes the slot throughapp_sound_library_setas a load lands, the iPad fromlibraryIdentity, the Tab in the JNI’s session asnativeVoxoLoadreturns. - The bounce is the replay library’s walker over a deviceless engine, and every shell binds the same two callbacks (
replay/src/sumi_replay_bounce.c,sumi_replay_bounce): the recording’s frames in order, each frame’s bytes throughpush, then that frame’s worth of samples (dt × rate, the fraction carried) throughrenderin 128-frame calls, a 32-bit float stereo WAV at 48 kHz written as it goes (the sizes patched at the end); the visuals’ events are not applied — the bounce is the sound alone; no engine dependency (the replay library stays pure C beside hostmpe and the presets). Each shell bindspush→voxo_push_midiandrender→voxo_renderon a freshvoxo_createnever started — Voxo deviceless, as every headless test renders — configured as its live instance is: the desktop throughapp_sound_configure(ONE body, factored out ofmain.cpp’ssound_applywithapp_settings_suzu_params: the gain, the dialect, the tempo, the device, the source and the patch, the tuning, the CC map’s bus routes, the instrument loaded synchronously), the iPad throughapplySource(to:)/applyTuning(to:)and a synchronousvoxo_load_preset, the Tab fromg.vcfg— the record of what its natives applied (the gain, the dialect, the tempo, the source and the patch index, the patch file, the tuning, the preset). The WAV lands beside the recording:<config>/replays/<name>.wav(the desktop’s Bounce to WAV), Documents/Replays (the iPad’s waveform button, the WAV shareable from the row), the app’s Replays (the Tab’s bounce, wav exporting it). Synchronous — a large library loads first; the tablets run it off the main thread, the desktop in the settings window’s call (recorded as [ITERATE]: a worker for a long bounce). - The gate, as read. The live device’s output cannot be byte-aligned to the frame clock: the device’s callbacks fall where they fall against the frames’ pushes, so two live replays of the same recording differ in which block a note-on lands in. “Byte-identical to the live render at the same seed” is therefore read as: the bounce IS the deterministic rendering of a performance — the same recording, engine configuration, rate and block give the same bytes, on every shell by construction (one walker, one engine, one configuration body), measured on the desktop by the bench’s
--bounce-test(two bounces through the host’s own path —app_sound_configureon fresh instances — compared byte for byte) and in the replay suite over a synthetic engine. Measured: the bench’s four-second demo recording (480 frames, 794 events) through the Dan Tranh library bounced twice to 192 000 frames (4.00 s, 597 MIDI events reach the sound, peak 0.036), 1 536 044 bytes identical, the hash164241f1; the replay suite’s four frames of 1/120 s give 1600 frames in sixteen render calls (three of 128 and a 16, four times), the WAV 44 + 8 × 1600 bytes, two bounces the same, the header a RIFF/WAVE of format 3, two channels, 32 bits. The bench’s--replay-wavnow shares the walker (its own loop beside the visuals is gone). Found: a bench gate that renders no frame hangs the lab at exit —sg_shutdownwaits on Metal’s frame semaphore that no frame ever signalled — so the two new gates render one frame first (t19_step); the first run sat twelve minutes at 0 % CPU beforesamplenamed the wait. - The device.
voxo_backend_enumeratelists miniaudio’s playback devices on a fresh context (thenonebackend answers 0);voxo_backend_starttakeswant_deviceand opens the device of that name, else the default (DECISIONS_6 #42’s rule: a name that is gone falls back);voxo_set_deviceon a running instance stops the backend and starts it again on the new name with the voices, the bus and the preset untouched — a short gap, no stop the shell toggles (voxo_core_device_openedresets the run’s counters and adopts the new device’s rate; the publicvoxo_stop, which silences the pool, is not called). The desktop’s Sound section gains Output device (“System default” first, the names refreshed every two seconds, a chosen name no longer present shown as such), the INIsound_device,sound_apply→voxo_set_device. Measured (--device-test): four devices on the author’s Mac (the LG display, the speakers, Teams, Zoom); started on the speakers, moved to the LG while running — running, the stats naming it, 188 callbacks in the half second after the move — a name that does not exist falling back to the speakers, and back to the default, 188 callbacks each. - The gates (
tests/preset_tests.c: the slot’s round trip with escapes in the name, a bad hash reading as 0 = name only, a file without the slot leaving it absent, the schema 3;tests/replay_tests.c: the bounce over the synthetic engine;tests/voxo_tests.cpp: the device list, the name kept for the next start, no preset = hash 0;tests/voxo_preset_tests.cppblock 42: the identity off the file and off the loaded instrument agreeing for a.dspresetand a.dslibrary, a missing file false; the bench’s--bounce-testand--device-test), the Release suites, the Debug ctest, the fuzz minute, the storm, the web and lab gates (the wasm’snonebackend builds the enumeration as 0), both tablets under the spike on 0.31.0. - Not built, recorded: the tablets’ output-device row (the desktop’s alone, as the roadmap says); a worker thread for the desktop’s bounce; a bounce at the device’s rate rather than 48 kHz; the bounce of the visuals (a video); the slot’s search beyond one folder level; a hash over the samples; Voxo Dorean (§12.6).
Evidence (docs/evidence/step91/SUMMARY.md): the Release suites (the five), the Debug ctest, the fuzz minute, the bounce and device gates’ whole outputs, the storm, the web and lab gates, both tablets under the spike.
Phase close — 2026-10-26
Section titled “Phase close — 2026-10-26”Steps 87–91 shipped (#1–#5), each Voxo additive (0.27.0 → 0.31.0) and the core frozen throughout; the preset schema 3; TO_PROJECT_SPEC.md §12.7–§12.12 stand as shipped (their fold into PROJECT_SPEC.md is the author’s, at spec v6); the pre-release tag v3.0.0-alpha.5 is the author’s to cut. Folded with Part XV on 2026-10-26, the author’s word.
midi-sink 2.0.0 · AGPL-3.0 · source
