Skip to content

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).

.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.

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).

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.

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.

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.

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.

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).

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.

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).

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).

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.

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.

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.

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.

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.

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.

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.

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).

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=…&param=…&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).

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).

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.

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.

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).

~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).

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.

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.

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.

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.

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