Patcher: presets#
Goal: give a patch a bank of presets. Store the current settings of its controls in numbered slots, switch between them, and have the patch come up in slot 0 every time it is loaded.
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 stores two presets, recalls them, checks that the sound changes with them, and loads a saved copy.
The patch#
.f 60 .f 1000 .f 0.5 controls: note, cutoff, level
| | |
.mtof | |
| | |
~saw -> ~lp 1000 -> ~* 0.5 -> ~dac (both channels)
.loadmess 0 -> .preset 8 8 slots; slot 0 on load
struct PresetSynth {
YSE::pHandle* note;
YSE::pHandle* cutoff;
YSE::pHandle* level;
YSE::pHandle* presets;
};
PresetSynth BuildPresetSynth(YSE::patcher& patch) {
// Three controls. A .f sends its value on whenever it is set.
YSE::pHandle* note = patch.CreateObject(".f", "60");
YSE::pHandle* cutoff = patch.CreateObject(".f", "1000");
YSE::pHandle* level = patch.CreateObject(".f", "0.5");
YSE::pHandle* mtof = patch.CreateObject(".mtof");
YSE::pHandle* osc = patch.CreateObject("~saw");
YSE::pHandle* filter = patch.CreateObject("~lp", "1000");
YSE::pHandle* amp = patch.CreateObject("~*", "0.5");
YSE::pHandle* dac = patch.CreateObject("~dac");
patch.Connect(note, 0, mtof, 0);
patch.Connect(mtof, 0, osc, 0);
patch.Connect(osc, 0, filter, 0);
patch.Connect(cutoff, 0, filter, 1);
patch.Connect(filter, 0, amp, 0);
patch.Connect(level, 0, amp, 1);
patch.Connect(amp, 0, dac, 0);
patch.Connect(amp, 0, dac, 1);
// Eight preset slots. The .loadmess recalls slot 0 whenever a saved copy
// of this patch is loaded.
YSE::pHandle* presets = patch.CreateObject(".preset", "8");
YSE::pHandle* startup = patch.CreateObject(".loadmess", "0");
patch.Connect(startup, 0, presets, 0);
return {note, cutoff, level, presets};
}
The controls#
A .f is a number box: it holds a float, and sends it on whenever it is
set. The creation argument is only its starting value; it sends nothing until
something sets it. The host sets it with SetFloatData(0, value), and an
editor would draw it and set it the same way.
.preset captures exactly the objects that can have their state written
back: the controls. .f, .i, .slider, .dial, .t and the
other controls listed in GUI controls and presets take part. The cutoff of the
~lp itself is not captured. The .f that sets it is, which is why
every setting you want in a preset needs a control in front of it.
Storing and recalling#
Set the controls, then send store <n> to the .preset. It captures
every control’s current value into slot n:
// A dark, quiet sound in slot 0 ...
synth.note->SetFloatData(0, 48.f);
synth.cutoff->SetFloatData(0, 400.f);
synth.level->SetFloatData(0, 0.3f);
synth.presets->SetListData(0, "store 0");
// ... and a bright, louder one in slot 1.
synth.note->SetFloatData(0, 55.f);
synth.cutoff->SetFloatData(0, 4000.f);
synth.level->SetFloatData(0, 0.6f);
synth.presets->SetListData(0, "store 1");
A number recalls that slot. .preset sends each stored value back into
its control, and each control sends it on, so the sound changes exactly as if
the controls had been set by hand:
synth.presets->SetIntData(0, 0); // back to slot 0
Other messages: a bang recalls the active slot again, clear <n> empties a
slot and clearall empties them all. presets->GetGuiValue() returns the
active slot number, or -1 when no slot is active, which is what an editor
highlights in a row of preset buttons.
Recalling from inside the patch#
Calling the .preset handle from the host thread, as above, recalls at
once: the controls hold the slot’s values when the call returns. A
.loadmess or .loadbang does the same (see below).
A recall can also come from inside the patch, for example through a named receiver:
YSE::pHandle* recall = patch.CreateObject(".r", "recall");
patch.Connect(recall, 0, synth.presets, 0);
patch.PassData(1, "recall"); // recalls slot 1 shortly after the next block
PassData delivers its value while the patch renders, on the audio thread,
and so do MIDI input objects: a .pgmin wired into the .preset recalls
a slot on every program change. Storing and recalling are too much work for
the audio thread, so .preset queues these messages and runs them on the
timer thread about a millisecond later, in the order they arrived. The test
for this page waits for the PassData recall to land. GUI controls and presets
lists which routes run at once and which a moment later.
Coming up in a preset on load#
A saved patch holds the creation arguments of its controls, not their live
values. Loaded on its own, the patch would come up with the note at 60, the
cutoff at 1000 and the level at 0.5 — the .f arguments — whatever was
playing when it was saved.
The .preset slots, on the other hand, are saved with the patch. The
.loadmess 0 in front of the .preset sends 0 when the patch has
loaded, which recalls slot 0:
YSE::patcher copy;
copy.create(2);
copy.ParseJSON(patch.DumpJSON());
After ParseJSON returns, the loaded patch plays slot 0, even though slot
1 was active when it was saved. A load signal is sent on the thread that runs
ParseJSON, so the patch is already in slot 0 when ParseJSON returns.
To come up in whatever slot was
active instead, use a .loadbang: a bang recalls the active slot, and the
active slot is saved too.
Load a patch with presets into an empty patcher. The slots refer to their controls by position in the file, and that only lines up when the loaded objects are the only ones in the patcher. What is saved with the patch has the details.
Next#
GUI controls and presets — the GUI value protocol that
.presetis built on, every control, and what a host editor polls.GUI controls — reference for
.presetand the controls.Patch file format — what a saved patch holds.