Search guides and the APICtrl K
  1. API reference
  2. System, devices and network

audio_workgroup.h

Audio workgroups: putting a helper thread under the audio deadline.

#include <sonore/audio_workgroup.h>
On this page

Design notes

What a workgroup is

From macOS 11 every audio device's IO thread belongs to an os_workgroup, and the scheduler budgets the WHOLE group against that device's deadline. A helper thread outside it -- a voice renderer, a convolution partition -- does not count toward the deadline at all: it competes with the IO thread rather than being scheduled beside it, and on Apple silicon it can sit on an efficiency core at the moment the buffer is due. Joining puts it in the group. That is the whole feature, and it only matters to a DSP that renders one block across several threads.

Where a workgroup comes from

standalone   kAudioDevicePropertyIOThreadOSWorkgroup on the device the
             standalone opened (CoreAudioOutput::ioWorkgroup).
AU plugin    kAudioUnitProperty_RenderContextObserver: the plugin hands
             the OS a block, and the OS calls it with the render context
             -- workgroup inside -- whenever the render thread's context
             changes (au_wrapper.h).
CLAP, VST3   NO API. Neither format tells a plugin which workgroup its
and LV2      host's audio thread is in, so a DSP built as one of them is
             never handed a workgroup and its helper threads run outside
             the deadline. Said here rather than papered over: there is
             no call that would find it.

A DSP is told by declaring void workgroupChanged(const sonore::AudioWorkgroup&) -- detected like every other optional hook -- and typically hands it to a realtime ThreadPool (thread_pool.h), whose workers join it themselves.

The token

os_workgroup_join makes the CALLING thread a member and fills in a token; os_workgroup_leave, on the same thread, with the same token, ends it. WorkgroupToken is that token, and it leaves when it is destroyed -- so it must be destroyed on the thread that joined, which is where a worker keeps it: on its own stack. On a platform with no workgroups a join fails, the token is inactive, and describe() says why. Nothing pretends to have happened.

Interval and period hints

os_workgroup_interval_start/update/finish belong to the workgroup's OWNER: for a device's group that is CoreAudio's IO thread, which marks each cycle itself. A thread that joins does not call them -- what it owes the group is to be a real-time thread whose period matches the cycle. That is RealtimeThreadHints: Mach's time-constraint policy (period, computation, constraint), derived from the block length and the sample rate.

Resolved at run time

os_workgroup_* exist from macOS 11, and os_retain/os_release are spelled as Objective-C messages when the including file is Objective-C. Both problems go away by finding the six functions with dlsym: no availability checks, no SDK-version requirement, no header that changes meaning under ARC, and a Mac older than 11 gets an AudioWorkgroup that says so instead of a crash. The table is resolved the first time anything asks -- call AudioWorkgroup::isSupported() once off the audio thread, as the AU wrapper and the realtime pool both do, because dlsym takes a lock.

Compiled, never run

There is no Mac in this loop and macOS CI is not running. This file is compiled for macOS by MSVC against tests/au_shim (tests/mac_syntax_mac_host.cpp), which proves every name and call is consistent and nothing about whether Apple agrees. tests/mac_host_selftest.cpp is the program a Mac runs to find out: it joins the default output device's workgroup from a real-time thread and prints what each call returned.

Types 3

How a real-time thread asks the scheduler for time.

Mach's THREAD_TIME_CONSTRAINT_POLICY in milliseconds: every periodMs the thread needs computationMs of CPU, finished within constraintMs of the period starting. For audio the period is one block. The kernel refuses a computation under 50 microseconds or over 50 ms, and a constraint shorter than the computation; realtimePolicyTicks() clamps into that rather than letting thread_policy_set fail on a tiny block.

An audio workgroup: on macOS an os_workgroup_t, held by reference.

Copying retains and destroying releases -- both atomic counts, no allocation, so a copy is safe to make on the audio thread. An empty one (the default, and the only kind on other platforms) is false, joins nothing, and says why.

One thread's membership of a workgroup.

Filled by AudioWorkgroup::join() and ended by leave() or the destructor -- on the thread that joined, because that is the only thread the OS lets end it. A leave from any other thread is refused and reported rather than attempted. Neither copyable nor movable: it is the thread's, and it stays where that thread put it.

Functions 2

Macros 1

↑↓ to move↵ to openEvery public name in the headers, and every guide