Patcher: a voice in a subpatcher#
Goal: build a synth voice once, as a subpatcher with one inlet and one
outlet, and use it like any other object. The voice takes a note as the list
pitch velocity, fades in when the velocity is above 0 and fades out when
it is 0.
This tutorial assumes you have worked through Patcher: modular synthesis in code. Its code is compiled and run by the test suite (Tests/patcher/test_patcher_tutorials.cpp), which plays a note, checks its pitch, releases it, and loads a saved copy of the patch. Patcher: a synth played from MIDI uses four of these voices.
The voice#
inlet 0: "pitch velocity"
.inlet 0
|
.unpack 0 0
| \
.mtof ./ 127 velocity to 0-1
| |
~saw ~line 0 20 20 ms ramp to the new level
| |
~* <------+ (right inlet: gain)
|
~outlet 0
outlet 0: audio
A subpatcher is an object of type patcher. You create its contents with
the same CreateObject as everything else, and put them inside with
SetContainer. Its pins are the boundary objects inside it: .inlet 0
is its inlet 0 (for messages), and ~outlet 0 is its outlet 0 (for audio).
// One voice: the list "pitch velocity" in, audio out. Velocity 0 releases it.
YSE::pHandle* CreateVoice(YSE::patcher& patch) {
YSE::pHandle* voice = patch.CreateObject("patcher");
YSE::pHandle* in = patch.CreateObject(".inlet", "0");
YSE::pHandle* split = patch.CreateObject(".unpack", "0 0"); // pitch, velocity
YSE::pHandle* mtof = patch.CreateObject(".mtof");
YSE::pHandle* osc = patch.CreateObject("~saw");
YSE::pHandle* scale = patch.CreateObject("./", "127"); // velocity to 0-1
YSE::pHandle* env = patch.CreateObject("~line", "0 20"); // 20 ms ramps
YSE::pHandle* amp = patch.CreateObject("~*");
YSE::pHandle* out = patch.CreateObject("~outlet", "0");
for (YSE::pHandle* h : {in, split, mtof, osc, scale, env, amp, out}) {
patch.SetContainer(h, voice);
}
patch.Connect(in, 0, split, 0);
patch.Connect(split, 0, mtof, 0); // pitch
patch.Connect(mtof, 0, osc, 0);
patch.Connect(split, 1, scale, 0); // velocity
patch.Connect(scale, 0, env, 0);
patch.Connect(osc, 0, amp, 0);
patch.Connect(env, 0, amp, 1);
patch.Connect(amp, 0, out, 0);
return voice;
}
What each part does:
.unpack 0 0splits the list into two ints. Its outlets fire right to left, so the velocity leaves before the pitch..mtof→~sawsets the pitch../ 127scales the velocity to 0-1, and~line 0 20ramps to that level over 20 ms. The ramp is the envelope: it rises on a note-on and falls back to 0 on a release (velocity 0). Without it, every note would click on and off.~*multiplies the oscillator by the envelope. Its right inlet takes an audio signal here, not a number.
Draw the cords after the objects are inside the subpatcher, as the function does. Subpatchers explains why.
Using the voice#
From the outside, the voice is one object with inlet 0 and outlet 0. Connect to it by pin number, as to any other object:
YSE::pHandle* BuildMonoSynth(YSE::patcher& patch) {
YSE::pHandle* note = patch.CreateObject(".r", "note");
YSE::pHandle* voice = CreateVoice(patch);
YSE::pHandle* dac = patch.CreateObject("~dac");
patch.Connect(note, 0, voice, 0); // lands on the voice's .inlet 0
patch.Connect(voice, 0, dac, 0); // leaves from its ~outlet 0
patch.Connect(voice, 0, dac, 1);
return voice;
}
Connect(note, 0, voice, 0) draws a cord to the .inlet 0 inside the
voice, and Connect(voice, 0, dac, 0) draws one from its ~outlet 0. The
parent never names what is inside.
Attach the patcher to a sound (see Patcher: modular synthesis in code), then play a note and release it:
patch.PassData(std::string("57 100"), "note"); // A3 at velocity 100
patch.PassData(std::string("57 0"), "note"); // velocity 0: release
The host can also skip the .r and send straight into the voice’s inlet:
voice->SetListData(0, "57 100").
Several voices#
CreateVoice is a function, so a patch can hold as many voices as it
calls it. Two things to know when you do:
A signal inlet takes one cord. To play several voices through one
~dac, add them up with~+first (or give each its own~dac: a patch’s~dacobjects are summed). Patcher: a synth played from MIDI sums four voices with a chain of~+.Names are not local to a subpatcher. A
.r freqinside a voice would receive everyfreqsent to the patcher, in every copy of the voice. That is why this voice has an inlet instead of a receiver: a parent can address one copy through its pins. Subpatchers covers this.
Saving and loading#
DumpJSON saves the voice as a record of type patcher, and every
object inside it with a container key that points to it. ParseJSON
rebuilds the same nesting, and the loaded copy plays the same way. The test
for this page saves the patch, loads it into a new patcher and plays a note
through the copy. Patch file format describes the records.
Next#
Patcher: a synth played from MIDI — four of these voices, played from a MIDI keyboard.
Subpatchers — pins, nesting, editing a subpatcher while it plays, and the C API.
Subpatchers — reference for
patcher,.inlet,.outlet,~inletand~outlet.