Skip to content

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.

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.

DimensionSetting0 (default)1
Per-note pitch bendbend_modeglide — drags the drop along the pitch axisripple — shimmers the water, the drop holds
Channel pressure (0xD0)press_modeink feed — unbounded boundary growthLamb–Oseen swirl
CC 74 (slide)slide_modeper-drop hue / ink densityHamiltonian 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

ConstantValue
Member-channel bend range (declared by the MCM, RPN 0)±48 semitones
Master-channel bend range±2 semitones (the MPE default; never re-declared)
ZoneLower 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 window300 ms — our own bytes mirrored back are consumed
Externally held channel, stuck-note timeout30 s
Ingest queue4096 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.

MessageChannelConsumerNotes
Note OnmemberVoiceBegin — 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 OffmemberVoiceEnd — lift: the drop sets, feeding stopsNothing is stamped. A Note Off naming a note the channel no longer sounds is ignored.
Pitch BendmemberVoiceGlide — bend_mode 0: drags the drop along the pitch axis (a local tine); bend_mode 1: feeds the ripple, drop holdsRange ±48 st by default (RPN 0 honoured). One consumer owns the note bend; the switch is clean both ways.
Channel Pressure (0xD0)memberVoicePress — press_mode 0: sustained ink feed (boundary growth, unbounded); press_mode 1: the Lamb–Oseen swirlSmoothed (~30 ms), one value per voice per frame. The Osmose behaviour.
Poly Key Pressure (0xA0)memberVoiceSwirl — the Lamb–Oseen swirl at the voice's centre, unconditionallyNo reference hardware sends it; the Play surface's Y-down does. Sign from band parity.
CC 74memberVoiceSlide — slide_mode 0: per-drop hue/ink density; slide_mode 1: Hamiltonian pinch at the dropReserved: not routable through the CC map on member channels.
Pitch BendmasterGlobal shear tine (range ±2 st)In both bend modes.
CC 1anyGlobalCtl vortex strength (exponential profile) — the mod wheelDefault CC map; remappable.
CC 64anyIgnored 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 / 112masterThe 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 layoutPhase 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 113masterThe trombone's slide: 0 closed to 127 = six semitones down, continuous, smoothed over 10 ms128 steps over six semitones, 4.7 cents a step.
CC 120 / CC 123anyAll 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 122anyLocal Control: whether the play surface's own notes sound inside the app (the sound engine); the canvas is unaffectedTracked by Voxo (step 54); the visualizer ignores it.
CC 101 / 100 / 6 (RPN 6)masterMPE Configuration Message — zone membershipSingle zone; dual-zone configs are logged and ignored. Default with no MCM: master 1, members 2–16.
CC 101 / 100 / 6 (RPN 0)memberPer-channel bend rangeDefault 48 on members, 2 on the master.
CC 20–29, any mapped CCanyGlobalCtl — 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—IgnoredOut 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.

MessageChannelConsumerNotes
Note OnsingleThe 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 OffsingleVoiceEnd when the last held note releases
CC 2 / CC 7 / CC 11 / Channel PressuresingleBreath → 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 BendsingleGlobal 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.

MessageChannelConsumerNotes
Note On / OffsingleEach note is its own voice — strike only (drop radius ∝ √velocity)
Pitch BendsingleGlobal shear tine (±2 st)
CC 1singleVortex strength (mod wheel)
CC 64singleIgnored (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.

MessageChannelConsumerNotes
Note On / Offmember · singleA 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 releaseSame-channel overlap steals, as in the visualizer.
Pitch Bendmember · singleThe glide: the sampler's pitch; every Suzu cell retuned per frame with its orbit re-based, the chain re-derived, the bores re-cutThe member range the MCM declared (48 by default).
Channel Pressure (0xD0)member · singleSuzu'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 windsTwo consumers under that patch flag, declared as such (DECISIONS_7 #12).
Poly Key Pressure (0xA0)memberThe lattice's coupling: κ += 0.5·swirl per block
CC 74memberThe 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 1anyThe kicked rotor's K, up to 2.5, smoothed 20 msThe same wheel throws the Chirikov map on the canvas under Anod.
CC 2 / CC 11anyBreath: 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 64anySustain, honoured as a pedal by the sampler and by Suzu's release
CC 120 / CC 123anyAll Sound Off / All Notes Off: every voice ends
CC 122anyLocal Control: 0 = the play surface's own bytes do not sound inside; external bytes always doThe tablets' "The play surface sounds here" switch.
any mapped CCanyThe 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.

MessageChannelMeaningNotesEmitted
CC 101masterRPN 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 100masterRPN LSB = 6 → the MPE Configuration Messagealways
CC 6masterMCM: lower zone, 15 member channelsalways
CC 101memberRPN MSB = 0always
CC 100memberRPN LSB = 0 → bend rangealways
CC 6memberBend range = 48 semitones, on all 15 membersThe 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.

MessageChannelMeaningNotesEmitted
Pitch BendmemberCentre (8192) before every Note On; then X → semitones along the cell's pitch axis: pb = 8192 + round(st/48·8192), one semitone = ±171 countsSoft knee inside the cell circle, identity beyond it — a one-cell drag is exactly one semitone.always
Note OnmemberCell 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)memberY up → ink feed, 0…127 on the knee; 0 before every Note OffTwo-byte message on the wire.always
Poly Key Pressure (0xA0)memberY down → the Lamb–Oseen swirl, keyed by the voice's noteMany MPE synths ignore 0xA0 — primarily a visualizer dimension that happens to be recordable.when used
Note OffmemberRelease velocity 64 (unmeasured)always
CC 74memberFingers emit no CC 74, ever — timbre belongs to the stylusAsserted 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.

MessageChannelMeaningNotesEmitted
Pitch BendmemberCentre 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 retriggerNever accumulates past half a cell: the retrigger re-anchors it.always
Note OnmemberVelocity from tip force: Pencil 96 + (max(force,1) − 1)·15.5; S-Pen 96 + 31·tA legato Note On is followed, not preceded, by the old note's Off.always
Note OffmemberThe old note after a retrigger; the last note after pressure 0 on liftalways
Channel Pressure (0xD0)memberTrue tip force, change-only; 0 before the final Note Offalways
CC 74memberY about the strike → 64 ± 63 (up = brighter), change-onlyUnder 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)memberThe stylus has no swirl axisnever

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.

MessageChannelMeaningNotesEmitted
Pitch BendmasterPitch spring wheel, ±2 st; release ramps ~50 ms to a guaranteed exact-centre final messageUnaffected by bend_mode.when used
CC 1masterMod latch wheel — relative deltas, no absolute entry pointLoopback routes it to the vortex.when used
CC 23masterAssignable A (default; loopback: viscosity)Reassignable by long-press; protocol CCs (1, 6, 38, 64, 98–101, 120–127) refused.when used
CC 24masterAssignable B (default; loopback: roughness)Natural homes for the ripple's CC 102/103.when used
CC 64masterSustain — the strip's pad, the Pencil Pro squeeze, the S-Pen barrel button: one engine. Momentary by default (127 / 0), toggle behind a settingNever dropped; asserted balanced with the last value 0. Not a paper dip in Play mode.always
CC 123master + membersPanic: 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-announcedExempt 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).

MessageChannelMeaningNotesEmitted
CC 110masterValve 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 111masterValve 2: a semitone downalways
CC 112masterValve 3: a minor third down; the three addalways
CC 113masterThe 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 BendmasterThe pitch spring wheel's announce (centre) — the strip's, restated with the fingeringwhen used
CC 1masterThe mod latch wheel's announcewhen used
CC 23masterAssignable A's announcewhen used
CC 24masterAssignable B's announcewhen used
CC 64masterSustain'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).

MessageChannelMeaningNotesEmitted
CC 113masterThe slide: 0 closed to 127 = six semitones down, continuousA continuous dimension: ≤ 100 Hz latest-wins on the rate class, budgeted on BLE. The engine smooths it over 10 ms.always
CC 110masterValve 1, restated by the announce (0 on the trombone)when used
CC 111masterValve 2's announcewhen used
CC 112masterValve 3's announcewhen used
Pitch BendmasterThe pitch spring wheel's announcewhen used
CC 1masterThe mod latch wheel's announcewhen used
CC 23masterAssignable A's announcewhen used
CC 24masterAssignable B's announcewhen used
CC 64masterSustain's announcewhen 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