Collections, dictionaries and arrays#
A patch often needs to remember more than one value: a preset table, the notes that are held down, a cue list, a mapping curve. The patcher has a family of store objects for this. Each one keeps its data inside the object, in a table of fixed size, and answers messages that add, change, look up and dump that data.
This page explains what the stores have in common: how some of them share their contents by name, how dictionaries and arrays travel between objects, what their size limits are, what happens when a limit is reached, and what is saved with the patch. The full message list of each object is on its reference page: Collections, Dictionaries, Arrays and Sequencing. Reading and writing files is covered in File input and output.
The stores at a glance#
Object |
Holds |
Shared by name |
Saved with the patch |
Files |
|---|---|---|---|---|
|
messages at number or symbol addresses |
yes |
yes |
read and write |
|
key/value pairs, nested with |
yes |
yes |
through |
|
an ordered list of single values |
yes |
yes |
no |
|
an unordered set (or multiset) of ints |
no |
no |
no |
|
a dense array of ints, addressed by index |
no |
yes, unless |
no |
|
(x, y) pairs sorted by x |
no |
only after |
no |
|
a ring of the last values that arrived |
no |
no |
no |
|
lines of text |
no |
no (the file is the storage) |
read and write |
|
a timed cue list |
no |
yes |
read and write |
|
recorded message tracks with timing |
no |
only after |
read and write |
|
recorded raw MIDI bytes with timing |
no |
no (the file is the storage) |
read (MIDI file or text), write (MIDI file) |
.value is a store too, but it holds a single value. It is described with
the other host-facing objects in Talking to the host.
Dictionaries and arrays travel by name#
A cord carries a bang, a number, a list or an audio buffer. It cannot carry
a reference to a dictionary or an array. So .dict and .array do not
send their contents down a cord. They send their name, as a message:
A bang on a
.dict mydictsendsdictionary mydictout of outlet 1.A bang on an
.array mylistsendsarray mylistout of outlet 1.
This message is the reference. It holds the local name, without the
patcher.<patcherName>. prefix. The receiving object adds its own
patcher’s name, the same way the sender did.
The .dict.* and .array.* objects (.dict.iter, .dict.serialize,
.array.length, .array.at …) bind their store from their creation
argument, just like .dict and .array. A reference arriving on their
inlet is accepted only when it names the store they are already bound to. A
reference that names any other store is refused. So write the name into
each object:
[.dict voices] [.dict.serialize voices]
outlet 1 ("dictionary voices") -> inlet 0 -> JSON text
A reference can also go through .s and .r, or through the named bus
to the host. What travels is still only the name. The contents stay in the
store.
Dictionaries#
.dict holds key/value pairs. The main messages are:
Message |
Effect |
|---|---|
|
store a value at a key path, replacing what was there |
|
send the value out of outlet 0, or bang outlet 2 if there is none |
|
remove one entry |
|
empty the dictionary |
|
send the number of entries out of outlet 0 |
|
send the top-level keys out of outlet 0 |
bang |
send the reference |
A value is stored as the text that arrived, so set chord 0 4 7 stores one
value of three numbers. On the way out it is typed by its spelling: a single
number leaves as an int or a float, anything else as a list.
Nested keys#
Keys nest with ::: voice::1::freq is one entry, three
levels deep. Inside the object the table is flat and the nesting is part of
the key. That keeps every change a fixed-size write, which is what makes the
object safe on the audio thread. The price is that a sub-tree is not a value:
get voice::1 does not return the freq entry below it; it is a miss.
To work on a sub-tree, use .dict.slice (split it off into another
dictionary), .dict.strip (remove it) or .dict.iter (walk the
entries).
When the dictionary leaves the engine, the nesting becomes real again. A
saved patch and .dict.serialize both write a nested JSON object:
{"voice": {"1": {"freq": 440, "gain": 0.5}}}
and .dict.deserialize flattens such a document back into :: keys.
If a key path runs into a key that already holds a value (a holds a value
and a::b is stored as well), the JSON output skips the later entry rather
than overwrite the value.
Arrays#
.array holds an ordered list. Each element is one value: a single
int, float or word. append 0 4 7 adds three elements, and getvalue
sends them back as the list 0 4 7. Because every element is a single
token, an array and the list it spells always match. A value with inner
structure belongs in a .dict.
Message |
Effect |
|---|---|
|
add elements at the end |
|
add elements before a position |
|
replace one element |
|
send one element out of outlet 0, or bang outlet 2 if the index is out of range |
|
remove one element and close the gap |
|
empty the array |
|
send the length out of outlet 0 |
|
send the whole array as a list out of outlet 0 |
bang |
send the reference |
Indices start at 0. An index outside the array, and any negative index, is
refused. It is never wrapped or clamped. .array.wrap and .array.rotate
exist for the cases where you want wrapping.
insert and delete move every element after the position. An object
that walks an array while another object writes to it can therefore see
elements shift under it.
.array.deserialize fills an array from a JSON array such as
[60,62.5,"kick"]. Nested arrays and objects in the document are skipped,
because an element cannot hold them.
Size limits#
Every store has a fixed capacity. Its table is allocated in full when the object is created and never grows, so no message has to allocate memory on the audio thread.
Object |
Capacity |
|---|---|
|
256 entries. A message is at most 256 characters, an address at most 64. |
|
256 entries. A key path is at most 128 characters (including the
|
|
256 elements of at most 64 characters each. |
|
256 numbers. |
|
1 to 4096 entries, set by the size argument (default 128). A larger size is clamped to 4096. |
|
256 pairs. |
|
1 to 512 items, set by the first argument (default 512). A word is at most 64 characters. |
|
256 lines of at most 256 characters. |
|
256 cue lines of at most 256 characters. |
|
1 to 32 tracks, each with 256 events of at most 128 characters. |
|
4096 events. An event is one MIDI byte, so this is about 1365 three-byte messages such as note-ons. |
What happens at a limit#
A message that does not fit is refused whole. A value is never cut short, because half a message is a different message. The refused message changes nothing and sends nothing. It is also not logged: the message may be running on the audio thread, where writing a log line is not allowed. If data goes missing, check it against the limits above.
.capture is the one exception. It is a recorder, so when it is full the
oldest item is dropped to make room for the new one.
The same rule covers a store that is busy. Every store is protected by a
flag that is taken without waiting. Messages usually reach a store on the
audio thread, one at a time. But a message can also arrive on another
thread, for example from a millisecond .metro, which runs on the
engine’s timer thread (see Time, scheduling and clocks). If two threads touch the same store
at the same moment, the one that loses the flag does nothing at all rather
than wait.
Loading data from a file has its own rules: records past the limit are dropped and the rest of the file still loads. See File input and output.
What is saved with the patch#
DumpJSON writes an object’s creation arguments. Some stores also write
their contents, under the object’s state key. The exact JSON of each
one is listed under Saved contents in Patch file format.
.coll,.dict,.arrayand.qlistsave their contents when they are not empty..tablesaves its values unless it was sentembed 0. The flag itself is always saved..funbuffand.mtrsave their contents only afterembed 1..bag,.capture,.textfileand.seqsave no contents. The arguments are still saved, so a.textfile notes.txtor a.seq song.midreads its file again when the patch loads (see File input and output).Run-time positions are never saved: the read pointer of
.colland.funbuff, the cursor and tempo of.qlist, and the transport state of.mtrand.seq.
For a shared store (.coll, .dict, .array), every object with the
name writes the contents, but only the object that creates the store
reads them back on load. So:
several objects of one name in a file load the contents once, and
a patch loaded while its store name is already in use in the engine joins the running store. The saved contents are not applied.
.dict.serialize, .dict.deserialize and .array.deserialize save
only their arguments. The contents belong to the store they are bound to.
Real-time behaviour#
Stores can be used freely from any message, including messages the audio thread delivers:
no message allocates memory, takes a lock or waits,
the name is looked up once, on the control thread, when the object is created (or when the patcher is renamed),
a store never holds its lock while it sends, so an outlet wired back into the same store works,
the stores do no work of their own per audio block. They only react to messages.
The two steps that cannot run on the audio thread happen elsewhere. Parsing
JSON in .dict.deserialize and .array.deserialize runs on the
background pool, and the result is installed one block later. File access
runs on the background pool as well (see File input and output). See
Real-time model for the patcher’s overall rules.