Spec version: 1.2 (unreleased diagnostics) · the released 1.1 mapping applies to the midi feature (Audio edition ≥ v0.5.0)
Patternflow is a MIDI device: four knobs and four buttons in, four knobs and four buttons out, and a pattern selector. This document is the contract between the firmware and anything that speaks MIDI to it — a DAW, a hardware controller through a computer, a phone app. Build against this file, not the firmware source.
MIDI is the second performance transport beside OSC. Where OSC
is a bespoke address space, MIDI is the thing every DAW and controller already
speaks, and it carries one thing OSC deliberately does not: absolute knob
values. A Live clip made from the Director's .mid export
(director-midi.md) drives the panel with the identical
automation, because both sides agree on the same four controllers.
Like OSC it does not carry device management — no pattern installation, no Wi-Fi, no firmware, no sleep. That is the HTTP API (rest-api.md).
The mapping below is transport-independent. What ships today:
| RTP-MIDI (AppleMIDI, RFC 6295) over Wi-Fi | The panel is a session listener on UDP 5004 (control) / 5005 (data) — and an initiator toward one remembered host, see Settings —, session name Patternflow, advertised over Bonjour as _apple-midi._udp so it appears by name. macOS/iOS: Audio MIDI Setup → Network (or any CoreMIDI app). Windows: the free rtpMIDI driver — add the panel by name or IP, connect, and it is a MIDI port in Live. Linux: rtpmidid. Two participants at most. |
| USB (a USB-MIDI class device — the MIDI edition) | The DevKit's USB port, the one the panel is flashed over, plugged into the computer. No driver and no session: the panel enumerates as a MIDI device named Patternflow (a virtual serial port comes with it — that is the console's log and Improv, leave it be). MIDI edition only: the port has to be the S3's USB-OTG controller for a MIDI device to exist, and that is a build-time choice (EDITIONS.md). Data only — the panel is powered from J4 as always, never from that cable. Both transports share the map below and can be up at once. |
Channel: 1 by default (PF_MIDI_CHANNEL; 0 listens on every channel
and sends on 1). MIDI Thru is off: the panel never echoes a host's CC or note
back to it. A Program Change from the host comes back as the pattern change it
made, as every pattern change does.
| Message | Meaning |
|---|---|
| CC 20 · 21 · 22 · 23 | Knob 1–4, absolute. 0..127 maps to the device's 0..1000 bus (rounded; 127 = 1000). The value holds until a hand turns that knob, exactly like POST /api/params and MQTT param/N. |
| CC 24 · 25 · 26 · 27 | Knob 1–4, relative. 64 = no motion, 65 = +1 detent, 63 = −1, and so on (binary offset). Merged with physical motion at 1× per step — the same lane as OSC's /patternflow/knob/N/delta. Does not release an absolute hold. |
| Note 60 · 61 · 62 · 63 (C4–D♯4) | Button 1–4. Note-on = press (edge), the button reads as held for as long as the note is held; note-off (or velocity 0) releases. |
Program Change n |
Select pattern index n (0-based, the order on /patterns). Treated as a person's choice: it is remembered across reboots, like a knob pick. Ignored while a show claims the panel. |
Everything else is ignored. Messages on another channel are ignored unless
the channel is 0.
The device's rules, not MIDI's: a hand on an encoder beats everything. An absolute CC pins a knob until that encoder moves; relative CCs and notes are just more input, and the audio lanes yield to the hand and to an absolute CC.
| Message | When |
|---|---|
| CC 24–27 | Encoder 1–4 turned by a hand. Default (outMode=abs): the value is a virtual position 0..127 the panel keeps per knob — starts at 64, moves by that knob's outMul steps per outDiv detents, clamps at the ends — so every DAW reads it as an ordinary knob with nothing to auto-detect; the DAW's takeover mode handles pickup after a clamp. outMode=rel: 64 ± steps since the previous message (binary offset), for hosts that map relative encoders explicitly (Max, TouchDesigner). Live's map-time auto-detection of relative encoders is not reliable with this stream; that is why abs is the default. Pacing (1.1): at most one message per knob every 50 ms (≤ 20/s per knob, PF_MIDI_OUT_MIN_INTERVAL_MS), trailing-edge so the final value always arrives — in abs only a position that differs from the last one sent goes out, in rel the steps sum between messages and a sum leaving ±63 is sent at once. Hand motion only (1.1): a knob that a lane is moving — the on-board microphone, the browser or phone audio path, a weather value — is not reported; PF_MIDI_OUT_LANE_MOTION 1 at build time echoes it anyway. |
| Note-on 60–63, velocity 127 | Encoder button 1–4 pressed |
| Note-off 60–63 | Released |
Program Change n |
The pattern changed (by anyone: knob, console, OSC, MIDI, a show), for n ≤ 127 |
What the host receives is the hand's share only: a CC or note the host itself sent is never reflected back, so mapping the panel's output onto the same track that automates it does not double-record.
GET /api/status on a build with this feature lists "midi" in caps and
carries
"midi": {"runtime": true, "channel": 1, "outDiv": [1,1,4,1], "outMul": [1,2,1,1], "outMode": "abs", "host": "192.168.0.23", "rtpPeers": 1, "rtpPeer": "MacBook", "rx": 812, "tx": 40}runtime is the device's own switch (MIDI on this panel on the console's
MIDI page; the NETWORK screen shows the first two switchable features, so
on the Audio edition, where OSC and AUD come first, MIDI has no row there);
off means the port stays open and everything is dropped, the same convention
as OSC and AUD.
Unreleased 1.2 adds rxDetail to both midi in /api/status and the root of
GET /api/midi. These are cumulative diagnostics since boot, not settings:
| Field | Meaning |
|---|---|
worker |
The independent transport worker is running. false means the frame-polling fallback; MIDI still works, but receive capacity again depends on frame time. |
abs, rel |
Four accepted CC counters, one per knob, after channel/runtime filtering. Absolute and relative controllers are counted separately. |
queueDropped, txQueueDropped |
Incoming/outgoing event queue overflow counts. The finite queues preserve accepted event order; they cannot guarantee delivery at unlimited rates. |
packetGaps |
Positive RTP sequence gaps reported by the transport. Includes possible loss or reordering; it is not an exact count of lost CCs. |
parseErrors, bufferOverflows |
Library parse and buffer-full reports. |
pollMaxGapUs, pollMaxUs |
Maximum interval between transport slices and maximum slice wall time. Scheduling and socket calls can exceed the nominal slice budget. |
queueMaxAgeUs |
Maximum time from a parsed event entering the queue to frame-side dispatch. Excludes time before parsing and panel presentation. |
stackMin |
Minimum free bytes observed on the transport worker's 4,096-byte stack; zero before measurement. |
The transport is serviced independently of display frames. Events cross a fixed 128-entry receive queue and a 64-entry transmit queue; their payload storage uses PSRAM. The frame task alone applies incoming controls. The wire mapping, channel and precedence rules above are unchanged. Disabling MIDI ignores controls and output while continuing session maintenance and draining received events, so re-enabling does not replay an accumulating disabled queue.
The MIDI edition adds midiUsb beside midi in /api/status: mounted (a host has
configured the device and is awake), rx (packets read that carried a channel
message), rxUnhandled (sysex, realtime and pitch bend — read and ignored), tx,
and txDropped (the 16-packet transmit FIFO was full: nobody reading, or more than
16 messages in one frame). This transport has no worker and no queue: USB flow
control makes the host wait while the panel's FIFO is full, so the frame task reads
it directly, one batch per frame, and nothing is lost — only delayed, by a frame at
most.
GET /api/midi |
channel, outDiv, outMul, outMode, host, session state, counters |
POST /api/midi?outMode=abs|rel |
knobs out as a virtual position (default) or as relative steps. Persisted. |
POST /api/midi?outMul=N |
outbound sensitivity the other way: steps per detent, 1..8. Setting one of outMul/outDiv above 1 resets the other to 1; the console page shows both as one scale from ×8 through 1:1 to 1/16. |
...&knob=1..4 |
Apply outMul/outDiv to one knob; without it, to all four. Each knob keeps its own ratio; GET reports outMul and outDiv as four-element arrays. |
POST /api/midi?outDiv=N |
outbound sensitivity: detents per outbound step, 1..16, persisted on the panel. The encoders have 24 detents a turn, so 1 is 24 steps per turn and 4 is 6. Default 1 (PF_MIDI_OUT_DIVISOR). |
POST /api/midi?host=<ip> |
A host to invite: the panel sends the session invitation itself on boot and again every 20 s while no session is up, so a panel that reboots comes back into the DAW without anyone reopening rtpMIDI or Audio MIDI Setup. The host must accept invitations (rtpMIDI: Who may connect to me: Anyone; macOS: the same setting in the Network MIDI window). Empty string clears it. Persisted. |
-
CC 20–27 are undefined controllers in the MIDI spec — nothing in a DAW listens to them by accident, and the Director's export already chose 20–23. Relative lanes sit right after.
-
Notes 60–63 are the four keys any keyboard or pad grid has under the hand at middle C.
-
Program Change is what a pattern list is in MIDI's vocabulary. The limit of 128 covers every panel anyone has filled.
-
1.3 (unreleased) — a second transport: the DevKit's USB port as a USB-MIDI class device, in the MIDI edition;
midiUsbin/api/status. The map, the channel rule and precedence are unchanged, and both transports can be up at once.
- 1.2 (unreleased) — additive receive diagnostics, transport service independent of display frames, and suppression of outbound CCs generated from absolute-bus compatibility clicks. No new transport or CC mapping. Where this file said two things, the section that defines the message now stands: CC 24–27 are binary offset, as their examples always were; a host's Program Change comes back as the pattern change it made; the audio lanes yield to a hand and an absolute CC.
- 1.1 (2026-09-06) — outbound CC 24–27 paced: one message per knob per
50 ms at most (
PF_MIDI_OUT_MIN_INTERVAL_MS), the final value always sent;relcarries the steps summed since the previous message. On the Audio edition a microphone lane moving a knob 1/127 per frame was one UDP packet per frame, enough to exhaust the Wi-Fi TX buffers and starve the console. Lane motion (microphone, browser/phone audio, weather) is no longer reported at all — the row always said "turned by a hand" — unless the build setsPF_MIDI_OUT_LANE_MOTION. Notes and Program Change are unchanged. - 1.0 — CC 20–23 absolute in; CC 24–27 relative in/out; notes 60–63
buttons in/out; Program Change in/out; RTP-MIDI listener on 5004;
outDivsensitivity,outMode(absolute position by default) and a rememberedhostto invite, over/api/midi.