Sessions and devices#

A session is everything between System().init() (or initOffline()) and System().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 what happens to MIDI devices. For which thread may make these calls, and what System().update() does each time you call it, see Threading model.

Starting a session#

There are two ways to start one:

Call

What you get

System().init()

Starts the audio backend and opens the platform’s default output device. The audio thread renders the mix. Returns false if no device opens.

System().initOffline()

The same engine, channel tree and DSP graph, but no audio backend and no audio thread. You render blocks yourself with System().renderOffline(blocks). Use it for tests, benchmarks and headless tools.

Both create the master channel and the five built-in channels (ambient, FX, music, GUI, voice), install a stereo speaker layout and set maxSounds to 50. Calling init() again while a session is running logs a message and returns true without doing anything.

init() returns true only when a stream on the default output device is running. It returns false when the audio backend fails to start, when there is no default output device, or when the device refuses to open or start the stream (for example because another application holds it exclusively). The reason is logged. A failed init() leaves no session behind: you can call init() again, or initOffline() to run the engine without a device. 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’s side of every sound and channel. You can call init() again afterwards to start a new session. The requested sample rate and the renderThreads() setting carry over to it.

Sample rate#

The sample rate is fixed for a whole session. To choose it, call System().requestSampleRate(rate) before init() or initOffline():

YSE::System().requestSampleRate(44100);
YSE::System().init();
double rate = YSE::System().getSampleRate();   // what the device agreed to
  • With a device, the engine asks the device for the requested rate. If the device refuses it, the engine logs a warning and opens at the device’s own default rate instead. This is not an error. getSampleRate() reports the rate the session really runs at.

  • Without a request (or after requestSampleRate(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, or at 48 kHz if you did not request one.

A request made during a session is only stored for the next one. To change the rate of a running engine, call close() and then init(). The request survives close(), so you can set it once for repeated sessions.

Two getters report the rate:

  • getSampleRate() is the session rate. It stays the same through pause() and resume(), and is 0 before init(). Use it for anything you count in samples.

  • getActiveSampleRate() is the rate of the device that is open right now. It is 0 while no device is open: before init(), after pause() or close(), and in an offline session.

Offline sessions#

System().renderOffline(blocks) runs the same work the audio thread does (applying queued changes, rendering the channel tree, mixing, reverb) for blocks × 128 samples, on the thread that calls it. The rendered audio is discarded. Call System().update() between renders as you would in a device session: without it, new sounds never start.

YSE::System().requestSampleRate(48000);
YSE::System().initOffline();
YSE::System().setChannelConfiguration(YSE::CT_51, 6);   // optional

// ... create sounds, channels, patchers ...

for (int i = 0; i < 100; ++i) {
    YSE::System().update();
    YSE::System().renderOffline(1);
}
YSE::System().close();

Speaker layout. An offline session starts with the stereo layout. Change it with System().setChannelConfiguration(conf, outputs), called after initOffline() (initialisation installs stereo itself and overwrites an earlier call). The next rendered block switches to the new layout. outputs must be at least 1; a zero-output layout is refused with a log line. CT_CUSTOM creates outputs speakers whose positions you set yourself. In a device session this call configures the mixer without asking the device, so use openDevice() there instead.

An offline session stays offline. resume() restarts the device a session already had. An offline session has none, so resume() does nothing there, and autoReconnect() cannot open a device on it either.

The one way to give an offline session a device is openDevice() (see below). When it succeeds, the session becomes a device session: pause() and resume() work on it, and an audio thread now renders the mix. Stop calling renderOffline() after that.

On desktop this needs the audio backend (PortAudio) to be running. initOffline() does not start it, so that headless machines never probe audio hardware, and openDevice() does not start it on demand either. Call System().refreshDevices() first: it starts the backend and lists the devices without opening one (see Choosing an audio device). Without it, a process that has only ever run offline sessions has an empty device list, and openDevice() returns false with a log line.

YSE::System().initOffline();
YSE::System().refreshDevices();
// pick a device from getDevice(i), then openDevice(setup, ...)

The stream opens at the offline session’s rate: the requested one, or 48 kHz. If the device refuses that rate, openDevice() logs a warning naming both rates and returns false. To be safe, request the device’s rate (for example getDevice(i).getAvailableSampleRate(0)) before initOffline().

Choosing an audio device#

init() opens the default output device. To use a different one, list the devices after init() and open one with openDevice():

YSE::System().init();

const YSE::device* chosen = nullptr;
for (unsigned int i = 0; i < YSE::System().getNumDevices(); ++i) {
    const YSE::device& d = YSE::System().getDevice(i);
    if (d.getNumOutputChannelNames() >= 6) { chosen = &d; break; }
}

if (chosen != nullptr) {
    YSE::deviceSetup setup;
    setup.setOutput(*chosen).setBufferSize(0);   // 0: let the backend choose
    if (!YSE::System().openDevice(setup, YSE::CT_AUTO)) {
        // Refused and logged. The previous device is still playing.
    }
}

Listing devices.

  • getNumDevices() and getDevice(i) work in every build. getDevices() returns the whole std::vector and is only for builds that link libYSE statically.

  • getDevice(i) throws std::out_of_range for an index at or past getNumDevices(). The list is empty in an offline session until refreshDevices() fills it, and on a machine with no audio hardware, so even index 0 can be out of range.

  • The indexed getters on YSE::device (getOutputChannelName, getAvailableSampleRate, getAvailableBufferSize and the input equivalent) throw std::out_of_range in the same way.

  • getDefaultDevice() and getDefaultHost() name the platform default device and its host API (WASAPI, ALSA, …).

Listing devices without opening one. init() fills the device list as part of opening the default device, which may be the wrong device, held by another application, or at the wrong rate. System().refreshDevices() fills it without opening anything, so you can find a stored device first and then open exactly that one. It works before init(), in an offline session and in a running device session:

if (!YSE::System().refreshDevices()) {
    // The audio backend could not start. The list is empty; the reason
    // is logged.
}
for (unsigned int i = 0; i < YSE::System().getNumDevices(); ++i) {
    const YSE::device& d = YSE::System().getDevice(i);
    if (d.getName() == storedName && d.getTypeName() == storedHost) { /* found */ }
}
  • It never touches a stream that is open. With no stream open, the backend restarts so that devices plugged in or removed since it started show up. With a stream open, PortAudio cannot re-scan, so a new device appears only after you close the device and refresh again.

  • The refresh replaces the list. YSE::device references and deviceSetup objects built from the old list are invalid afterwards, and an index can now name a different device. Match devices by name and host, not by index.

  • On a bare headless Linux machine, starting PortAudio can hang, so headless tools should not call it.

Special values on a device.

  • getID() is the backend’s index for the device, or -1 when no device has been assigned. -1 is what a YSE::device you construct yourself reports. Opening such a device is refused with a log line. Every device from getDevice() has a real index, 0 or higher.

  • getDefaultBufferSize() is 0 when the host does not advertise one, which on desktop is every device. 0 means unspecified, not zero frames. Passed to deviceSetup::setBufferSize() it lets the backend pick the size. After the device is open, System().getActiveBufferSize() reports the size it chose.

Opening the device. openDevice(setup, conf) returns true when a stream is running on the new device. It returns false, logs the reason, and leaves the running stream and the speaker layout alone when:

  • the setup has no output device;

  • the device ID does not exist on this system (for example -1);

  • the backend could not open or start the stream;

  • the backend was never started (see Offline sessions).

On success the speaker layout follows the device that opened: conf is applied with that device’s number of output channels. CT_AUTO derives the layout from the channel count. A layout with a different number of outputs makes the next audio block reallocate the mix buffers, so switch devices at setup, not while something audible plays.

The new stream runs at the session’s sample rate, even when that is not the new device’s default rate. A device that refuses it is logged and openDevice() returns false. The session rate cannot change while the session runs, so deviceSetup::setSampleRate() only matters when it differs from it: openDevice() then logs a warning that names the requested rate and opens the stream at the session rate anyway. Leave it at 0 (the default) or at the session rate, and choose the rate with requestSampleRate() before init().

On Android there is a single device (“Android Audio”, stereo, through Oboe). openDevice() does not switch anything there. It only applies the speaker layout.

Live device state. getActiveSampleRate(), getActiveBufferSize() (frames per device callback, which is not the engine’s 128-sample block) and getActiveOutputLatency() (in samples) describe the open device and return 0 while none is open. cpuLoad() is the audio callback’s run time as a fraction of the buffer it produces. 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 and any other value means it is not.

A non-zero value does not always mean something is wrong. A stream that has just started (by init(), resume() or openDevice()) takes some time, typically tens of milliseconds, to deliver its first callback. To wait for a device to come up, poll until the count is 0:

YSE::System().init();
for (int i = 0; i < 100 && YSE::System().missedCallbacks() != 0; ++i) {
    YSE::System().update();
    YSE::System().sleep(10);
}

A value that keeps climbing means the device has gone away or the audio thread is starved.

Reconnecting automatically. System().autoReconnect(on, delay) makes update() restart the output stream when the device has delivered nothing for delay milliseconds. The restart reopens the device the session had: after openDevice(), that device, found again by name and host, with the channel count and buffer size you opened it with; otherwise the default device as it is at that moment, which covers cases like unplugged headphones. While the device stays unavailable, the engine tries again every delay milliseconds. A negative delay counts as 0.

autoReconnect(on, delay, fallbackToDefault, maxAttempts) adds two choices. With fallbackToDefault set, a device that cannot be reopened is replaced by the default output device rather than retried; without it (the default) the session never moves to another device by itself. A positive maxAttempts stops the watchdog after that many failed attempts in a row.

To see what happened, read reconnectState() (RS_RESTORED, RS_FELL_BACK_TO_DEFAULT, RS_RECONNECTING, RS_FAILED, or RS_NONE before the first attempt), reconnectCount(), and getActiveDevice() / getActiveHost() for the device that is actually playing. On desktop a device that was unplugged and plugged back in may only be found again after refreshDevices(), which you can call while the state reads RS_RECONNECTING.

  • A stream that has just started and has not delivered its first callback yet gets at least half a second before the watchdog acts, whatever the delay. A small delay therefore cannot tear down a device that is still starting.

  • Up to and including v2.4.0, delay counted update() calls, not milliseconds. If you passed a number of frames, convert it: at 60 frames per second, 120 frames is 2000 ms.

  • The watchdog runs inside update(). If you stop calling update(), nothing reconnects.

Each init() turns autoReconnect off again, so enable it after init().

System().pause() closes the stream and resume() opens it again. Changes you make while paused are queued and applied, in order, on resume().

MIDI devices#

MIDI device I/O exists only in builds with YSE_ENABLE_MIDI_DEVICE (on by default on Windows and Linux, off on Android). Without it, the functions below are not declared, so guard your code with #if YSE_ENABLE_MIDI_DEVICE.

  • System().getNumMidiInDevices() and getNumMidiOutDevices() count the ports. They return 0 when the MIDI backend could not start.

  • getMidiInDeviceName(id) and getMidiOutDeviceName(id) return an empty string for an ID at or past the count, and for every ID when the MIDI backend is down. Do not treat an empty name as a device.

  • Open a port by index with YSE::midiIn::create(port) or YSE::midiOut::create(port). midiIn callbacks run on RtMidi’s input thread, not on your control thread (see Threading model).

Where to go next#

  • Threading model: the threads, what update() drives, and which calls belong on the control thread.

  • Core and Audio devices: the reference for system, device and deviceSetup.

  • MIDI: midiIn and midiOut.