Skip to content

MIDI I/O

Resonon isn’t a closed world — it speaks MIDI, so it can drive a hardware synth, sequence a DAW, or take notes and knob-turns from a controller. In this chapter you’ll connect an output port, send a pattern to it, read a controller, and map a knob to a parameter with MIDI learn.

First, see what’s out there. midi_ports() returns the output ports your system exposes:

let ports = midi_ports();
print(ports); // ["IAC Driver Bus 1", "Prophet Rev2", ...]

Which ports this rig has is a fact about the rig, not about the music, so it lives in ~/.resonon/preferences.toml and is opened before any of your code runs. Every entry gets an alias — a short name your patterns use:

[[midi.outputs]]
alias = "default"
port = "IAC Driver Bus 1"

resonon setup walks you through the ports this rig has and writes the entries for you; resonon --list-ports prints one per attached device, ready to paste. reload_preferences() puts a change in force without restarting.

On macOS the IAC Driver is the usual way to pipe MIDI into a DAW — enable it in Audio MIDI Setup → MIDI Studio.

A MidiTrack is the MIDI counterpart to the AudioTrack you’ve been using — but instead of feeding an instrument, its notes go out the port on a chosen channel. Send it a pattern with <<, exactly like an audio track:

let melody = MidiTrack(1); // channel 1
melody << [C4 E4 G4 E4];
PLAY;

Point a software or hardware instrument at channel 1 and you’ll hear your pattern. Everything you know about patterns — chords, rests, Euclidean rhythms — works unchanged; only the destination is different.

Give each entry its own alias and you can fan tracks out to different ports and channels — your DAW on one, a hardware synth on another:

[[midi.outputs]]
alias = "daw"
port = "IAC Driver Bus 1"
[[midi.outputs]]
alias = "synth"
port = "Prophet Rev2"
optional = true
let drums = MidiTrack(10, 110, "daw"); // to the DAW, channel 10, velocity 110
let bass = MidiTrack(1, 90, "synth"); // to hardware, channel 1
let pad = MidiTrack(3, 80, "synth"); // to hardware, channel 3
drums << [bd _ sd _, ch ch ch ch];
bass << [C2 _ C2 G2];
pad << [C4, E4, G4];
PLAY;

MidiTrack(channel, velocity, alias, name) — velocity, alias, and the display name are all optional (defaulting to 100, "default", and unnamed). Re-route a track at any time with .output:

bass.output("daw", 1); // move bass onto the DAW, channel 1

When you lose track of what’s going where, midi_routing() prints the whole picture — every output, input, track route, and the clock state.

Input mirrors output. midi_input_ports() lists controllers, and a [[midi.inputs]] entry opens one under an alias:

let in_ports = midi_input_ports();
print(in_ports); // ["Arturia KeyStep 37", ...]
[[midi.inputs]]
alias = "kb"
port = "KeyStep"
optional = true

optional = true is for a controller you do not always have plugged in: it is reported once and the session continues.

Route that input into a track with .input, and choose how it monitors — whether incoming notes pass straight through to the output:

let keys = MidiTrack(1, 100)
.input("kb")
.monitor("in")
.output("daw");
PLAY;

Now the KeyStep plays through resonon and out to your DAW. The monitor modes:

ModeBehavior
"off"No pass-through (the default)
"in"Always pass input straight to output
"auto"Pass through only while no pattern is playing on the track

You can narrow input down to one channel, or open it up to everything:

let bass = MidiTrack(2, 100).input("kb", 1); // only channel 1 from "kb"
let omni = MidiTrack(3, 80).input("all"); // every port, every channel

Controller knobs and faders send CC (control-change) messages, and resonon hands them to you as signals — the same signals you can route into any parameter. Cc(n) reads CC n from any channel; Cc(channel, n) pins it to one channel (channels are 0–15):

let cutoff = Cc(74); // CC 74, any channel
let mod_wheel = Cc(0, 1); // CC 1 on channel 0

A CC arrives normalized to 0.0–1.0. Shape it and send it into a parameter with <<, just like an LFO:

use "std/effects" { Lowpass, Delay };
let filter = Lowpass("lowpass", 2000);
let delay = Delay("delay", 0.25, 0.5);
drums.fx(#[filter, delay]);
filter.param("Cutoff") << Cc(74).smooth(50).range_exp(200, 8000);
delay.param("Feedback") << Cc(71).smooth(30).range(0.1, 0.6);

.smooth(ms) irons out the zipper noise of stepped 7-bit values, and .range / .range_exp map the 0–1 signal onto a useful span — reach for range_exp on anything frequency-like, where your ear hears ratios rather than steps.

Don’t know which CC a knob sends? Let resonon listen. Cc_learn() waits up to ten seconds for the first CC message, then returns a signal bound to it:

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

Once you know the number, hard-code it so your script doesn’t pause to learn every time:

let knob = Cc(0, 74);

To lock tempo with external gear, either send MIDI clock to your outputs or follow a clock coming in:

midi_clock_send(true); // become the clock master
setbpm(120);
PLAY;
midi_clock_follow("daw"); // follow an incoming clock instead

Following needs the incoming port declared as a [[midi.inputs]] entry with that alias.

Sending and following are two ends of the same wire — a session is either the master or a follower, not both.

MIDI is aligned with your audio automatically, plugin latency included, so you should not have to touch anything. If one device still sits early or late against the rest, trim that device:

midi_latency(); // where the offset comes from
midi_port_trim("cv", -2.5); // this synth is late; pull it forward
midi_port_clock_trim("cv", -2.5); // extra nudge for its clock input only

That’s the whole round trip — out to your gear, back from a controller, and locked to a shared clock. The MIDI chapters go deeper on each piece: