Skip to content

Routing, Buses & Sends

So far every track has run straight to the master on its own. Routing is how you change that — feeding tracks through a shared effect, grouping them into a submix, or splitting a copy off to a parallel processor. In DAW terms this chapter is the mixer’s routing matrix: output buses, aux sends, and sidechain inputs.

Two operators do the wiring. >> sets a track’s one output destination; send_to splits off a copy to somewhere else. Here’s a reverb send — drums stay dry on the master, with a copy feeding a reverb bus:

use "std/instruments" { Sampler, Kit };
use "std/effects" { Reverb };
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
let reverb_bus = AudioTrack("reverb");
reverb_bus.load_effect(Reverb(0.8, 0.3));
drums.send_to(reverb_bus, 0.3); // 30% of the drums also hit the reverb
PLAY;

Now the line-by-line.

Every track has exactly one output. By default it’s master, which is why sound reaches your speakers without any wiring. The >> operator sets that output explicitly:

drums >> master;

>> replaces the output route — a track has one destination, and routing again re-points it. You can chain hops, and each hop carries the signal onward at unity gain:

kick >> drum_bus >> master;

Any AudioTrack can act as a bus: a track with no instrument of its own that exists to collect and process other tracks. Route several sources into it with >>, then load an effect on the bus to treat them as one:

use "std/instruments" { Sampler, Kit };
use "std/effects" { Lowpass };
let drum_bus = AudioTrack("drum_bus");
let kick = AudioTrack("kick");
let snare = AudioTrack("snare");
let hats = AudioTrack("hats");
kick.load_instrument(Sampler(Kit("CR-78")));
snare.load_instrument(Sampler(Kit("CR-78")));
hats.load_instrument(Sampler(Kit("CR-78")));
kick >> drum_bus; // all three feed the bus...
snare >> drum_bus;
hats >> drum_bus;
drum_bus.load_effect(Lowpass(4000)); // ...and the filter shapes the whole kit
kick << [bd*4];
snare << [_ sd _ sd];
hats << [hh*8];
PLAY;

The bus reaches master automatically, so the filtered drum group lands in the mix with one fader to ride.

A send routes a copy of a track’s audio somewhere else while the original keeps flowing to its normal destination. This is how you share one reverb or delay across several tracks: build the effect on a return track, then send each source to it.

send_to(destination, amount) adds a copy at the given level. The dry signal still goes to the master at full volume, so the send is additive — it layers wet on top of dry:

use "std/instruments" { Sampler, Kit };
use "std/effects" { Delay };
let delay_bus = AudioTrack("delay_bus");
delay_bus.load_effect(Delay(0.3, 0.5));
let perc = AudioTrack("perc");
perc.load_instrument(Sampler(Kit("CR-78")));
perc << [rs _ cl _];
perc.send_to(delay_bus, 0.3); // dry perc + 30% into the delay return
PLAY;

One track can feed several returns — a short slap and a long throw, each at its own level:

perc.send_to(short_delay, 0.2);
perc.send_to(long_delay, 0.4);

xsend_to(destination, amount) is the exclusive send: it routes to the destination while pulling the dry signal back to 1.0 - amount. At 0.7 you get 70% through the bus and 30% dry, so total energy stays roughly constant — a crossfade from dry to wet rather than a layer on top.

use "std/instruments" { Sampler, Kit };
use "std/effects" { Reverb };
let wet_bus = AudioTrack("wet");
wet_bus.load_effect(Reverb(0.9, 0.2));
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
drums.xsend_to(wet_bus, 0.7); // 70% wet, 30% dry to master
PLAY;

Reach for send_to for parallel effects and reverb/delay returns, and xsend_to when you want to route a track through a bus without doubling its level. xsend_to can’t target master directly — its whole job is to divert the dry path.

The amount isn’t limited to a fixed number. Hand send_to a signal or a pattern and the send level moves on its own — an LFO swelling the delay, or stepped values per beat:

use "std/instruments" { Sampler, Kit };
use "std/effects" { Delay };
use "std/signals" { Sine };
let delay_bus = AudioTrack("delay_bus");
delay_bus.load_effect(Delay(0.3, 0.5));
let perc = AudioTrack("perc");
perc.load_instrument(Sampler(Kit("CR-78")));
perc << [rs _ cl _];
perc.send_to(delay_bus, Sine(0.5).range(0.0, 0.6)); // send swells with a slow LFO
PLAY;

To automate a send you’ve already created, grab its level with send_level(dest) and drive it like any other parameter:

drums.send_to(reverb_bus, 0.3);
drums.send_level(reverb_bus) << automation(#[0, 0.3], #[8, 0.8]);

Signals and automation are the subject of the next chapter.

Some effects have a second audio input — a sidechain. A classic use is ducking: a compressor on the drums that clamps down whenever the bass plays. Feed one track into an effect’s named input port with connect_input:

use "std/instruments" { Sampler, Kit };
dsp effect Ducker {
input main: stereo;
input sidechain: mono;
output: stereo;
param amount: 0.8 range(0, 1);
state env: 0.0;
fn process(main_l, main_r, sc) -> (out_l, out_r) {
let level = __native("abs", sc);
env = env + (level - env) * 0.01;
let gain = 1.0 - amount * env;
return (main_l * gain, main_r * gain);
}
}
let drums = AudioTrack("drums");
drums.load_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
let bass = AudioTrack("bass");
bass.load_instrument(Sampler(Kit("CR-78")));
bass << [bd _ bd _];
let duck = Ducker();
drums.load_effect(duck);
duck.connect_input("sidechain", bass); // bass triggers ducking on the drums
PLAY;

The port name matches a declared input on a dsp effect. For an external plugin it’s the plugin’s own sidechain bus name — often "Sidechain" or "Aux Input". Unlike a send, connect_input is read-only: it feeds the source into the effect’s detector without mixing that audio into the output. A mono port auto-downmixes a stereo source to (L + R) / 2, and Resonon orders the graph for you, so the source just can’t form a routing cycle with the destination.

When the wiring gets dense, routing() prints the whole graph; pass one or more tracks to narrow it, or call .routing() on a track:

routing(); // every track
routing(drums, bass); // just these two
drums.routing(); // method form

To start over, reset_routing() clears a track’s routes and send modulations back to the default master route. It’s chainable, so you can clear and re-wire in one line:

drums.reset_routing().send_to(reverb_bus, 0.5);

You can route tracks to buses, split parallel sends, and feed sidechains. Next, make those send amounts and effect parameters move on their own.

  • Signals & Automation — drive parameters and send levels with LFOs, ramps, and breakpoint envelopes
  • Plugins — load external VST3 and CLAP effects and instruments
  • The Audio Graph — how routing, buses, and the master fit into the whole signal graph