Patcher: a step sequencer#
Goal: build an eight-step sequencer in a patch. A .metro sets the pace,
a .counter counts the steps, and a .coll holds the note for each step.
The host starts and stops it, changes its speed and rewrites steps while it
plays.
This tutorial assumes you have worked through Patcher: modular synthesis in code. The code on this page is compiled and run by the test suite (Tests/patcher/test_patcher_tutorials.cpp), which checks that the pattern plays in order and that the patch makes sound.
The patch#
.r run .r rate
| |
.metro 125 a bang every 125 ms
|
.counter -1 0, 1, 2 ...
|
.% 8 0 ... 7, 0 ... 7
|
.coll pattern <--- .r edit
|
.mtof -> ~saw -> ~lp 1200 -> ~* 0.2 -> ~dac (both channels)
In code:
struct StepSequencer {
YSE::pHandle* metro;
YSE::pHandle* counter;
YSE::pHandle* pattern;
};
StepSequencer BuildStepSequencer(YSE::patcher& patch) {
// The clock: a bang every 125 ms, a sixteenth note at 120 BPM.
YSE::pHandle* run = patch.CreateObject(".r", "run"); // 1 starts, 0 stops
YSE::pHandle* rate = patch.CreateObject(".r", "rate"); // step length in ms
YSE::pHandle* metro = patch.CreateObject(".metro", "125");
patch.Connect(run, 0, metro, 0);
patch.Connect(rate, 0, metro, 1);
// The step number. .counter adds its step before it sends, so starting it
// at -1 makes it count 0, 1, 2 ...; folded into 0-7, that is the step.
YSE::pHandle* counter = patch.CreateObject(".counter", "-1");
YSE::pHandle* wrap = patch.CreateObject(".%", "8");
patch.Connect(metro, 0, counter, 0);
patch.Connect(counter, 0, wrap, 0);
// The pattern: a step number goes in, the note stored for it comes out.
YSE::pHandle* pattern = patch.CreateObject(".coll", "pattern");
YSE::pHandle* edit = patch.CreateObject(".r", "edit"); // "<step> <note>" rewrites a step
patch.Connect(wrap, 0, pattern, 0);
patch.Connect(edit, 0, pattern, 0);
// The sound: a filtered sawtooth at the note's pitch, on both channels.
YSE::pHandle* mtof = patch.CreateObject(".mtof");
YSE::pHandle* osc = patch.CreateObject("~saw");
YSE::pHandle* filter = patch.CreateObject("~lp", "1200");
YSE::pHandle* level = patch.CreateObject("~*", "0.2");
YSE::pHandle* dac = patch.CreateObject("~dac");
patch.Connect(pattern, 0, mtof, 0);
patch.Connect(mtof, 0, osc, 0);
patch.Connect(osc, 0, filter, 0);
patch.Connect(filter, 0, level, 0);
patch.Connect(level, 0, dac, 0);
patch.Connect(level, 0, dac, 1);
return {metro, counter, pattern};
}
The clock#
.metro 125 sends a bang every 125 ms once it is started: a sixteenth note
at 120 BPM. Its left inlet starts it with any non-zero number or a bang, and
stops it with 0 or stop. Every start sends a bang straight away. A
number in the right inlet changes the interval, also while it runs. Both
inlets are wired to receivers, so the host controls it by name.
A millisecond .metro runs on the engine’s timer thread, not on the audio
thread, so each step is heard at the start of the next audio block. That is
fine for a sequencer that plays on its own. When the steps must stay in time
with other things (clips, a second sequencer, a tempo that changes), give the
metro a beat interval on a domain clock instead: send clock main to its
left inlet (main being a clock the host created) and 16n to its right
inlet. Time, scheduling and clocks explains the clocks and what each one is
accurate to.
The step number#
.counter adds its step (1) to its value and sends the result. Its
argument is the value it starts at, so .counter -1 sends 0 on its first
bang, then 1, 2 …. .% 8 folds that into the range 0-7, so the pattern
repeats every eight steps. To change the pattern length, change the 8.
A number in the counter’s right inlet changes the step: send it 2 and
the sequencer plays every other step.
A reset message puts the counter back at its start value (-1), so the
next bang plays step 0 again. The test on this page uses it to replay the
first step.
The pattern#
.coll stores messages at addresses. A list whose first item is a number
stores the rest of the list at that address, and a number on its own recalls
what is stored there. So "3 63" stores note 63 at step 3, and the step
number 3 coming from .% 8 sends 63 on to .mtof.
Fill the pattern by sending one list per step to the .coll:
const int notes[8] = {48, 55, 60, 63, 48, 58, 60, 67};
for (int step = 0; step < 8; step++) {
seq.pattern->SetListData(0, std::to_string(step) + " " + std::to_string(notes[step]));
}
SetListData delivers the list straight to the object’s inlet, on the
calling thread, before it returns. A step that holds nothing sends nothing,
so the previous note keeps sounding.
The collection is named pattern. Every .coll pattern in the same
patcher shares one collection, so a second .coll pattern elsewhere in the
patch can read or edit the same steps. The contents are saved with
DumpJSON and come back with ParseJSON. Collections, dictionaries and arrays covers
.coll and the other stores.
The sound#
.mtof turns the note into a frequency for the ~saw. The ~lp
softens it and the ~* sets the level. One outlet can feed several inlets,
so the ~* feeds both channels of the ~dac.
Playing it#
Attach the patcher to a sound and start the metro:
YSE::System().init();
YSE::patcher patch;
patch.create(2);
StepSequencer seq = BuildStepSequencer(patch);
// ... fill seq.pattern as above ...
YSE::sound sound;
sound.create(patch);
sound.play();
patch.PassData(1, "run"); // start
patch.PassData(100, "rate"); // faster: 100 ms per step
patch.PassData(0, "run"); // stop
While it plays, rewrite a step from the host through the edit
receiver:
patch.PassData(std::string("0 36"), "edit"); // step 0 now plays note 36
PassData queues the value, and it reaches the .coll at the start of
the next audio block. .coll is safe to write on the audio thread: it
never allocates or waits there.
Things to try#
Replace everything after the
.collwith the voice from Patcher: a voice in a subpatcher, and store a velocity with each note ("3 63 100"). The.collthen sends the list63 100, which is exactly what the voice takes, and a step stored with velocity 0 ("5 63 0") is a rest.Build a second
.metro→.counter→.coll patternchain at a different rate. Both.collobjects share one pattern, so a second line reads the same notes at its own pace.
Next#
Patcher: a voice in a subpatcher — package a voice as a subpatcher.
Time, scheduling and clocks — metronomes, delays, tempo and clocks.
Collections, dictionaries and arrays —
.coll, dictionaries and arrays.Patcher object reference — every object on this page, in detail.