Skip to content

Recording & Input

Everything so far has made sound inside Resonon. But a track can also listen to the outside world — a microphone, a guitar, a synth, anything plugged into your audio interface. You route an input to a track, arm it like a channel in a DAW, monitor it through the track’s effects, and capture takes to disk. Live input and recorded patterns coexist on the same tracks, so you can sing over a beat you’re coding and bounce the result.

Here’s the whole loop — route an input, arm it, record, and grab the take:

let mic = AudioTrack("mic");
mic = mic.input("MacBook Pro Microphone"); // listen to a device
mic = mic.arm(); // ready to record
mic = mic.monitor("auto"); // hear it while armed
RECORD; // start capturing on every armed track
// ... play or sing ...
PAUSE; // stop capturing
let take = mic.take(); // pull the recording off the track
if take != NUL {
take.save("vocals.wav");
}

Each piece of that — finding devices, arming, monitoring, the transport, and retrieving takes — is worth a closer look.

audio_devices() returns a list of every audio device on your system. Each entry is a dict carrying the device name, its input_channels and output_channels counts, the default sample_rate in hertz, and an is_default flag. To find something you can record from, keep the ones with at least one input channel:

let devices = audio_devices();
for d in devices {
if d["input_channels"] > 0 {
PRINT d["name"] + " (" + d["input_channels"] + " inputs)";
}
}

If you always record through the same interface, naming your inputs once in your config file (~/.resonon/config.toml) beats typing channel numbers every session:

[audio.input]
device = "Focusrite Scarlett 2i2"
[audio.input.channels]
mic = [0]
vocals = [0, 1]
guitar = [2]

A single-element array like [0] is a mono input from that channel; a two-element array like [0, 1] is a stereo pair. Once they’re named, you pass the channel name to .input() instead of the device name, and Resonon resolves it to the right channels on the right interface.

.input(name) points a track at an input source. The argument is either a device name or one of your configured channel names:

let mic = AudioTrack("mic");
mic = mic.input("MacBook Pro Microphone");

You don’t need a config file for basic use — if no input stream is running yet, .input() starts one for the named device automatically. Like the other track methods, it returns the track so calls chain.

A track won’t capture anything until it’s armed, and arming requires an input source — calling .arm() on a track with no .input() is an error. Only armed tracks record when you trigger the transport. .disarm() undoes it:

let mic = AudioTrack("mic");
mic = mic.input("MacBook Pro Microphone");
mic = mic.arm();
// ... record ...
mic = mic.disarm();

Monitoring is whether you hear the live input through the output, set with .monitor(mode). "in" always passes the input through; "auto" passes it only while the track is armed or recording; "off" keeps it silent.

let mic = AudioTrack("mic");
mic = mic.input("MacBook Pro Microphone");
mic = mic.monitor("in"); // always hear the mic
mic = mic.monitor("auto"); // hear it only when armed/recording
mic = mic.monitor("off"); // silent

The transport drives recording. RECORD starts playback and begins capturing on all armed tracks at once:

let mic = AudioTrack("mic");
mic = mic.input("MacBook Pro Microphone").arm().monitor("auto");
RECORD;
// ... perform ...
PAUSE;

PAUSE and STOP both halt the transport, but they finalize differently. PAUSE freezes playback while the recording engine keeps the captured audio intact — call .take() or STOP to finalize it. STOP finalizes the take, resets the transport to the start, and mutes the master output, leaving the track armed so you can RECORD another pass without re-arming.

CommandStops transportFinalizes takeKeeps armedMutes master
PAUSEYesNoYesNo
STOPYes (+ reset)YesYesYes

Because RECORD captures every armed track simultaneously, tracking a band is just a matter of arming more than one. Each track keeps its own independent recording:

let vocals = AudioTrack("vocals");
vocals = vocals.input("vocals").arm().monitor("auto");
let guitar = AudioTrack("guitar");
guitar = guitar.input("guitar").arm().monitor("auto");
RECORD;
// ... perform ...
PAUSE;
let vocal_take = vocals.take();
let guitar_take = guitar.take();

Punch recording arms the transport to a cycle range rather than the whole performance — perfect for overdubbing one section or fixing a single phrase without re-recording around it. .punch(in, out) sets both ends; recording goes live at the in cycle and stops at the out cycle:

let rec = AudioTrack("rec");
rec.input("MacBook Pro Microphone").arm();
rec.punch(4, 12); // capture only cycles 4–11
RECORD; // playback rolls; recording arms itself at cycle 4

Set one end only with .punch_in(cycle) (open-ended — starts there, no auto-stop) or .punch_out(cycle) (starts immediately, stops there). .clear_punch() removes the points, and .is_punched() tells you whether any are set:

let rec = AudioTrack("rec");
rec.input("MacBook Pro Microphone").arm();
rec.punch_in(8); // open-ended from cycle 8
show(rec.is_punched()); // true
rec.clear_punch();
show(rec.is_punched()); // false

A few things worth knowing: punch transitions land on cycle boundaries, so they stay in musical time; punch points persist across RECORD/STOP, so you can punch the same window over several takes; and PAUSE inside a punch window finalizes whatever partial take you’ve captured.

There are two ways to pull a recording off a track, and both consume the buffer — once you’ve taken a recording, calling either again returns nothing until you record afresh. Pick one per take.

.take() returns a RecordedAudio object you can inspect before saving, or NUL if nothing was captured. It exposes .duration() (seconds), .samples() (total sample count), .sample_rate() (hertz), and a chainable .save(path):

let mic = AudioTrack("mic");
mic = mic.input("MacBook Pro Microphone").arm();
RECORD;
PAUSE;
let take = mic.take();
if take != NUL {
show(take.duration());
show(take.sample_rate());
take.save("take1.wav");
}

.save(path) on the track itself is the shortcut — it writes the recording straight to a WAV and returns the track so it chains:

let mic = AudioTrack("mic");
mic = mic.input("MacBook Pro Microphone").arm();
RECORD;
PAUSE;
mic.save("takes/vocals_01.wav");

A monitored input flows through the track’s effect chain before it reaches the output, so the same effects you’d put on a sampler shape a live signal too. Load them before you arm and monitor, so the chain is ready when audio starts moving:

use "std/effects" { Reverb, Highpass };
let vox = AudioTrack("vocals");
vox = vox.input("MacBook Pro Microphone");
vox = vox.load_effect(Highpass(120));
vox = vox.load_effect(Reverb(0.3, 0.5));
vox = vox.monitor("in");
// now you hear your voice, filtered and reverbed, in real time

Routing works on a live input as well. Send processed input to a shared bus with .send_to(), exactly as you would a sampler track:

use "std/effects" { Reverb };
let reverb_bus = AudioTrack("reverb_bus");
reverb_bus = reverb_bus.load_effect(Reverb(0.5, 0.7));
let vox = AudioTrack("vocals");
vox = vox.input("MacBook Pro Microphone").monitor("in");
vox = vox.send_to(reverb_bus, 0.4); // 40% to the reverb bus

When you record while playing back, the captured audio arrives a little late by the round-trip latency of your interface (input plus output). recording_offset() trims that off the front of every recording. Read it with no argument, set it with a millisecond value:

recording_offset(10); // trim 10 ms — match your interface
show(recording_offset()); // read the current offset

The right value depends on your hardware; start with your interface’s advertised round-trip latency and fine-tune by recording a click. To see what Resonon currently measures, audio_latency() returns a dict with buffer_ms (audio buffer latency), pdc_ms (plugin-delay-compensation path latency), sample_rate, and the underlying buffer_frames and pdc_samples:

let lat = audio_latency();
show(lat["buffer_ms"]);
show(lat["pdc_ms"]);
show(lat["sample_rate"]);

You can persist the offset in your config so it’s set every session:

[audio]
recording_offset_ms = 10.0

For outboard gear — running a track out through a hardware effect or a guitar amp and back — use .delay(ms) to compensate for that device’s own round-trip delay. It shifts the track’s output later in time, and the PDC system nudges every other track to keep the whole mix aligned (recordings on the delayed track shift to match too):

let guitar = AudioTrack("guitar");
guitar.delay(8); // ~8 ms round trip through an amp
show(guitar.delay()); // read it back

Once you can capture and shape audio, give your piece a structure.