Embedding through the C API#
yse_c/yse_patcher.h is the patcher’s C interface. It mirrors
YSE::patcher and YSE::pHandle function for
function, and adds what a binding needs that C++ gets for free: owned
strings, error codes, and the object metadata an editor builds its palette
from. This page groups the header by task, states the ownership and error
rules once, and links to the page that explains each concept. Every function’s
own documentation is in the generated C ABI (language bindings) reference.
#include "yse_c/yse_patcher.h" /* or yse_c/yse_all.h for everything */
A first patch#
A patcher is created empty, given its output count with
yse_patcher_init, filled with objects, and handed to a sound, which plays
it:
#include <stdio.h>
#include "yse_c/yse_patcher.h"
#include "yse_c/yse_sound.h"
YsePatcher* p = yse_patcher_create();
if (p == NULL || yse_patcher_init(p, 2) != YSE_OK) {
fprintf(stderr, "patcher: %s\n", yse_last_error());
return;
}
yse_patcher_set_name(p, "lead"); /* the bus scope: patcher.lead.<slot> */
YsePHandle* note = yse_patcher_create_object(p, ".r", "note");
YsePHandle* mtof = yse_patcher_create_object(p, ".mtof", NULL);
YsePHandle* osc = yse_patcher_create_object(p, "~sine", "440");
YsePHandle* amp = yse_patcher_create_object(p, "~*", "0.2");
YsePHandle* dac = yse_patcher_create_object(p, "~dac", NULL);
yse_patcher_connect(p, note, 0, mtof, 0);
yse_patcher_connect(p, mtof, 0, osc, 0);
yse_patcher_connect(p, osc, 0, amp, 0);
yse_patcher_connect(p, amp, 0, dac, 0);
yse_patcher_connect(p, amp, 0, dac, 1);
YseSound* voice = yse_sound_create();
if (yse_sound_load_patcher(voice, p, NULL, 1.0f) != YSE_OK) { /* NULL: master channel */
fprintf(stderr, "sound: %s\n", yse_last_error());
}
yse_sound_play(voice);
yse_patcher_pass_int(p, 64, "note"); /* reaches ".r note" at the next block */
/* ... later, in this order: */
yse_sound_destroy(voice);
yse_patcher_destroy(p);
To use a patch as an effect instead, wrap it with
yse_dsp_patcher_insert_create (yse_c/yse_dsp_modules.h) and attach
the result with yse_sound_set_dsp or yse_channel_set_dsp. See
Patcher for both ways of hosting a patch.
Ownership#
What you hold |
Who releases it |
|---|---|
|
You, with |
|
The patcher. Remove an object with |
Text copied into your buffer by a |
You. The engine only writes into the buffer you pass. |
|
You, with |
The |
You, with |
|
Nobody. The engine keeps these strings until the process ends, so they can be cached. |
A YseDspObject* from yse_dsp_patcher_insert_create is owned by you
and released with yse_dsp_object_destroy. It borrows the patcher: the
patcher must outlive it.
yse_patcher_destroy detaches the send callback first and returns only
once no other thread is still inside it. Do not call it from inside that
callback.
Errors and “no such” answers#
No C++ exception crosses the C boundary. A function that fails leaves the
reason in yse_last_error(), which is kept per thread and stays valid
until your next yse_* call on that thread. How the failure itself shows
depends on the return type:
Status functions (
yse_patcher_init,yse_patcher_set_name,yse_patcher_parse_json,yse_patcher_set_send_callback) returnYSE_OKor an error code:YSE_ERR_INVALID_HANDLEfor aNULLpatcher,YSE_ERR_INVALID_ARGUMENTfor a bad argument,YSE_ERR_NOT_INITIALIZEDforyse_patcher_parse_jsonbeforeyse_patcher_init, andYSE_ERR_EXCEPTIONwhen the engine threw.Handle functions return
NULL.yse_patcher_create_objectreturnsNULLfor an unknown type name and for an argument that does not parse. Only the second one setsyse_last_error().Functions without a result do nothing when a handle is
NULL. Most refusals (a cord to a pin that does not exist, a handle from another patcher) are written to the engine log, not toyse_last_error(). Install a log callback (yse_log_set_callback) to see them.Queries return 0,
NULLor an empty string for aNULLhandle, except where 0 is a real answer. Object IDs and inlet numbers start at 0, so those queries answerYSE_PATCHER_ID_NONEorYSE_PATCHER_INLET_NONEinstead, andyse_phandle_output_data_typeanswersYSE_OUT_INVALID. Building and editing graphs has the full table.
Reading text#
Every call that returns text copies it into a buffer you provide, like
snprintf. It returns the full length (without the terminator) whatever
cap is, truncates to fit, and always terminates the buffer when cap
is not 0. Pass a NULL buffer to ask for the size first:
#include <stdlib.h>
size_t len = yse_patcher_dump_json(p, NULL, 0);
char* json = malloc(len + 1);
if (json != NULL) {
yse_patcher_dump_json(p, json, len + 1);
/* ... write json to disk ... */
free(json);
}
The same pattern works for yse_patcher_get_name,
yse_phandle_get_type, yse_phandle_get_params,
yse_phandle_get_gui_value, yse_phandle_get_gui_value_at and
yse_phandle_get_gui_property. A value can change between the size query
and the read, for example when a control moves, so compare the second return
value against your buffer size before trusting the copy.
yse_phandle_get_name currently returns the type name, the same string as
yse_phandle_get_type.
The header by task#
Task |
Functions |
Explained in |
|---|---|---|
Create, name, destroy |
|
Talking to the host (the name as bus scope) |
Build and edit |
|
|
Subpatchers |
|
|
Save and load |
|
|
Walk the graph |
|
|
Send values in |
|
|
Get messages out |
|
|
Draw controls |
|
|
Store editor layout |
|
|
Build a palette |
|
Metadata for editors below |
Two things about sending values in are easy to miss:
yse_patcher_pass_*returns 1 when a.rwith that name exists or a send callback is installed, which then gets the value instead. It does not mean the value has been handled yet: that happens at the next block.yse_phandle_set_*runs the object and everything wired after it before it returns, on your thread. On a subpatcher handle, the inlet number is the subpatcher’s pin.
Metadata for editors#
The metadata calls describe every registered object type without a patcher: its description and category, whether it is a signal object, and a label, doc string and range for every inlet, outlet and argument. The object reference in this documentation (Patcher object reference) is generated from the same data. A binding can build its node palette, tooltips and “which cords are legal” checks from it:
#include <stdio.h>
#include "yse_c/yse_patcher.h"
int n = yse_patcher_get_type_count();
for (int t = 0; t < n; t++) {
const char* type = yse_patcher_get_type_name(t);
printf("%s (%s)\n", type, yse_patcher_get_type_description(type));
for (int i = 0; i < yse_patcher_get_inlet_count(type); i++) {
const char* label = NULL;
unsigned int accepts = 0;
yse_patcher_get_inlet_info(type, i, &label, NULL, NULL, &accepts); /* NULL: not needed */
printf(" in %d %s%s\n", i, label,
(accepts & YSE_IN_ACCEPTS_BUFFER) ? " [signal]" : "");
}
for (int o = 0; o < yse_patcher_get_outlet_count(type); o++) {
const char* label = NULL;
YseOutType kind = YSE_OUT_INVALID;
yse_patcher_get_outlet_info(type, o, &label, NULL, NULL, &kind);
printf(" out %d %s%s\n", o, label, kind == YSE_OUT_BUFFER ? " [signal]" : "");
}
}
The rules:
Types are listed in name order. An index outside
0toyse_patcher_get_type_count() - 1answers"".An unknown type name answers 0,
"",YSE_PCAT_UNSETorYSE_OUT_INVALID. Out-pointers you pass asNULLare skipped.The accept mask of an inlet is a combination of
YSE_IN_ACCEPTS_BUFFER,_FLOAT,_INT,_BANGand_LIST.These counts describe a type created without arguments. Objects whose arguments set their pin count (
.gate,.route,.trigger) have a different count once created; ask the handle withyse_phandle_get_inputsandyse_phandle_get_outputs.The first metadata call builds a cache by creating every object type once. That takes a moment, so make it early, for example at startup.
yse_patcher_get_metadata_json returns all of this as one JSON document,
keyed by type name, with the fields name, category, is_dsp,
requires_midi_device, description, inlets, outlets and
params. It is the same layout python yse.py dump-patcher-meta writes,
so a binding can generate code from a file at build time and read the live
data at run time. requires_midi_device marks the objects that exist only
in builds with MIDI device support, so a palette built on one platform can
flag them. Release the string with yse_free_string.
Threads#
Real-time model explains the threading model. For the C API it comes down to this:
Functions |
May be called from |
|---|---|
Everything in |
Your own threads, never the audio callback. Edits from several
threads are serialised by the patcher, but coordinate object lifetime
yourself: a handle one thread holds is freed when another deletes the
object. A handle does survive |
|
One control thread, as their documentation says. |
GUI value reads |
Your GUI thread. Read a given object either whole or cell by cell each frame, never both: some reads clear what they report (see GUI controls and presets). |
And for the send callback:
It runs synchronously on the thread that sent the message: yours for a
yse_patcher_pass_*oryse_phandle_set_*call, the engine’s timer thread for a millisecond.metro. Never the audio thread.It may be called from several threads at once. Keep it short and thread safe.
Installing or clearing it may still let one call that had already started reach the previous callback, always with its own
user_data.yse_patcher_set_send_callbackandyse_patcher_destroywait for calls running on other threads to finish. Do not call them while holding a lock the callback also takes. Callingyse_patcher_set_send_callbackfrom inside the callback is allowed; callingyse_patcher_destroythere is not.The strings stay valid after the callback returns, so an asynchronous host (a Dart
NativeCallable.listener, for example) can read them later and free them withyse_patcher_free_message.
Where to go next#
Building and editing graphs: creating, connecting and editing objects, with the full “no such” table.
Talking to the host: the queue, the bus and the send callback.
GUI controls and presets: polling controls and writing their state back.
Real-time model: which thread runs what, and why edits never block the audio.
C ABI (language bindings): every C function, generated from the headers.