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#