GUI controls and presets#
The patcher has no user interface of its own. Controls such as .slider,
.t, .umenu or .matrixctrl are ordinary objects that hold a value.
The engine owns that value. A host-side editor draws each control, polls
its value to repaint it, and sends messages to change it. The engine does not
push changes to the editor, and the editor never writes into an object’s
state directly.
This page describes that contract, the GUI value protocol: how the host
reads a control’s state, how it writes state back, which objects take part,
and how .preset uses the same contract to store and recall a whole patch.
The inlets, outlets and arguments of each control are listed in
GUI controls.
GUI value or GUI property?#
An object carries two kinds of editor data. They are not interchangeable.
GUI value |
GUI property |
|
|---|---|---|
What |
The live control state: a slider’s position, a menu’s selection, a grid’s cells. |
Editor decoration: position on the canvas, size, colour, anything the host wants to keep with the patch. |
Who writes it |
The object itself, when a message arrives from the host or down a cord, often on the audio thread. |
Only the host. |
Calls |
|
|
Threads |
Safe to poll from the host thread while the patch renders. |
Control thread only. Not synchronised. |
Saved with the patch |
No (except for |
Yes, under the |
The engine never reads a GUI property. Store layout and styling there, and never live state.
The GUI value#
GUI state is a list of string cells. A scalar control such as .slider
has one cell. A structured control has several: .rslider has two (the
two ends of the range), .kslider has 128 (one per key), a .matrixctrl
has one per grid cell.
C++ ( |
C API |
Returns |
|---|---|---|
|
|
The whole state as one string: the value of a scalar control, or every cell space-separated in index order. Empty for an object with no GUI state. |
|
|
How many cells there are. 1 for a scalar control. The C function
answers 0 for a |
|
|
Cell |
|
|
Whether the object accepts its own state back on inlet 0 (see Writing state back). The C function returns 1 or 0. |
The C string functions work like snprintf: they return the full length
and accept a NULL buffer as a size query. See the caution under
Polling before you use that size query on a GUI value.
A cell is not always one token. A .function cell is one breakpoint,
<x> <y> <curve>, and a .nodes cell is one node, <x> <y> <radius>.
Numbers are written as text, so a float .slider reports 0.500000, an
.i reports 42 and a .t reports on or off. Treat the
strings as data to draw from, and hand them back unchanged when you restore.
Polling#
A host editor usually polls every visible control once per frame, on its own thread, while the audio thread keeps rendering. The calls take no lock, so the audio thread never waits on a repaint. The price is a few rules:
Use one form per object per frame. Either read the whole state with
GetGuiValue, or read the count and then the cells with
GetGuiValueAt, but never both for the same object in the same frame.
Some reads are destructive (see below), and an event would then reach one
read and not the other.
Per-cell reads are not atomic across cells. Each call samples the state
on its own. A repaint can see cell 3 from before an edit and cell 4 from
after it, or a count that a live SetParams has just changed. A cell past
the current count simply reads as empty. That is fine for drawing. When you
need a consistent snapshot, for example to save it, use GetGuiValue: it
reads everything in one call.
The per-cell form exists for large controls. A 256 × 256 .matrixctrl
builds a string of more than 100 KB for GetGuiValue. A host that repaints
one changed cell should read only that cell.
Some reads are destructive. .b reports on once for a press and
clears it in the same step, so the next read says off. A .led or
.textbutton created with the momentary keyword does the same with
1 and 0. Poll each of these from one place only, or presses get lost.
Warning
The C API’s size query is a read too. Calling
yse_phandle_get_gui_value(h, NULL, 0) to learn the length and then
again to fetch the text polls the object twice, and a .b press is
consumed by the first call. Poll with a buffer that is large enough (a
few bytes suffice for .b). If the result is longer than the buffer,
the second call is a new poll, not a re-read of the first.
An empty read can be transient. .function guards its breakpoints with
a flag that a reader does not wait for. When a write holds it at the moment
you poll, the read comes back empty. Treat an empty result from an object
that normally has state as “nothing new this frame” and keep the last value
you drew. .textedit is the exception in the other direction: its read
waits the few nanoseconds a write holds the text, so it is always complete.
Writing state back#
There is no GUI-value setter, and there will not be one. State goes into
an object the same way every other value does: as a message on an inlet,
with SetListData, SetFloatData, SetIntData or SetBang (C:
yse_phandle_set_list and friends). The message passes through the
object’s own handler, so every clamp, side effect and outlet send happens
exactly as if the value had come down a cord.
An object that answers true to GuiValueIsSettable() promises two things
about inlet 0:
The round trip. A list holding the exact string
GetGuiValue()returned restores the state it described:std::string saved = h->GetGuiValue(); // ... the user moves things ... h->SetListData(0, saved); // back to where it was
The cell write.
set <index> <value>writes a single cell:range->SetListData(0, "set 1 96"); // .rslider: move one end to 96
The set keyword is what tells the two apart. For a two-cell control such
as .rslider, 0 1 is the whole state and set 0 1 means “cell 0
becomes 1”. Without the keyword the two could not be told apart. For
.function and .nodes, whose cells hold several numbers, the value
part is all of them: set 2 0.5 0.8 0. rewrites breakpoint 2.
The one exception is .textedit. Its single cell is free text, and
free text can start with the word set. So .textedit takes everything
on inlet 0 verbatim, and there is no set form. With one cell, the
whole-state write already is the cell write, so the round trip still holds
and GuiValueIsSettable() is still true.
Whether a restore emits is up to the object. Most controls have a hot
inlet 0 and send their new value on, so restoring a snapshot drives the rest
of the patch just like the user moving the control. A few are silent, and
.kslider sends only what changed. The table under The controls lists
which is which. Control objects handle a host message synchronously, on the
calling thread: when SetListData returns, the object has its new state
and whatever it emitted has already run downstream.
Round trip in a host editor#
The example below is a small editor. It repaints the controls every frame, takes a snapshot of every settable object, and puts the patch back later.
#include <map>
#include <string>
#include "yse.hpp"
YSE::patcher patch;
patch.create(2);
YSE::pHandle* range = patch.CreateObject(".rslider", "0 127");
YSE::pHandle* steps = patch.CreateObject(".multislider", "8 0 1");
YSE::pHandle* mute = patch.CreateObject(".t");
YSE::pHandle* flash = patch.CreateObject(".b");
// ... connect them to the rest of the patch ...
// Every frame: repaint. One form per object per frame.
void repaint(YSE::pHandle* h) {
unsigned int cells = h->GetGuiValueCount();
for (unsigned int i = 0; i < cells; i++) {
std::string cell = h->GetGuiValueAt(i);
if (cell.empty()) break; // the count changed under us: stop
// draw cell i ...
}
}
// A snapshot: the whole-state string of every settable object, by ID.
std::map<unsigned int, std::string> snapshot(YSE::patcher& p) {
std::map<unsigned int, std::string> out;
for (unsigned int i = 0; i < p.Objects(); i++) {
YSE::pHandle* h = p.GetHandleFromList(i);
if (h == nullptr || !h->GuiValueIsSettable()) continue; // skips .b
std::string value = h->GetGuiValue();
if (!value.empty()) out[h->GetID()] = value;
}
return out;
}
// Restore: send each object its own string back, as a list on inlet 0.
void restore(YSE::patcher& p, const std::map<unsigned int, std::string>& s) {
for (const auto& [id, value] : s) {
YSE::pHandle* h = p.GetHandleFromID(id);
if (h != nullptr && h->GuiValueIsSettable()) h->SetListData(0, value);
}
}
Checking GuiValueIsSettable() before reading does two jobs. It skips
objects that could not take the value back, and it means the snapshot never
polls .b, so taking a snapshot cannot eat a press.
An object ID names an object only while it is alive. After a delete, the next
object created can take over the number (see Building and editing graphs). A snapshot
that must survive edits should also keep the Type() of each object and
check it on restore. .preset does exactly that.
The same round trip in the C API:
char value[4096];
unsigned int n = yse_patcher_objects(patch);
for (unsigned int i = 0; i < n; i++) {
YsePHandle* h = yse_patcher_get_handle_from_list(patch, i);
if (!yse_phandle_gui_value_is_settable(h)) continue;
size_t len = yse_phandle_get_gui_value(h, value, sizeof value);
if (len == 0 || len >= sizeof value) continue; /* empty, or too long for this buffer */
/* store yse_phandle_get_id(h) and value ... */
}
/* later, to restore one object: */
yse_phandle_set_list(h, 0, value);
The controls#
Every object in the table answers true to GuiValueIsSettable().
Object |
Cells |
One cell holds |
A restore |
|---|---|---|---|
|
1 |
the position, 0 to 1 |
emits |
|
1 |
the knob position, 0 to 1, not the mapped value |
emits |
|
1 |
the number |
emits |
|
1 |
the stored whole number |
is silent |
|
1 |
|
emits |
|
1 |
|
emits |
|
1, or one per item with |
the selected index; with |
emits |
|
1 |
the text (no |
emits |
|
2 |
the low end, then the high end |
emits |
|
2 |
x, then y |
emits |
|
2 |
the pitch, then the accidental ( |
emits |
|
the live bank size |
one slider value |
emits the whole bank |
|
columns × rows |
one grid cell; cell |
emits every cell as |
|
128 |
the velocity held on that key, 0 when the key is up |
emits only the keys that changed |
|
the number of breakpoints |
|
is silent |
|
the number of nodes |
|
emits |
Notes on individual controls:
.taccepts its ownon/offback, and also a number. A restore sets the state; it never flips it the way a bang does..incdechas twosetforms.set <n>(one number) is a silent set;set 0 <n>(two numbers) is the protocol’s cell write..rsliderstores the two ends as they were written and sorts them when it reports them, so cell 0 is always the low end..multislidertakes its size from a whole-state list, so restoring a snapshot also restores the bank size..matrixctrl: on a grid of exactly three cells, a three-number list is read as the whole state, never as a<column> <row> <value>cell write..function: an empty function reports the wordclearrather than an empty string.GetGuiValueCount()is then 0.clearround-trips like any other state, so a snapshot of an empty envelope empties it again on restore..nodes: the whole state carries the node count, so restoring it into a fresh object restores the field’s size too.A momentary
.led/.textbuttonstill holds the round trip, but its value is a pending press. Restoring a snapshot taken at the moment of a press replays the press.
Objects without a settable value#
These report a GUI value but answer false to GuiValueIsSettable(), so
neither a host snapshot as above nor .preset captures them:
.b: its value is a press event that the read consumes, not a state. Writingonback would replay the press. For a momentary control whose state can be restored, use.led momentaryor.textbutton momentary..preset: its value is the active slot, read-only (see below)..mand.l: the value is the message text the box holds.Non-GUI objects that expose a read-only display value, such as
.counter,.accum,.drunkor.uzi.
.text is a fixed label and has no GUI value at all. Its text is its
creation argument.
Before issue #846 the scalar controls (.slider, .i, .f, .dial,
.incdec, .t) were outside the protocol too, because their inlet 0 only
took a number. They now accept their own display string back and are
settable.
Presets#
.preset is a bank of numbered slots, each
holding a snapshot of the patch’s controls. It is the host snapshot from the
example above, built into the patch.
YSE::pHandle* presets = patch.CreateObject(".preset", "8"); // 8 slots
presets->SetListData(0, "store 0"); // capture the patch into slot 0
// ... the user moves things ...
presets->SetIntData(0, 0); // recall slot 0
Messages#
.preset has one inlet:
Message |
Effect |
|---|---|
an int or a float |
Recalls that slot. A float is truncated. |
|
The same, spelled out. |
|
Captures the patch into slot |
bang |
Recalls the active slot again. Does nothing when no slot is active. |
|
Empties slot |
|
Empties the active slot. |
|
Empties every slot. |
Outlet 0 sends the number of the slot just recalled, after every value has been pushed, so whatever it triggers already sees the patch in the preset. Outlet 1 sends the number of the slot just stored. Recalling an empty or out-of-range slot does nothing and sends nothing. Clearing the active slot also clears the active marker.
The creation argument is the number of slots: 32 by default, clamped to
1–1024. Changing it with SetParams replaces the object, and the
replacement starts with empty slots.
The GUI value of .preset is the active slot number, or -1 when none is
active. That is what a host highlights in a row of preset buttons. It is
read-only.
What a slot captures#
A store captures exactly the objects that answer true to
GuiValueIsSettable(), which are the controls in the table above. The rule
is the protocol’s own promise: an object that is captured is also one that
can be restored. As a result:
.presetnever captures another.preset, or itself..bis never captured, and a store never even polls it, so storing cannot eat a press.Objects inside subpatchers are captured too. A preset belongs to the whole patch, not to one level of it.
An object whose value is empty at store time (an empty
.textedit) is left out, and a recall leaves it untouched.Only controls are captured. The cutoff of a
~lpis not; the control that sets it is.
A recall sends each captured object the exact string it reported, through
SetListData on inlet 0, the same call a host makes. Each object’s own
handler decides whether to emit (see the table). Before an entry is pushed,
.preset checks that its object ID still names a live object of the same
type. IDs are reused after a delete, and this check keeps a recall from
writing one control’s state into whatever object took over its number. A
stale entry is skipped; the rest of the slot is still recalled.
There is no interpolated recall: a recall is instant. To morph
between states, build it in the patch, for example .xyslider into
.nodes into the gains you want to blend.
Threads#
Storing walks the whole patch and recalling runs whole subgraphs, so
.preset never does that work on the audio thread. Every way of driving it
works, but not every way runs at the same moment:
Runs immediately: a
SetListData/SetIntData/SetBangcall on the.presethandle from the host thread, a.loadbang(see below), and a millisecond.metro. These deliver on control-side threads, and the recall has happened when the call returns.Runs a moment later: anything that reaches the
.presetwhile the patch renders, on the audio thread. That isPassData/PassBanginto a.rthat feeds the.preset(host messages sent that way are delivered at the start of the next block, see Talking to the host), MIDI input (the input objects deliver their events while the patch renders, see MIDI), and anything fired inside the render pass, such as a.delayor.pipeoutput. The.presetdoes not run these on the audio thread: it queues them and runs them on the timer thread about a millisecond later, in the order they arrived. Queuing is lock-free and allocation-free, so it costs the audio thread nothing noticeable.
So a .pgmin wired into a .preset recalls a preset on a program change.
Keep in mind that the recall lands after the block that carried the
program change, not inside it. A bang or a bare clear reads the active
slot when it runs, so a store 3 followed by a bang in the same block
recalls slot 3.
At most 64 of these queued messages can wait at once. Past that, further ones are dropped and counted rather than logged, because the audio thread must not log.
Editing the patch while a store or recall runs on another thread is safe. An object deleted halfway through a recall stays allocated until the recall is done (see Real-time model). Whether it still receives its value depends on which comes first; a recall never touches freed memory either way.
What is saved with the patch#
A saved patch (DumpJSON) holds each object’s creation arguments and GUI
properties. For almost every control it does not hold the live GUI value:
reloading brings each control back in its initial state. A host that wants to
reopen a patch as the user left it has two options:
keep its own snapshot, as in the example above, and restore it after
ParseJSON; orput a
.presetin the patch.
Two controls do save their contents:
.presetsaves all its slots and the active slot marker. Loading does not recall anything. A.loadbangconnected to the.presetinlet recalls the saved active slot when the patch loads; put a.m 0in between to always come up in slot 0. You can also send the recall from the host afterParseJSONreturns..loadbangfires on the thread that ranParseJSON, so the patch is in the slot whenParseJSONreturns..functionsaves its breakpoints.
In the file, a .preset entry names its object by its position among the
saved objects rather than by its raw ID. ParseJSON gives loaded objects
new IDs in that same order, so the presets still point at the right objects
even when deletes had left gaps in the numbering. This relies on loading
into an empty patcher. Loaded into a patcher that already holds objects,
the numbering starts past them and the entries point at the wrong objects;
the type check turns most of those into skips rather than wrong writes.