Signal flow: MIDI in, OSC out
A note never passes through the Linux UI on its way to a voice. The XMOS voice controller takes MIDI in, decides which voices play, and emits OSC to the voice cards.
Sections 1 to 5 describe the instrument as shipped. Section 6 is the modification this project makes to get per-note MPE. Each diagram can be read at three levels: simplified, for anyone who knows MIDI; intermediate, showing the stages; and detailed, showing every step and message.
1. The whole path
flowchart TB CTRL(["MIDI controller
USB or DIN"]) KEYS(["Built-in keyboard,
wheels and pad"]) SEQ(["Sequencer and
arpeggiator"]) BRAIN["Voice controller
decides which Synth plays each note
and which voice sounds it"] VOICES[["Analog voices"]] CTRL -->|"notes on a MIDI channel"| BRAIN KEYS -->|"notes with no channel"| BRAIN SEQ -->|"notes on a MIDI channel"| BRAIN BRAIN -->|"start and stop each voice"| VOICES BRAIN -->|"bend, aftertouch and pad,
one value per Synth"| VOICES
flowchart TB CTRL(["External controller
USB-MIDI / DIN"]) KEYS(["Built-in keybed,
wheels, XY pad"]) subgraph FE["Linux frontend"] UI["UI and presets"] SEQ["Sequencer / arp"] end subgraph VC["XMOS voice controller"] subgraph ING["Ingress"] RX["MIDI receive"] SCAN["Panel scan"] PERF["Performance port
UDP 7771"] PARAM["Parameter port
UDP 7770"] end subgraph ROUTE["Routing"] R{"Router"} TF["Timbre filters x3
channel, note range"] LF["Local filter x3
note range, mask cache"] end subgraph OUT["Voices"] AL["Voice allocator
per timbre"] GX["Timbre expression"] EM["OSC emitter"] end end CARDS[["Voice cards
2 voices each"]] CTRL -->|"MIDI, channel 0-15"| RX KEYS -->|"key and control state"| SCAN SEQ -->|"/r/sq/0/midi-arp ,iii"| PERF UI -->|"/x ,b parameter records"| PARAM RX -->|"MIDI event"| R PERF -->|"sequencer event"| R SCAN -->|"local event, channel = -1"| LF R -->|"offered to each timbre"| TF TF -->|"notes"| AL LF -->|"local notes"| AL TF -->|"bend, pressure, CC"| GX LF -->|"wheels, pad, aftertouch"| GX PARAM -->|"patch values"| EM AL -->|"voice assignments"| EM GX -->|"one value per timbre"| EM EM -->|"/vN/n /vN/nw /vN/p
per voice"| CARDS EM -->|"/m/pw /m/a /m/xyz
per timbre"| CARDS EM -.->|"note echo, UDP 7772"| UI
The frontend and the voice controller share an internal USB-Ethernet link. The rear Ethernet jack is also routed through the voice controller, so a hung controller takes the whole box off the network.
2. External controller
USB and DIN MIDI terminate on the voice controller. Each message becomes an internal event carrying its type, input port, MIDI channel, note, raw 14-bit data and velocity. The router offers the event to each of the three timbres; a timbre accepts it if it passes that timbre's filter for that input port. Matching is independent per timbre, so two timbres on the same channel layer.
flowchart TB
NOTE(["Note on MIDI channel 2"])
ASK{"Which Synths
listen on channel 2?"}
S1["Synth 1
takes a free voice"]
S2["Synth 2
takes a free voice"]
S3["Synth 3"]
BEND(["Pitch bend or aftertouch
on channel 2"])
V[["Voices"]]
NOTE --> ASK
ASK -->|"set to channel 2"| S1
ASK -->|"also set to channel 2"| S2
ASK -.->|"set to channel 5, ignores it"| S3
S1 -->|"one voice per note"| V
S2 -->|"one voice per note"| V
BEND -->|"moves every note
Synth 1 and 2 are holding"| V
flowchart TB IN(["MIDI in
status | channel, d1, d2"]) EV["Event
type, port, channel,
note, raw14, velocity"] R{"Router"} subgraph T1["Timbre 1"] F1["Filter
channel + note range"] A1["Allocator"] G1["Expression"] end subgraph T2["Timbre 2"] F2["Filter"] A2["Allocator"] G2["Expression"] end subgraph T3["Timbre 3"] F3["Filter"] A3["Allocator"] G3["Expression"] end CARD[["Voice cards"]] IN -->|"parse"| EV EV --> R R -->|"offer"| F1 R -->|"offer"| F2 R -->|"offer"| F3 F1 -->|"note"| A1 F1 -->|"bend, pressure, CC"| G1 F2 -->|"note"| A2 F2 -->|"bend, pressure, CC"| G2 F3 -->|"note"| A3 F3 -->|"bend, pressure, CC"| G3 A1 -->|"/vN/n /vN/nw /vN/p"| CARD A2 -->|"/vN/n /vN/nw /vN/p"| CARD A3 -->|"/vN/n /vN/nw /vN/p"| CARD G1 -->|"/m/pw /m/a /m/xyz"| CARD G2 -->|"/m/pw /m/a /m/xyz"| CARD G3 -->|"/m/pw /m/a /m/xyz"| CARD
A voice v lives on card v / 2, slot 1 + v mod 2, which
is why card addresses are only ever /v1 or /v2.
The frontend sees none of this directly. It learns which notes are sounding from the echo the controller sends back on UDP 7772.
3. Built-in keyboard
The keybed and the left-hand controls are not MIDI and do not arrive over the OSC performance port. They enter the voice controller as local events with no channel (the channel field is โ1).
flowchart TB
KEY(["Key pressed"])
ASK{"Which Synths cover
this key?"}
S1["Synth 1
lower split"]
S2["Synth 2
upper split"]
WH(["Wheel, pad or aftertouch"])
V[["Voices"]]
KEY --> ASK
ASK -->|"key is in its range"| S1
ASK -.->|"key is outside its range"| S2
S1 -->|"one voice per note"| V
WH -->|"moves every held note"| V
flowchart TB
subgraph ON["Key down"]
KD(["Key down"])
LEV["Local event
channel = -1"]
LF["Inline filter per timbre
local note range"]
MASK["Cache routing mask
for this key"]
AL["Allocator(s)"]
end
subgraph OFF["Key up"]
KU(["Key up"])
RM["Read cached mask"]
REL["Release the same timbres"]
end
subgraph EXPR["Wheels, XY pad, aftertouch"]
PAD(["Control moved"])
GRP["Timbre expression"]
end
CARD[["Voice cards"]]
KD --> LEV
LEV --> LF
LF -->|"timbres that matched"| MASK
MASK --> AL
KU --> RM
RM --> REL
PAD --> GRP
MASK -.->|"same mask"| RM
AL -->|"/vN/n"| CARD
REL -->|"/vN/nw"| CARD
GRP -->|"/m/pw /m/a /m/xyz"| CARD
4. The two paths compared
| External controller | Built-in keyboard | |
|---|---|---|
| Enters at | MIDI receive (USB / DIN) | Panel scan, binary panel plane |
| Channel | 0โ15 from the status byte | None (โ1) |
| Timbre selection | Shared router; per-timbre MIDI channel and note range | Its own inline filter; local note range (splits, layers) |
| Note-off routing | Not yet characterised | Mask cached at note-on |
| Visible on UDP 7771 | No | No |
| Expression | Per timbre | Per timbre, global to the instrument |
5. The frontend's own notes
The sequencer and arpeggiator live in the Linux frontend, so their notes do cross the performance port, as raw MIDI bytes wrapped in OSC:
/r/sq/0/midi-arp ,iii 144 60 100 note-on, channel 1, note 60, velocity 100
The same triple is accepted on several router addresses (/r/midi,
/r/midi-force, per-timbre /g/<t>/midi). Details on the
protocol page.
6. Per-note MPE
As shipped, bend, pressure and timbre reach every note a Synth is holding, so an MPE controller's per-finger expression collapses into one value. The voice cards, though, already accept those three messages addressed to a single voice; the stock controller never sends them that way. This project changes the voice controller, and nothing else, so that it does.
flowchart TB MPE(["MPE controller
each finger on its own channel"]) ZONE["MPE Synth
listens on every channel"] VA["Voice A
remembers channel 2"] VB["Voice B
remembers channel 3"] OUT[["Each note bends, swells
and brightens on its own"]] MPE -->|"notes, one channel per finger"| ZONE ZONE -->|"first note"| VA ZONE -->|"second note"| VB MPE -.->|"bend, pressure, slide
on channel 2"| VA MPE -.->|"bend, pressure, slide
on channel 3"| VB VA --> OUT VB --> OUT
flowchart TB
IN(["MIDI in, member channel c"])
R{"Router"}
subgraph NOTE["Note path"]
F["MPE timbre filter
channel compare skipped"]
AL["Allocator"]
TAG["Voice v
tagged with channel c"]
end
subgraph EXPR["Expression path"]
X["Bend, pressure or CC74
on channel c"]
M{"Voices tagged c"}
SUP["Timbre-wide messages
dropped for this timbre"]
end
CARD[["Voice cards"]]
IN --> R
R -->|"note"| F
F --> AL
AL --> TAG
R -->|"expression"| X
X --> M
X -.->|"/m/pw /m/a /m/xyz"| SUP
TAG -.->|"channel tag"| M
TAG -->|"/vN/n /vN/nw"| CARD
M -->|"/vN/m/pw /vN/m/a /vN/m/pady
to matching voices only"| CARD
- One timbre is designated the MPE zone and accepts notes from every channel.
- Each allocated voice remembers the channel its note came in on.
- Bend, pressure and CC74 on a member channel go only to voices tagged with that channel. The card sums them into pitch, amplitude or filter every tick, independent of gate state.
- The timbre-wide messages for that timbre are dropped, since per-voice and timbre-wide writes land in the same slot and the last writer wins.
All three axes have been verified on one instrument, on one firmware version. The other two timbres behave as shipped.
Known boundary: the built-in XY pad still acts on the MPE timbre as a global brightness control, because it arrives by the local path in section 3 and never crosses the point where the timbre-wide messages are dropped.