Skip to content

Your First Mix

On this page you build a small mix from nothing: three drum tracks, a filter, a shared reverb, and the master fader. At the end, you put the whole rig into one function that you can edit while it plays.

Work in a project, as in Your First Sound. Write each step into src/main.non, select it, and press Cmd+Enter. Every step adds to what the previous one built.

Finish Language for a Reusable Piece so that fn, return and tuple unpacking are familiar. Open your project’s src/main.non, evaluate RESET; on its own, then remove that line. Check that all earlier tracks are silent. This page builds a new mix with bundled kits; evaluate the blocks in order.

A track is a channel on a mixer. It holds one instrument, has a fader, and sends its sound to the master.

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.set_instrument(Sampler(Kit("CR-78")));
drums << [bd sd bd sd];
PLAY;

You hear a kick and a snare, looping.

  • AudioTrack("drums") makes a track called drums. Run the line again and you get the same track, not a second one.
  • set_instrument(...) puts an instrument on it — here a Sampler with the built-in CR-78 drum kit.
  • << sends a pattern to the track. The track plays it every cycle until you send another one.

Every track has a volume in decibels and a pan from -1 (left) to 1 (right). Set them with <<:

drums.volume << -6;
drums.pan << -0.5;

The drums are quieter, and to the left.

Give every track its own name. Replace the combined beat with three separate parts. First send silence to the earlier drums track so it does not double the new kick and snare:

drums << [_];
let kick = AudioTrack("kick");
let snare = AudioTrack("snare");
let hats = AudioTrack("hats");
kick.set_instrument(Sampler(Kit("CR-78")));
snare.set_instrument(Sampler(Kit("CR-78")));
hats.set_instrument(Sampler(Kit("CR-78")));
kick.volume << -9;
snare.volume << -9;
hats.volume << -15;
kick << [bd*4];
snare << [_ sd _ sd];
hats << [hh*8];
PLAY;

Three tracks play together. You can change each one without touching the others.

Effects process a track’s sound after the instrument. set_effects sets a track’s effect chain, as a list. The sound goes through the list in order:

use "std/effects" { Highpass, Delay };
hats.set_effects(#[Highpass("highpass", 2000), Delay("delay", 0.125, 0.3)]);

The hats are thinner, and they echo. The first argument of each effect is its name in the chain.

Now change one setting. Keep a handle to the effect, put it on the track, and set a parameter through the handle:

let echo = Delay("delay", 0.125, 0.3);
hats.set_effects(#[Highpass("highpass", 2000), echo]);
echo.parameter("Feedback") << 0.6;

The echoes last longer. The chain was not rebuilt: the delay called "delay" was already there, so it kept playing.

A reverb for every track would be wasteful. Make one reverb track, and send a copy of the snare to it:

use "std/effects" { Reverb };
let verb = AudioTrack("verb");
verb.set_effects(#[Reverb("room", 0.8, 0.3)]);
snare.set_send(verb, 0.3);

The snare plays dry as before, and 30% of it also goes through the reverb. The verb track has no instrument — it only processes what other tracks send to it.

Every track reaches master, the last fader before your speakers. You never made it; it is always there. Turn the whole mix down:

master.volume << -2;

Every setter returns the track, so a whole channel strip can be one expression. Read it from top to bottom: what makes the sound, what processes it, how loud.

use "std/effects" { Lowpass };
let bass = AudioTrack("bass")
.set_instrument(Sampler(Kit("CR-78")))
.set_effects(#[Lowpass("lowpass", 400)])
.set_volume(-4);
bass << [bd _ _ bd _ _ bd _];

.set_volume(-4) is the same as bass.volume << -4. Where the track goes is a statement of its own: a new track goes to master, and bass >> verb; would route it through the reverb instead.

A function that builds tracks and returns them is a layout. This last exercise returns to one drums track and the shared reverb. Send silence to the separate parts and the bass first; otherwise those tracks would keep playing under the new layout:

kick << [_];
snare << [_];
hats << [_];
bass << [_];
fn stage() {
let verb = AudioTrack("verb")
.set_effects(#[Reverb("room", 0.8, 0.3)]);
let drums = AudioTrack("drums")
.set_instrument(Sampler(Kit("CR-78")))
.set_effects(#[Lowpass("lowpass", 4000)])
.set_volume(-9)
.set_pan(0)
.set_send(verb, 0.3);
return (drums, verb);
}
let (drums, verb) = stage();
drums << [bd sd bd sd];
PLAY;

The earlier tracks no longer trigger notes; their existing tails can finish. The drums fader and pan are set explicitly so the first exercise’s left pan does not carry into this layout.

While it plays, add an effect to the drums’ .set_effects(#[...]) list and run the whole block again. The new effect is inserted, and nothing else stops: the tracks and effects with the same names are kept. Remove the effect and run it again, and it is gone.

STOP; stops playback and goes back to the start. In VSCode, Shift+F5 does the same, and F5 toggles play and pause.

STOP;