Sessions and devices#
A session is everything between System.instance.init() (or
initOffline()) and System.instance.close(). This page covers how a
session starts, which sample rate it runs at, how to pick and switch audio
devices, how to tell whether audio is flowing, and how the engine
reconnects a device that went away. Upstream’s
Sessions and devices is the
C++ version of this page.
Every call on this page belongs on the isolate that called init(); see
Mental model.
Starting a session#
Call |
What you get |
|---|---|
|
Starts the audio backend and opens the platform’s default output
device. The audio thread renders the mix. Throws
|
|
The same engine, channel tree and DSP graph, but no audio backend and
no audio thread. You render blocks yourself with
|
init() succeeds only when a stream on the default output device is
running. It throws when the backend fails to start, when there is no
default output device, or when the device refuses to open or start the
stream (another application holds it exclusively, say). The reason is in
the engine log (Log). A failed init() leaves no session
behind, so the usual pattern for a host that must also run without audio
hardware is:
final sys = System.instance;
try {
sys.init();
} on YseException catch (e) {
print('No audio device ($e); running offline');
sys.initOffline();
}
Up to libYSE 2.4 (package:yse 0.1.0) init() returned normally with
nothing open. A stream that has just started can still take a moment to
deliver audio; see Is audio flowing?.
System.close() ends the session: it closes the device, stops the
engine threads and frees the engine side of every sound and channel. It
also removes the System.occlusion callback, closes every
Bus subscription and tap stream, closes every
Patcher.messages stream and forgets every
Channel.busName. You can init() again afterwards. The
requested sample rate and the System.renderThreads setting carry
over to the next session.
Sample rate#
The sample rate is fixed for a whole session. Choose it with
System.requestedSampleRate before init() /
initOffline():
sys.requestedSampleRate = 44100;
sys.init();
print(sys.sessionSampleRate); // what the device agreed to
With a device, the engine asks the device for the requested rate. A device that refuses it is a log line and the device’s own rate instead, never an error.
System.sessionSampleRatereports what the session really runs at.Without a request (or after
requestedSampleRate = 0),init()opens the device at its default rate.Offline, there is no device to ask, so the session runs at exactly the requested rate. With no request it keeps the engine’s current rate: 48 kHz in a fresh process (libYSE 2.4 used 44.1 kHz), but the previous session’s rate in a process that already ran one (upstream #1033). Request a rate whenever an offline render must run at a known one.
A request made during a session is only stored for the next one; to change
a running engine’s rate, close() and init again. The request survives
close(). DeviceSetup.sampleRate cannot change a running
session’s rate either.
Two getters report the rate:
System.sessionSampleRateis the session rate. It stays the same throughpause()/resume()and is 0 beforeinit(). Use it for anything you count in samples.System.activeSampleRateis the rate of the device that is open right now: 0 beforeinit(), afterpause()orclose(), and in an offline session.
Offline sessions#
System.renderOffline() runs the work the audio thread would do
for blocks * System.blockSize frames (128 per block), on the calling
thread; the rendered audio is discarded (measure it with the channel peak
meters). Call update() between renders as in a device session, or new
sounds never start:
sys.requestedSampleRate = 48000;
sys.initOffline();
sys.setChannelConfiguration(ChannelType.surround51, 6); // optional
// ... create sounds, channels, patchers ...
for (var i = 0; i < 100; i++) {
sys.update();
sys.renderOffline(1);
}
sys.close();
Speaker layout. An offline session starts stereo. Change it with
System.setChannelConfiguration(), called after
initOffline() (init installs stereo itself and overwrites an earlier
call). outputs must be at least 1 (ArgumentError otherwise), and
ChannelType.custom creates outputs speakers whose positions you set
yourself. The new layout is applied by the next block that mixes a
sound: with no sound alive the engine skips the mix and the resize with
it, so Channel.master.numOutputs reports the old count until a sound
exists (upstream #1033). In a device session use openDevice()
instead, which derives the layout from the device.
An offline session stays offline. resume() restarts the device a
session already had; an offline session has none, so resume() is a
no-op there and the auto-reconnect watchdog never opens one either. The
one way to give an offline session a device is openDevice(). On
desktop that needs the audio backend (PortAudio), which initOffline()
does not start, so call System.refreshDevices() first:
sys.initOffline();
if (sys.refreshDevices()) {
// pick a Device from sys.devices, then sys.openDevice(setup)
}
When openDevice() succeeds the session becomes a device session: an
audio thread now renders the mix, so stop calling renderOffline().
The stream opens at the offline session’s rate; a device that refuses it
makes openDevice() throw. Request the device’s rate before
initOffline() to be safe.
Choosing an audio device#
init() opens the default output device. To use another one, list the
devices and open one with System.openDevice():
final chosen = sys.devices
.where((d) => d.outputChannelNames.length >= 6)
.firstOrNull;
if (chosen != null) {
final setup = DeviceSetup()
..output = chosen
..bufferSize = 0; // 0: let the backend choose
try {
sys.openDevice(setup, layout: ChannelType.auto);
} on YseException {
// Refused and logged; the previous device is still playing.
}
}
Listing devices without opening one. init() fills
System.devices as a side effect of opening the default device,
which may be the wrong device, held by another application, or at the
wrong rate. System.refreshDevices() fills the list without
opening anything, so you can find a stored device first and open exactly
that one. It works before init(), in an offline session and in a
running device session:
if (!sys.refreshDevices()) {
// The audio backend could not start; the list is empty and the
// reason is in the engine log.
}
final stored = sys.devices.where(
(d) => d.name == storedName && d.hostName == storedHost,
);
It returns
false(with an empty list) when the backend cannot start, and throws only on an unexpected engine error.It never touches an open stream. With no stream open the backend restarts, so devices plugged in or removed since show up. With a stream open, PortAudio cannot re-scan, so a new device appears only after you
closeCurrentDevice()(orclose()) and refresh again.The refresh replaces the list:
Devicedescriptors andDeviceSetups built from the old list are invalid afterwards, and an index may now name another device. Match byDevice.nameandDevice.hostName, never by index.defaultDevice/defaultHostare re-read too.An offline session lists nothing until you refresh, and a machine with no audio hardware may list nothing at all.
On a bare headless Linux machine starting PortAudio can hang, so headless tools that never need a device should not call it.
Special values. Device.id is -1 for a descriptor with no
device behind it, and openDevice() refuses it; every device in
devices has a real index. Device.defaultBufferSize is 0 when
the host does not advertise one (every desktop device): 0 means
unspecified, and passed as DeviceSetup.bufferSize lets the backend
choose. After opening, System.activeBufferSize reports the size
it chose.
Opening the device. openDevice(setup, layout:) returns when a
stream is running on the new device. It throws YseException
(YSE_ERR_AUDIO_DEVICE), and leaves the running stream and the speaker
layout alone, when:
the setup has no output device;
the device’s ID is -1 or no longer resolves;
the backend could not open or start the stream (including a device that refuses the session rate);
the backend was never started (see Offline sessions).
On success the speaker layout follows the device: layout is applied
with that device’s output-channel count, and ChannelType.auto derives
the layout from the count. A different number of outputs makes the next
block reallocate the mix buffers, so switch devices at setup, not while
something audible plays.
On Android there is a single device (“Android Audio”, stereo, through
Oboe); openDevice() only applies the speaker layout there.
Live device state. System.activeSampleRate,
System.activeBufferSize (frames per device callback, which is
not the engine’s 128-frame block) and System.activeOutputLatency
(samples) describe the open device and are 0 while none is open.
System.activeDevice / System.activeHost name it
('' when none is). System.cpuLoad is the audio callback’s
run time as a fraction of its buffer; near 1.0, dropouts are likely.
Is audio flowing?#
System.missedCallbacks counts the update() calls in a row
during which the device delivered no audio. It goes back to 0 at the first
update() that sees a callback, so 0 means audio is flowing.
A stream that has just started (by init(), resume() or
openDevice()) reads non-zero until its first callback lands, typically
tens of milliseconds. To wait for a device to come up, poll until it is 0:
sys.init();
for (var i = 0; i < 100 && sys.missedCallbacks != 0; i++) {
sys.update();
sys.sleep(10);
}
A value that keeps climbing means the device has gone away or the audio thread is starved. An offline session only ever climbs.
Reconnecting automatically#
System.setAutoReconnect() makes update() restart the output
stream when the device has delivered nothing for delayMs
milliseconds:
sys.setAutoReconnect(
on: true,
delayMs: 1000,
fallbackToDefault: false,
maxAttempts: 30,
);
Which device. The one the session had: after
openDevice(), that device, found again by name and host, with the channel count and buffer size it was opened with. A session that never calledopenDevice()reopens whatever the default device is at that moment, which covers unplugged headphones. Up to libYSE 3.0 every attempt opened the default device.Fallback. When the session’s device cannot be reopened, nothing is opened and the watchdog tries again
delayMslater. WithfallbackToDefault: truethe default output device is opened instead (narrowing the layout if it has fewer outputs); the session does not move back by itself when its own device returns.Attempt limit. A positive
maxAttemptsstops after that many failed attempts in a row; 0 (the default) retries forever.Timing. A stream that has just started gets at least half a second before the watchdog acts, whatever the delay. Negative delays count as 0. Up to libYSE 2.4
delayMscountedupdate()calls: at 60 updates per second, a value of 120 meant 2000 ms.The watchdog runs inside
update(). Stop callingupdate()and nothing reconnects.System.missedCallbacksitself is not acted on; the watchdog keeps its own wall-clock timing.
The watchdog has no callback; poll its outcome:
System.reconnectState— aReconnectState:none(nothing happened yet),reconnecting,restored,fellBackToDefaultorfailed(gave up aftermaxAttempts). It keeps the last attempt’s outcome and resets tononeon init, on a successfulopenDevice()and on everysetAutoReconnectcall, which also re-arms a watchdog that gave up.System.reconnectCount— attempts made this session, so you can notice a reconnect you did not see happen.System.activeDevice/System.activeHost— the device actually playing, e.g. afterfellBackToDefault.
On desktop a device unplugged and plugged back in may only be found again
after System.refreshDevices(), which you can call while the state
reads reconnecting (no stream is open then):
// in the host's update loop
if (sys.reconnectState == ReconnectState.reconnecting) {
sys.refreshDevices();
}
pause() closes the stream and resume() opens the same device again;
resume() never falls back to another device.
MIDI devices#
MIDI device I/O exists in builds with YSE_ENABLE_MIDI_DEVICE, which is
on by default on Windows and Linux and off on Android (and macOS). Without
it the counts below are 0.
System.midiInDeviceCount/System.midiOutDeviceCountcount the ports; 0 when the MIDI backend could not start.System.midiInDeviceName()/System.midiOutDeviceName()return''for an index with no port, and for every index when the backend is down. A non-empty name always means a real port.Open a port by index with
MidiOut.open()orMidiIn.open().MidiIncallbacks arrive on RtMidi’s input thread and are bridged to your isolate as streams.
Where to go next#
Mental model — the threads and what
update()drives.Core and Devices — the reference for
System,DeviceandDeviceSetup.Upgrading to package:yse 0.2.0 (libYSE 3.1) — everything that changed since libYSE 2.4.