Messages and dispatch#
Control objects talk to each other with messages. This page covers what a message can be, which inlets act on one, and in what order a message and everything it causes travel through the graph. Every patch depends on these rules.
Message types#
A cord carries one of these kinds of message. The YSE::OUT_TYPE of an
outlet (pHandle::OutputDataType) tells you which kind it sends:
BANGA bare “do it now” with no value.
INT/FLOATOne number. The patcher keeps the two types apart, and most number inlets accept both.
LISTText. Every list, symbol and word-plus-arguments message (
note 60 100,stop,1 2 3) travels as one string whose atoms are separated by whitespace. There is no separate symbol type: a symbol is a one-token list.ANYUsed by an outlet that can send more than one of the kinds above. The rightmost outlet of
.sel, for example, passes on whatever arrived in its own type.BUFFERA block of audio samples. Only signal (
~) objects send these, once per audio block. See Patcher for the difference between signal and control objects.
Each inlet registers a handler for each kind it accepts. The object
reference (Patcher object reference) lists them in the Accepts column. The
patcher does not convert at an inlet. A message the inlet has no handler
for is dropped without a trace, and the object does nothing. Connect does
not check types either. It will draw a cord from a LIST outlet to an
inlet that only takes floats, and nothing is delivered along it.
How numbers are spelled inside lists#
Objects that read lists split the text on whitespace, and they all agree on what a token is:
A token is a number only when the whole token reads as one finite number.
5abc,infandnanare symbols.A number is a float when it is spelled with a
.or an exponent (5.,0.5,1e3), and an int otherwise. When an object sends a list back out, it keeps that spelling, so1 2 3does not come back as1. 2. 3..When a list object (
.zl,.unpack,.routeand the like) ends up sending a single atom, it sends it as the int, float or one-token list it spells, not as a one-element list. That way the result can still reach an inlet that only takes numbers.
List length limits#
List objects (.zl, .pack, .unpack, .join, .iter, the
.array family and others) work on a fixed-size, pre-allocated list. This
keeps them from allocating when a message arrives on the audio thread. The
limits are:
at most 256 atoms. An object can lower its own limit (
.zl’szlmaxsize) but cannot raise it;at most 1024 characters of atom text in total.
When a list goes over a limit, the atoms that do not fit are refused,
not cut short. The atoms that already fit are kept. So a list that is too
long loses its tail, and the object records the refusal. From an inlet it
increments a counter (.zl exposes it as Dropped()) because it cannot
log from the audio thread. When the list comes from a creation argument, which
is parsed on the control thread, it writes a log line. Many objects whose
port count comes from their arguments (.sel, .trigger, .bondo)
use the same limit of 256.
Hot and cold inlets#
Each inlet is either hot or cold:
A hot inlet stores the value and then makes the object compute and send its result.
A cold inlet only stores the value. Nothing leaves the object until something reaches a hot inlet.
Unless the object reference says otherwise, the leftmost inlet (inlet 0) is
hot and every other inlet is cold. .+ is the typical case:
YSE::pHandle* add = patch.CreateObject(".+");
add->SetFloatData(1, 3.f); // cold: stores 3, sends nothing
add->SetFloatData(0, 4.f); // hot: sends 7
add->SetBang(0); // .+ does not accept a bang: dropped, nothing sent
add->SetFloatData(0, 1.f); // hot again: sends 4, the stored 3 is still there
Objects can differ. .bondo makes every inlet hot, because releasing
the whole set whenever any one of its values changes is the point of that
object. The object reference describes each inlet’s role. Words like “fires”
or “stored until” in a description tell you whether it is hot or cold.
Signal objects have one more rule. When a host sends a number to a signal
object’s hot inlet (for example the frequency inlet of ~sine), the value
is stored and takes effect in the next audio block. Signal objects only
compute while a block is rendering.
Dispatch is synchronous and depth first#
The patcher has no message queue between objects. When an object sends a
message, outlet::Send* calls the connected inlets’ handlers directly, one
after another. Each of those handlers runs to completion, including every
message it sends in turn, before the outlet moves on to the next cord. The
whole cascade happens inside the call that started it, on the thread that
started it:
A host call such as
pHandle::SetFloatDatareturns only after everything that value caused has run.A branch that is still running cannot be interrupted by another branch. The second one waits until the first has finished completely.
Two consequences follow:
The order of cords from one outlet is not guaranteed. When one outlet
feeds several inlets, the patcher does not promise which one is served first.
Do not build a patch that depends on it. Use .trigger instead.
Feedback loops are cut off, not queued. A cord path that leads back to
where it started (directly, or through .s / .r) would recurse
forever. Instead, the patcher stops passing a message on once a chain of sends
is 64 deep on one thread, and the rest of that fan-out is dropped. This keeps a
wiring mistake from crashing the audio thread. It is not a way to build
loops. To repeat something, use .uzi or a timed object (see
Time, scheduling and clocks).
Right to left, and .trigger#
Every object that sends from more than one outlet for one input (.trigger,
.bangbang, .bondo, .unpack, .uzi and others) sends from its
rightmost outlet first and its leftmost outlet last.
Combined with depth-first dispatch, it gives a firm guarantee: everything
connected to the right outlet has finished before the left outlet sends
anything.
This order is designed to work together with hot and cold inlets. Connect right outlets to cold inlets and the left outlet to the hot inlet, and the values arrive before the hot inlet makes the object compute.
.trigger exists only to make this order explicit. Take a number that
must reach both inlets of .+ so that the patch computes x + x. With
two cords from one outlet, the order is not defined. If the hot inlet happens
to be served first, .+ adds the previous value. A .trigger i i fixes
the order:
YSE::pHandle* in = patch.CreateObject(YSE::OBJ::G_RECEIVE, "x"); // ".r x"
YSE::pHandle* trig = patch.CreateObject(".trigger", "i i");
YSE::pHandle* add = patch.CreateObject(".+");
patch.Connect(in, 0, trig, 0);
patch.Connect(trig, 1, add, 1); // right outlet: sent first, into the cold inlet
patch.Connect(trig, 0, add, 0); // left outlet: sent last, into the hot inlet
patch.PassData(5, "x"); // on the next block: .+ sends 10, never 5 + (old x)
.trigger takes one argument per outlet and converts its input for each
outlet: i (int), f (float), b (bang), l (list), s
(text), or a constant that the outlet always sends. .trigger b i is the
common “store, then fire” pattern. The int goes to a cold inlet first, and
only then does the bang leave the left outlet to fire the object with the new
value. The full list of conversions is in Control flow.
Logical events#
Some objects need to know whether two messages came from the same
stimulus. A stimulus is one host call, one GUI action, one tick of a timed
object, or one incoming MIDI event. It is also called a logical event: two
clicks on a bang are two events, but the two bangs of a bang, bang
message box are one event, however long the patch takes to process them.
The patcher has no scheduler to group messages by, but synchronous dispatch
gives the same grouping. Everything a stimulus causes runs inside the call
that started it. Each inlet dispatch opens a dispatch frame. The outermost
frame on a thread starts a new event, and every frame nested inside it belongs
to that event. The engine exposes the current event as
YSE::PATCHER::CurrentMessageEvent() in inlet.h. It returns a 64-bit
id that is unique across threads and never reused, and it returns 0 when no
dispatch is in progress. So:
two host calls are two events, however close together they come;
the whole right-to-left fan-out of one
.triggeris one event;two ticks of a
.metroare two events.
Messages that are deferred, such as a .bondo release with a delay
argument or anything the patcher’s deferred-message scheduler delivers later,
get their own fresh frame when they are delivered. The engine uses the
messageEventScope guard in inlet.h for this. Such a message therefore
counts as one new event, not as several unrelated ones. Audio buffers do not
open a frame. A control message sent from a signal object’s handler starts its
own event.
Two objects depend on this:
.nextSends a bang from its left outlet for the first message of an event, and from its right outlet for every later message of the same event. “Once per burst” is its main use. For example, it can reset a counter once per
.colldump without knowing how long the dump is:YSE::pHandle* trig = patch.CreateObject(".trigger", "b b b"); YSE::pHandle* next = patch.CreateObject(".next"); patch.Connect(trig, 0, next, 0); patch.Connect(trig, 1, next, 0); patch.Connect(trig, 2, next, 0); trig->SetBang(0); // .next: one bang left (first), then two bangs right trig->SetBang(0); // a new host call is a new event: again one left, two right
.nextmeasures no time. Two messages a microsecond apart belong to different events if they came from two different stimuli..bondoReleases its whole set of stored values inside the call frame of the message that triggered the release. The release is therefore one logical event, and a
.nextdownstream of it sees one separated message and the rest continued. With a delay argument the release is deferred, and it still arrives as one fresh event.
Selector matching: .sel, .route and .routepass#
Three objects branch on what a message is, and they use one shared matcher so that swapping one for another does not change which messages match.
Each creation argument is a selector, and it is classified once, when the object is created:
An argument that reads as one whole finite number is a numeric selector. It matches by value, with an int widened to a float, so
5matches the int 5 and the float 5.0.Anything else is a symbol selector and matches by exact text.
A numeric selector never matches a symbol, and a symbol selector never matches a number.
Each incoming message is matched on one item:
an int or float: the number itself;
a bang: the symbol
bang;a list: its first token only, which counts as a number if it reads as one and as a symbol otherwise.
The comparison is exact. A computed float can miss a selector it looks equal
to: 0.1 + 0.2 is not 0.3 in binary floating point, so put a .round in
front if that matters. A NaN matches nothing. If the same selector appears
twice, only the leftmost of its outlets is used. Exactly one outlet fires per
message. A message that matches no selector leaves the rightmost outlet
unchanged and in its own type, so you can chain several of these objects by
connecting each rightmost outlet to the next object’s inlet.
The three objects differ in what they send when a message matches:
Object |
On a match, the matching outlet sends |
With no arguments |
|---|---|---|
|
A bang. |
Matches the number 0 (two outlets). |
|
The rest of the message, with the matched item removed. |
Matches the number 0 (two outlets). |
|
The whole message, unchanged. |
No selectors. Everything leaves the one outlet. |
.sel with exactly one numeric argument also gets a cold right inlet. A
number sent there silently replaces the selector’s value.
What .route sends after removing the matched item depends on what is left:
.route note
"note 60 100" -> outlet 0: the list "60 100"
"note 60" -> outlet 0: the int 60
"note 0.5" -> outlet 0: the float 0.5
"note C4" -> outlet 0: the one-token list "C4"
"note" -> outlet 0: bang
"ctl 7 64" -> outlet 1: "ctl 7 64", unchanged
.route 5
5 or 5.0 -> outlet 0: bang (the number was the whole message)
Changed in version 3.0: .route now removes the matched item (issue #672). Before this change
it forwarded the whole message, which is what .routepass does. If a
saved patch relied on the old behaviour, replace that .route with .routepass. The same change fixed float
matching: previously .route 5 never matched the float 5.0.
Changed in version 3.0: A .route with no arguments now has two outlets and matches the number
0, like .sel (issue #679). Before this change it
had no outlets and dropped every message.
The message box#
When banged, .m sends its text as the message it spells. Each receiving
inlet reads the text by the same rules as the list objects above:
one number is an int or a float, by its spelling (
60,0.5);the single word
bangis a bang;text that starts with a number is a list (
1 2 3);text that starts with any other word (
stop,note 60 100) is a command. The objects that take commands get it as one:~line(stop),.midiout(allnotesoffand the other control words),.midiinfo,.loadmess(set ...),.print,.mand.l. Every other object gets it as a list, which is how.routeand.selsee it.
So a .m 60 wired into .mtof sends the frequency of note 60, and a
.m note 60 into .route note sends the int 60. A message that the
inlet has no handler for is dropped like any other, and the object does not
compute. The one exception is an object that takes commands: a message its
inlet has no handler for reaches it as a command instead. That is how a
number still reaches .m and .l, which have no number handler:
.m 60 into a .m stores 60.
Changed in version 3.0: .m used to hand its text only to the seven objects above, and a
cord from it to any other object’s hot inlet made that object compute with
the value it already held: .m 60 into .mtof sent 8.18 Hz (issue
#933).
Where to go next#
Building and editing graphs: creating and connecting objects, and editing a graph while it plays.
Talking to the host: getting messages into and out of a patch from the host.
Time, scheduling and clocks: the timed objects whose ticks are separate events.
Subpatchers: how messages cross a subpatcher’s boundary.
Patcher object reference: every object’s inlets, outlets and accepted types.