Changelog
Condensed from the per-step DONE evidence (docs/evidence/ in git history,
removed from the working tree when each phase ships — last at the Phase-9
fold, 2026-10-06).
Spec: PROJECT_SPEC.md; decision log: DECISIONS.md (Part III = Phase 4,
referenced below as DECISIONS_3 #n; Part IV = Phase 5, DECISIONS_4 #n;
Part V = Phase 6, DECISIONS_5 #n; Part VI = Phase 7, DECISIONS_6 #n;
Part VII = Phase 8, DECISIONS_7 #n; Part VIII = Phase 9, DECISIONS_8 #n).
Unreleased
Section titled “Unreleased”The documentation site deploys from main
Section titled “The documentation site deploys from main”.github/workflows/pages.yml is now the only workflow that deploys GitHub
Pages (DECISIONS_4 #82): the docs from the pushed tree, /marble/ rebuilt
from the newest stable tag (cached per tag; the footer version is that tag),
/marble/rc/ for a newer release candidate, /apt/ from every published
release — publish-apt.yml folded into it and removed. It runs on a push to
main touching site/ or the notes, after a successful release run, on
release: published, and by hand; the release web lane only produces the
dist-web asset. The install page and the README link the App Store and
Google Play listings.
v2.0.0 — toward 2.0: Phase 6, the Medium (steps 35–46; pre-release v2.0.0-alpha.1), Phase 7, Sound (steps 47–55; pre-release v2.0.0-alpha.2, cut at the 55b fold), Phase 8, Suzu — the synth (steps 55b, 56–59, 59b–59c; pre-release v2.0.0-alpha.3) and Phase 9, Instruments (steps 60–66; pre-release v2.0.0-alpha.4)
Section titled “v2.0.0 — toward 2.0: Phase 6, the Medium (steps 35–46; pre-release v2.0.0-alpha.1), Phase 7, Sound (steps 47–55; pre-release v2.0.0-alpha.2, cut at the 55b fold), Phase 8, Suzu — the synth (steps 55b, 56–59, 59b–59c; pre-release v2.0.0-alpha.3) and Phase 9, Instruments (steps 60–66; pre-release v2.0.0-alpha.4)”Phase 10 — publish; step 67, the documentation (2026-10-06, DECISIONS_9 #1–#5; evidence docs/evidence/step67). The book caught up with Phases 6–9. The operator book gained the five electric operators from the Phase-6 drafts checked against Part V — wave torsion, Chladni (the keys as eddies, the Taylor–Green flow kept as the gesture), the burst with the author’s serendipity note and its Jaffer-lineage line, the spark as the Anod strike on its charge (#88), Chirikov with the 1.25 ceiling — plus the Anod medium and the palette model, and its index now tables fourteen operators by class. A Suzu book of nine chapters mirrors it: the thesis and the DSP class table, the cells (the Gordon–Smith oscillator), the modal voice and the bow, strings and chaos, the winds, the orbit trace, the sound profiles, the patches and knobs, and the lab — each voice family’s live panel embedded through a new <Lab> component, the lab composed at /marble/suzu/ from the same tag as the marble app (DECISIONS_9 #2; lab-core.js’s docs root two levels up; compose-marble.mjs and pages.yml copy suzu-dist). The user guide gained the instruments, the medium, sound, presets and replay pages, and every 1.0 page learned the thirteen layouts, the strip’s Panic and Next, the fingering panel, the S Pen’s limit, the prints ledger, the two media, the web’s overlays and ?replay=; the settings reference is rewritten from the desktop window’s twenty sections; the architecture book gained the replay page and hostmpe’s fingering engines. The MIDI chart gained the fingering CCs, CC 120/123/122, a Voxo input section and two output sections verified against the step-63 iPad logs now in tests/fixtures/bytelogs/ (chart_check.py: 52 ok). Citations: the electric medium’s and the synth’s papers, Moser and Gordon–Smith among them; thanks with Professor Jaffer first. check.mjs requires all seventeen scenes and the six lab pages embedded, admits no other iframe, and reports a pending gallery replay. The gallery’s three new cards (an Anod piece, a trumpet piece, a bowed Suzu piece with the trace) are pending with their replay links — the recordings and captions are the author’s, after the book (#4). Every [ITERATE] of the four specs is resolved by an entry or documented as a limit on its page (#1, the table). The author’s call on the live site (#2): the first v2 release candidate ships the book to the testers — pages.yml embeds /marble/rc/ and names the RC in the footer while an RC is newer than the stable, the lab’s docs root is read from its own address, and the gallery’s nine 2.0 cards are “coming soon” until the captures land. The author’s two fixes on the book (#6, #7): the medium toggle on every scene, permanent from the first strike — and every scene’s strike is the medium’s, the spark on its charge under Anod (the Anod scene no longer switches at the end), and the first v2 RC’s red Windows lane — MSVC has no clock_gettime; the deviceless Voxo backend reads the performance counter there, proven by the push’s Windows lane before the next RC — which failed again: the Windows box takes it (HANDOFF_WINDOWS.md), and a second hand-off, HANDOFF_SUZU_VCSL.md, briefs a later session on fitting Suzu’s patches to the Versilian Community Sample Library. The install page no longer says the app makes no sound of its own. The author’s two asks on the fitted sounds (#10, #11): Voxo 0.16.0 owns a patch table — the tablets’ five and the six VCSL fits, one list for the desktop’s new Suzu patch picker (with a Suzu attack knob) and both tablets’ Sound pages, pinned to presets/*_vcsl.json by a test gate — and the orbit trace became a shared host library (trace/, the desktop’s step-59 bridge under a pure-C surface) with the tablets’ Sound pages gaining the Synth trace rows: the orbits into the water, the scope on the canvas. The RC’s Linux lane (#12): the deb check expected v1.0.0’s eleven files and the deb has carried the demo instrument’s eighteen since Phase 7 — the count is 29 now, with the no-header intent asserted directly. The RC’s macOS lane (#13): the synth suite’s two headroom gates read a preempted block on the shared runner as a stall (one 5 ms block in a second averaging 0.3 ms) — they now measure the second up to three times and read the best attempt, every attempt’s worst printed; the criterion stands.
The Windows box on the RC lane (DECISIONS_9 #8; evidence docs/evidence/step67/windows). The second red was three MSVC stops behind the clock, none in the core: the INI loader’s else-if chain of 134 keys past MSVC’s 128 nested blocks (the chain restarts at the Suzu keys), a GCC attribute on the web reference’s exports (the macro is empty where there is no __attribute__), and M_PI in the Suzu suite (defined when absent). Build clean, ctest 12/12 (one unreproduced voxo_tests crash under a parallel compile), the release configure’s --version reading the injected tag, and the D3D11 gates green: the field at the reference tier with step 55b’s numbers, the composite within one step, the replay bitwise on the recording box with the wall-time negative diverging, the storm at 0 XRuns; the live recorder round and the window’s Record by hand.
Phase 9 — the instruments. The stateful probe the ABI has carried since
step 41 is filled: the trumpet and the trombone sound their partials under
the fingering CCs (valves 110–112, the slide 113, global on the master
channel, decoded by the normalizer into the engine’s layout state —
libsumi 1.4.0), then Wicki–Hayden, the strings under three tunings and
the theremin (1.5.0): every named layout ships. hostmpe learned the
widgets — the valve buttons, the positional slider, the brass retune, the
theremin surface, panic, quick-switch, mirroring, the per-device offer —
and the iPad and the Tab play all eight keyed layouts with a fingering
panel (on the Tab in a window of its own, the S Pen and the fingers never
meeting; the Tab’s input reader keeps them apart anyway). Session replay:
a pure-C library beside the presets records the bytes, gestures, settings,
dips and frame boundaries exactly — the producer stages, the render thread
drains — and every shell plays a recording on the scripted clock, the
desktop re-sounding it; bit for bit on the recording device, the Pixel’s
GLES recording on the Mac within the mobile tier, the iPad’s a few percent
of displacement off (the open question of Part VIII #24). The web draws
every keyed layout as an overlay and plays a recording. Decisions:
DECISIONS.md Part VIII #1–#26 (DECISIONS_8 #n). The pre-release tag
v2.0.0-alpha.4 marks the close.
Phase 8 — the synth. Voxo gains Suzu, a source beside the sampler:
symplectic phase-space cells — the leapfrog with its orbit re-based on
retune, the shears, a modal lattice whose coupling is compensated at load
and refused past its bound, a breath bow that is an energy servo, a Verlet
chain under the CFL bound, a hybrid string, a Duffing cell, a kicked rotor
and a chaotic modulator, an acoustic bore with a jet, a reed and a pair of
lips whose gate is the mouth’s energy ledger — every element declaring its
class, every voice shipping its sound profile. The synth draws its own
phase portrait into the water through the gesture ABI, and the scope view
(libsumi 1.3.0, the phase’s one core touch, additive) shows the
polylines on the canvas. Suzu compiled to WebAssembly plays in the browser
as itself: the lab’s six panels, each proven against a native build and in
headless Chrome. The field’s payload became a displacement first (step
55b, libsumi 1.2.0). Decisions: DECISIONS.md Part VII #1–#35
(DECISIONS_7 #n). The pre-release tag v2.0.0-alpha.3 marks the close.
Phase 7 — the app has a sound. Voxo, a sibling library behind a pure C
ABI, plays Decent Sampler presets and libraries from the same MIDI bytes the
visuals draw — velocity layers, round robins, release samples, loops, a
low-pass under the slide, the MPE sources through the preset’s bindings, a
reverb and a delay on the bus, an advisory memory gate, a compat report
whose copy is the docs’ by test — on CoreAudio, AAudio, WASAPI and
PulseAudio; every shell gained a Sound setting, the tablets an instrument
library with imports, and a CC0 demo (the Dan Tranh) sounds at first launch.
The engine (libsumi) was not touched: the field fixture stayed bitwise
trivially. The Phase-7 steps follow the Phase-6 ones below.
Phase 6 — the Medium.
The engine gains a second medium. Five operators join the shared library,
each declaring its class and passing a four-part conservation gate that was
built first; the one ABI break of the arc lands (libsumi 1.0.0) and
everything after it is additive (1.1.0); Anod reads the same field as a
discharge record — strain-glow over black glass, the water drawing the
field’s deformed grid, a bloom — with its own binding table; palettes become
one model with a library and an editor, the substrate gets knobs, presets
get one pure-C serializer shared by four shells, prints re-export at any
size from a ledger; the marble gestures follow the medium; the iPad, the web
page and the Android shell carry the session; and the last mile measured GL,
D3D11 and the Adreno. Decisions: DECISIONS.md Part V #1–#89
(DECISIONS_5 #n). The author’s step numbering: 44a iOS, 44b web, 45a
Linux, 45b Android, 46 Windows.
Step 35 — Phase opening & the conservation gate
Section titled “Step 35 — Phase opening & the conservation gate”DECISIONS_5.md opened with the phase’s resolutions (Anod, four
phases and 2.0.0, one ABI break carrying the probe state early, the operator
families, the fingering CCs, one preset serializer, the phase invariant —
#1–#12). The four-part gate (midi-sink --dev --soak <operator|all>,
--soak-negative, tools/soak_report.py): the step-19 pinch soak
generalised to every pass, driven through its real ctl or gesture route on
the scripted 512² clock — (a) the declared class, (b) ±k pairs invert (mass
±5 %, pre-image within 4 texels; sub-stepped fields: one a/4 step keeps the
Jacobian > 0.5), (c) zero fabrication (≤ 0.5 %, the route proven alive), (d)
erosion ≤ 2× the glide tine’s steady 1.15·10⁻⁵ per pass (#13). Baselines for
the nine v1.0 passes; three negative controls proven red (#14). Found on
the way: the resampled medium gains at ink boundaries under strong pairs
(#15); the crossed pinch’s ±k is not an inverse (#17, a v1 finding, left to
the author). No core change; the fixture bitwise.
Step 36 — Wave torsion (0.10.0)
Section titled “Step 36 — Wave torsion (0.10.0)”SUMI_VORTEX_TORSION: θ′ = θ + A·sin(k·r − φ)·e^(−r/R), r′ = r — a third
vortex profile, exact, reachable through every vortex route (#18); the
wavenumber and phase as controls (CC 104/105, #19); the engine’s first
episode — the note-on torsion sweep, per-voice, time-driven, per-frame
deltas merged under the budget, outliving the note (#20, the pattern the
burst and the spark reuse). The gate’s (c) learned ”≤ 0.5 % or ≤ 5·10⁻⁵ per
pass” with an edge/interior split (#21). Scene torsion; the page draft.
Step 37 — Chladni cellular flow (0.11.0), rewritten before step 43
Section titled “Step 37 — Chladni cellular flow (0.11.0), rewritten before step 43”The author redirected the design three times in review (#23): not the
roadmap’s kick-drift lattice but the Taylor–Green cellular flow on the
layout’s cells, bake only, steadily driven (#24–#25; the author’s
ponderomotive derivation in specs/chladni.md, now in git history). Then,
debugging before step 43, the cells became the eddies (#59–#62): every
display disc the shells draw (the polar lattice for the fifths, imaginary
circles for the rolls, sumi_layout_cells) turns as a ring profile
(4ρ²(1−ρ²))², exact inside each disjoint disc through an RGBA16F index map
at field resolution, the odd cells’ sense from the balance; an emission
floor because a fresh sheet’s first sub-quantum passes round to nothing
(#61); chladni_mode 1 = Inverse Chladni, the eddies summed into one
sub-stepped flow that may grow past the keys (#62). Both modes gated
(--soak chladni, chladni-field), --chladni-test 7/7, the plate guide
on the bench (N), scene chladni, two page drafts.
Step 38 — Viscous multipole burst (0.12.0)
Section titled “Step 38 — Viscous multipole burst (0.12.0)”The derivation checked first (tools/multipole_verify.py, 44 checks: Φ_m
elementary for m ≥ 2, the divergence-free displacement, the hyperbolic
core, the far field, the fold budget β_m — #28–#29) and the literature check
recorded before any wording; then SUMI_DEFORM_BURST, sub-stepped (peak
≤ β_m × the core, the wake’s rule generalised), and the strike with a
lifetime: sumi_add_burst starts an episode whose age ℓ² = a² + 4νt grows
over burst_life, the emission floor the field’s half-float quantum (#30).
--burst-test 9/9, --soak burst 3/3 (erosion ×0.81 of the tine), scene
burst, the page draft with the lineage line and the author’s note.
Step 39 — Spark shear & the composed strike (0.13.0)
Section titled “Step 39 — Spark shear & the composed strike (0.13.0)”SUMI_DEFORM_SPARK: a kick-drift shear with a stack of triangle waves or
hash noise, exact for any profile (#34; the noise an integer hash,
bit-identical across backends); the shear as a decaying episode (#36) and
sumi_add_spark, the composed strike — the drop (the Joule blast), the
burst, the shear — sub-stepped by inheritance (#37); SUMI_CTL_SPARK_K and
slide_mode 2 (#35). The first stream read mass 0: fifteen hundred exact
drops push the ink off the canvas, so the gate’s strikes are blast-less
(SUMI_DROP_NONE, #39). --spark-test 5/5, two soaks 3/3, scene spark.
Step 40 — Chirikov standard map, the boss gate (0.14.0)
Section titled “Step 40 — Chirikov standard map, the boss gate (0.14.0)”SUMI_DEFORM_CHIRIKOV: the kick then the ε-scaled drift, exact, the inverse
undoing the drift first (#40); the throw route (SUMI_CTL_CHIRIKOV_K,
delta-driven: a throw of δ is one step at δ²·K_max, #41). The erosion
sweep: whole-window (d) is red at every K — the drift flushes the
rotating orbits off a non-wrapping canvas (#43) — and after the flush the
ceiling SUMI_CHIRIKOV_K_CEIL = 1.25 is the last K where (d) holds, above
Greene’s threshold (#42, the author’s options laid out). The KAM transition
in scene chirikov. Carried out of the phase: the author’s feel of the
operator (#89).
Step 41 — The ABI event: libsumi 1.0.0
Section titled “Step 41 — The ABI event: libsumi 1.0.0”The one break (#45): sumi_layout_probe gains the layout-state argument,
sumi_cell_info_t its flags, sumi_params_t its medium, sumi_set_palette
and SUMI_PALETTE_CUSTOM (#46), layouts 8–12 reserved; every call site moved
mechanically (the iOS shell compiled in-step, the Android JNI at step 45b).
Behaviour unchanged by construction and proved twice — the field gate for
the field, the new composite gate (--composite-dump,
tools/composite_gate.py, bitwise against composite_512_metal.rgba, red
on a corrupted copy — #48) for the pixels.
Step 42 — The Anod medium (1.1.0)
Section titled “Step 42 — The Anod medium (1.1.0)”The composite branches per medium (#50): the charge glows by the strain the
one-sided-min estimator reads off the half-float field (#56), the seam mask
found not stored (#52), the water draws the field’s deformed grid —
iso-lines of position plus gained displacement, faded below a texel and
where it would alias (#57), anod_pitch with 0 = off (#58) — near-black
glass with the washi’s grain as phosphor speckle, three Anod palettes.
Live switching is a feature (#53); prints are the same machinery,
medium-styled (#54). The binding tables (#51): SUMI_MODE_MEDIUM_DEFAULT
resolves each mode to the medium’s column; the strike, the poly-pressure and
the mod-wheel dimensions are the medium’s outright. The Sumi path bitwise
against the 1.0.0 print fixture; --anod-test.
Step 43 — Palettes, substrate, presets & prints (1.1.0, additive)
Section titled “Step 43 — Palettes, substrate, presets & prints (1.1.0, additive)”One palette path (#63): the built-ins re-expressed as presets through
the same shader path, bitwise (eight FNV-1a hashes), a library of twelve
with the colour-blind-considerate sets (Cobalt & amber, Viridis, Cividis),
the morph ring with the custom slot (#64), the editor in the settings
window with accent and clear-water fields (#65). Substrate knobs — paper
tint, fiber scale, glass darkness, phosphor grain — bitwise at their
defaults (#66). Presets: presets/, the one serializer, pure C11 with
no allocation, JSON schema 1 stamped with sumi_version, unknown keys
ignored and missing keys defaulted (presets/SCHEMA.md), preset_tests
30/30; the desktop’s last_session.json and presets/*.json (#67).
Prints: sumi_read_field, sumi_export_begin/poll up to 8192 a side,
Anod over alpha; the desktop’s print ledger — fields kept per dip,
thumbnails, re-export at Screen / 2K / 4K / 8K (#68). Then the glow: a
bloom after the Anod composite (bloom.glsl, 13-tap down / tent up over
anod_bloom_levels octaves, a shoulder toward white) and the author’s
look — black glass, grain 0.5, glow 0.20, bloom 0.75 over 3 (#69). Then the
author’s table after the hour of playing: the bend stirs the Chladni cells,
poly pressure tunes the wavenumbers (#70); the strike’s flood measured
side by side and anod_drop added, the burst out (#71); the stir found
dead on the desktop — a mapped, silent CC does not own a dimension, last
writer wins; a note lifted while bent stills it (#72). --palette-test,
--print-test, --gesture-test, --anod-strike-render.
Step 44a — iOS shell
Section titled “Step 44a — iOS shell”The iPad holds one session persisted through the preset serializer
(import SumiPreset): every param, the custom palette, the CC map, the
input dialect, the controls, the strip’s wheel assignments —
last_session.json restored at launch, named presets in Files, the 1.0
rows migrated once; the canvas applies the session whole (#73). Pages:
Medium & look (the switch, Palette, Substrate, Presets, Operators),
Expression routing with every 1.1 mode; the print ledger in Swift with
export to Files and the share sheet; “Dip the paper — keep the print” /
“Clear the canvas — discard”; the joysticks, lattice and control strip draw
light on Anod’s glass. A desktop preset loaded on the iPad and written back
byte-identical; the same six strikes printed the same look on both.
Step 44b — Web marble
Section titled “Step 44b — Web marble”The preset serializer compiled into the wasm and a shim so JS never lays
out a C struct (#74): the page’s session is preset JSON in localStorage,
named presets, export and import of any preset — the desktop’s session
round-trips byte-identical through the page (web_gate.mjs --preset, a
permanent gate mode); the panel gains Medium, Substrate, the palette
library and the custom editor, Presets, the 1.1 modes, the dip / clear
copy; check.mjs learns the Phase-6 scenes and cross-checks scenes.js.
Then the marble gestures follow the medium — in the core (#75): five
additive calls (sumi_gesture_tap / _pinch / _twist / _press / _press_end), Sumi bitwise the 1.0 calls, Anod the author’s table — tap the
strike, pinch the burst, twist the torsion vortex, the long press the
torsion feed and, pulled, the Chladni stir; every shell calls them.
Step 45a — Phase 6 on the Linux desktop (GL)
Section titled “Step 45a — Phase 6 on the Linux desktop (GL)”Built from scratch: GCC refused a C++-linkage forward declaration defined
inside extern "C" (#76); the print ledger ran the core from the settings
window’s foreign GL context — dip and export deferred to tick() (#77).
The field gate exactly Part IV #44’s numbers; the composite one 8-bit step
darker on 2.4 % of samples — NVIDIA’s washi arithmetic, a GL tier proposed
(#78). Everything else green; the ROLI over ALSA left to the author.
Step 45b — Android shell
Section titled “Step 45b — Android shell”The 1.1.0 JNI compiled and ran unchanged on the Tab (no sokol validation
errors for the bloom, the cells or the export on GLES). One session,
natively — a sumi_preset_t in the JNI, Kotlin editing it with JSON
patches through the one serializer; the sheet’s pages, the ledger with
MediaStore and the share sheet, the gestures, the play surface on the
glass, the 0.x migration, the 16 KB check (#79). The desktop preset
round-trips byte-identical; the six-strike print did not match — the
Adreno’s spark tearing drifts (#80); a host spark quantum was built, played
and rolled back at the author’s word (#81).
Step 46 — Phase 6 on the Windows desktop (D3D11)
Section titled “Step 46 — Phase 6 on the Windows desktop (D3D11)”MSVC refused a constant-folded NaN in the lab bench (#82); the field gate
bit-identical to Step 11’s own dump (never bitwise with Metal on this box —
Part IV #35 corrected, #83); the composite GL’s one step to the pixel; a
CRLF settings.ini had poisoned the print folder — the loader drops the
carriage return (#84); UTF-8 paths through the narrow CRT via the
application manifest (#85). The ROLI simulated over loopMIDI; the
installer round-trips.
After 46 — the fixes the author asked for
Section titled “After 46 — the fixes the author asked for”A failed background PNG write reports its outcome in the settings window,
never only on stdout (#86). The GL / D3D11 measurements live in the tools:
composite_gate.py --backend with its tier, --palette-test with a hash
column per backend — no test red “by design” (#87). The Adreno drift
measured to the bottom — frame-locked, at the same field: two fragments
against six charges; a manual highp bilinear and an RGBA32F field tried and
rejected (the drift is the Adreno’s shader arithmetic) — and the Anod
strike made the classic spark on its charge: drop, burst and shear on the
charge radius, anod_drop 0.57; the Tab keeps all six charges (#88). The
field stored as a displacement, the real lever, goes on the roadmap (#89).
Step 47 — Voxo skeleton & the desktop backend
Section titled “Step 47 — Voxo skeleton & the desktop backend”voxo/ opened (DECISIONS_6 #1–#6): voxo/include/voxo.h pure C with the
callback-thread contract at its top (no allocation, lock, log or blocking
call on the audio thread; the ring drained once at block start; the shell’s
settings through atomics), voxo/src/ C++20 compiling
core/src/midi_normalizer.cpp from source as the second SPSC ring, a
16-voice pool keyed by (channel, note) with a sine per voice, miniaudio
0.11.25 (one implementation TU; the frameworks linked on Apple) on the
default output; the block-size table’s first row (macOS 128 as asked);
tests/voxo_tests.cpp with a counting global allocator asserting zero
allocations across a storm, tests/voxo_c_compile.c; the desktop’s harness
tap fanning every byte into Voxo, the Sound section (switch, volume, status
line), --dev --voxo-storm <s> as the ROLI’s proxy — 30 s at 128 frames, 0
XRuns, render max 0.065 ms. The author’s hands-on: sound2midi MPE, “really
nice”. Android moved to the Mac (the Tab plugged in).
Step 48 — The mobile latency spike
Section titled “Step 48 — The mobile latency spike”Voxo 0.2.0’s latency probe (note_ons, last_note_on_seconds,
voxo_now_seconds as the one clock) and the backend’s own numbers (burst,
buffer, the platform’s underruns, the timestamp-derived output latency, the
low-latency verdict). Android (#7): miniaudio’s AAudio path granted
LOW_LATENCY with 192-frame bursts on the Tab; the default eight-burst
buffer set to two — output latency 55 → 29 ms, 0 AAudio underruns under the
visual storm, touch-down → callback 3.1 ms median; miniaudio confirmed, the
tuner carried to 54. iOS (#8): the shell owns the AVAudioSession; the
iPad granted 48 kHz and exactly 128 frames, 9.7 ms session output latency,
0 XRuns under the canvas storm; miniaudio confirmed. The table’s mobile rows
measured (#9). The Mac’s microphone beside each tablet heard the tones.
Step 49 — Voice dispatch & pitch: it glides
Section titled “Step 49 — Voice dispatch & pitch: it glides”Voxo 0.3.0: the voice a sample player — voxo_set_sample, the ratio
2^((note − root + bend)/12) recomputed per block and ramped LINEARLY per
sample (a one-pole toward a stepped target had left a block-rate ripple),
4-point Hermite with linear as the lab’s comparison, the sample swapped into
the callback by a pending → current → retired protocol (#10–#11); Local
Control tracked, applied by the shell (#12). The glide check: --dev --voxo-bounce renders a six-partial tone recorded at 12 kHz (its top
partial at 0.22 of its own Nyquist) across the full ±48 sweep and two static
holds, uv run tools/voxo_glide_check.py (numpy through uv, a
Blackman-Harris window, the partials’ bins widened by each frame’s
excursion) — Hermite −61.8 / −62.4 dB on the holds, −59.9 dB worst on the
sweep, linear 12.8–14.8 dB worse; at 0.44 of Nyquist only 7 dB apart, the
recorded limit. The desktop’s WAV sample row (wav_io); the author’s piano
sample under the ROLI, “works great”.
Step 50 — Decent Sampler subset & the compat report
Section titled “Step 50 — Decent Sampler subset & the compat report”Voxo 0.4.0 (#13–#15): pugixml over .dspreset (the cascade <groups> →
<group> → <sample>, zones, ADSR, loops, sequences, triggers, effects,
<midi> and <modulators> sources, the UI controls’ starting values),
miniz over .dslibrary, dr_wav / dr_flac and an AIFF reader of our own
(the stock Basic Piano ships AIFF — the [ITERATE] answered), all
compiled into the archive; the compat report — the summary line, then one
canonical sentence per note in a documented order, voxo/COMPAT_REPORT.md
its copy asserted verbatim by test; refusals only for what is not a
preset. Fixtures under tests/fixtures/dspresets/ (every feature, the
formats, a zip, malformed inputs), voxo_preset_tests, voxo_fuzz — an
hour of mutation fuzzing: 24 960 464 loads, no crash. The six libraries on
the Mac loaded (the Bösendorfer’s 1 580 zones in 0.9 s; the Spellsinger’s
928 MB in 4.3 s); the desktop’s Instrument row with its report; the zone
under middle C as the sound until 51.
Step 51 — Layers, round robins, release samples, loops, filter, smoothing & bindings
Section titled “Step 51 — Layers, round robins, release samples, loops, filter, smoothing & bindings”Voxo 0.5.0 (#16–#18): the parsed model compiled into what the callback
plays; a performance voice stacking up to eight layers — velocity layers
crossfading at equal power over overlaps, round robins by sequence
position, random, release samples on note-off — each with its ADSR, its
loop with an equal-power crossfade (a 3 s pad holds thirty seconds), a
2-pole state-variable low-pass; pressure, the slide (CC 74) and swirl
smoothed by the preset’s rising/falling times, mapped through the preset’s
<mpePressure> / <mpeTimbre> bindings or the defaults (CC 74 sweeps the
cutoff three octaves either way); <cc> and <velocity> bindings, the UI
starting values, TAG_VOLUME. Fifteen voices of six stacked, looping,
filtered layers under a storm: 0 allocations, 0.225 ms per 128-frame block.
Step 52 — Bus reverb & delay, preload gate
Section titled “Step 52 — Bus reverb & delay, preload gate”Voxo 0.6.0 (#19–#21): Freeverb and a feedback delay after the sum (buffers
allocated once per rate; the preset’s parameters, knob defaults and <cc>
bindings), the advisory gate — the decoded size read off the sample headers
before decoding against the shell’s advice (the desktop’s free memory
× 0.6), a memory note first in the report and the load proceeding — the
desktop’s worker-thread load, the demo instrument’s slot. The storm with a
looping pad through both effects: 0 XRuns; the Bösendorfer over a 100 MB
advice warned and loaded.
Step 53 — iOS
Section titled “Step 53 — iOS”SoundController.swift (#22–#27): the AVAudioSession, the device with the
scene phase (foreground only), interruptions and route changes, the
instrument library in Documents/Instruments with the Files import (a
.dslibrary or a preset’s folder; the Decent Sampler types declared so
AirDrop and “Open in” hand libraries over), the load on a background queue
behind os_proc_available_memory, the Sound page, Local Control tagging the
canvas’s own bytes. The demo instrument is the VCSL Dan Tranh, CC0 —
tools/fetch_dan_tranh.py makes the bundled demo (16 samples, 2.9 MB) and
a three-layer test library. The author’s first hands-on found MPE’s zone
missing in Voxo (the master’s pedal, bend, pressure and slide now reach
every member, #25) and the dialect never mirrored into Voxo; the second, the
plist’s missing document types (#26); the third, the play surface’s reach
(cells outside the instrument hidden) and the bus in the CC map (six
targets from 1000, #27); then a settings sheet re-rendering every second
(its timer rows isolated). The plist keeps the audio background mode
CoreMIDI needs, flagged against SOUND §4 (#23).
Step 54 — Android
Section titled “Step 54 — Android”Sound.kt (#28–#29): the settings, audio focus (stop on loss, start on
gain), the foreground-only lifecycle, the instrument library with the
Storage Access Framework’s document and tree pickers and “Open with”, the
demo installed from the APK’s assets, the gate on the activity manager’s
available memory, a once-a-second tick that paces the AAudio buffer tuner
(one burst per underrun) and restarts a dropped device; the Sound page, the
bus targets in the CC map, the reach; the JNI’s local flag. On the Tab
with the demo playing: touch-down → callback 2.75 ms median, output latency
29–31 ms, 0 AAudio underruns — the step-48 bar. Then the author’s hands: the
strip and the cells could not be touched together (three Compose-hosted
views; one native frame now, #30), the CC map’s rows became two-way
pickers, and routes to the bus were dropped by three pre-Voxo range checks
on the Tab, the desktop and the iPad (#31).
Step 55 — Desktop shells & the combined stress
Section titled “Step 55 — Desktop shells & the combined stress”The acceptance suite (#32): --voxo-preset <heavy library> --voxo-storm <s>
— pass = 0 XRuns, 0 dropped, the visuals at or above 58 fps, the period
reported (the Mac with the Bösendorfer: 97.5 fps, 0 XRuns). The Linux
install carries the demo (share/midi-sink/demo). The Linux box (#33–
#38): PulseAudio on PipeWire’s shim, the callback at the asked 256; the
suite three times green at 60.0 fps once the GL host paced its loop to the
display (#37; it had free-run at 103–121 fps, contending with the pulse
protocol thread); the audio worker at SCHED_FIFO 70 (#35); GCC’s -O2
eliding the allocator’s negative control and a divide-by-zero on a 6-bit
WAV header found by the fuzzer, both fixed (#36, #38); five of the author’s
libraries loaded, one (FlixDrums) refused for two attributes with no space
between them; the stream followed a default-sink switch without a stop; the
.deb carries the demo. The Windows box (#39–#41): WASAPI shared mode’s
480-frame engine period against the 256 asked, the callback kept at 256 and
the XRun proxy taught the device’s period (6 120 false XRuns before, 0
after; 164 fps); fixtures made byte-exact against CRLF checkouts (#40); the
stream followed a default-endpoint switch without a stop; the zip lane and
the installer now carry the demo (#41). The glide check passes identically
on every desktop. Output-device selection was not built: the default
follows (#42).
Phase close — 2026-09-28
Section titled “Phase close — 2026-09-28”DECISIONS_6 #42: the fold (Part VI, this section, ROADMAP Part 6,
specs/TO_PROJECT_SPEC.md §12 with specs/SOUND_SPEC.md out of the
tree, the spike runners to tools/voxo_spike/,
the WASAPI helpers to tools/windows_wasapi/, the licensing page drafted
in site/drafts/voxo/), and what carries to the author: the tag
v2.0.0-alpha.2, output-device selection on the open roadmap as a
Phase-9 nice-to-have (step 65), WASAPI’s
period and the Pulse readouts, the demo’s own recording if wanted, SOUND
§4’s background-mode line.
Step 55b — The field as displacement (libsumi 1.2.0)
Section titled “Step 55b — The field as displacement (libsumi 1.2.0)”DECISIONS_7 #1–#2. The field’s texel stores its DISPLACEMENT (u − x,
v − y, ink, aux) instead of its pre-image: every pass computes the same
inverse lookup and re-bases what it reads through one helper, the identity
and the ingress rule write zero, and the half-float quantum of a stored
value follows the displacement (2^-24 canvas heights at rest, 2^-15 at a
sixteenth of the canvas) instead of the coordinate (2^-11 over the outer
half of the sheet). Ink and aux never depended on the coordinates: the
re-captured field_512_metal.bin is bitwise the old fixture in both, the
pre-images agree to the old rounding (1.6e-3), and the Sumi print fixture
did not move; the Anod composite reads J = I + ∇d and its four palette
hashes were recaptured for Metal. One semantic kept on purpose: the passes
without an ingress branch re-base with the sampler’s clamped coordinate, so
the rim is the 1.1.0 rim. The web tier’s distance from the Metal fixture
fell from ~6e-4 to 3.9e-9 mean. The emission floors are re-derived from the
displacement’s quantum (2^-13, a quarter of the old step); #61’s experiment
repeated with no floor turns the stir’s ring 1.36 rad where the old payload
turned 0.07 — the rounding-back is gone, the floors stay as a pass economy
(the trade measured: three times the passes, six percent less rotation,
twice the radial scatter; the author’s knob). The soak’s (b) pre-image
tier goes from 8 to 32 texels (#3): the 8 was the coordinates’ freeze —
the resampler’s per-pair smoothing rounded back under a 2^-11 quantum —
and the displacement keeps it honestly (the exact operators 9–22 after
500 pairs, the non-inverse control 73, the ink mass bitwise). The Tab
(#4): its field’s coordinate channels 14× closer to the Metal fixture
(mean 7.9e-5, was 6.1e-4), and #88’s six-strike script under the author’s
own session prints the same six charges on the Tab and the Mac within
10 % — under the harsh shear where two had thinned to 1.3 k and 0.8 k px,
none thins; and #71’s thin strike, run through a lab switch, keeps
six charges on the Tab at both shears (six fragments at 45 % of the
Mac’s lit share under the harsh one, where the coordinates had left two
at 0.02 %). The thin strike is the strike again (#5, the author’s
call): the small charge (anod_drop back to 0.33) and the shear on the
full Sumi radius, no burst, in the mapper and the marble tap alike; the
switch is gone. Every harness suite, both gates and the soak green on
the Mac. The boxes (#6, #7): the field gate on GL and on D3D11 against
the new Metal fixture reads the same numbers to four digits — dx 2.4e-4,
dy 3.7e-4, ink 3.9e-3 (the ink’s D3D11 deviation unchanged, ink being
bitwise), mean 2.26e-6 — where GL sat at max 1.5e-2 / mean 6e-4 under the
coordinates (the mean 270× down, the same silicon’s arithmetic through two
APIs); the composite gate holds its one-step tier on both; the Sumi hashes
matched untouched and the Anod four are recaptured in both columns; the
soak at the 32-texel tier is 47 of 48 on both, the exact operators’
pre-image deviation 8.1–22.0 texels, the crossed pinch the known red, the
table the Linux box’s to the second decimal on Windows. A bench finding on
D3D11 (#7, the author’s): the paper dip’s readback lands after 3, 4 or 5
frames, so an Anod print — whose state runs on the clock — is frame-
dependent there (the palette test’s Anod cases hash to their column four
runs in five; the gesture test’s pairs differ by a payload ULP) — the
proposed edit pads the dip to a fixed frame count on every backend, with a
Metal recapture. The author, with the build on both tablets: “they look as
expected.” The pre-release tag v2.0.0-alpha.2 (the author’s, cut at this
fold — the tag the Phase-7 close named and never cut, so it covers Sound and
the displacement field together; alpha.3 stays Phase 8’s end) marks the
displacement field; its notes are this v2.0.0 section. The fold: the
evidence (docs/evidence/step55b/, the boxes’ gate-55b/) left the tree
with this entry as its condensation — git history keeps it —
tools/strike_compare.py outlives it.
Step 56 — Suzu cells (Voxo 0.8.0)
Section titled “Step 56 — Suzu cells (Voxo 0.8.0)”DECISIONS_7 #8. Suzu, the symplectic phase-space synth (SYNTH §1–§2.4,
§4), as a source beside the sampler inside Voxo — the same callback, voice
model and bus; the desktop’s Sound section gains the source row. The cell
is the magic-circle leapfrog with exact tuning (0.000 cent across MIDI
21–108 at 44.1 k and 48 k), its orbit re-based on every retune so a
glide does not amplitude-modulate (0.097 dB over a ±48-semitone sweep;
the plain form’s one-block jump 0.63 dB beside it), a phase-space shear
for harmonics, the Chamberlin SVF on CC 74 with its resonance the declared
dissipation — tuned for the damped ringing frequency, 0.008 cent — all in
a 2× section; the release a declared contraction; every element with its
class in voxo/src/suzu.h’s table. The drift test holds (ten minutes,
+0.0001 dB, 0.000 cent; the naive update proven red), FTZ/DAZ is set on
the rendering thread and the 64-voice decay tail stays flat, sixteen
voices cost 3 % of the callback. tests/voxo_suzu_tests.cpp is the suite.
Then the author’s ask (#9): every synth ships with its SOUND PROFILE — the
bench’s --voxo-profile and tools/sound_profile.py draw the level across
the keyboard, the harmonics per note and a spectrogram of every note struck
offline — in the evidence and drafted for the docs; Suzu’s bass reads flat
to 0.09 dB (the ear and the speaker are what vary), the Sound section
gained the patch’s knobs, and the shears were made forces scaled with ε
(the fixed kick sent every note below C3 to NaN), normalized by the orbit’s
amplitude and self-calibrated for their detune at create (0.19 cent at full
gain, from 110 sharp).
Step 57 — The modal voice & the breath bow (Voxo 0.9.0)
Section titled “Step 57 — The modal voice & the breath bow (Voxo 0.9.0)”DECISIONS_7 #10–#12. Suzu’s voice becomes a LATTICE of cells (SYNTH
§2.5): up to sixteen modes at a preset’s ratios — harmonic string, stiff
bar, bell, glass, and the plucked string as Karplus–Strong in modal form
(f_k = k·f₀·√(1 + Bk²), the pluck position’s sin(kπp)/k² kick profile) —
each with its declared decay (γ_k = α + β(r_k² − 1): the fundamental’s T60
is the patch’s, the highs die first), coupled by the shared-potential kick
computed from the pre-update positions (the chain’s Laplacian, κ relative
to the lowest mode’s stiffness, the swirl 0xA0 adding up to 0.5). The
coupling’s detune — 149 cents on the first lattice — is compensated
exactly: at patch load the coupled chain’s normal modes are solved
(Jacobi + Newton) and each mode’s own stiffness set so every partial sits
on its ratio, tabled over κ and corrected per pitch to first order (0.08
cent across C2–C8 at the default κ and with the swirl at full; 0.76 at
the far end); the load gate rejects a patch whose coupling would take a
mode’s own spring or whose lattice reaches the sampling bound anywhere in
MIDI 21–156, with the message on the log, and voxo_suzu_coupling_bound
names the patch’s limit. The energy ledger holds (the zero-decay bell ten
minutes within 0.002 dB; restored T60s within 0.1 %); mode splitting is
charted (two cells against the joint map’s normal modes, 0.01 %); the
sampling bound’s red control blows up on the primitive at 5 % over, the
compensation’s at 21 cents off with the gate bypassed. The BREATH BOW
(§2.6): an energy servo per mode toward the breath’s target (CC 2, or the
patch’s, CC 11 as alias), the onset its time constant, bowed only while
breath is held — from silence and from 2× alike (4.9 τ / 1.7 τ), zero
breath to silence (−66 dB in 2.2 s, the declared decay alone), the servo’s
ledger balanced to 0.000 %, the give-only control red (10³× in 0.43 s);
a mode sings under the bow only if its decay is slower than the onset
(the reach — the first three partials at the defaults). Patches ride the
QoL preset file (suzu), the desktop’s Sound section carries every knob,
the bench profiles each preset and the bowed voice (--voxo-suzu-preset,
--voxo-suzu-breath), the storm passes twice at 96 fps with 0 XRuns, and
sixteen voices cost 3.8 % of the callback. The binding table as built is
#12, the author’s to sign by ear; the Brisa’s singing tone is the
author’s judgement.
Step 58 — Strings & chaos (Voxo 0.10.0)
Section titled “Step 58 — Strings & chaos (Voxo 0.10.0)”DECISIONS_7 #13–#17. Four voices join the cells and the lattice, each
with its class and its gate. The VERLET CHAIN (SYNTH §2.8): a mass–spring
string that tunes itself under the CFL bound by shedding nodes per note (48
to C6, 10 at C8; 0.000 cent across MIDI 21–108), its forced k·dt² over 1
rejected at patch load and the bypass blowing up within a second — the red
control. The HYBRID STRING (§2.9): a lossless delay (a ring, a Thiran
allpass) into a bridge of cells through a scattering junction whose balance
is exact per sample — the midpoint rule on the cell’s synchronized
velocity, after two forms that pumped energy — with the bridge’s reflection
phase solved in closed form and folded into the delay so the fundamental
stays on the note (0.144 cent) while the partials feel the body; passivity
gated twice: the load-time probe (the bound on the reflection’s gain reads
1.0016 above the conserving 1; 1.05 rejected with its message) and the
ten-minute soak with every declared damping zeroed (−0.018 dB), the
bypassed 1.05 going non-finite in 31 s. The DUFFING CELL (§2.10): the cubic
on the position — C4 at velocity 127 clangs 258 cents sharp and settles on
the note as it decays (charted); driven by the press it bifurcates at a
drive of 0.87 (charted). The KICKED ROTOR (§2.3): the standard map as an
oscillator, its momentum the pitch on the torus, kicked once per nominal
cycle, K from the mod wheel delta-smoothed — the pure tone at K = 0, the
island’s libration, the band widening past K_c (the K sweep charted beside
the visual Chirikov’s). The CHAOTIC MODULATOR: a double pendulum at control
rate — RK4 with its energy projected, since a leapfrog is not symplectic
for it and drifted — its energy set by each strike’s velocity, bounded,
routed to one smoothed parameter. And the LAYERED source: the sampler and
Suzu sounding together, for the combined stress — the fifteen-channel storm
with the author’s Bösendorfer library under the Verlet chain, 0 XRuns, 0
dropped, 96 fps, twelve layered voices. Ten strings at 80 nodes cost 21 %
of the callback (the SIMD layout stays an [ITERATE]). The desktop’s Sound
section carries every knob, the preset file the fields, the bench the
voice kind and the charts; the four profiles and the three charts are in
the evidence and the drafts.
Step 58b — The bore & the jet: the flute (Voxo 0.11.0)
Section titled “Step 58b — The bore & the jet: the flute (Voxo 0.11.0)”DECISIONS_7 #18–#19. The first wind. THE BORE (SYNTH §2.11): Webster’s
horn system on a staggered leapfrog grid — the chain in acoustic variables,
S(x) per node, the same CFL bound derived by power iteration and gated with
its red control (1.05× the bound blows up in 57 sub-steps), pitch by bore
length with λ absorbing the fraction. The three geometries’ series measured:
the closed–open cylinder on the odd harmonics and the open–open on the
integers within 0.0 cent, the cone within 8.7 (a truncated cone’s stretch,
counted to its apex); the closed lossless bore holds ten minutes within
0.0003 dB in the staggered energy the leapfrog conserves exactly. The open
ends are declared radiation ports whose loss rises with frequency (a
positive-real highpass) and whose reactance is an end correction the tuning
counts (81 cents until it did). THE JET (§2.13): the flue’s acoustic
displacement, delayed by the jet’s travel time, through a receptivity band
whose centre rises with the breath, partitioned at the labium by a tanh,
driving the bore through the labium’s dipole — a pressure port limited per
sample to the mouth’s own work, so the mouth-power ledger holds by
arithmetic (0.06 % of ∫P_mouth·Q_in stored on a scripted phrase). Nothing is
programmed to overblow: on a breath ramp at A4 the tone rises from −287
cents through the note to +257 and jumps to the octave within 11 cents by
itself (charted), and soft blowing flattens 36 cents. In this first version
the embouchure follows the note — the jet’s delay in periods, its gain with
the pitch, its area with the inverse root — so one breath plays the
keyboard (C2–C7 within 8 dB, every note sounding, gated); the bore carries
a declared wall loss so the bass speaks at once. Voice kind 6 with its
knobs in the desktop’s Sound section and the preset file; the profile and
the ramp chart are in the evidence and the drafts. The press blows the
winds and the bow as breath does (a switch, on by default): a controller
without a breath CC plays them.
Step 58c — The reed & the lips: the saxophone and the trumpet (Voxo 0.12.0)
Section titled “Step 58c — The reed & the lips: the saxophone and the trumpet (Voxo 0.12.0)”DECISIONS_7 #20–#23. The two valve winds. THE VALVE (SYNTH §2.12): one
kick–drift cell with a pressure force — inward for the reed, outward for the
lips — its Bernoulli aperture solved implicitly against the bore’s one-step
impedance (a quadratic in closed form), its swept volume and its energy
counted in the mouth-power ledger, the reed stopped at the lay; the
load-time probe blows the closed loop twice with the bore’s losses zeroed
and a retune halfway and asserts the ledger (the naive explicit junction
reads 1e300× the mouth’s work and is rejected; the ABI’s red control goes
non-finite within a second). THE SAX (§2.11’s cone): normalized at the reed,
a mouthpiece holding the missing apex’s volume (Benade’s rule — the pinned
cone’s peaks on the integers within 0.1 cent), truncated at a quarter of its
length: a direct impedance measurement showed a deeply truncated cone’s
first peak is its weakest and the reed had taken the third register; its
bell corner and wall loss scale with the note, its reed is quasi-static, its
intonation is calibrated at C3, C4, C5 and C6 when the patch loads (the
period as the measure, the pull lerped between). Every third semitone C3–C6
within 5 cents at breath 70; every onset of 104 in its first register;
C2–C7 within 7 dB; it speaks from a light breath (4–10 of 127, as the
trumpet does — a first map kept it silent to half breath, the author’s
finding) and its level follows the breath over 10 dB, a declared dynamics
where the beating reed’s own gives under 2. THE TRUMPET: a cylinder with a Bessel flare, the note on
the bore’s measured third peak, the lips at 0.95 of the note bent an octave
either way by CC 74 — the register key is the embouchure: 0 → the peak below
(−772 cents on all thirteen notes C3–C6), 127 → the octave (+1276 to
+1296); a declared bell shear for the brass. Voice kinds 7 and 8 with their
knobs in the desktop’s Sound section and the preset file; a released wind’s
mouth goes to zero (a lingering half-reference blow sang into the next
note); the profiles and the two charts (the sax’s breath ramp, the trumpet’s
embouchure staircase) are in the evidence and the drafts. The bore’s step is
fused with its damping and multiplies by a stored 1/S (a third of its cost).
Step 59 — The orbit trace & the phase close (Voxo 0.13.0)
Section titled “Step 59 — The orbit trace & the phase close (Voxo 0.13.0)”DECISIONS_7 #24–#26. The synth draws itself. Each traced voice keeps the
last 85 ms of its orbit in a lock-free ring (the cell’s (x, y); the
lattice’s sum; the Duffing cell’s and the rotor’s own; the strings’ and
the winds’ output against its scaled derivative — the phase plane of the
sound); a poll from the shell takes what is new, decimates it
curvature-weighted to a few segments and hands it over unit-normalized with
its amplitude — the rendering is bit-identical with the trace on or off
(gated), a mask of 0 captures nothing. THE GESTURE ROUTE, the shell’s
bridge and no core change: the orbit lands at the note’s cell centre from
the layout probe’s table as tine (exact) or wake (sub-stepped) segments
through the existing gesture ABI, budgeted at 24 a frame over all voices
with the overflow merged within each voice — ten rotor voices under a held
press peak at 20 with the mapper’s own budget untouched, and the field
with the trace OFF is bit-identical to a run with no trace at all. THE
SCOPE lives in the Sound section as a miniature of the canvas (the canvas
window is the core’s swapchain; the on-canvas overlay waits for a core
reopening). Per voice kind, the kicked rotor on by default; the demo — the
rotor scribbling its chaos into Anod as the wheel sweeps K, the synth
drawing its own phase portrait in ink — is on video in the evidence. Found
for the author: Anod’s strikes are not bit-reproducible run to run through
the gesture ABI (Sumi’s are), so the toggle’s gate runs in Sumi. THE SCOPE
VIEW (libsumi 1.3.0, #27, the author’s ask): the live composite draws the
shell’s polylines screen-locked — over the water, or alone on the scope’s
dark glass with the medium hidden (“Suzu trace on the canvas”) — the print,
the export and the fixtures untouched (gated bitwise); sumi_set_scope,
additive. The phase end moved to step 59c when the author added 59b–59c
(2026-10-04).
Step 59b — Suzu on the web: the engine in a worklet, the flute panel (Voxo 0.14.0)
Section titled “Step 59b — Suzu on the web: the engine in a worklet, the flute panel (Voxo 0.14.0)”DECISIONS_7 #28–#30. A step the author added: the synth’s pages play the
real engine, not JavaScript sketches of it. All of Voxo compiles to a
standalone WebAssembly module with no audio device — 122 KB, no imports —
and an AudioWorklet owns it: it renders every 128-frame block and posts the
orbit trace and a new inspection of each voice’s state (the bore’s pressure
along its length, the jet, the strings, the valves, the modes — additive)
to the page by messages, so a static host serves it as it is. One engine
module drives both the worklet and a node gate, which compares the wasm
with a native build of the same surface record by record: seven voice kinds
bit-identical, the rotor’s chaos included; the lattice under a glide, the
flute and the trumpet within −80 dB, where the platforms’ math libraries
differ in the last bit. The flute panel draws the bore’s standing wave and
its envelope — one half-sine on the note, two humps with a node in the middle
on the octave — the jet swinging across the labium, the sound’s phase plane
and spectrum, and the pitch it hears. In Chrome the breath ramp overblows by
itself at breath 116 and lands 4 cents off the octave, gated headlessly. Served
locally by tools/web_serve.py --dist build-web/suzu-dist; the pages join the
site at the docs step. The roadmap gained 59b and 59c; the phase now closes
at 59c.
Step 59c — The Suzu lab: every voice family (Voxo 0.15.0)
Section titled “Step 59c — The Suzu lab: every voice family (Voxo 0.15.0)”DECISIONS_7 #31–#34. Every Suzu voice now has a lab panel in the browser,
playing the real engine: the cell and its shears (the orbit, the conserved
size, the harmonics combed in); the modal voice and the bow (each mode’s
energy at its ratio, the bow’s target and where the servo settles, the
coupling against the load gate); the strings (the Verlet chain, the hybrid’s
ring and bridge, the modal pluck, A/B by one switch); Duffing and the kicked
rotor (the Chirikov section filling the plane past K_c, the clang); the flute
(59b’s, moved onto the shared core); the saxophone and the trumpet (the
bore with its valve, the valve’s portrait, the mouth-power ledger as a live
meter, CC 74’s register staircase). A front page lists them. Each page’s red
controls switch the engine’s own lab parameters (the naive cell, the CFL
number forced over, the naive junction, the bypassed gates), so a reader
watches the gates’ reasons go red; the worklet mutes any quantum at full
scale and restarts the engine on a blow-up, because Voxo’s clip would
otherwise hide a runaway as a full-scale wave. Voxo 0.15.0 (additive): the
trace’s density and its recent points at full density with two aux channels
per kind, and the winds’ ledger in the inspection (the mouth’s work and the
energy held; the held never exceeds the work, gated). Every page’s
scripted check passes in headless Chrome, and the node gate covers every
kind, the recent trace included. Found and recorded for the author: the
bow is a proportional servo, so a mode settles at E/E_t = 1 − γ_k·τ, not on
its target (measured within 0.38 dB); the sax at A3 sounds quasi-periodic
about 18 cents sharp at the playing breath, natively and in wasm alike (an
engine [ITERATE]). The pages join the site at the docs step. The phase
closed the same day (below); the evidence (docs/evidence/step59c/) left
the tree the day after its commit — git history keeps it at 5111748.
Phase close — 2026-10-04
Section titled “Phase close — 2026-10-04”DECISIONS_7 #35: the fold (Part VII, this section, ROADMAP Part 7; the
evidence of steps 56–59b to git history at df110c9, step 59c’s at
5111748; the chart scripts and the Suzu gates already in tools/;
specs/SYNTH_SPEC.md folded into specs/TO_PROJECT_SPEC.md §13 and out of
the tree the day after, 2026-10-05, on the author’s word, with the
standalone visual drafts visuals/ the lab superseded — git show 5111748:specs/SYNTH_SPEC.md), and what carries to the author: the tag v2.0.0-alpha.3
and the devices played, the sax’s quasi-periodic A3 (#33), the bore grid’s
damping (#30), the D3D11 dip (#7), Anod’s run-to-run reproducibility for
the core’s reopening (#25), the entries’ [ITERATE]s (#12–#16, #19, #23),
the web/desktop bit-identity question (#29), the lab’s address, red
controls and browsers (#34), a unison preset for live mode splitting (#34).
Step 60 — Stateful layout cores: the trumpet and the trombone (libsumi 1.4.0)
Section titled “Step 60 — Stateful layout cores: the trumpet and the trombone (libsumi 1.4.0)”DECISIONS_8 #1–#4 — Phase 9 opens, DECISIONS_8.md. The first two
instrument layouts, and the first STATEFUL ones: the trumpet’s eight
partial cells and the trombone’s seven are the harmonic series a B♭
instrument speaks, and a cell sounds its partial minus what the hands do —
the three valves’ offset (1 = −2, 2 = −1, 3 = −3, summing) or the slide’s
0..6 semitones, continuous. The hands travel as MIDI, the numbers Part V
fixed: valves CC 110/111/112 (≥ 64 pressed), the slide CC 113, global state
on the master channel (any channel in classic and wind mode); the normalizer
decodes them into the engine’s own copy of the layout state — the slide
smoothed over a 10 ms one-pole so the 7-bit staircase reads as a ramp —
hands it to the mapper before the frame’s notes are placed, and
sumi_get_layout_state (1.4.0, additive) gives it to a shell; a play
surface mirrors the bytes it sends. The probe, still pure, answers the
cell’s note under whatever state it is handed, the cell’s centre and
radius, and a lip-bend axis along +x at one semitone per cell radius. A
note from the wire lands in the partial its fingering selects — else the
standard fingering, else the nearest partial. A column by default (the
lowest partial at the bottom), the trumpet’s on an arc over the sheet by a
params flag (trumpet_arc; the author’s choice by eye at step 63). The
valves’ and the slide’s bends nudge the drop under the mapper’s cap: the
strip will show the fingering (step 62), the canvas stays ink. The desktop
lists both layouts, keeps the flag, draws their cells as the plate guide
and keys the orbit trace’s placement by the fingering; the bench’s
--layout-shot prints a layout over a scripted fingering phrase. Goldens
(24 483 checks): every valve combination × every partial on both
arrangements, the slide at its seven positions and the midpoints between
them, the standard fingerings and the gaps, the normalizer’s decode on the
master channel, and a recorded fingering stream replaying into identical
states, probe answers and placements. The field gate and the composite gate
bitwise on Metal — the fixture untouched; iOS, Android and the web compile
against 1.4.0 (the tablets’ pickers stay on 0–7 until their shell steps).
Step 61 — Stateless layouts: Wicki–Hayden, strings, the theremin (libsumi 1.5.0)
Section titled “Step 61 — Stateless layouts: Wicki–Hayden, strings, the theremin (libsumi 1.5.0)”DECISIONS_8 #5–#8. The last three named layouts ship, and every layout the
header names now answers. WICKI–HAYDEN: the concertina’s hex button-field —
a whole tone to the right, a fifth up-right, a fourth up-left, the octave two
rows straight up — six buttons a row (the width at which every note has
exactly one button: one echo by construction), fifteen rows covering C1..B7;
pitch is a plane over the sheet, so the probe’s axis is its gradient and a
twelve-semitone glide lands two rows up. STRINGS (the 1.0.0 name FRETS kept
as an alias): string-rows × chromatic frets, the open string and two
octaves, the lowest string at the bottom, under one of three fixed tuning
presets — standard guitar, a twelve-string whole-tone tapping grid, all
fourths (string_tuning) — a note placed on its three lowest-fret strings
(the first position first), the axis along the string at one fret a
semitone: dragging along a string is a string bend. THEREMIN: no cells —
five octaves across the width, the probe answering anywhere with the
nearest semitone, its slot’s x as the centre, the half height as the press
axis’s travel and the cell’s CONTINUOUS flag set at last; the drop travels
under the hand. The desktop lists thirteen layouts and the tuning sub-picker.
Goldens (41 600 checks): every button, every (string, fret) under every
preset, the theremin across the field; the bijection, the intervals, the
echoes, the clamps, the gradient. The field and composite gates bitwise on
Metal; iOS, Android and the web compile; both tablets launched with the
rebuilt core.
Step 62 — hostmpe: the fingering on the wire, the brass retune, the theremin surface, the small UX items
Section titled “Step 62 — hostmpe: the fingering on the wire, the brass retune, the theremin surface, the small UX items”DECISIONS_8 #9–#12. The host-side MPE library learns the instruments: three
momentary VALVE buttons on the strip emit CC 110/111/112 on the master
channel (buttons, never dropped), and a POSITIONAL slider — the latch
wheel’s opposite, the hand IS the value — emits CC 113 (a continuous
dimension the limiters police); the strip’s re-announce restates the
fingering, and a strip reset (sustain off, valves up, the spring home) is
the panic’s strip half. A held brass voice RETUNES with the fingering: a
per-voice pitch offset beside the joystick’s bend, moved by what the probe
reports — ramped over 30 ms for a valve change, at once for the slide —
and a touch may begin with the hand’s fraction so the attack is in tune
between semitones. The THEREMIN surface sets pitch absolutely from the
hand’s x through the pen’s same-channel re-anchor (one re-anchor past 47
semitones, never a retrigger inside) with the finger’s bipolar Y. The
engine honours All Notes Off and All Sound Off (every held voice ends), the
desktop’s settings window gained a panic button, and hostmpe gained the
layout quick-switch over a chosen subset, left-handed mirroring and a
per-device profile the shells OFFER. Three byte traces — a trumpet phrase,
a trombone glissando, the theremin’s stream — are the goldens and the MIDI
chart’s new rows (step 67); every assert of the byte-log analyser holds on
them, the analyser now accepting an attack within half a semitone of
centre. hostmpe 3 158 checks, the normalizer 41 610, ctest 10 of 10; the
field and composite gates bitwise; the tablets rebuilt and launched.
Step 63 — The iOS play surface: the instruments on the iPad
Section titled “Step 63 — The iOS play surface: the instruments on the iPad”DECISIONS_8 #13. The iPad plays the eight keyed layouts. The picker lists
all thirteen, with the trumpet’s arrangement (column or arc, both captured
for the author’s eye) and the strings’ tuning preset under their layouts —
the preset serializer learned the two fields. The strip grows the valves on
the trumpet and the positional slide on the trombone (its ticks the seven
positions), a Next pad when a quick-switch subset is chosen, and Panic;
left-handed it moves to the top-right while the lattice, the touches and
the hand’s delta mirror. The overlay hands every probe the fingering it
mirrors from the bytes it sent, and a valve change or a slide move retunes
every held brass voice through hostmpe — the device log shows CC 110–112 on
the master channel with the voice’s bend ramping under them, the slide’s
CC 113 with a bend per step. The theremin is the field: its semitone slots
drawn along the middle, the hand’s x the pitch, re-probed on every move. A
known controller appearing is offered, never applied. The Sound page gained
step 56’s source row — Sampler, Suzu, Both — and five Suzu patches; the
bowed string is the default and sings under the valves’ bends. Lab launch
arguments script a fingering phrase through the real path and capture the
screen; six runs’ byte logs pass every analyser assert. The author’s
session file was saved and restored around the runs. The author’s fixes on
the device (DECISIONS_8 #14): the valves and the slide became a large
panel at the side at mid-height — three stacked pads, a vertical slide with
its seven positions numbered — with the strip moved to the opposite corner
on the brass layouts (the two swap sides left-handed); a string note now
lights every string that reaches it (the echo cap 3 → 12, the Jankó rule);
the Sound row names what sounds — “Suzu: Trumpet” when the synth is the
source. The second round (DECISIONS_8 #15, the author’s second look
#16): the panel in both forms — vertical at the side or horizontal along
the bottom edge, the pads side by side with 1 under the index finger and
the slide’s 1st position at the hand’s near side, chosen by a toggle under
the brass layouts or by the panel’s own rotate button — and the trumpet’s
arc re-cut in the core: a ring of radius 0.30 canvas heights (0.42 before)
centred 0.08 right of the middle with its own middle the sheet’s, cells of
0.055 instead of half the chord; the arc golden follows and the gates stay
bitwise; a scripted --layout now clears the arc flag unless
--trumpet-arc is given; the device runs pass the analyser again. The
fourth round (#17): the trombone’s seven partials on the same ring under
the one brass flag — the desktop’s checkbox and the iPad’s toggle under
both brass layouts, the suite’s trombone block on the ring — and the
strip at the right stopping short of the settings gear, which had covered
Panic.
Step 64 — The Android play surface: the instruments on the Tab
Section titled “Step 64 — The Android play surface: the instruments on the Tab”DECISIONS_8 #18. The Tab plays the eight keyed layouts as the iPad does —
every step-63 mechanism one for one on the Tab’s own architecture (hostmpe
on the AMidi poller thread, every UI call a posted command). The picker
lists the thirteen layouts with the brass arc, the strings’ tuning and the
fingering panel’s form under their layouts; the panel (FingeringPanelView.kt)
is the Swift view’s twin — the pads or the slide, vertical at the side or
horizontal along the bottom, the rotate button, mirrored left-handed. Its
mirror is exact by construction (the slide quantised as the engine sends
it) and rides every probe; the brass cells’ notes follow a fingering
change. The retune lives natively on the MIDI thread — the held cells kept
beside the voice, the trumpet re-probed under old and new state and ramped
over 30 ms, the trombone shifted at once, hostmpe_tick in the drain; the
trombone’s attack offset and the theremin path as the iPad’s. The native
panic resets the strip in the same command; the strip’s row is dynamic
(Next, Panic) and sits at the top-right on the brass layouts clear of the
gear; quick-switch, left-handed mirroring (a canvas flip), the per-device
offer alert and the Sound page’s source row with five Suzu patches. Lab
extras script a layout, the mirror and the form (transient), the fingering
demo and the sound source; the author’s session and preferences are saved
and restored around the runs. The latency gate, measured: identical scripted
taps on the committed tree before the step and on this one — touch-down to
rendered frame median 4.89 ms before, 4.57 after, beside Phase 4’s 3.73.
One core touch, a fix (#19): the core’s default params were undefined
for the two Phase-9 fields (the Tab’s session read trumpet_arc 200);
default_params zeroes the struct and names both, the bench’s
--defaults-test pins the documented defaults, the Android session heals a
stored out-of-range value from the core’s clamped params; libsumi stays
1.5.0, the gates bitwise. The author’s report on the Tab, “the previous
layout persists” (#20): a layout pick patched the session and posted the
apply, but the lattice was swept before the render thread wrote the probe
snapshot — a gap since step 45b; the patch updates the snapshot at once now. The
second, “we cannot play the partials with the S Pen while playing the
valves or the slider” (#21): Android’s stylus palm rejection cancels
every finger gesture the moment the pen comes within hover range of the
glass and drops the fingers until it leaves — the dispatcher’s
one-device-per-window rule with the stylus preferred, measured with
virtual devices through uinput (the sequences in the evidence). The
rule is per window, so the fingering panel is a window of its own now
(a split-touch, non-modal sub-window over the frame on the brass layouts)
and the fingers’ stream never meets the pen’s: a valve pressed before or
during a pen note holds, its lift arrives, a press under a held pen note
re-articulates it on the pen’s next move (the pen’s glide re-probes the
fingering; the ramped retune is the fingers’ — a flag for a fix round).
On the real glass the Tab’s input reader cancels and skips the fingers
while the S Pen hovers or touches (its own log line, FLAG_CANCELED on
the cancel), below every window: the pen plays alone there, the brass
with the pen is the wire’s fingering CCs from any controller.
v1.0.0 — Phase 5 shipped: every release lane, the beta, and the feedback batches (steps 28–33)
Section titled “v1.0.0 — Phase 5 shipped: every release lane, the beta, and the feedback batches (steps 28–33)”The release-candidate line v0.5.0-rc.1…rc.5 (below) carried the spine, the
web, the docs and the macOS lane; this release adds the remaining lanes and
procedures — Windows, Linux and apt, the iOS and Android checklists — and the
Step-33 feedback batches, the one step allowed to touch the core: ABI
0.5 → 0.9.0, every fix landing with the test that would have caught it,
every suite green on all five platforms, the §4.6 field fixture untouched.
Decisions: DECISIONS.md Part IV #37–#81 (#81: the first v1.0.0 run
failed its Windows gate because PowerShell handed CMake 1 for
-DSUMI_APP_VERSION=1.0.0; the configure steps now run under bash with the
version quoted, and CMake refuses an injected version that is not X.Y.Z).
Step 66 — Web marble: the instruments as overlays, replay in the browser
Section titled “Step 66 — Web marble: the instruments as overlays, replay in the browser”DECISIONS_8 #25–#26. The web page lists the five Phase-9 layouts (trumpet,
trombone, Wicki–Hayden, strings, theremin) and draws every keyed layout as an
OVERLAY over the water — a probe sweep of the sheet, one circle and note name
per cell, the theremin’s pitch axis with a tick at every C — on a second
canvas that takes no pointer: Play stays web-deferred (INSTRUMENT §4). The
probe shim carries the engine’s fingering (sumi_get_layout_state), so the
brass partials read under the valves and the slide a controller sends (CC
110–113); the panel gains the brass arrangement, the strings’ tuning, the
overlay toggle and, under the brass, valve and slide controls that send those
CCs. Replay playback in the browser: the replay library compiled into the
wasm, ?replay=<url> (the gallery’s “watch it again” link form) or “Replay a
recording…” from the panel plays a .sumireplay from any shell on the
scripted clock with the live input muted and the recording’s look — the
picture alone, the web having no sound core. tools/web_gate.mjs --replay
replays a file at its recorded size and compares the field after it at the
web tier, then proves the wall-time re-bucketing diverges; --fullshots
captures the overlays. Measured in headless Chrome on the Mac: the field gate
green at the web tier (mean 4e-9), 17/17 scenes, the page’s preset form
byte-identical; the Mac’s demo replayed under WebGPU within mean 3e-7 of the
Mac’s Metal field, the Pixel’s recording within mean 6e-5 (its native figure),
the iPad’s at the same 8e-3 gap it shows natively — the browser on the Mac
sides with the Mac. Phase 9 closes here: the tag v2.0.0-alpha.4 and the
fold of DECISIONS_8 are the author’s.
Step 65 — Session replay: the byte stream is the recording
Section titled “Step 65 — Session replay: the byte stream is the recording”DECISIONS_8 #22–#24. A recording is the session, then every frame’s
bytes, gesture calls, state changes, resizes and dips — a plain-text file
(replay/FORMAT.md, .sumireplay, version-stamped, the source device
named) written and read by the new pure-C library replay/ beside the
presets, the one code path for the desktop, the iPad and the Tab (the web’s
playback is step 66’s). The frame boundary is the drain point: while a
recording runs the shell’s MIDI producer stages its bytes in the recorder’s
wait-free ring and the render thread hands them to the core at the start of
each frame, stamped with it — exact by construction, no counter race; the
gesture calls, the settings’ apply, the ledger’s dip and the resizes report
beside. Playback drives the scripted clock through the recorded boundaries:
one update at the recorded dt and one render per frame, as many per display
frame as the wall clock asks (Metal’s display sync off on the desktop
meanwhile), the live input muted, the viewer’s size and palette kept — the
replayed sheet stays for the dip and the print — and the replay re-sounds
through Voxo; the banner names the source. The desktop’s settings window
gains a Replay section (Record, the list, Play, a file from another device),
the tablets a Replay page (record, list, play, share or export, import) and
a top banner with Stop; lab flags on every shell (--record-demo,
--replay, --replay-wall, --replay-wav, --record-live,
--replay-live; the iPad’s --record-lab, --replay-file --replay-dump;
the Tab’s --es recordLab, --es replayFile); tools/replay_gate.py
replays a file against the recording device’s field dump at the tier and
then proves the wall-time re-bucketing diverges; tests/replay_tests.c in
ctest. Measured: the Mac replays its own recording bit for bit and the
wall-time re-bucketing diverges (mean 3e-3, max 1.4); the iPad replays its
own bit for bit; the iPad’s recording on the Mac — and the Mac’s on the iPad
— differ by a few percent of displacement amplitude accrued while passes
run (the ink bands identical to the half-float ulp; mean 8e-3 / max 5.5e-2
for a 20-s passage), outside the seven-pass §4.6 tier — the same MSL on
both, no iOS branch in the core: the iPad’s Metal stack or something in the
iOS host, the two telling experiments named; the author’s eye finds it
almost imperceptible, and the premise “Metal bitwise across devices” is
flagged against the measurement. The fix round after the commit: the
author’s Pixel 9 Pro (GLES, 120 Hz, standing in for the Tab) records and
the Mac replays it within the mobile tier (mean 6e-5, max 1.0e-2) —
“visually ok”; the Android build had failed silently (helpers before
their definitions, two linkage clashes), fixed. The Linux box is out of
commission; the Tab was not connected.
Step 28 — iOS release procedure (manual by design)
Section titled “Step 28 — iOS release procedure (manual by design)”No CI lane: iOS is built and released from the Mac (#38, flagged against the
roadmap’s compile-check line). ios/prepare_release.sh derives marketing
version, build number and the full describe from the tag, rebuilds libsumi
with SUMI_APP_VERSION injected (the stale-core trap) and regenerates the
project; About shows X.Y.Z (build) · describe and libsumi a.b.c;
ios/RELEASING.md is the checklist (tag → prepare → Archive → TestFlight →
About on the iPad); ios/metadata/ the listing, App Privacy “Data Not
Collected” and the screenshot plan (#37). ITSAppUsesNonExemptEncryption = false answers export compliance once.
Step 29 — Windows release lane
Section titled “Step 29 — Windows release lane”The windows job: MSVC + Ninja, dist-windows = an Inno Setup per-user
installer (Start-menu entry, uninstaller, settings survive uninstall unless
opted in, #39) and a portable zip, both sha256’d; static CRT so a clean VM
needs no redistributable, VERSIONINFO from the tag (#40); signing if
WINDOWS_CERTIFICATE is present, an unsigned release otherwise with the
SmartScreen note in the README (#41); publish-winget.yml on release: published, skipping pre-releases, with the staged Vibetuned.MidiSink
manifest conventions (#42). Found on the way: the settings window’s HiDPI
scale, captured in Step 23 and never applied, is applied (#43). Verified on
the author’s Windows 11 box: 69 023 loopMIDI messages in 30 s at 164 fps, zero
dropped.
Step 30 — Linux release lane
Section titled “Step 30 — Linux release lane”cmake/LinuxPackaging.cmake: a CPack DEB of the desktop-integration
component only — eleven files, absolute Exec, Debian version 0.5.0~rc.N
from 0.5.0-rc.N (#44) — plus the bare tarball; the linux job builds on
ubuntu-22.04 for glibc reach and installs, runs and removes the package in
a clean container. publish-apt.yml rebuilds the Pages tree on every
published release and adds /apt/ with stable and rc suites signed by
APT_GPG_PRIVATE_KEY (#45); the install page carries the keyring line. The
Flatpak spike closed as “spike, not a channel” (#46): ALSA MIDI needs
--device=all. The ROLI played over ALSA from the installed deb: 1 728
channel messages in 18 s, MPE per-note bend and pressure present.
Step 31 — Android release procedure (manual by design)
Section titled “Step 31 — Android release procedure (manual by design)”Version from git in build.gradle.kts (versionName numeric, versionCode
= commit count, BUILD_DESCRIBE for About, -DSUMI_APP_VERSION into the
CMake arguments; no Android CI job by the author’s decision, #47);
android/prepare_release.sh, android/RELEASING.md (Generate Signed Bundle →
Play internal → About → USB-MIDI to the Linux box), android/metadata/ with
Data safety “nothing collected”. The Android paper dip became two buttons —
save the print to Pictures, or discard (#48).
Step 33 — Feedback incorporation (the scoped unfreeze)
Section titled “Step 33 — Feedback incorporation (the scoped unfreeze)”Opened early, while the beta ran; twelve batches on the Mac, then a
verification pass on each machine. Core (0.5 → 0.9.0, all additive):
the §4.3(7) swirl as a gesture (SUMI_VORTEX_LAMB_OSEEN) and the
interior-copying feed drop (SUMI_DROP_FEED), the print double-buffer
recycling its older unread print (#49, #51); the viscous stylus stroke —
the 2-D unsteady Stokeslet displacement kernel, derived, numerically checked
and sub-stepped ≤ a/4, behind wake_profile / wake_spread (#53); the input
dialect as a setting (MPE by default, never a heuristic) with MPE mode
giving a channel-1 keyboard per-note voices and wind mode reading the IMU
layer on its one voice (#60); the palette-morph CC travelling the whole ring
(#61); CC 64 never dips the paper (#62); wind mode = MPE plus a wake
between notes, breath unbounded, the wandering brush retired (#63); two
more piano rolls, from the right and from the bottom (#64); the bend-driven
ripple four times more sensitive (#66); the Airwave map as symmetric
hands — Raise/Glide/Slide per hand, the left a vortex, the right a new
Lamb–Oseen swirl control trio, Grasp the pinches, Tilt the ripple, Flex free,
five new control dimensions (#69, written on the Windows box). Every
shell: the Marble-mode pressure gesture (long press on the tablets,
Shift + right drag on desktop: hold/push = feed, pull = swirl, #49); the
stylus draws its wake in Marble mode on the tablets (#54); the measured then
redesigned Airwave map (#50, #69); settings parity — palette, look, tempo
and roll speed, vortex profile, ripple, the CC map editor and the MIDI inputs
list on iOS and Android, with the desktop window as the contents spec (#55,
#56); --window <w>x<h>, Fullscreen with its chord and --fullscreen, and
the title bar kept on every platform after the borderless experiment (#57,
#58, #59 → #67 → #70); Canvas (paper dip) as the first settings section (#78);
a persisted CC map that is an older stock map upgrading itself (#71); a
single-arch local DMG dry run (#72). Platform verifications: Windows —
the borderless canvas could not be snapped or dragged, so it keeps its title
bar, and layouts 6/7 did not survive an INI reload (% 6 → % 8) (#67,
#68); Linux — leaving fullscreen on X11 under mutter re-asserts the windowed
geometry (#73), the Wayland launch overlap of the two windows accepted as is
(#76); Android — the native library linked for 16 KB pages (#74), the control
strip a setting hidden by default on phones (#75), the persisted ripple
sliders re-sent at startup (#77). Docs: a settings reference page, the
guide rewritten for the setting instead of the detection and for the new wind
model, the operators’ index moving the wind legato under Wake, the web’s
icons rounded like every other platform’s (#79), and the gallery’s five real
performances with synth and “based on” fields, Ali Paşa kept as the Jaffer
tribute (#80). Tests: --pressure-test, --stokeslet-test, the mapper
suites at 18 455 checks on every platform, the field gate bitwise on Metal and
bit-stable on D3D11 and GL, the wasm scene sweep and WebGPU gate green, the
Play-mode byte-log asserts green on a Pixel.
Store submission prep (after step 34)
Section titled “Store submission prep (after step 34)”The technical fields of both consoles answered from the tree
(ios/metadata/submission-answers.md, android/metadata/…), screenshot
sets captured from real v1.0.0 sessions (iPad 13” and iPhone 6.9”
simulators, the Galaxy Tab over adb) and cropped to the stores’ sizes, the
Play icon and feature graphic; the scripts in tools/store_screenshots/.
Findings kept for the author: the app segfaults in the iOS Simulator
(SUMI_SHDC_SLANG has no metal_sim; device builds unaffected);
ios/Info.plist is tracked but regenerated by xcodegen, so a release build
can stamp -dirty; TARGETED_DEVICE_FAMILY is iPhone and iPad, so review
runs on an iPhone; the AGPL-3.0 App Store conflict resolved by
LICENSE-APPSTORE-EXCEPTION.md (an additional permission under section 7,
store copies only — worth a lawyer’s read); libsumi 0.9.0 beside app 1.0.0
is correct; the EU DSA trader status and the Play age band are the
author’s; the Play device catalog is small by design.
v0.5.0 — Phase 5, first release candidate: product, spine, web, docs, macOS lane (steps 23–27)
Section titled “v0.5.0 — Phase 5, first release candidate: product, spine, web, docs, macOS lane (steps 23–27)”The engine leaves the lab. The desktop harness became the product (a real
macOS .app, a settings window shared by macOS/Windows/Linux, a lab bench
behind --dev); one tag-triggered workflow is the release spine with the
§4.6 field regression as its gate on every renderer; the core gained its one
permitted Phase-5 seam — a WebGPU swapchain — and the same bytes now run in
the browser; a documentation site with live demos of every operator owns
midi-sink.vibetuned.com; and the macOS lane signs, notarizes and staples a
universal DMG that bumps a Homebrew cask. Version strings come from the git
tag everywhere; no number is edited by hand. iOS and Android stay manual
procedures by design. Decisions: DECISIONS.md Part IV #1–#34.
Step 23 — Desktop productization (macOS)
Section titled “Step 23 — Desktop productization (macOS)”One name, midi-sink, bundle id com.vibetuned.midi-sink (#1). The
settings window: Dear ImGui v1.92.9b in a second GLFW window with its own
GL 3.2 context, one implementation for all three desktops (#2) — layout &
look, expression routing, ripple, a CC-map editor on a host-side mirror (#7),
the live MIDI-input list with rescan age (#8), paper dip and print export,
About with app version · commit · engine version; ⌘ , / Ctrl , reopens
it. Settings persist in a plain INI in the platform config directory (#4).
--dev gates the whole lab bench — every debug key, scripted flag and
--field-dump — so a release build accepts only --help/--version (#5).
macOS is a real .app with Info.plist, .icns, hardened-runtime
entitlements and an ad-hoc signature straight out of the build; the runtime
Dock-tile hack retired (#6). SUMI_APP_VERSION is a CMake cache variable
injected from the tag, defaulting to git describe (#3). Three Cocoa facts
fixed along the way: GLFW’s chdir into Resources, off-screen settings
placement, and the Latin-1 font (#9). A push/PR workflow builds and runs the
headless suites on ubuntu, windows and macos.
Step 24 — Release orchestration spine
Section titled “Step 24 — Release orchestration spine”release.yml: a version job (tag → X.Y.Z[-pre], release notes from this
file, --strict on real tags), a gates matrix (macOS/Metal, Windows/D3D11,
Ubuntu/GL under Xvfb + Mesa) that builds with the injected version, runs the
suites, asserts --version carries the tag and runs the §4.6 field
regression through each real renderer with a negative control that proves
the gate can fail (tools/field_gate.py), and a publish job that drafts a
GitHub release from every lane’s dist-* artifact — pre-release when the
version carries a -. Humans publish; channel bumps are separate
release: published workflows. The lane interface is documented at the top
of the file: four CI lanes (web, macOS, Windows, Linux), one job each;
iOS and Android manual (#10–#14).
Step 25 — WebGPU backend & marble web
Section titled “Step 25 — WebGPU backend & marble web”The core’s fourth swapchain TU, swapchain_webgpu.cpp (sokol SOKOL_WGPU
through Emscripten’s Dawn port), SUMI_BACKEND_WEBGPU and
sumi_webgpu_surface_t in the ABI (sumi_version() 0.5.0, additive), the
WGSL dialect in the shader pipeline, and a readback seam every backend
implements (injected copy-source textures, non-blocking begin/poll field
read). web/: the C-ABI as the wasm export surface, a page of JS as the
host — pointer/touch/pen gestures, Web MIDI on Chrome/Edge, a lil-gui panel
mirroring the desktop settings window, and the scene API
(?scene=…¶m=…&embed=1) whose sliders are the formulas’ symbols. The
web tier joined the §4.6 regression at the desktop tolerance (max |Δ|
9.8e-4 vs the Metal fixture); Lighthouse mobile performance 1.0, first
marble 252 ms. tools/web_gate.mjs drives headless Chrome for the field
dump, the scene sweep and page captures. Found on the way: the port never
exports Module.WebGPU (device via preinitializedWebGPUDevice), map
callbacks must be AllowSpontaneous, and the “no WebGPU” overlay that hid a
working canvas was a CSS specificity bug (#15–#21).
Step 26 — Documentation site
Section titled “Step 26 — Documentation site”site/: Astro Starlight with KaTeX, deployed from the release tag next to
the marble app (/marble/) at https://midi-sink.vibetuned.com/ — frozen
URLs /, /privacy/, /support/ for every later lane and store listing
(#22). Five books and a chart: user guide (10 pages), The Operators (one
page per deformation — formula, invariants, ownership rule, and a live embed
of the release wasm; no second implementation, enforced by a post-build
check), architecture, a performance gallery rendered from a runtime manifest
(the Jaffer tribute piece identified from his own video credits as Ali
Paşa, #27), design notes and this changelog generated verbatim from the
repository (#24), and the MIDI implementation chart — data rendered by
the page and verified against the Play-mode byte logs by
tools/chart_check.py (#26). Citations checked against the publications.
After review the operator scenes were paced (frames per step) and two-sited
so the mathematics is seen taking effect, the pressure feed got its own scene,
and the swirl shows its far field (#29); a Marble-mode feed/swirl gesture is
recorded for the Step-33 unfreeze (#30).
Step 27 — macOS release lane
Section titled “Step 27 — macOS release lane”packaging/macos/release.sh, one script for CI and local runs: universal
(arm64 + x86_64) .app → Developer ID signature with a secure timestamp →
notarize → staple the app → DMG → sign → notarize → staple the DMG →
spctl assess → sha256; ad-hoc without credentials, notarization required
on a real tag with a certificate (#31). The macos job in the spine
attaches midi-sink-<version>-macos-universal.dmg; publish-cask.yml opens
a pull request on vibetuned/homebrew-tap when a release is published,
so a release-candidate cask never reaches the tap’s main (#33). Credentials
are the organization secrets the author’s other apps use (#32); a
release-candidate tag published as a pre-release is the end-to-end test (#34).
v0.4.0 — Phase 4: Touch & Stylus MPE Play Surface (steps 15–22)
Section titled “v0.4.0 — Phase 4: Touch & Stylus MPE Play Surface (steps 15–22)”The tablets become MPE instruments: every touch is a joystick on a pitch
lattice, the pencil plays legato, a floating control strip rides the master
channel, and the generated stream feeds both the loopback visualizer and
real DAWs over USB, virtual, network and BLE transports. All host-side MPE
generation lives in one shared pure-C library, hostmpe/, consumed by Swift
(module map) and Kotlin (JNI) — one implementation, 1,569 headless checks,
zero drift between platforms. The core grew four deformation operators
(v0.4) and the layout probe; otherwise it stayed frozen.
Close-out (after step 22)
Section titled “Close-out (after step 22)”Pencil Pro squeeze = sustain pedal, the S-Pen barrel button’s twin
(#62): UIPencilInteraction drives the same hostmpe_strip_t engine as the
palette’s pad, transition-only, Play-mode only; Pencil 2 double-tap latches.
iOS byte log adopts Android’s src taxonomy (pen = 4, #63) so
tools/pen_trace.py and tools/midi_asserts.py read both platforms’
logs — the §3.3 stylus-only CC74 rule became provable (1,237 pen / 0 finger
CC74 in a live session). In-app evidence capture (timed full-screen PNG
burst through the render-server path, which carries Metal content) and a
log flush, pulled with devicectl device copy from (#64). Echo
suppression in hostmpe (#66): a live iPad log showed 99.5% of “external”
MIDI was our own output mirrored back (median 0.3 ms), marking our own
channels externally held and painting every note twice; every delivered
byte is now recorded at the single transport emit point and matching input
inside 300 ms is consumed — the post-guard session carries zero external
rows and passes every assert. The sustain pedal no longer wipes the
canvas (#67): CC 64 → paper dip is honoured only outside MPE mode
(§2.4’s classic-keyboard mapping survives); the dip became a deliberate
settings control. Flagged, unchanged: with the bend booster engaged, a cell
crossing is no longer pitch-continuous (#68 — 69% of a free-play session’s
crossings carried boosted bends; expressive by design, options recorded).
Step 22 — Android port (Galaxy Tab S8 Ultra, on the Linux box)
Section titled “Step 22 — Android port (Galaxy Tab S8 Ultra, on the Linux box)”Full parity: sumi_play.cpp hosts hostmpe_t, the strip engine, one
limiter per transport and the byte log on the AMidi poller thread (the
§5.2 single producer, #46); a params snapshot lets the instance-free probe
answer on the UI thread (#47). Kotlin PlayOverlayView (probe-swept
two-tone lattice, joystick indicators, fingers, S-Pen, hover), floating
ControlStripView with the long-press CC editor, MidiOutputs (USB
gadget as primary sink, MidiDeviceService virtual device, a hand-rolled
BLE-MIDI GATT peripheral — MidiManager is central-only, #44), Marble pinch.
Measured on the Linux side: gadget appears in amidi -l within ~150 ms of
the USB mode flip; MCM/RPN 0 = 48 on 15/15 members in wire order on USB and
BLE simultaneously; BLE − USB latency +29.8 ms median; the full hostmpe
(1,559) + normalizer (14,997) suites pass on-device; GLES3 field dump
reproduces the Metal fixture numbers exactly; a 10-minute mixed session
(three sinks + 10-voice storm + scripted play) — 834,018 wire messages,
1,149/1,149 notes released, worst slot 92/s under the 100 Hz policy, +1 ms
cumulative lag, 0 dropped, 120 fps, PSS flat at 253 MB, 0 EGL errors. An
independent review of the diff fixed real bugs (#54): an inverted §5.4
teardown block, a lost command-queue wakeup, an AMidiDevice double-free,
duplicate ingestion after relaunch, BLE flow-control stalls, and two false
greens in the analysers (now negative-tested). Late additions: the piano
grid’s accidental knob made aspect-invariant in the core (#57), the S-Pen
barrel button as sustain (#58), a natural’s drawn cell re-centred on its
octave pair (#60), the black-key glissando corridor (#61); a black-key
glissando experiment rolled back in full (#59).
Step 21 — Stylus legato, wake & pinch (iOS)
Section titled “Step 21 — Stylus legato, wake & pinch (iOS)”The pen redesigned same-day into per-cell legato retriggers (#39): the
shell probes the cell under the tip each move, a crossing emits
bend→Note On(live-force velocity)→old Note Off on ONE channel (the legato
overlap idiom — DAWs record real notes, mono/MPE synths glide), the in-cell
offset is the bend, ±0.65 st boundary hysteresis keeps vibrato from
machine-gunning at cell edges; the ±47 re-anchor and piano retune ramps were
retired. Velocity in UIKit force units (baseline tap = 96, force 3 = 127,
#38). Barrel controls became derivative-only gestures (#40, four
iterations): the Pencil Pro roll multiplies the in-cell bend ×1→×3 with a
0.4 s decay; azimuth tail-stir is the fallback; tilt→CC1 and every
orientation→vortex path removed after repeated leaks. sumi_add_wake on
every stroke (physical, never MIDI); slide_mode = 1 drives the pinch with
the fold axis from azimuth, CC74 then outbound-only. Polish batch (#41):
Marble two-finger pinch on iOS, piano accidentals narrowed to 0.6 keys with
white-key tops between them (natural→natural glissandi), the two-tone
lattice (paper halo under each ring, accidentals drawn on top), and the
§3.4 lift ring removed. Goldens: a 12-cell scripted glissando tracks pitch
< 1 cent across every crossing; device evidence: a live 10-note glissando
reconstructed from the byte log, pen_trace.py PASS on 10 iPad strokes,
screen captures of the strip lit by the squeeze.
Step 20 — Lamb–Oseen swirl & bipolar press (core + iOS)
Section titled “Step 20 — Lamb–Oseen swirl & bipolar press (core + iOS)”SUMI_DEFORM_SWIRL (θ = S/(2πr²)·(1−e^(−r²/r_c²)), series guard below
x < 1e-3), 0xA0 decoded as poly pressure → the swirl dimension keyed by
the voice’s note, press_mode arbitration of 0xD0 (feed or swirl, one
consumer), r_c = the voice’s boundary, band-parity sign, echo fan-out (#37).
hostmpe bipolar Y: one radial knee, up → 0xD0 feed, down → 0xA0 swirl on the
voice’s channel, lift releases the engaged half before Note Off. 19/19
scripted checks: core coherence (ring sharpness inside 0.7 r_c retained
exactly through ~4 rad while the annulus moved 29 k texels), counter-rotation
of adjacent notes, small-r stability (half-float ULP freeze documented),
full step-19 battery and fixture bitwise.
Step 19 — v0.4 deformation operator batch (core, desktop harness)
Section titled “Step 19 — v0.4 deformation operator batch (core, desktop harness)”sumi_version() → 0.4.0. Vortex Rankine profile (rigid core, 1/r²
exterior; 20 rotations leave the interior unblurred: mean |Δink| 0.00000 vs
0.758 for the exponential control); dipolar wake sumi_add_wake
(lab-frame doublet, sign corrected from the draft, rigid tip body, ≤ a/4
sub-steps because a/2 is the fold threshold; a one-frame 8×a flick keeps a
positive Jacobian everywhere); Hamiltonian pinch sumi_add_pinch
(streamline-windowed saddle, det = 1) plus the crossed-tine variant the
user kept behind pinch_variant (#34), also reachable from CC74 deltas via
slide_mode = 1; sine ripple live (composite-time view displacement,
prints un-rippled, amp = 0 bit-identical to v0.3) and baked (delta-driven
passes). bend_mode (#35/#36): the PER-NOTE bend either glides the drop or
raises the ripple by its distance from center, permanently (phase drift per
vibrato cycle), stilling on re-center/release/mode flip — unit-tested as
glide-XOR-ripple. Pinch soak (#33): ink MASS is the observable — 500
reversible pairs hold ±0.5%, zero fabrication after the §3.4 ingress rule
was extended to every new pass (the edge clamp had fabricated +9.5%), per-pass
erosion 1.9× the glide tine’s. Field-dump fixture bitwise, all stress
regressions clean.
Step 18 — Performance control strip (iOS)
Section titled “Step 18 — Performance control strip (iOS)”hostmpe_strip_t: spring wheel (master bend ±2, ~50 ms return, guaranteed
exact-center final message), latch wheels (Mod CC 1 + two assignables,
relative accumulation so a regrasp never jumps, protocol CCs refused),
momentary/toggle sustain, announce after every MCM re-sync; 128 master-channel
CC slots joined the limiters so wheels are policed while buttons ride the
never-dropped class (#30). 494 checks. First device test moved the strip
from a docked band — which displaced every drop from its touched cell — to a
compact floating palette top-left over the full-canvas lattice, and
fixed the long-press recognizer cancelling a held sustain (#31); sustain
stays momentary by default.
Piano-grid layout (between steps 17 and 18)
Section titled “Piano-grid layout (between steps 17 and 18)”SUMI_LAYOUT_PIANO_GRID = 5 (#29): the chroma grid’s frame with each octave
drawn as a two-row keyboard — accidentals above at white-key units
{1, 2, 4, 5, 6}, naturals below; position + inverse probe, true-step glides,
goldens over all 84 notes at two aspects. R_max revised after the first
device test to half of min(key width, octave-pair height) so the knobs
match the chroma grid’s feel.
Step 17 — Outbound transports & per-pipe rate limiting (iOS)
Section titled “Step 17 — Outbound transports & per-pipe rate limiting (iOS)”Dispatcher fan-out: loopback full-rate; outbound change-only + per-voice
per-dimension latest-wins decimation (≤ 100 Hz), or a token budget with
round-robin fairness for BLE (~300 msg/s); Note On/Off and center bends
exempt. Sinks: virtual CoreMIDI source (which also sends explicitly to the
AppleIDAMDriver destination — a virtual source is NOT bridged to a tethered
Mac, #27), rtpMIDI network session, BLE peripheral (MIDISend to the
Bluetooth destination; the local mirror duplicated delivery, #25), with
MCM/RPN 0 = 48 re-sent on every sink appearance (#28) and a “Re-sync DAW”
button. Wire-order captures on every transport: handshake ordered, 100% of
Note Ons bend-preceded, worst per-slot 70/s under the 100 Hz policy, BLE
315/s at the sender, no cumulative lag over 60 s. Found by instrumenting:
MIDISourceCreate needs UIBackgroundModes: [audio] (#24); an analyser
that re-sorted equal-timestamp records had faked an rtpMIDI reordering
(#22 — delivery order IS the data). Panic + per-sink silence primitives
(#26).
Step 16 — hostmpe allocator & loopback Play mode (iOS, fingers)
Section titled “Step 16 — hostmpe allocator & loopback Play mode (iOS, fingers)”LRU-by-release round-robin allocator with external-occupancy masking (30 s timeout), saturation = silent drop + HUD blink; joystick → MIDI with the semitone-exact bend (one grid column = exactly ±171 counts at ±48; the knee is a deadband, not a travel limit — identity beyond the circle, #10; absolute deadband floor for finger-sized jitter, #16); Y-up is pressure, fingers emit no CC74 (#19); Jankó’s semitone axis is horizontal in the core (#18); true-step glides on the lattices (#20). Live on the iPad with a ROLI Piano held over Bluetooth: 2,766 external messages, zero touch allocations on held channels, touch-down → visible drop 0.39 ms median, all 15 member channels rotated, 0 stuck voices.
Step 15 — Layout probe ABI & Play-mode overlay (iOS)
Section titled “Step 15 — Layout probe ABI & Play-mode overlay (iOS)”sumi_layout_probe + sumi_cell_info_t (#6): an instance-free pure
function callable from any thread — the shells’ hit-test — sharing the
semitone-step derivation with the glide axis (#7). Goldens round-trip every
cell center on CHROMA_GRID and all three Jankó echo rows (suite 8,934 →
12,480 checks); lattice/drop alignment ≤ 0.0004 canvas (½ px). hostmpe/
seeded with the §3.2 soft knee behind a pure-C header (#8). iOS Marble/Play
toggle and PlayOverlayView — a faint lattice built by sweeping the probe
(#9), per-touch joystick indicators, zero MIDI yet. Version 0.3.0.
v0.2.0 — spec v2: hardening, layouts, piano rolls, all five platforms
Section titled “v0.2.0 — spec v2: hardening, layouts, piano rolls, all five platforms”Step 14 — Android shell (Jetpack Compose)
Section titled “Step 14 — Android shell (Jetpack Compose)”Compose AndroidView/SurfaceView shell with a one-file JNI host
(android/cpp/sumi_jni.cpp): dedicated render thread owning a host-created
EGL context, §5.4 blocking teardown (condvar; context outlives the surface),
render-thread command queue for UI→core calls, AMidi + BLE-MIDI
(MidiManager.openBluetoothDevice) ingestion, PowerManager thermal listener
driving sim_scale 0.75 ↔ 0.60. SOKOL_GLES3 split in the shared GL swapchain
TU with ES3 extension checks. On a Galaxy Tab S8 Ultra (Adreno 730): 10-minute
Osmose stress at min 119.6 / avg 120.9 fps (vsync-limited), 1.37 M messages,
0 dropped, PSS flat; live thermal step-down and recovery with the drawing
preserved; ×10 surface destroy/recreate mid-stress with zero EGL errors,
zero crashes, flat memory; GLES3 field dump passes the §4.6 regression vs the
Metal fixture (max 1.51e-2 under the documented mobile tier); ROLI over
Bluetooth MIDI painting MPE end-to-end.
App icons (all platforms)
Section titled “App icons (all platforms)”tools/gen_icons.py generates every platform’s icon from
images/midi-sink.jpg: Android adaptive/legacy/monochrome mipmaps (cream
keyed to alpha so the mask never crops ink), iOS 1024 asset catalog, Windows
.ico + .rc + runtime window icon, X11 runtime icon, Wayland .desktop + XDG
hicolor theme (with install component and icon-cache refresh), and a macOS
Dock tile set at runtime from an embedded PNG (bare executable — no bundle).
Step 13 — iOS shell (SwiftUI)
Section titled “Step 13 — iOS shell (SwiftUI)”~200-line Swift shell, no Objective-C wrapper: import SumiCore via
module.modulemap, CAMetalLayer-backed UIViewRepresentable, CADisplayLink
(paused when backgrounded — Metal in background is a crash), CoreMIDI with
notification-driven hotplug + CoreAudioKit Bluetooth pairing, touch gestures
(tap drop / drag tine / two-finger twist vortex), settings sheet (layout
picker, sim_scale toggle — default 1.0 on Metal GPU family apple7+),
activity-gated idle-timer hold. On an iPad Air 11” (M4) with the ROLI over
Bluetooth: 5.5-minute traced session at a flat 60.00 fps, zero seconds below
50, thermal Nominal for the entire Instruments trace; backgrounding/rotation
crash-free. Fixed on the way: metal_ios added to the shdc dialects (device
abort without it), and resize now carries the drawing across on every
platform (passthrough resample of the resolution-independent field;
pristine fields keep exact identity init so the §4.6 dump stays
bit-identical). Core purity held: identical exported-symbol tables macOS↔iOS,
zero platform conditionals in core.
Step 12 — Linux backend (OpenGL 4.1 core)
Section titled “Step 12 — Linux backend (OpenGL 4.1 core)”swapchain_gl.cpp hosts SOKOL_IMPL for GL; host-owned GLFW context (§5.1
exception), ALSA MIDI via the same 1 Hz rescan. §4.6 resolved at the shader
DIALECT level: @glsl_options flip_vert_y on offscreen vertex shaders makes
every GL offscreen target top-left-origin exactly like Metal/D3D11 (MSL/HLSL
outputs byte-identical), leaving the final composite as the single divergence
point — zero runtime flip branches; a control build without the directive
fails the regression with the v channel mirrored. Non-blocking PBO+fence
print readback. Field regression vs the D3D11 dump: max|Δ| 1.34e-3, aux
bit-identical, no orientation correction. Dip PNG parity with Metal;
mpe/wind stress green on NVIDIA (Wayland and X11).
Step 11 — Windows backend (D3D11)
Section titled “Step 11 — Windows backend (D3D11)”swapchain_d3d11.cpp hosts SOKOL_IMPL; DXGI flip-model swapchain, feature
level 11.1 with 11.0 fallback, vsync Present. DECISIONS #7 resolved: objects
compile twice on Windows only (dllexport baked into the shared set; no .def
file), ABI test links both artifacts. The print-readback seam became
backend-neutral (sg_image + bytes-per-pixel through the swapchain API;
staging-texture + Map(DO_NOT_WAIT) polling). Introduced the §4.6
cross-backend field regression: a canonical 7-pass deform script, harness
--field-dump, and tests/field_dump_compare — two D3D11 runs bit-identical.
WinMM stress feeder (mpe_stress_win.cpp) with absolute-clock pacing;
Steps 3–7 checks green on Windows at 165 Hz.
Step 10 — Piano-roll layouts & BPM scroll
Section titled “Step 10 — Piano-roll layouts & BPM scroll”SUMI_LAYOUT_ROLL_H/ROLL_V: drops spawn on a fixed now-line and the whole
field translates as a closed-form scroll pass (emitted first, once per frame,
outside the deform budget) at (bpm/60) × roll_speed canvas-lengths/s, with
ingress as an explicit fresh-water shader branch (sampler clamp modes cannot
express it). Spec conflict resolved with the author: default roll_speed
0.0625 (16 beats = 4 bars of 4/4 across the canvas). Harness keys B/Shift-B
nudge BPM. Evidence: metronome drops evenly spaced to 0.1%, held-pressure
comet trails, vortex smear over scroll with the washi grain bit-identical
across prints (screen-locked), pristine ingress after 6 canvas-lengths, and a
10-minute soak: RSS flat (+160 KB), 98.2 fps, 0 dropped.
Step 9 — Layout system: chromatic grid & Jankó
Section titled “Step 9 — Layout system: chromatic grid & Jankó”Pluggable pure pitch→position layouts (layouts.cpp) with echo sets
(SUMI_MAX_ECHOES = 3): Jankó stamps all three parity rows as fully-fed
triplets — one voice owns the set, dynamics fan out, one drop counter tick,
all-or-none budget reservation. Chromatic grid C1–B7 (7×12, edge-row clamping
keeps pitch class). Layout-derived glide axis (shortest-neighbor rule) —
grids glide along their row, including the B→C wrap. Params v0.2
(sumi_layout_t, bpm, roll_speed), sumi_version() → 0.2.0. Golden-position
tests (128 notes × layouts × aspects); live layout switching under MPE stress
at 98.3 fps with no voice teleport.
Step 8 — v1 hardening & visual polish
Section titled “Step 8 — v1 hardening & visual polish”Overflow-armed stuck-voice timeout (~10 s, grace measured from the overflow moment); double-buffered dip prints (third dip refused with WARN, newest read first); drop-counter/aux rebase on dip (3000-drop session stays bounded); §4.4 feed episodes with onset/release hysteresis stamping nested rings; composite polish — per-region fiber angle drift and segment break-up (no lattice), near-black pooled ink, ink thickness via neighborhood band probing (fract-based thickness fails on feed micro-shells). 500-drop sharpness and Osmose stress unchanged (98+ fps, 0 dropped).
v0.1.0 — spec v1: core engine + macOS harness (steps 1–7)
Section titled “v0.1.0 — spec v1: core engine + macOS harness (steps 1–7)”Step 7 — Washi paper, palettes, paper dip
Section titled “Step 7 — Washi paper, palettes, paper dip”§4.5 composite: simplex mulberry-fiber strands, sizing mottle, absorption
grain scaled by paper_roughness; sumi/indigo/ochre palettes with morph
blending and aux-driven per-drop hue drift; linear math, manual sRGB encode.
Full paper dip: freeze → async GPU readback on a separate queue (never blocks
the loop; dip-window worst frame 14 ms at 2560×1440) → sumi_read_print →
identity reset with a “lift the paper” fade. PNG export moved to a detached
thread after an inline encode stalled 1080 ms.
Step 6 — Wind mode & Airwave CC routing
Section titled “Step 6 — Wind mode & Airwave CC routing”Wind mode: one wandering-brush voice — breath (CC2/CC7/CC11/chanAT) drives a
breath-proportional line width, legato = VoiceMigrate with a wake tine.
Runtime CC routing table (sumi_map_cc) with Airwave defaults; global
controls as smoothed dt-scaled state. Mode detection became
activity-windowed after live playing latched MPE forever (user-reported);
mode changes flush the old dialect’s voices.
Step 5 — Full MPE mode
Section titled “Step 5 — Full MPE mode”MCM (RPN 6) zone config, ±48 member bend with RPN 0 override, per-member- channel voice table with note steal, per-voice smoothing/coalescing, §4.4 press feeds converted to area-correct boundary growth (spec-literal radius steps looked frozen), per-voice glide tines, lift surfactant rings, slide→aux, 64-pass/frame deform budget with overflow merging. Osmose stress (10 voices × 200 press/s): 99.6 fps, 0 dropped.
Step 4 — MIDI ingest, normalizer, classic mode
Section titled “Step 4 — MIDI ingest, normalizer, classic mode”Lock-free SPSC ring (4096, drop-oldest + diagnostics counter), stateful
decoder (14-bit bend, RPN/NRPN, running status), §2.5 dialect auto-detection,
classic mode lowering (fifths layout, sqrt-velocity radii, bend shear, CC1
vortex, CC64 dip). libremidi quirks found: virtual endpoints need
track_virtual, hotplug callbacks never fire → 1 Hz port rescan. 10 k
messages/30 s with 0 dropped.
Step 3 — Jaffer deformations via mouse
Section titled “Step 3 — Jaffer deformations via mouse”Drop/tine/vortex passes (§4.3 closed forms, aspect-corrected), continuous ink-phase encoding, ring-banding composite, mouse gestures. Two foundational bugs fixed: the y-down texture-space convention (raw NDC interpolant flipped the field every pass — consecutive deformations cancelled) and the RGBA16F-safe parity ink phase (seam speckle past 256 drops). 500 drops stay pixel-sharp; 40 × z/40 tine == 1 × z tine.
Step 2 — Ping-pong targets & identity pass
Section titled “Step 2 — Ping-pong targets & identity pass”RGBA16F ping-pong field targets, identity/passthrough passes, deformation queue, sim_scale end to end. The 1000-swaps/frame stress test exposed a ~4 GB/s autorelease leak in a runloop-less Metal host — fixed with the core-owned per-frame autorelease pool (applies to iOS unchanged).
Step 1 — Skeleton, build system, black window
Section titled “Step 1 — Skeleton, build system, black window”CMake + FetchContent pins (sokol, glm, libremidi, GLFW, stb, pinned sokol-shdc binary download), static+shared libsumi from one object set, pure C11 ABI test linking the dylib, GLFW harness with CAMetalLayer glue, clean create/destroy at ~100 fps with Metal validation on.
midi-sink 2.0.0-rc.5 · AGPL-3.0 · source
