RouteMIDI
RouteMIDI is a multi-platform command-line tool that makes it very easy to connect MIDI ports together, passing MIDI messages between them while filtering, transforming and converting them along the way.
All the heavy lifting is done by the wonderful JUCE library.
The project website is https://github.com/gbevin/RouteMIDI
RouteMIDI is the third member of a family of command-line MIDI tools, alongside SendMIDI (send MIDI messages) and ReceiveMIDI (receive and monitor MIDI messages). It deliberately shares their concise command vocabulary, number and note-name parsing, and JavaScript scripting, so if you know one of them, you already know most of RouteMIDI.
Purpose
RouteMIDI turns your computer into a flexible MIDI patchbay. It connects one or more MIDI input ports to one or more MIDI output ports, and lets you decide exactly which messages flow through and how they are modified on the way. Typical uses are merging and splitting ports, remapping channels, transposing a keyboard, scaling velocities, filtering out clock or aftertouch, or running arbitrary JavaScript on each message.
It can also be driven programmatically. RouteMIDI publishes a machine-readable command schema (--schema json) and includes a built-in Model Context Protocol server (--mcp), so scripts and AI assistants can discover the commands, start routes, and inspect or live-edit them while they run; see AI.md.
ShowMIDI
If you're looking for a beautiful GUI to effortlessly visualize MIDI activity without having to wade through log files, please take a look at my other tool ShowMIDI: https://github.com/gbevin/ShowMIDI
Download
You can download pre-built binaries from the release section: https://github.com/gbevin/RouteMIDI/releases
Since RouteMIDI is free and open-source, you can also easily build it yourself. Just take a look into the Builds directory when you download the sources.
If you're using the macOS Homebrew package manager, you can install RouteMIDI with:
brew install gbevin/tools/routemidi
Usage
To use it, simply type "routemidi" or "routemidi.exe" on the command line and follow it with a series of commands. These commands have purposefully been chosen to be concise and easy to remember, so that it's extremely fast and intuitive to set up a MIDI routing.
These are all the supported commands:
Routing and ports:
in name Add a MIDI input (- for stdin text); a new route starts
after outputs
out name Add a MIDI output to the route (- for stdout text)
vin (name) Add a virtual MIDI input to the route (Linux/macOS)
vout (name) Add a virtual MIDI output to the route (Linux/macOS)
list List the available MIDI input and output ports
panic Send all-notes-off on disconnect, exit and zone change
Configuration:
file path Load commands from the specified program file
dec Interpret the next numbers as decimals by default
hex Interpret the next numbers as hexadecimals by default
omc number Set octave for middle C, defaults to 3
Monitoring:
nn Monitor notes as numbers instead of names
ts Prefix monitored messages with a timestamp
mon Print each routed message (quiet by default)
src Prefix monitored messages with the input port name
Filters:
not Negate the next filter, blocking matching messages
ch number Restrict the route to a MIDI channel (1-16)
voice Pass all Channel Voice messages
note Pass all Note messages
on (note) Pass Note On, optionally for note (0-127)
off (note) Pass Note Off, optionally for note (0-127)
pp (note) Pass Poly Pressure, optionally for note (0-127)
cc (number) Pass Control Change, optionally for controller (0-127)
cc14 (number) Pass 14-bit CC, optionally for MSB controller (0-31)
nrpn (number) Pass NRPN traffic, optionally for parameter (0-16383)
rpn (number) Pass RPN traffic, optionally for parameter (0-16383)
pc (number) Pass Program Change, optionally for program (0-127)
cp Pass Channel Pressure
pb Pass Pitch Bend
sr Pass all System Real-Time messages
clock Pass Timing Clock
start Pass Start
stop Pass Stop
cont Pass Continue
as Pass Active Sensing
rst Pass Reset
sc Pass all System Common messages
syx Pass System Exclusive
syf path Capture routed System Exclusive to a .syx file
tc Pass MIDI Time Code Quarter Frame
spp Pass Song Position Pointer
ss Pass Song Select
tun Pass Tune Request
noterange low high Pass notes within a note range (key split)
velrange low high Pass note-ons within a velocity range (vel split)
ccrange number low Pass a Control Change only when its value is in a range
high
cc14range number low Pass a 14-bit CC only when its value is in a range
high (0-16383)
inscale root scale Pass notes that belong to a scale (root and name)
mpemanager zone[:n] Pass the manager channel of an MPE zone (e.g. lower)
mpemember zone[:n] Pass the member channels of an MPE zone (e.g. upper:7)
mpezone zone[:n] Pass a whole MPE zone (its manager and member channels)
Transforms:
chmap from to Remap channel-voice messages from one channel to
another
chset number Force all channel-voice messages onto a channel
chadd number Add N to the channel, wrapping 1-16 (may be negative)
transp semitones Transpose notes by N semitones (out-of-range dropped)
dtransp root scale Transpose within a scale by N scale steps (stays in
steps key)
notemap from to Remap a specific note number to another
scale root scale Snap notes to the nearest note of a scale (root, name)
chord intervals Stack notes at the given semitone intervals (a chord)
latch (mode) Keep notes on after release; toggle (default) or hold
mono (priority) Force monophony; priority last (default), low or high
sustain Apply the sustain pedal (CC 64) to the notes themselves
sost Apply the sostenuto pedal (CC 66) to the notes
themselves
notecc note cc Turn a note into a Control Change (velocity as value)
ccnote cc note Turn a Control Change into a note (64+ on, else off)
notepc note Turn a note-on into a Program Change (note-off dropped)
program
velscale factor Scale note-on velocity by a factor (clamped 1-127)
velset number Set a fixed note-on velocity (1-127)
veladd number Add an offset to note-on velocity (clamped 1-127)
velcurve gamma Apply a gamma curve to note-on velocity (1-127)
velclip min max Clamp note-on velocity into a min-max range
velcomp amount Squeeze note-on velocity toward the mid-range (0-1)
velinvert Invert note-on velocity (soft becomes loud)
ccmap from to Remap a Control Change controller number
ccadd number Add an offset to a controller's value (clamped 0-127)
value
ccscale number Scale a controller's value by a factor (clamped 0-127)
factor
cccurve number Apply a gamma curve to a controller's value
gamma
ccinvert number Invert a controller's value (0-127 mirrored)
ccrescale number Rescale a controller's value from one range onto
inlow another (a reversed output range inverts)
inhigh
outlow
outhigh
ccset number Set a fixed value for a controller (0-127)
value
cc14add number Add an offset to a 14-bit CC value (clamped to its
value resolution)
cc14scale number Scale a 14-bit CC value by a factor (clamped to its
factor resolution)
cc14curve number Apply a gamma curve to a 14-bit CC value
gamma
cc14invert number Invert a 14-bit CC value (0-16383 mirrored)
cc14rescale number Rescale a 14-bit CC value from one range onto another
inlow (a reversed output range inverts)
inhigh
outlow
outhigh
cc14set number Set a fixed 14-bit CC value (0-16383)
value
nrpnadd param Add an offset to an NRPN value (clamped to its
number resolution)
nrpnscale param Scale an NRPN value by a factor (clamped to its
factor resolution)
nrpncurve param Apply a gamma curve to an NRPN value
gamma
nrpninvert param Invert an NRPN value (mirrored in its resolution)
nrpnrescale param Rescale an NRPN value from one range onto another (a
inlow reversed output range inverts)
inhigh
outlow
outhigh
nrpnset param Set a fixed NRPN value (scaled to its resolution)
value
rpnadd param Add an offset to an RPN value (clamped to its
number resolution)
rpnscale param Scale an RPN value by a factor (clamped to its
factor resolution)
rpncurve param Apply a gamma curve to an RPN value
gamma
rpninvert param Invert an RPN value (mirrored in its resolution)
rpnrescale param Rescale an RPN value from one range onto another (a
inlow reversed output range inverts)
inhigh
outlow
outhigh
rpnset param Set a fixed RPN value (scaled to its resolution)
value
pcmap from to Remap a Program Change number
pcadd number Add an offset to Program Change number (clamped 0-127)
cpadd number Add an offset to Channel Pressure (clamped 0-127)
cpscale factor Scale Channel Pressure by a factor (clamped 0-127)
cpset number Set a fixed Channel Pressure value (0-127)
cpcurve gamma Apply a gamma curve to Channel Pressure
cpinvert Invert Channel Pressure (0-127 mirrored)
ppadd number Add an offset to Poly Pressure (clamped 0-127)
ppscale factor Scale Poly Pressure by a factor (clamped 0-127)
ppset number Set a fixed Poly Pressure value (0-127)
ppcurve gamma Apply a gamma curve to Poly Pressure
ppinvert Invert Poly Pressure (0-127 mirrored)
pbadd number Add an offset to Pitch Bend (clamped 0-16383)
pbscale factor Scale Pitch Bend around center by a factor (0-16383)
pbset number Set a fixed Pitch Bend value (0-16383)
pbinvert Invert Pitch Bend around the center (up becomes down)
js code Transform each message with this script
jsf path Transform each message with the script in this file
MPE routing:
mpe zone[:n] Relocate an MPE stream between zones (e.g. lower
zone[:n] upper), remapping channels
mpemono zone[:n] Collapse an MPE zone onto a single channel for non-MPE
channel gear (e.g. upper 1)
mpexp channel Spread a channel's notes across an MPE zone's member
zone[:n] channels (e.g. 1 lower)
mpesplit zone[:n] Distribute an MPE zone's voices over the output ports,
(channel) one per port, each rechanneled to channel (default 1)
mpebend zone[:n] Rescale member-channel pitch bend from one semitone
from to range to another
mpesens zone[:n] Declare a member-channel pitch bend range (RPN 0) for
semitones synths that honor it
Conversion:
convert srctype Convert a value between cc, cc14, rpn, nrpn, pb, cp, pc
[number] & pp. Types cc, cc14, rpn and nrpn take a controller or
dsttype parameter number and pp a note (optional on a source,
[number] meaning any note), while pb, cp and pc take none; the
value is rescaled to the destination resolution
Options:
-h or --help Print Help (this message) and exit
--version Print version information and exit
--schema json Print machine-readable command JSON and exit
[experimental]
--mcp Run a stdio MCP server [experimental]
--print-mcp-config Print an MCP client configuration block and exit
--install-mcp [client] Add RouteMIDI to an MCP client's configuration and exit
-- Read commands from standard input until it's closed
Use --schema json for command metadata for scripts, MCP servers and AI agents. Use --mcp to let MCP clients control RouteMIDI over stdio. These two features are experimental and fast-moving: their JSON and the MCP tools may change between releases. See AI.md for the generation contract, the schema and the MCP server with its tools and security model.
To connect RouteMIDI to a local AI assistant without hand-editing configuration, run routemidi --install-mcp <client>: for a JSON client (claude-desktop, cursor) it merges the entry into that client's configuration file, and for a client with its own command (codex, claude-code) it prints the one-liner to run, such as codex mcp add routemidi -- routemidi --mcp. Use routemidi --print-mcp-config [client] to print the entry in a client's format (JSON, or TOML for Codex) to paste elsewhere. See AI.md for the per-client setup, including the double-click Claude Desktop extension.
Each command can also be written with its long name instead of the short one:
input output virtual-in virtual-out decimal hexadecimal octave-middle-c
note-numbers timestamp verbose monitor-source channel note-on note-off
poly-pressure control-change control-change-14 program-change
channel-pressure pitch-bend system-realtime continue active-sensing reset
system-common system-exclusive system-exclusive-file time-code song-position
song-select tune-request note-range velocity-range control-change-range
control-change-14-range in-scale mpe-manager mpe-member mpe-zone channel-map
channel-set channel-add transpose diatonic-transpose note-map sustain-pedal
sostenuto-pedal note-to-control-change control-change-to-note
note-to-program-change velocity-scale velocity-set velocity-add
velocity-curve velocity-clip velocity-compress velocity-invert
control-change-map control-change-add control-change-scale
control-change-curve control-change-invert control-change-rescale
control-change-set control-change-14-add control-change-14-scale
control-change-14-curve control-change-14-invert control-change-14-rescale
control-change-14-set nrpn-add nrpn-scale nrpn-curve nrpn-invert nrpn-rescale
nrpn-set rpn-add rpn-scale rpn-curve rpn-invert rpn-rescale rpn-set
program-change-map program-change-add channel-pressure-add
channel-pressure-scale channel-pressure-set channel-pressure-curve
channel-pressure-invert poly-pressure-add poly-pressure-scale
poly-pressure-set poly-pressure-curve poly-pressure-invert pitch-bend-add
pitch-bend-scale pitch-bend-set pitch-bend-invert javascript javascript-file
mpe-mono mpe-expand mpe-split mpe-bend mpe-sensitivity
Routes
A route connects one or more MIDI inputs to one or more MIDI outputs. Every input on a route is forwarded to every output on that route, so a route can split one input to several outputs, merge several inputs into one output, or both. Any filter and transform commands also belong to the route.
A route starts with an in (or vin) command. Further in commands keep adding inputs to the same route until an out (or vout) command is given; the next in after that begins a new route. So this splits, merges, and starts a second independent route:
routemidi in "Keyboard" out "Synth A" out "Synth B" \
in "Pads" in "Drums" out "Sampler"
On Linux and macOS, vin and vout create virtual ports that other applications can connect to, which is handy for bridging software that has no direct connection of its own:
routemidi vin "RouteMIDI In" out "USB MIDI" # apps send to the virtual input -> hardware
routemidi in "USB MIDI" vout "RouteMIDI Out" # hardware -> a virtual output apps can read
Filter and transform commands can be placed anywhere within a route's commands:
routemidi in "LinnStrument" ch 1 transp 12 out "Bidule 1" \
in "Keystep" cc out "Bidule 1" out "Synth"
Each message a route receives flows through the route's processing stages in a fixed order, no matter where the commands appear between the route's inputs and outputs:
- Filters decide whether the message passes at all.
- Transforms (including
jsscripts) modify it, applied in the order they were written. - MPE operations (
mpe,mpemono,mpexp,mpebend,mpesens) rechannel, fold or expand it, in the order they were written. - Conversions (
convertand the 14-bit CC/RPN/NRPN value transforms) reassemble and convert controller values. - The result goes to every output of the route (or is distributed across them by
mpesplit).
Only the relative order within the transform stage and within the MPE stage matters: a transp always runs before an mpemono, even when it is written after it.
By default RouteMIDI works silently. Add the mon command to print each routed message (after filtering and transformation) for debugging, optionally with ts for timestamps and nn for note numbers instead of names.
Filters
Filters select which messages are allowed to pass through a route. As soon as one or more positive filters are present, only the messages matching at least one of them are forwarded (everything else is dropped). With no filters, everything passes.
Prefix any filter with not to make it block instead: matching messages are dropped and everything else passes. Positive and negative filters can be combined: a message must then match at least one positive filter and none of the negative ones.
The ch command narrows a route to a single MIDI channel; combined with type filters it restricts them to that channel.
The number that selects which message a filter matches (for ch, on, off, pp, cc, cc14 and pc) may be a single value or an inclusive range written as lo..hi. The range form also accepts note names. The .. separator is used (rather than -) so it doesn't clash with note names like C-2.
routemidi in "Keyboard" cc 1..10 out "Synth" # only CC 1 through 10
routemidi in "Keyboard" on C3..C5 out "Synth" # only note-ons in a range
routemidi in "Keyboard" ch 1..4 out "Synth" # only channels 1-4
The noterange and velrange filters pass notes within a note or velocity range, which (combined with the multi-route model) makes keyboard and velocity splits easy. Unlike on lo..hi (which matches only note-ons), noterange matches note-ons, note-offs and poly pressure together, and velrange always passes note-offs so a velocity split can't leave notes stuck.
The ccrange filter is the same idea for a controller: it passes a Control Change only when its value falls within a low high window, so ccrange 1 64 127 lets the modulation wheel through only in its upper half. As always, not ccrange 1 64 127 inverts it (blocking that window and passing everything else). cc14range does the same for a 14-bit CC with a 0-16383 window: it remembers the controller's MSB per channel, judges each LSB with the exact assembled value (an MSB half on its own counts as value MSBx128), and passes or blocks both halves of the pair.
The cc14, nrpn and rpn filters match the constituent Control Change messages of a 14-bit CC, an NRPN or an RPN. cc14 without a number passes every 14-bit-capable controller (MSB 0-31 together with its LSB 32-63), or with a number just that MSB controller and its LSB. nrpn passes the NRPN traffic (CC 98, 99 plus the data-entry CC 6, 38) and rpn the RPN traffic (CC 100, 101 plus CC 6, 38). Both also take an optional parameter number (0-16383) to pass only one parameter's traffic: RouteMIDI follows the parameter selection per channel, so the shared data-entry CC 6 and 38 are matched only while that parameter is the one selected (nrpn 1000 isolates NRPN 1000, rpn 0 the pitch-bend-sensitivity RPN). The deselecting (N)RPN null (select bytes of 127) always passes as framing and ends the selection, so a complete transmission keeps its closing null and nothing matches after it.
The inscale filter passes only the notes that belong to a key, taking a root and a scale name from the same list as the scale transform. It's the filtering counterpart of scale: where scale bends stray notes onto the nearest scale note, inscale simply drops them (and not inscale keeps only the out-of-key notes). It matches note-ons, note-offs and poly pressure together, so a note that passes is always released.
routemidi in "Keyboard" note out "Synth" # only note messages
routemidi in "Keyboard" not clock out "Synth" # everything except clock
routemidi in "Keyboard" ch 1 cc 1 out "Synth" # only CC 1 on channel 1
routemidi in "Knobs" rpn out "Synth" # only RPN traffic
routemidi in "Knobs" nrpn 1000 out "Synth" # only NRPN 1000's traffic
routemidi in "Keyboard" inscale C major out "Synth" # only notes in C major
routemidi in "Wheel" ccrange 1 64 127 out "Synth" # mod wheel only in its top half
routemidi in "Fader" cc14range 7 8192 16383 out "A" # 14-bit fader, upper half only
# key split: bottom half to a bass, top half to a lead
routemidi in "Keyboard" noterange C-2 B2 out "Bass" \
in "Keyboard" noterange C3 G8 out "Lead"
# velocity split: soft notes to a pad, hard notes to a lead
routemidi in "Keyboard" velrange 1 63 out "Pad" \
in "Keyboard" velrange 64 127 out "Lead"
Transforms
Transforms modify the messages that pass the filters, and are applied in the order in which they appear in the route. A transform that would push a value out of range (for instance transposing a note past 0-127) drops that message.
routemidi in "Keyboard" transp 12 chset 2 out "Synth" # octave up, on channel 2
routemidi in "Multi" chmap 10 1 out "Synth" # move channel 10 onto channel 1
routemidi in "Pads" notemap 36 60 out "Drum" # remap note 36 to 60
routemidi in "Fader" ccmap 7 11 out "Mixer" # remap CC 7 to CC 11
routemidi in "Fader" ccscale 7 0.5 out "Mixer" # halve the values of CC 7
routemidi in "Keyboard" velscale 0.5 out "Synth" # halve note-on velocities
routemidi in "Keyboard" pbscale 0.5 out "Synth" # halve the pitch-bend depth
routemidi in "Keyboard" cpscale 0.5 out "Synth" # halve channel pressure
The velcurve, cccurve and cpcurve transforms apply a gamma curve to note-on velocity, a controller's value, or channel pressure. A gamma below 1 boosts low values (a concave curve), above 1 attenuates them (convex), and 1 is linear; the minimum, maximum and overall shape are preserved.
routemidi in "Keyboard" velcurve 0.5 out "Synth" # easier to play loud
routemidi in "Fader" cccurve 7 2.0 out "Mixer" # finer control at low end
Two more velocity transforms tame dynamics. velclip clamps note-on velocity into a min max window (the order of the two bounds doesn't matter), so nothing plays quieter or louder than you want. velcomp squeezes velocity toward the mid-range by an amount from 0 to 1: 1 leaves it untouched, 0.5 halves the distance from the centre, and 0 flattens everything to a single value, a quick way to even out an uneven playing hand.
routemidi in "Keyboard" velclip 40 100 out "Synth" # never too soft or too loud
routemidi in "Keyboard" velcomp 0.5 out "Synth" # tighten the dynamic range
The invert transforms mirror a value across its range: velinvert flips note-on velocity (soft becomes loud), ccinvert flips a controller's value (0 becomes 127 and vice versa, for instance to reverse a pedal or fader that works backwards), cpinvert flips Channel Pressure, ppinvert flips Poly Pressure per note, and pbinvert mirrors Pitch Bend around its centre so a bend up becomes the same bend down. Poly Pressure has the same add/scale/set/curve family as Channel Pressure (ppadd, ppscale, ppset, ppcurve), so per-note pressure from a poly aftertouch keyboard can be reshaped without collapsing it to a channel-wide value.
routemidi in "Pedal" ccinvert 11 out "Synth" # reverse an expression pedal
routemidi in "Keyboard" pbinvert out "Synth" # bend up becomes bend down
ccrescale maps a controller's input range onto a different output range: it takes the controller number followed by inlow inhigh outlow outhigh, clamps the incoming value into the input range and rescales it linearly onto the output range. That calibrates a controller that never quite reaches its extremes, tames one that is too sensitive, or offsets its response; reversing the output bounds inverts the response at the same time. All of these value transforms also exist for 14-bit CCs and (N)RPNs (cc14add, rpnscale, ...); those live in the conversion stage because their values span several MIDI messages.
routemidi in "Pedal" ccrescale 11 20 108 0 127 out "Synth" # use the pedal's real travel
routemidi in "Fader" ccrescale 7 0 127 40 100 out "Mixer" # keep the level in a window
routemidi in "Pedal" ccrescale 11 0 127 127 0 out "Synth" # invert while rescaling
A few transforms change a message from one type to another. notecc turns a specific note into a Control Change (the note-on velocity becomes the value, and the note-off sends 0); ccnote turns a Control Change into a note (a value of 64 or more triggers a note-on with that value as the velocity, below 64 a note-off); and notepc turns a note-on into a Program Change (the note-off is dropped). ccnote is aimed at switch- or pedal-style controllers; a continuously changing CC would retrigger the note. Since notecc uses the velocity as the value, put a velset in front of it for a fixed value.
routemidi in "Pad" notecc 60 64 out "Synth" # a pad key holds the sustain CC
routemidi in "Foot" ccnote 64 C1 out "Drums" # a sustain pedal plays a kick
routemidi in "Pads" notepc 36 0 notepc 37 1 out "Synth" # two pads select two patches
The scale transform snaps every note to the nearest note of a musical key, so a performance always stays in tune. It takes a root (a note name such as C, F# or Bb, or a number) and a scale name. Notes that are already in the scale pass through unchanged, and a note exactly between two scale notes snaps to the lower one; note-offs snap the same way as note-ons so held notes are always released. Scale names are case-insensitive and any spaces, dashes and underscores are ignored, so harmonicminor, harmonic-minor and Harmonic Minor are all the same.
These scale names are supported (aliases in parentheses):
| Scale name | Semitones from the root |
|---|---|
chromatic |
0 1 2 3 4 5 6 7 8 9 10 11 |
major (ionian) |
0 2 4 5 7 9 11 |
minor (aeolian, naturalminor) |
0 2 3 5 7 8 10 |
dorian |
0 2 3 5 7 9 10 |
phrygian |
0 1 3 5 7 8 10 |
lydian |
0 2 4 6 7 9 11 |
mixolydian |
0 2 4 5 7 9 10 |
locrian |
0 1 3 5 6 8 10 |
harmonicminor |
0 2 3 5 7 8 11 |
melodicminor |
0 2 3 5 7 9 11 |
majorpentatonic (majpent, pentatonic) |
0 2 4 7 9 |
minorpentatonic (minpent) |
0 3 5 7 10 |
majorblues (majblues) |
0 2 3 4 7 9 |
minorblues (minblues, blues) |
0 3 5 6 7 10 |
diminished (dim) |
0 2 3 5 6 8 9 11 |
wholetone |
0 2 4 6 8 10 |
spanish (phrygiandominant) |
0 1 4 5 7 8 10 |
romani (gypsy, hungarianminor) |
0 2 3 6 7 8 11 |
arabian |
0 2 4 5 6 8 10 |
egyptian |
0 2 5 7 10 |
ryukyu |
0 4 5 7 11 |
augmented (maj3rd) |
0 4 8 |
diminished7 (dim7, min3rd) |
0 3 6 9 |
fifth (power, 5th) |
0 7 |
You can also give a custom scale as a comma-separated list of semitone degrees from the root, for instance scale D 0,2,3,5,7,9,10 for a D minor scale.
The dtransp transform is a diatonic transpose: it shifts notes by a number of scale steps within a key rather than by a fixed number of semitones, so the result always stays in the scale. It takes a root, a scale name (the same list as scale) and a step count that may be negative. In C major, dtransp C major 2 turns every note into the note a diatonic third above it (C becomes E, D becomes F, and so on), and seven steps of a seven-note scale is exactly one octave. Notes that aren't already in the key are snapped into it before the shift. This is different from transp, which moves everything by the same chromatic interval regardless of key.
routemidi in "Keyboard" dtransp C major 2 out "Synth" # harmonize a diatonic third up
routemidi in "Keyboard" dtransp A minor -2 out "Synth" # a third down, staying in A minor
The chord transform turns each note into a chord by stacking extra notes at fixed semitone intervals above (or below) the one that was played. chord 4 7 adds a major third and a fifth, chord 3 7 a minor third and a fifth, and negative intervals stack notes underneath (chord -12 doubles an octave down). Each note-on emits the whole chord and each note-off releases it, and chord notes that fall outside 0-127 are dropped.
Because transforms run in order, chord and scale combine into a diatonic harmonizer: stack a plain chromatic chord and then snap the result into a key. For example chord 4 7 scale C major plays a triad on every note that always belongs to C major, so an E becomes E-G-B and an F becomes F-A-C.
routemidi in "Keyboard" scale C minor out "Synth" # keep a solo in C minor
routemidi in "Keyboard" chord 4 7 out "Synth" # play major triads
routemidi in "Keyboard" chord 4 7 scale C major out "Synth" # diatonic triads in C major
The latch transform keeps notes sounding after their keys are released, so you can hold a drone or a chord without keeping your fingers down. It has two modes, given as an optional argument. In toggle mode (the default) each press of a note flips it on or off: press a key to start it, press the same key again to stop it. In hold mode the note or chord you play keeps sounding until you start a new gesture, and pressing a fresh key (after letting go of all the others) releases the previous notes and latches the new ones, like an arpeggiator's hold. Incoming note-offs are swallowed in both modes, and everything is released when the route hits panic (on disconnect, exit or an MPE zone change). Because transforms run in order, putting latch after chord latches whole generated chords at once.
routemidi in "Keyboard" latch out "Synth" # tap notes to toggle a drone
routemidi in "Keyboard" latch hold out "Synth" # hold the last chord you played
routemidi in "Keyboard" chord 4 7 latch hold out "Synth" # hold whole triads hands-free
The mono transform forces monophony (only one note sounds at a time), which is handy for driving a mono synth from a polyphonic controller. When you hold several notes it picks the winner by an optional priority: last (the default) lets each new note take over, low keeps the lowest held note (classic bass-synth behaviour) and high the highest. Releasing the sounding note falls back to the next note still held, retriggered at its own velocity, and notes that lose priority simply wait silently until they win it. It works per channel, so to collapse a polyphonic MPE performance onto one mono synth, put a chset in front of it (or run it after an MPE collapse) to bring the notes onto a single channel first.
routemidi in "Keyboard" mono out "Synth" # last-note mono
routemidi in "Bass" mono low out "Synth" # lowest note wins
routemidi in "LinnStrument" chset 1 mono out "Mono" # collapse MPE to a mono synth
The sustain and sost transforms apply a pedal to the note stream itself, for synths and samplers whose MIDI implementation ignores the pedal (or has none). sustain holds back every note-off that arrives while the sustain pedal (CC 64) is down and sends them when the pedal is lifted; sost does the same for the sostenuto pedal (CC 66), but only for the notes whose keys were down at the moment the pedal was pressed, so notes played afterwards are unaffected. The pedal message itself is consumed, since its effect is already applied, and re-striking a note that is sounding with its key released retriggers it cleanly (a note-off is sent first). Both pedals are tracked per channel, and everything held is released when the route hits panic (on disconnect, exit or an MPE zone change).
routemidi in "Keyboard" sustain out "Sampler" # pedal works on any sampler
routemidi in "Keyboard" sost out "Synth" # sostenuto for synths without it
For ultimate flexibility, the js and jsf commands run JavaScript on each message and can inspect it, rewrite it, drop it, or emit additional messages (so one note can become a chord, for instance). See the JAVASCRIPT.md documentation file for details.
Conversions
The convert command translates a value from one controller type to another, reassembling and regenerating the underlying MIDI messages as needed:
convert <srctype> [number] <dsttype> [number]
where each type is one of:
| Type | Meaning | number argument |
Value resolution |
|---|---|---|---|
cc |
7-bit Control Change | controller number (0-127) | 7-bit |
cc14 |
14-bit Control Change (MSB 0-31 + its LSB 32-63) | MSB controller (0-31) | 14-bit |
rpn |
Registered Parameter Number | parameter (0-16383) | 14-bit |
nrpn |
Non-Registered Parameter Number | parameter (0-16383) | 14-bit |
pb |
Pitch Bend | none | 14-bit |
cp |
Channel Pressure | none | 7-bit |
pc |
Program Change | none | 7-bit |
pp |
Poly Pressure | note (optional as source) | 7-bit |
The cc, cc14, rpn and nrpn types are followed by a number selecting which controller or parameter. The pb, cp and pc types have a single value per channel, so they take no number.
pp is the only per-note type, so its number is a note (a name like C3 or a number). Because poly pressure carries a note that the other types don't, the note behaves differently on each side: as a destination it is required (poly pressure has to land on a specific note), while as a source it is optional: give one to convert just that note, or omit it to fold every note's poly pressure into the destination. That "any note" collapse (convert pp cp) is the usual way to feed a synth that has no poly aftertouch.
When several notes are held, the any-note collapse combines them with the maximum: the destination follows the hardest-pressed key, and drops back when it is released. This matches how MPE itself combines channel pressure (and what RouteMIDI's own mpemono does), so the loudest finger drives the value rather than whichever note happened to move last.
routemidi in "Knobs" convert nrpn 245 cc14 1 out "Synth"
routemidi in "Fader" convert cc14 1 nrpn 245 out "Module"
routemidi in "Wheel" convert pb nrpn 1000 out "Synth"
routemidi in "After" convert cp cc 11 out "Synth"
routemidi in "Knob" convert cc 7 pb out "Synth"
routemidi in "MPE" convert pp cp out "Mono Synth" # any note -> channel pressure
routemidi in "Pad" convert cp pp C3 out "Sampler" # channel pressure -> note C3
routemidi in "Grid" convert pp 60 cc 74 out "Synth" # one pad's pressure -> CC 74
Several conversions can be configured on one route; only the matching messages are converted, everything else passes through untouched. When several rules share the same source, each one emits, so a single source can fan out to several destinations.
Notable details:
- Bit scaling by default uses a Min-Center-Max method, which preserves minimum, center and maximum across resolutions and round-trips losslessly: widening a 7-bit value to 14-bit and narrowing it back yields the original. So a 7-bit value of 127 becomes the 14-bit maximum 16383, 64 becomes the center 8192, and 0 stays 0.
- For RPNs whose parameter LSB is 0-31 (the classic absolute MIDI 1.0 RPNs such as pitch-bend sensitivity, tuning and the MPE Configuration Message), the Zero-Extension with Rounding method is used instead, for exact backward compatibility. Upscaling pads with zeros (7-bit 127 becomes 16256, not the full 16383) and downscaling rounds to the nearest value. NRPNs and all CCs always use Min-Center-Max.
- Conversions to
rpn/nrpnappend the RPN/NRPN null (CC 101/100 = 127 or CC 99/98 = 127) to deselect the parameter afterwards. - When a route converts any
rpnornrpn, the converter reassembles every (N)RPN from the RPN controller set (CC 6, 38, 96-101) on that route: targeted parameters are converted (their constituent CCs, data increments and closing null are consumed) and any other (N)RPN passes through untouched. Avoid filtering out those CCs on a converting route. - CC 6 and 38 do double duty in MIDI: inside an (N)RPN parameter selection they are data entry, outside one they are the halves of a plain 14-bit CC. The converter tracks the selection, so a device's genuine (N)RPN work (a Pitch Bend Sensitivity declaration, an MPE Configuration Message) is never hijacked by a
cc14 6rule, and bare CC 6/38 pairs still convert even when the same route also hasrpn/nrpnrules. - A device streaming MSB+LSB pairs (a 14-bit CC, or 14-bit (N)RPN data) converts to exactly one destination value per pair once its pairing has been observed; a sender that only ever transmits the MSB keeps converting on every MSB.
No comments yet
Be the first to share your take.