Skip to content

MIDI In & CC

Route MIDI from external keyboards, controllers, and instruments into Resonon tracks.

midi_input_ports() returns an array of available MIDI input port names:

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

midi_input_connect(port, alias) opens a connection and assigns it an alias for later reference:

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

The alias is a short name you choose. Use it when routing input to tracks.

The port name does not need to be exact. Resonon tries an exact match first, then falls back to substring matching:

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

If multiple ports contain the substring, the first match is used. Use a more specific substring or the full name to avoid ambiguity.

Each alias must be unique. Connecting a second port with the same alias produces an error:

Input port alias 'kb' already connected

Disconnect the existing alias first if you need to reuse it.

.input(alias) routes all MIDI channels from the named input to the track:

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

.input(alias, channel) filters to a single MIDI channel:

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

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

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

Monitor mode controls 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 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 MIDI events are forwarded to the track’s output port. Several things happen during thru-play:

Incoming MIDI is remapped to the track’s output channel. If your keyboard sends on channel 1 but the track outputs on channel 5, the forwarded notes arrive on channel 5. This keeps thru-play consistent with pattern output.

If an input port and output port form a loop (for example, both connected to the same IAC bus), events can circulate endlessly. Resonon detects this automatically: if thru-play exceeds the rate limit, it is temporarily suppressed. You will see a warning in the console:

[resonon] MIDI feedback loop detected on ch1 — thru-play suppressed (will retry after 1000ms cooldown)

After a brief cooldown, thru-play re-enables automatically. To avoid feedback loops, use separate ports for input and output, or set monitor mode to "off".

Incoming CC messages are automatically captured and made available as Cc() signal sources. For example, if your controller sends CC 1 (mod wheel), that value is immediately usable in patterns and modulation. See MIDI CC for details on using CC signals.

The track methods return the track, so you can chain .input(), .monitor(), and .output() in a single expression for a complete input-to-output routing:

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

This creates a track that receives from the "iac" input, monitors everything, and sends to the "iac" output.

midi_clock_send(true) enables 24 PPQ clock output to all connected MIDI output ports. Resonon becomes the clock master, sending Start, Stop, and Continue transport messages alongside clock ticks.

midi_connect("IAC Driver Bus 1", "daw");
midi_clock_send(true);
setbpm(120);
PLAY;

Disable with midi_clock_send(false).

Clock send and clock follow are mutually exclusive — enabling one disables the other.

midi_clock_follow(alias) syncs Resonon’s tempo and transport to an external MIDI clock source arriving on the named input port:

midi_input_connect("DAW Output", "daw");
midi_clock_follow("daw");

When clock follow is active:

  • Tempo is derived by averaging 24 ticks (one quarter note)
  • Start resets playback to the beginning
  • Stop pauses playback
  • Continue resumes from the current position
  • A soft PLL applies phase correction (up to ~1ms per quarter note) to stay tightly locked

Disable clock follow by passing false:

midi_clock_follow(false);

midi_delay(ms) adds a global delay (in milliseconds) to all outgoing MIDI — notes and clock. This is useful for compensating audio output latency so that MIDI and audio stay aligned.

midi_delay(15); // delay all MIDI output by 15ms
PRINT midi_delay(); // getter: prints 15

midi_clock_offset(ms) adds an offset (in milliseconds) applied only to clock ticks. Positive values delay ticks, negative values send them earlier.

midi_clock_offset(-5); // send clock ticks 5ms early
PRINT midi_clock_offset(); // getter: prints -5

Clock offset is independent of midi_delay(). The total delay on clock ticks is midi_delay() + midi_clock_offset():

midi_delay(15); // all MIDI delayed 15ms
midi_clock_offset(-5); // clock ticks shifted 5ms earlier
// Effective clock delay: 15 + (-5) = 10ms
// Effective note delay: 15ms
FunctionDescription
midi_clock_send(true)Enable 24 PPQ clock output (master mode)
midi_clock_send(false)Disable clock output
midi_clock_follow(alias)Sync to external clock on input port
midi_clock_follow(false)Disable clock follow
midi_delay(ms)Set global MIDI output delay (notes + clock)
midi_delay()Get current output delay in ms
midi_clock_offset(ms)Set clock-only offset (positive = later, negative = earlier)
midi_clock_offset()Get current clock offset in ms

midi_routing() prints a summary of all MIDI connections, active slots, and clock state:

midi_routing();

Example output:

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")

In master mode, the clock section shows the send state:

Clock:
mode: master (sending)

If nothing is connected, it shows (no MIDI connections).

midi_input_disconnect(alias) closes the named input connection:

midi_input_disconnect("iac");

On disconnect, Resonon sends All Notes Off (CC 123) to any output ports that were receiving thru-play from the disconnected input. This prevents stuck notes when you unplug or disconnect a controller mid-performance.

A full workflow: list ports, connect with substring matching, create a track with input and monitoring, inspect routing, play, and clean up:

// List available ports
PRINT midi_input_ports();
PRINT midi_ports();
// Connect ports (substring match)
midi_input_connect("KeyStep", "kb");
midi_connect("IAC Driver Bus 1", "daw");
// Create a pass-through track
let keys = MidiTrack(1, 100)
.input("kb")
.monitor("in")
.output("daw");
// Check the routing
midi_routing();
PLAY;
// ... play your MIDI keyboard ...
PAUSE;
// Clean up
midi_input_disconnect("kb");
midi_disconnect("daw");
ErrorCauseFix
Input port alias '...' already connectedAlias already in useDisconnect the alias first, or choose a different alias
No MIDI input connected with alias '...'Clock follow with unrecognized aliasConnect the input port with midi_input_connect() first
Port not foundPort name not recognizedCheck midi_input_ports() for available names
MIDI feedback loop detected on ch...Thru-play forming a loopUse separate ports for input and output, or set monitor to "off"
// List available input ports
PRINT midi_input_ports();
// Show full MIDI routing state
midi_routing();

Read MIDI Control Change (CC) values as signals and send CC automation to external devices.

Cc(cc_num) creates a signal that reads the given CC number from any MIDI channel. The signal outputs a value from 0.0 to 1.0:

let cutoff_signal = Cc(74);

Cc(channel, cc_num) reads from a specific MIDI channel (0—15):

let cutoff_signal = Cc(0, 74);

Both forms require an active MIDI input connection (see MIDI Input).

Cc(cc_num) reads the last CC value received on any MIDI channel. This is convenient when you have a single controller and don’t care which channel it sends on.

Cc(channel, cc_num) isolates to a single channel (0—15). Use this when multiple controllers share the same CC numbers and you need to distinguish between them.

Cc_learn() provides interactive MIDI learn. It blocks for up to 10 seconds, waiting for you to move a knob or fader:

  1. Displays Waiting for CC input... (10s timeout) in the console
  2. Once a CC message arrives, returns a signal bound to that exact channel and CC number
  3. Prints Learned: Cc(channel, cc_num) — use this to hardcode the call later
let knob = Cc_learn();
// Console: "Waiting for CC input... (10s timeout)"
// Move a knob on your controller...
// Console: "Learned: Cc(0, 74)"

If no CC message is received within 10 seconds, an error is raised. An active MIDI input connection is required.

Use Cc_learn() interactively to discover the right CC number, then replace it with a hardcoded Cc() call for your script:

// Step 1: discover the CC number
let knob = Cc_learn();
// Console: "Learned: Cc(0, 74)"
// Step 2: replace with hardcoded call
let knob = Cc(0, 74);

CC values arrive as discrete 7-bit steps (0—127). To eliminate zipper noise when modulating audio parameters, apply .smooth(time_ms):

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

The argument is the smoothing time in milliseconds. A one-pole lowpass filter interpolates between incoming values, producing a continuous signal.

CC signals work anywhere a signal is accepted. Use << to route a signal into a parameter:

midi_input_connect("USB Controller", "ctrl");
let synth = AudioTrack("synth");
synth.filter.param("Cutoff") << Cc(74).smooth(50);
let track = AudioTrack("keys");
track.param("Gain") << Cc(11).smooth(20);
let track = AudioTrack("pad");
track.reverb.param("Mix") << Cc(1).smooth(30);
CCNameTypical Use
1Mod WheelVibrato, filter modulation
7VolumeChannel volume
10PanStereo panning
11ExpressionDynamic volume
64SustainOn/off (≥64 = on)
74BrightnessFilter cutoff

Use multiple CCs to control different parameters simultaneously:

midi_input_connect("USB Controller", "ctrl");
let synth = AudioTrack("synth");
// Mod wheel controls vibrato depth
synth.vibrato.param("Depth") << Cc(1).smooth(30).range(0, 1);
// CC 74 controls filter cutoff
synth.filter.param("Cutoff") << Cc(74).smooth(50).range(200, 8000);
// Expression pedal controls volume
synth.param("Gain") << Cc(11).smooth(20);

CC signals support all standard signal methods. See Signal Methods for the full list. Common ones:

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

Example — map a CC to a frequency range with linear scaling:

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

For parameters that feel more natural on a logarithmic scale (like frequency), use .range_exp():

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

CC values are stored atomically and updated from the MIDI input thread. They are safe to read from the audio thread without locks, making them suitable for real-time parameter control.

Send MIDI CC messages to external synths and DAWs. Use .cc(number) on a MIDI track to create a CC output, then route a value, signal, or pattern into it with <<.

Route an LFO or other signal to a CC parameter. The signal is sampled at ~30 Hz by default and sent as discrete CC messages with deduplication (identical consecutive values are skipped):

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

Send discrete CC values on the beat grid:

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

Send a fixed value once per cycle:

let synth = MidiTrack(1);
synth.cc(7) << 100;

Assign different sources to multiple CC parameters on the same track:

let synth = MidiTrack(1);
synth << [C4 E4 G4];
synth.cc(74) << Sine(0.25).range(20, 100);
synth.cc(1) << [0 32 64 127];
synth.cc(7) << 100;

By default, signal-driven CC is sampled at ~30 Hz. Use .send_rate(hz) on the CC output to change the rate:

let synth = MidiTrack(1);
synth.cc(74).send_rate(60) << lfo;

Higher rates produce smoother automation but use more MIDI bandwidth. Standard DIN MIDI supports about 1000 three-byte messages per second; USB MIDI is much faster.

The CC output is a value that can be stored in a variable:

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