Skip to content

MIDI In & CC

The last guide sent notes out. This one brings MIDI in — route a keyboard into a track, read a controller’s knobs as live signals, and send CC automation back to your gear. By the end you’ll have MIDI flowing both directions.

midi_input_ports() returns the MIDI input ports your system exposes:

let in_ports = midi_input_ports();
print(in_ports); // ["IAC Driver Resonon", "USB MIDI Keyboard"]

midi_input_connect(port, alias) opens a connection and gives it an alias — a short name you choose and reuse when routing input to tracks:

midi_input_connect("IAC Driver Resonon", "iac");

The port name doesn’t have to be exact. Resonon tries an exact match first, then falls back to a substring match:

// Full port name is "Arturia KeyStep 37"
midi_input_connect("KeyStep", "kb");

If several ports contain the substring, the first match wins — use a more specific substring or the full name to disambiguate.

A track pulls MIDI from an input with .input(alias).

let keys = MidiTrack(1, 100).input("iac");

Pass a channel to filter to just that one:

let bass = MidiTrack(2, 100).input("iac", 1);

The special alias "all" listens to every connected input port at once:

let omni = MidiTrack(3, 80).input("all");

Monitor mode decides whether incoming MIDI is passed through to the track’s output:

ModeBehavior
"off"No pass-through (default)
"in"Always pass input through to output
"auto"Pass through only when no pattern is playing
// Always hear the keyboard
let keys = MidiTrack(1, 100).input("iac").monitor("in");
// Hear the keyboard only when the track is idle
let keys_auto = MidiTrack(1, 100).input("iac").monitor("auto");
// Silent monitoring (record only)
let keys_off = MidiTrack(1, 100).input("iac").monitor("off");

When monitor mode is "in" or "auto", incoming events are forwarded to the track’s output, with a few things handled automatically:

  • Channel remapping — forwarded notes are remapped to the track’s output channel, so thru-play stays consistent with pattern output. A keyboard on channel 1 driving a channel-5 track arrives on channel 5.
  • Feedback detection — if an input and output form a loop (both on the same IAC bus, say), events could circulate forever. Resonon detects this and briefly suppresses thru-play, logging a warning, then re-enables after a cooldown. To avoid loops entirely, use separate ports or set monitor to "off".
  • CC capture — incoming CC messages are captured and made available as Cc() signal sources (covered below).

The track methods return the track, so a complete input-to-output route reads as one expression:

let thru = MidiTrack(1, 100)
.input("iac")
.monitor("in")
.output("iac");

midi_input_disconnect(alias) closes a named input connection:

midi_input_disconnect("iac");

On disconnect Resonon sends All Notes Off (CC 123) to any outputs that were receiving thru-play, so unplugging a controller mid-performance won’t leave stuck notes.

Controller knobs, faders, and wheels arrive as Control Change (CC) messages. Cc() turns one into a live signal — a continuous value from 0.0 to 1.0 you can route anywhere a signal is accepted.

Cc(cc_num) reads the given CC from any channel — convenient when you have a single controller and don’t care which channel it sends on:

let cutoff_signal = Cc(74);

Cc(channel, cc_num) isolates one channel (015) — use it when several controllers share CC numbers and you need to tell them apart:

let cutoff_signal = Cc(0, 74);

Both forms need an active MIDI input connection.

Don’t know a controller’s CC number? Cc_learn() waits up to 10 seconds for you to move a knob, then returns a signal bound to exactly that channel and CC — and prints the equivalent Cc(...) call so you can hardcode it later:

let knob = Cc_learn();
// Console: "Waiting for CC input... (10s timeout)"
// Move a knob on your controller...
// Console: "Learned: Cc(0, 74)"

If nothing arrives within 10 seconds it raises an error. The typical workflow is to learn interactively, then replace the call with the hardcoded Cc():

let knob = Cc(0, 74);

CC values arrive as discrete 7-bit steps (0127). Apply .smooth(time_ms) to eliminate zipper noise when modulating audio — a one-pole lowpass interpolates between incoming values into a continuous signal:

let cutoff = Cc(74).smooth(50);

CC signals work anywhere a signal is accepted. Route one into a parameter with <<:

let synth = AudioTrack("synth");
synth.filter.param("Cutoff") << Cc(74).smooth(50); // knob → cutoff
synth.param("Gain") << Cc(11).smooth(20); // expression pedal → volume
synth.reverb.param("Mix") << Cc(1).smooth(30); // mod wheel → reverb depth

Because CC signals are just signals, all the standard signal methods apply — see Signals & Automation for the full set. The ones you’ll reach for most:

MethodDescription
.smooth(time_ms)One-pole lowpass smoothing
.range(min, max)Scale output to a linear range
.range_exp(min, max)Scale output to an exponential range

Frequency-like targets feel more natural on a logarithmic curve, so reach for .range_exp() there:

let filter_freq = Cc(74).smooth(50).range_exp(200, 8000);

Common CC assignments, for reference:

CCNameTypical use
1Mod WheelVibrato, filter modulation
7VolumeChannel volume
10PanStereo panning
11ExpressionDynamic volume
64SustainOn/off (≥64 = on)
74BrightnessFilter cutoff

The other direction: send CC to external synths and DAWs. Call .cc(number) on a MIDI track to make a CC output, then route a signal, pattern, or value into it with <<.

The signal is sampled (~30 Hz by default) and sent as CC messages, skipping identical consecutive values:

let synth = MidiTrack(1);
synth << [C4 E4 G4 C5];
synth.cc(74) << Sine(0.5).range(0, 127);

Send discrete values on the beat grid:

synth.cc(1) << [0 64 127 64];

Send a fixed value once per cycle:

synth.cc(7) << 100;

Change the sampling rate of a signal-driven CC with .send_rate(hz) — higher is smoother but uses more bandwidth (standard DIN MIDI handles ~1000 three-byte messages per second; USB is far faster):

synth.cc(74).send_rate(60) << Sine(0.5).range(0, 127);

A CC output is a value like any other, so you can store it in a variable:

let cutoff = synth.cc(74);
cutoff << Sine(0.5).range(0, 127);

midi_routing() prints a summary of every connection, active slot, and the clock state — the fastest way to see what’s wired to what:

midi_routing();
MIDI Routing
Output Ports:
"daw" -> "IAC Driver Bus 1"
"synth" -> "Prophet Rev2"
Input Ports:
"kb" -> "Arturia KeyStep 37"
Active Slots:
ch1 -> "daw" (ch 1)
synth:ch1 -> "synth" (ch 1)
Clock:
mode: follower (from "kb")

If nothing is connected it prints (no MIDI connections).

ErrorCauseFix
Input port alias '...' already connectedAlias already in useDisconnect it first, or pick another alias
Port not foundPort name not recognizedCheck midi_input_ports() for the exact name
MIDI feedback loop detected on ch...Thru-play forming a loopUse separate in/out ports, or set monitor to "off"

List ports, connect with substring matching, build a pass-through track, inspect the routing, play, and clean up:

print(midi_input_ports());
print(midi_ports());
midi_input_connect("KeyStep", "kb");
midi_connect("IAC Driver Bus 1", "daw");
let keys = MidiTrack(1, 100)
.input("kb")
.monitor("in")
.output("daw");
midi_routing();
PLAY;
// ... play your keyboard ...
PAUSE;
midi_input_disconnect("kb");
midi_disconnect("daw");

With MIDI flowing both ways, lock Resonon’s clock to your other gear — or write your notes out to a file.