Upgrading from 2.4 to 3.0#
libYSE 3.0 is a major release. Most of the C++ API is source compatible with 2.4, but the C ABI changed, the patcher’s bus addresses moved, and a number of engine and patcher behaviours changed without any change to a signature. That last group is the one to read carefully: code that compiles cleanly against 3.0 can still behave differently.
This page lists every change that can break or alter a program written against 2.4.0, grouped by the layer you talk to. Each entry names the issue that made the change, so you can read the full reasoning on GitHub. New features that change nothing existing are not listed here; see the release notes for those.
Quick checklist#
Everyone: the default sample rate is now 48 kHz,
System().init()returnsfalsewhen no audio device opens, and theautoReconnectdelay is in milliseconds.C / FFI hosts: rebuild your bindings. Four functions now return
YseStatus, one getter changed its signature, two functions and three callback typedefs were renamed, and several “nothing here” answers moved from0to a sentinel. See C API.Patcher hosts: anything that publishes to or subscribes on a patcher slot from outside must add the
patcher.prefix. See Bus and store addresses.Patches: re-check anything that uses
.route,.metro,.counter,~saw, several~dacobjects or message boxes. See Patcher objects.
Engine and audio I/O#
These apply to C++ and C hosts alike. The C function is named where it differs from the C++ one.
- Default sample rate is 48 kHz (#646)
The engine’s initial rate is now 48000 Hz instead of 44100, on desktop and Android. A device still negotiates its own rate, and
System().getSampleRate()reports what was agreed. To ask for a rate, callSystem().requestSampleRate(rate)(yse_system_request_sample_rate) beforeinit()orinitOffline(). An offline session runs exactly at the requested rate, or at 48 kHz when none is set, so offline renders that assumed 44.1 kHz change length. See Sessions and devices.init()fails when no device opens (#973)System().init()used to returntrueeven when there was no default output device, or the device refused to open or start. It now returnsfalse, leaves no session behind and restores the previous sample rate.yse_system_initreportsYSE_ERR_AUDIO_DEVICE. Headless tools and CI should callinitOffline(), or fall back to it wheninit()fails.deviceSetup::setSampleRate()is honoured (#971, #972)openDevice()used to ignore the rate stored on adeviceSetup. With no session rate locked, the requested rate now takes precedence over the device default. On a running session a rate that differs from the session rate is refused with a warning, and the stream opens at the session rate.System().openDevice()now returnsboolinstead ofvoid(#900).autoReconnectdelay is in milliseconds (#681)The
delayargument ofSystem().autoReconnect(on, delay)was counted inupdate()calls, despite being documented (and nameddelay_msin C) as milliseconds. It is now milliseconds. A host that passed a tick count now waits a shorter wall-clock time: at 60update()calls per second,20used to mean about 333 ms and now means 20 ms. Negative values are clamped to 0. A stream that has just started gets a 500 ms grace period before the watchdog acts on it.missedCallbacks()still counts that start-up window.- A default
devicehas ID-1(#666) A hand-built
devicedescriptor used to have ID 0, which is a real PortAudio device index, so opening one silently opened the first enumerated device.device::getID()now defaults to-1, andopenDevice()refuses it with a log line and leaves the running stream alone.yse_device_get_id(NULL)returns-1rather than0. Descriptors fromSystem().getDevices()still carry their real index.- Out-of-range device lookups (#581)
System().getDevice(nr)throwsstd::out_of_rangefor an index past the end of the list instead of reading past it. An offline session enumerates no devices, so index 0 is already out of range there.yse_system_get_devicereturnsNULLwith the reason inyse_last_error().- MIDI device names without a backend (#585)
When the MIDI backend is not running (for example a headless Linux machine with no ALSA sequencer), the MIDI device-name getters return an empty string instead of
"Invalid Call".- Render threads (#650, #859–#862)
Rendering now runs on a task-graph scheduler. The number of render workers is public:
System().renderThreads(count)(yse_system_set_render_threads).-1(the default) means physical cores minus one, capped at 8;0renders everything on the audio thread;nstarts exactlynworkers. The interim cap of two automatic workers from #650 has been replaced by this policy. Set the count beforeinit()if you need a fixed number of threads, for example on constrained hardware. The mix is bit-identical at every worker count.channel::setVirtual(false)works (#864)setVirtual(false)used to reportfalsewhile the channel kept virtualising its sounds, so sounds pastmaxSoundsnever rendered. It now turns virtualisation off. If you called it and relied on the old (broken) behaviour to cap your voice count, remove the call.- Un-created and failed sounds (#579, #583)
Every method on a
YSE::soundthat has not been created, or whosecreate()failed, is now a no-op, and its queries answerfalse/0. In 2.4 they crashed.create()now also stores itsloopandvolumearguments on the interface, solooping()andvolume()report them straight away instead offalseand0. Code that readvolume() == 0aftercreate()as a sign of a fresh sound needs changing.- Clocks and clips can be destroyed in any order (#707, #974, #975)
Destroying a domain clock while a clip or a patcher is still bound to it no longer frees memory under them: the clock stops advancing and bound holders read a frozen beat. A clip that is still alive survives
System().close()and idles until you bind it again. Destroying a clip releases the notes it was playing, on its synths and on MIDI out. You no longer need to destroy clips beforeclose(), or stop a clip and wait a block before destroying it. See Clocks and clips.- Clocks advance per rendered block (#944)
Domain clocks and clip transports used to advance once per device callback, so with a device buffer larger than one engine block, beat events could fire up to one buffer early. They now advance per rendered block, as
renderOffline()always did. Beat-timed events land later and more precisely than before on large device buffers.- Random numbers (#410, #908)
The
rand()/srand()based helpers inutils/misc.hppwere replaced by a thread-safe generator that is safe on the audio thread. Signatures are unchanged, but:RandomF()and its range overloads return values in[0, 1)(half-open) instead of[0, 1].Random(max)withmax <= 0returns 0, andRandom(min, max)withmax <= minreturnsmin. Both used to divide by zero.Calling
srand()no longer affects the engine. UseYSE::Randomize()for a time-based seed, or the newYSE::RandomSeed(seed)for a fixed one. From C (or from any host that loads libYSE as a shared library) callyse_randomize()/yse_random_seed(), so the seed reaches the copy the engine draws from. Without either call the sequence starts from a fixed default seed.
- Engine time uses a wall clock (#667)
The engine’s update tick was measured with
std::clock(), which is processor time and ran fast under load. It now uses a monotonic wall clock. Doppler velocities are therefore correct instead of scaling with the number of busy threads, so doppler can sound different, most noticeably while the engine is busy.midiOut::Raw()sends what you give it (#748)midiOut::Raw(const std::string&)always sent exactly three bytes, padding shorter messages with zeros and truncating longer ones. It now sends the whole string. A newRaw(const unsigned char* data, std::size_t length)overload sends a byte span, andyse_midi_out_rawwraps it for C. Use these for two-byte messages (program change, channel pressure) and SysEx.- Enum count sentinels (#912)
The engine enums mirrored in the C API gained a trailing
*_COUNT_value (for exampleCT_COUNT_,OUT_TYPE_COUNT_). It is not a valid value. Aswitchover one of these enums without adefaultnow draws a-Wswitchwarning until it handles the sentinel.
C API#
The C ABI is not compatible with 2.4. Regenerate your bindings from the 3.0 headers. See C ABI (language bindings) for the conventions these changes follow.
- Renamed (#911)
2.4
3.0
yse_run_scriptyse_python_run_scriptyse_set_script_error_callbackyse_python_set_script_error_callbackyse_script_error_cbYseScriptErrorCallbackyse_bus_tap_cbYseBusTapCallbackyse_bus_sub_cbYseBusSubCallbackyse_free_stringmoved fromyse_patcher.htoyse_common.h. The symbol is the same, andyse_patcher.hstill includes it throughyse_common.h.- Return type changed to
YseStatus(#910) Function
2.4 returned
3.0 notes
yse_patcher_initvoidA negative
main_outputsis refused withYSE_ERR_INVALID_ARGUMENT.yse_patcher_parse_jsonvoidMalformed JSON returns
YSE_ERR_EXCEPTION; parsing beforeyse_patcher_initreturnsYSE_ERR_NOT_INITIALIZED.yse_clip_bindint(1 / 0)YSE_OKon success. Note that success used to be1and is now0.yse_system_create_clockint(1 / 0)YSE_OKon success, as above.Every call that returns
YSE_ERR_INVALID_ARGUMENTnow also sets a message foryse_last_error().yse_channel_get_namecopies into your buffer (#905)It used to return a
const char*into engine-owned storage. It is nowsize_t yse_channel_get_name(YseChannel* ch, char* buf, size_t cap), following the same snprintf-style contract as the other string getters.- New “nothing here” sentinels
Several getters used
0for “no answer”, where0is also a real answer:yse_phandle_get_idandyse_phandle_get_connection_targetreturnYSE_PATCHER_ID_NONE(0xFFFFFFFF) for aNULLhandle or an absent connection (#732). Object IDs now start at 0 in every patcher (see Patch files), so the first object is ID 0.yse_phandle_get_connection_target_inletreturnsYSE_PATCHER_INLET_NONE(0xFFFFFFFF) instead of inlet 0 for aNULLhandle, an absent outlet or an absent connection (#736). The C++pHandle::GetConnectionTargetInletreturnsUINT_MAXin the same cases.yse_device_get_id(NULL)returns-1(#666).
A binding that stores these in a signed 32-bit integer sees
-1.- Other contract changes
yse_system_get_devicereturnsNULLfor an out-of-range index (#581).C++ exceptions no longer escape any
extern "C"entry point; they are reported throughyse_last_error()and the function’s documented failure value (#901, #953).yse_synth_set_note_callbackalways calls your callback through its ownintsignature. Up to 256 distinct callbacks can be installed across the process; past that the call fails with ayse_last_error()message and leaves the previous callback in place (#899).
Patcher#
Bus and store addresses#
Every patcher-scoped address now carries the reserved patcher. prefix
(#894), next to the existing sound., channel. and synth.
namespaces. This covers .s / .r, .forward, the send
messages of .bag and .table, and the shared stores (.value,
.coll, .table, .dict, .array).
A host that subscribes to or publishes on <patcherName>.<slot> must
switch to patcher.<patcherName>.<slot>. Patchers with the same name
still share their slots, and an unnamed patcher still gets its own scope
through its patcher_<N> auto-name. A .s / .r name that would
make the full address longer than the bus allows is now refused
(#921, #922). See Talking to the host.
A .s value used to reach a .r in the same patcher twice once the
engine was running: once directly and once through the bus. It now arrives
once (#943).
Patch files#
- Object IDs are per patcher (#730, #733)
The IDs written by
DumpJSONused to come from a process-wide counter, so they depended on how many objects the process had created before. Each patcher now numbers its objects from 0, reusing the smallest free number, and saves them in ID order, so two identical patches save identically. Patches saved by 2.4 load unchanged and are renumbered on their next save. If you store object IDs outside the patch file, look them up again after loading.- The patcher name is saved (#897)
DumpJSONwrites a name you set withSetNameas a top-level"name"key. Loading applies it to a patcher that still has its auto-name; a name the host already set wins. Unnamed patches save exactly as before.ParseJSONadds, and rolls back on failure (#938, #939)ParseJSONhas always added the file’s objects to whatever the patcher already holds; the old documentation said it replaced them. CallClear()(yse_patcher_clear) first to replace. A load that fails part-way now removes the objects it had created instead of leaving them unwired in the patcher. See Patch file format.- Parameter strings (#935, #936)
Creation arguments are split on runs of spaces, tabs and line breaks; two spaces in a row no longer produce an empty argument. A live
SetParamsnow resets the parameters it leaves out to their defaults, as rebuilding the object always did, soSetParams("")resets all of them.
Patcher objects#
.route(#672, #679).routenow strips the matched selector and sends the rest, as in Max, and matches floats by value. If a patch relied on the selector arriving with the message, replace.routewith.routepass, which behaves as.routedid in 2.4.Input to
.route foo/.route 52.4 sent
3.0 sends
foo 1 2the list
foo 1 2the list
1 2foo 1the list
foo 1the int
1foothe list
fooa bang
the int
5into.route 5the int
5a bang
the float
5.0into.route 5no match
a bang
Unmatched messages still leave the rightmost outlet unchanged. A
.routewith no arguments now has Max’s0outlet plus the fall-through outlet, instead of no outlets at all..metro(#625, #711)A period change on the cold inlet or through its parameter now takes effect while the metro runs, keeping its phase; in 2.4 it only applied after a stop and start. The left inlet also accepts
bang(start, or restart in phase from now),stop, and a float (non-zero starts)..counter(#956)The start value argument now seeds the count, so
.counter -1sends 0 on its first bang. Inlet 1 sets the step, as documented, instead of the count. An int on inlet 0 sets the count but no longer changes the valueresetreturns to.~saw(#955)~sawoutputs a bipolar saw from -1 to 1, as documented. In 2.4 it sent a 0 to 1 ramp, which carried a DC offset.DSP::sawin C++ is still a 0 to 1 phasor.- Several
~dacobjects (#932) With more than one
~dacin a patcher, only the last one used to be heard, divided by the number of~dacobjects. They are now added together, as in Max and Pd. Lower the gain if a patch that used several~dacobjects is now louder.- Message boxes (#933)
.msends its text as the message it spells:.m 60sends the int 60 and.m bangsends a bang, instead of text that most objects ignored. A.mwired into.mtof,.iand similar objects now drives them. See Messages and dispatch..midioutbyte lists (#748).midioutaccepts a numeric byte list such as144 60 100, so it composes with.midiformatand.sxformat. A list with a value outside 0–255 is refused whole.~clipand~out(#436)~clipis now registered and can be created. TheOBJ::D_OUTconstant ("~out") was removed: no object ever implemented it.- Signal through subpatchers (#764)
~inletand~outletcarry audio through a subpatcher boundary. They share one pin numbering with.inlet/.outlet, soSubpatcherInlets/SubpatcherOutletscount both rates. See Subpatchers.
Where to go next#
Sessions and devices covers sample rates, devices and the offline session in full.
Patcher describes the patcher, which grew from 42 to over 300 objects in this release.
C ABI (language bindings) is the C API guide and reference.