MIDI implementation chart
midi-sink is two MIDI devices in one — three, with the sound engine. As a visualizer it accepts three dialects — MPE (the default), classic keyboard, wind — chosen by the Input setting. As an instrument (Play mode on the tablets) it generates a standard MPE stream that feeds its own visualizer through a loopback and leaves the device to your DAW. As a synth (Voxo, with Suzu inside) it consumes the identical bytes with consumers of its own. All of it is on this page.
The output sections below were verified against real byte logs — sessions
recorded at the shell’s single merge point on the iPad and the Galaxy Tab,
one per source: fingers, stylus, control strip, session configuration, and
since Phase 9 the fingering panel’s valves and slide.
tools/chart_check.py replays those logs against this very table and fails
if a documented message is missing, a forbidden one appears, or the log
contains anything the chart does not describe. The input sections are the
normalizer’s contract, pinned by its headless golden suite.
How the modes are chosen
Section titled “How the modes are chosen”The normalizer runs a light heuristic, overridable by the host
(sumi_set_input_mode):
- an MPE Configuration Message, or note-ons spread across channels 2+ with per-channel bend or pressure → MPE mode;
- notes only ever on one channel plus a dense CC 2 stream → wind mode;
- otherwise → classic mode.
Airwave-style CC streams are orthogonal — routed by the CC map in every mode. On the tablets, entering Play mode pushes the MCM and RPN 0 = 48 into the loopback first, so the visualizer’s mode and bend range are deterministic, never heuristic.
Ownership — one consumer per dimension
Section titled “Ownership — one consumer per dimension”| Dimension | Setting | 0 (default) | 1 |
|---|---|---|---|
| Per-note pitch bend | bend_mode | glide — drags the drop along the pitch axis | ripple — shimmers the water, the drop holds |
| Channel pressure (0xD0) | press_mode | ink feed — unbounded boundary growth | Lamb–Oseen swirl |
| CC 74 (slide) | slide_mode | per-drop hue / ink density | Hamiltonian pinch at the drop |
Under the Anod medium each row’s Medium default reads the medium’s table instead — the spark, torsion, the Chladni stir, the spark’s frequency (the medium) — and the rows gained the electric modes: per-note bend → torsion wavelength (2), spark frequency (3), Chladni stir (4); channel pressure → torsion (2); slide → spark frequency (2). Poly key pressure (0xA0) always drives the swirl under Sumi and tunes the torsion under Anod; the master bend always shears; switching a mode is clean in both directions (no stale-delta passes). The fingering CCs 110–113 are global state on the master channel, never a voice dimension.
The numbers
| Constant | Value |
|---|---|
| Member-channel bend range (declared by the MCM, RPN 0) | ±48 semitones |
| Master-channel bend range | ±2 semitones (the MPE default; never re-declared) |
| Zone | Lower zone: master channel 1, members 2–16 (15 voices) |
| One semitone of per-note bend | ±171 counts (8192 + round(st / 48 · 8192)) |
| Outbound rate class (USB, virtual, network) | ≤ 100 Hz per voice per dimension, latest-wins |
| Outbound budget class (BLE) | ~300 msg/s total, latest-wins, round-robin fairness |
| Echo window | 300 ms — our own bytes mirrored back are consumed |
| Externally held channel, stuck-note timeout | 30 s |
| Ingest queue | 4096 messages, drop-oldest; after an overflow a silent voice expires in 10 s |
Input · MPE mode
ROLI Piano / Seaboard, Expressive E Osmose, the tablets' own Play surface (loopback) and any DAW replaying them. Entered on an MPE Configuration Message, or when note-ons spread across channels 2+ with per-channel bend/pressure; forced with sumi_set_input_mode.
| Message | Channel | Consumer | Notes |
|---|---|---|---|
Note On | member | VoiceBegin — strike: drop radius ∝ √velocity at the layout's position(s) | One active note per member channel; a same-channel overlap steals the voice (the stylus legato relies on this). |
Note Off | member | VoiceEnd — lift: the drop sets, feeding stops | Nothing is stamped. A Note Off naming a note the channel no longer sounds is ignored. |
Pitch Bend | member | VoiceGlide — bend_mode 0: drags the drop along the pitch axis (a local tine); bend_mode 1: feeds the ripple, drop holds | Range ±48 st by default (RPN 0 honoured). One consumer owns the note bend; the switch is clean both ways. |
Channel Pressure (0xD0) | member | VoicePress — press_mode 0: sustained ink feed (boundary growth, unbounded); press_mode 1: the Lamb–Oseen swirl | Smoothed (~30 ms), one value per voice per frame. The Osmose behaviour. |
Poly Key Pressure (0xA0) | member | VoiceSwirl — the Lamb–Oseen swirl at the voice's centre, unconditionally | No reference hardware sends it; the Play surface's Y-down does. Sign from band parity. |
CC 74 | member | VoiceSlide — slide_mode 0: per-drop hue/ink density; slide_mode 1: Hamiltonian pinch at the drop | Reserved: not routable through the CC map on member channels. |
Pitch Bend | master | Global shear tine (range ±2 st) | In both bend modes. |
CC 1 | any | GlobalCtl vortex strength (exponential profile) — the mod wheel | Default CC map; remappable. |
CC 64 | any | Ignored by the canvas in every mode (it is a pedal for the synth downstream) | DECISIONS_4 #62: the paper dip is a settings action, never a MIDI message. |
CC 110 / 111 / 112 | master | The trumpet's valves (≥ 64 = pressed): the engine's layout state — the partial cells sound their partial minus the valves' offset (−2, −1, −3 semitones, additive) on the trumpet layout | Phase 9 (DECISIONS_8 #1): global fingering on the master channel, decoded by the normalizer into sumi_layout_state_t; a controller plays the brass layouts exactly as the tablets' fingering panel does. |
CC 113 | master | The trombone's slide: 0 closed to 127 = six semitones down, continuous, smoothed over 10 ms | 128 steps over six semitones, 4.7 cents a step. |
CC 120 / CC 123 | any | All Sound Off / All Notes Off: the channel's held voice ends silently (a member voice in MPE, the one brush in wind mode) | DECISIONS_8 #12: a panic from the strip or a DAW flushes the visualizer's voices as it flushes a synth's. Never a control. |
CC 122 | any | Local Control: whether the play surface's own notes sound inside the app (the sound engine); the canvas is unaffected | Tracked by Voxo (step 54); the visualizer ignores it. |
CC 101 / 100 / 6 (RPN 6) | master | MPE Configuration Message — zone membership | Single zone; dual-zone configs are logged and ignored. Default with no MCM: master 1, members 2–16. |
CC 101 / 100 / 6 (RPN 0) | member | Per-channel bend range | Default 48 on members, 2 on the master. |
CC 20–29, any mapped CC | any | GlobalCtl — the CC map: vortex strength / centre X / Y, Lamb–Oseen swirl strength / centre X / Y, saddle and crossed-tine pinches, viscosity, roughness, palette morph, ink flow, ripple amp / freq, the torsion's wavenumber and phase, the Chladni stir and balance, the spark's frequency, the Chirikov throw (centre Y dims reversed: CC up = up on screen) | Airwave defaults, each hand a stirring hand (measured L/R pairs, DECISIONS_4 #50/#69): 26/27 Raise → vortex / swirl strength, 24/25 Glide → centre X, 22/23 Slide → centre Y, 20/21 Grasp → saddle / crossed pinch, 28/29 Tilt → ripple wavelength / amount; 30/31 Flex free. Desktop also routes CC 102/103 to the ripple. The desktop's stock map also routes CC 104/105 (torsion k, φ), 106/107 (Chladni A, B), 108 (spark k), 109 (Chirikov throw). |
SysEx, MIDI 2.0 / UMP | — | Ignored | Out of scope by design; the ingest path does not preclude UMP later. |
Verified by tests/normalizer_tests.cpp (headless goldens, every row).
Input · Wind mode
Roland Aerophone Brisa, Odisei Travel Sax, any breath controller: notes on one channel plus a dense CC 2 stream.
| Message | Channel | Consumer | Notes |
|---|---|---|---|
Note On | single | The one voice: a wandering ink brush. First note = VoiceBegin; a legato note change = VoiceMigrate (the brush travels, drawing a wake) | Never a disconnected new drop while a note is held. |
Note Off | single | VoiceEnd when the last held note releases | |
CC 2 / CC 7 / CC 11 / Channel Pressure | single | Breath → the brush's WIDTH (relaxes toward 0.006 + 0.05·breath canvas heights) | Bounded on purpose: a 20 s line stays a line, not a blob. |
Pitch Bend | single | Global shear tine (±2 st) |
Verified by tests/normalizer_tests.cpp; tests/wind_breath_alsa.cpp / wind_breath.swift (live feeders).
Input · Classic mode
Any single-channel keyboard; the fallback when neither MPE nor wind is detected.
| Message | Channel | Consumer | Notes |
|---|---|---|---|
Note On / Off | single | Each note is its own voice — strike only (drop radius ∝ √velocity) | |
Pitch Bend | single | Global shear tine (±2 st) | |
CC 1 | single | Vortex strength (mod wheel) | |
CC 64 | single | Ignored (unmapped; route it in the CC map if wanted) | DECISIONS_4 #62 removed the classic-only paper dip. |
Verified by tests/normalizer_tests.cpp.
Input · the sound engine (Voxo)
What the sound engine consumes from the same bytes the visuals read (Phase 7–8). The sampler plays the note; Suzu's consumers are its own, one per dimension. Verified by the Voxo and Suzu headless suites.
| Message | Channel | Consumer | Notes |
|---|---|---|---|
Note On / Off | member · single | A sampler voice (the instrument's zone, velocity layers as the preset maps them), a Suzu voice (the strike: the cell's orbit at A, the lattice's kick profile, the chain's triangle, the modulator's energy), or both under the layered source; Note Off is the declared release | Same-channel overlap steals, as in the visualizer. |
Pitch Bend | member · single | The glide: the sampler's pitch; every Suzu cell retuned per frame with its orbit re-based, the chain re-derived, the bores re-cut | The member range the MCM declared (48 by default). |
Channel Pressure (0xD0) | member · single | Suzu's output gain 0.35 + 0.65·p; the Duffing drive's amplitude; and, with "the press blows", the mouth of the bow and the winds | Two consumers under that patch flag, declared as such (DECISIONS_7 #12). |
Poly Key Pressure (0xA0) | member | The lattice's coupling: κ += 0.5·swirl per block | |
CC 74 | member | The sampler's cutoff where the preset maps it; Suzu's filter ±3 octaves about the patch's; the trumpet's embouchure (the lips' resonance ±lip_range octaves) | |
CC 1 | any | The kicked rotor's K, up to 2.5, smoothed 20 ms | The same wheel throws the Chirikov map on the canvas under Anod. |
CC 2 / CC 11 | any | Breath: the bow's target per mode; the winds' mouth pressure (CC 2 the patch's breath_cc, CC 11 the alias) | No breath, no tone, on the bow and the winds. |
CC 64 | any | Sustain, honoured as a pedal by the sampler and by Suzu's release | |
CC 120 / CC 123 | any | All Sound Off / All Notes Off: every voice ends | |
CC 122 | any | Local Control: 0 = the play surface's own bytes do not sound inside; external bytes always do | The tablets' "The play surface sounds here" switch. |
any mapped CC | any | The bus effects through the CC map: reverb wet / room / damping, delay wet / time / feedback (targets 1000–1005) | Beside the engine's twenty dimensions; a preset carries the routes. |
Verified by tests/voxo_tests.cpp, tests/voxo_preset_tests.cpp and tests/voxo_suzu_tests.cpp (the storm acceptance on the desktop: 0 XRuns, 0 dropped).
Output · Session configuration (src 2)
Sent on both pipes — into the loopback on Play-mode entry, out on every sink at session open, on a sink appearing mid-session and on “Re-sync DAW”. Never rate-limited.
| Message | Channel | Meaning | Notes | Emitted |
|---|---|---|---|---|
CC 101 | master | RPN MSB = 0 (MCM, then RPN 0) | Order asserted: 101=0, 100=6, 6=15 on the master, then 101=0, 100=0, 6=48 on every member. | always |
CC 100 | master | RPN LSB = 6 → the MPE Configuration Message | always | |
CC 6 | master | MCM: lower zone, 15 member channels | always | |
CC 101 | member | RPN MSB = 0 | always | |
CC 100 | member | RPN LSB = 0 → bend range | always | |
CC 6 | member | Bend range = 48 semitones, on all 15 members | The strip sends NO RPN 0 on the master: the master stays at the MPE default ±2. | always |
Verified against device_byte_log_smoke.csv (src 2) by tools/chart_check.py.
Output · Finger voices (src 1)
One joystick per touch on the lattice. Touch-down: centre bend, then Note On. Lift: pressure 0 (and 0xA0 = 0 if the swirl half is engaged), then Note Off.
| Message | Channel | Meaning | Notes | Emitted |
|---|---|---|---|---|
Pitch Bend | member | Centre (8192) before every Note On; then X → semitones along the cell's pitch axis: pb = 8192 + round(st/48·8192), one semitone = ±171 counts | Soft knee inside the cell circle, identity beyond it — a one-cell drag is exactly one semitone. | always |
Note On | member | Cell note, velocity 96 (touch-size modulation behind a setting) | Allocator: least-recently-released round-robin over 15 channels; externally held channels masked; saturation = silent drop + HUD blink, never steal. | always |
Channel Pressure (0xD0) | member | Y up → ink feed, 0…127 on the knee; 0 before every Note Off | Two-byte message on the wire. | always |
Poly Key Pressure (0xA0) | member | Y down → the Lamb–Oseen swirl, keyed by the voice's note | Many MPE synths ignore 0xA0 — primarily a visualizer dimension that happens to be recordable. | when used |
Note Off | member | Release velocity 64 (unmeasured) | always | |
CC 74 | member | Fingers emit no CC 74, ever — timbre belongs to the stylus | Asserted in every log: 0 finger CC 74. | never |
Verified against device_byte_log_smoke.csv (src 1) by tools/chart_check.py.
Output · Stylus voices (src 4)
Apple Pencil / S-Pen: absolute-position play with per-cell legato. Crossing into a new cell (±0.65 st past the current note) emits bend → Note On (new) → Note Off (old) on the SAME channel.
| Message | Channel | Meaning | Notes | Emitted |
|---|---|---|---|---|
Pitch Bend | member | Centre before the first strike; the in-cell offset live at ×1 (vibrato), ×1→×3 while the Pencil Pro barrel rolls (τ = 0.4 s decay); the cell offset before each legato retrigger | Never accumulates past half a cell: the retrigger re-anchors it. | always |
Note On | member | Velocity from tip force: Pencil 96 + (max(force,1) − 1)·15.5; S-Pen 96 + 31·t | A legato Note On is followed, not preceded, by the old note's Off. | always |
Note Off | member | The old note after a retrigger; the last note after pressure 0 on lift | always | |
Channel Pressure (0xD0) | member | True tip force, change-only; 0 before the final Note Off | always | |
CC 74 | member | Y about the strike → 64 ± 63 (up = brighter), change-only | Under slide_mode = 1 it goes OUTBOUND ONLY: the shell pinches through sumi_add_pinch directly, and a looped CC 74 would pinch twice. | when used |
Poly Key Pressure (0xA0) | member | The stylus has no swirl axis | never |
Verified against pen_trace_byte_log.csv (src 4) by tools/chart_check.py.
Output · Control strip and pen pedal (src 3)
The floating palette. Every message on the MASTER channel — member channels are never touched. Wheels are policed as continuous dimensions on the outbound pipe; buttons are never dropped.
| Message | Channel | Meaning | Notes | Emitted |
|---|---|---|---|---|
Pitch Bend | master | Pitch spring wheel, ±2 st; release ramps ~50 ms to a guaranteed exact-centre final message | Unaffected by bend_mode. | when used |
CC 1 | master | Mod latch wheel — relative deltas, no absolute entry point | Loopback routes it to the vortex. | when used |
CC 23 | master | Assignable A (default; loopback: viscosity) | Reassignable by long-press; protocol CCs (1, 6, 38, 64, 98–101, 120–127) refused. | when used |
CC 24 | master | Assignable B (default; loopback: roughness) | Natural homes for the ripple's CC 102/103. | when used |
CC 64 | master | Sustain — the strip's pad, the Pencil Pro squeeze, the S-Pen barrel button: one engine. Momentary by default (127 / 0), toggle behind a setting | Never dropped; asserted balanced with the last value 0. Not a paper dip in Play mode. | always |
CC 123 | master + members | Panic: every voice released (pressure 0 → Note Off), then CC 64 = 0, CC 120 and CC 123 on the master and all members; the strip reset (wheels, valves, slide) and re-announced | Exempt from limiting; on the loopback and every transport. The strip's Panic pad and the settings' action. CC 120 rides with it (DECISIONS_8 #12); this session's log holds no panic. | when used |
Verified against pen_button_sustain_byte_log.csv (src 3) by tools/chart_check.py.
Output · The valves (src 3)
The fingering panel on the trumpet layout — three momentary valve pads whose CCs ride the master channel like the strip's; the announce after a re-sync restates them (DECISIONS_8 #9). Logged on the iPad with the fingering demo (step 63).
| Message | Channel | Meaning | Notes | Emitted |
|---|---|---|---|---|
CC 110 | master | Valve 1: 127 down, 0 up (a whole tone down on every partial) | Momentary, change-only (a held valve repeats nothing); a button, exempt from limiting. Every held voice retunes through a bend on its member channel, ramped over 30 ms. | always |
CC 111 | master | Valve 2: a semitone down | always | |
CC 112 | master | Valve 3: a minor third down; the three add | always | |
CC 113 | master | The slide, restated by the announce (0 on the trumpet) | The announce of nine after a re-sync: bend, CC 1, the two assignables, CC 64, the three valves, the slide. | when used |
Pitch Bend | master | The pitch spring wheel's announce (centre) — the strip's, restated with the fingering | when used | |
CC 1 | master | The mod latch wheel's announce | when used | |
CC 23 | master | Assignable A's announce | when used | |
CC 24 | master | Assignable B's announce | when used | |
CC 64 | master | Sustain's announce (0) | when used |
Verified against trumpet_byte_log.csv (src 3) by tools/chart_check.py.
Output · The slide (src 3)
The trombone's slide on the fingering panel — a positional slider (where you hold it is the position), policed like a wheel; held voices retune at once (DECISIONS_8 #9). Logged on the iPad with the fingering demo on the trombone (step 63).
| Message | Channel | Meaning | Notes | Emitted |
|---|---|---|---|---|
CC 113 | master | The slide: 0 closed to 127 = six semitones down, continuous | A continuous dimension: ≤ 100 Hz latest-wins on the rate class, budgeted on BLE. The engine smooths it over 10 ms. | always |
CC 110 | master | Valve 1, restated by the announce (0 on the trombone) | when used | |
CC 111 | master | Valve 2's announce | when used | |
CC 112 | master | Valve 3's announce | when used | |
Pitch Bend | master | The pitch spring wheel's announce | when used | |
CC 1 | master | The mod latch wheel's announce | when used | |
CC 23 | master | Assignable A's announce | when used | |
CC 24 | master | Assignable B's announce | when used | |
CC 64 | master | Sustain's announce | when used |
Verified against trombone_byte_log.csv (src 3) by tools/chart_check.py.
What a DAW recording does not capture
- The stylus WAKE (the dipolar wake behind every pen stroke) is physical, never MIDI: a DAW replay of a stylus performance reproduces notes, bends, CC 74 and pressure exactly — and no wakes, because a DAW has no stylus in the water.
- Marble-mode gestures (tap, drag, twist, pinch) generate zero MIDI.
- CC 74 under slide_mode = 1 reaches the DAW but not the loopback; a replay still pinches, via the normalizer's CC 74 route with the pitch-axis fold — a different fold axis than the live pen's azimuth.
- Outbound pipes are rate-limited (100 Hz per voice per dimension; ~300 msg/s total on BLE) while the loopback is full-rate: a recording is a faithful but decimated view of the continuous dimensions. Note On/Off, the centre bend, pressure-0-before-Note-Off and CC 64 are never dropped.
- Our own bytes echoed back by a transport (a DAW thru, a looping network session) are consumed inside 300 ms and never re-enter — the recording is what left the device.
- The fingering panel's valves and slide ARE captured — as CC 110–113 on the master and as the retuning bends on each held voice's member channel — so a DAW recording replays the brass as it was played. What a DAW cannot carry is midi-sink's own session: the gestures, the wakes and the frame boundaries, which the app's replay files hold.
Reading a DAW recording of a Play-surface performance
Section titled “Reading a DAW recording of a Play-surface performance”A recorded finger performance replays exactly: the centre bend before every strike keeps attacks in tune, X-bends are semitone-scaled against the declared ±48 range, pressure and poly pressure re-drive the feed and the swirl, and the Note Offs land after their pressure-0. A recorded stylus performance replays every note, bend, CC 74 and pressure — and no wakes. The wake is physical: it is the only mark on the water a DAW cannot make.
midi-sink 2.0.0-rc.5 · AGPL-3.0 · source
