au_wrapper.h
The Audio Unit (AUv2) format wrapper, for macOS.
#include <sonore/au_wrapper.h>- 2types
- 50other declarations
- Guides: Getting started, Platforms and formats
On this page
Design notes
Compiled and validated on macOS since 2026-09-01: the SDK workflow (.github/workflows/sdk.yml, a macos-14 runner) builds every AU here and Apple's auval passes all six examples with zero warnings: instantiation, parameters, render, state, bypass, latency, each through the selectors below. It took nine rounds of fixes to get there, and every one of them is now a comment in this file: a wrapper that has never been run is not "probably fine", it is unmeasured. What auval does NOT open is the CocoaUI view (au_view.h), so the editor path is compiled but still unrun.
Written against the C AudioComponent API rather than Apple's AUBase C++ classes, because AUBase was removed from modern Xcode and dragging in a copy would re-create the framework dependency this SDK exists to avoid. A modern AUv2 is a factory function returning an AudioComponentPlugInInterface whose Lookup() hands back an implementation per selector: plain C function pointers, which is the same shape CLAP and VST3 already use here.
Include it AFTER clap_wrapper.h: that header owns the shared machinery (the DSP instance, parameters, state, the webview bridge) and this one adapts it, so one source file builds .clap, .vst3 and .component without drift.
Types 2
One Audio Unit instance: the shared Instance every format drives, plus what AUv2 adds on top -- the host's handle, the pull-model input sources, the property and render listeners, the host's transport callbacks and the channel widths the host declared per bus.
Built by Open inside the block the factory allocates and destroyed by Close; always reached through unitOf(), never by casting the dispatch pointer (see there for why). Render runs on the host's render thread and reads only the fixed-capacity lists here; everything that grows is sized at initialize().
The block OUR factory allocates. Apple's public headers stopped shipping AudioComponentPlugInInstance with the classic CoreAudio SDK (macOS CI's first compile of this file proved it) -- but the only ABI the system cares about is that the returned pointer's first bytes are the AudioComponentPlugInInterface. The rest of the block is ours; the layout mirrors Apple's so the hard-won addressing rule stays true: the instance storage begins AT the mInstanceStorage field.
Functions 48
- fn
inline UInt32 elementCountFor(AudioUnitScope scope)How many buses a scope has. ONE definition, because the element count a host reads and the elements a host may then address have to be the same answer -- reporting two inputs and refusing input 1 is a contract a host cannot work with.au::elementCountFor · au_wrapper.h:73 - fn
inline bool validScope(AudioUnitScope scope, AudioUnitElement element)AU's scope/element pair addresses buses. Anything past the counts above is a request we cannot honour and must decline rather than ignore.au::validScope · au_wrapper.h:86 - fn
inline bool globalOnly(AudioUnitScope scope, AudioUnitElement element)Most of what an AU publishes belongs to the unit, not to a bus: latency, tail, bypass, presets, the editor. Answering for Input/0 as well is not harmless breadth -- a host that believes a per-bus latency exists will ask for it per bus and get the same number back, and auval says so plainly: "returning valid information for scope/element for [Input/0], which should be invalid".au::globalOnly · au_wrapper.h:96 - fn
inline Unit* unitOf(void* self)THE ONE WAY to reach the instance from a dispatched call. Apple's dispatch doesau::unitOf · au_wrapper.h:308(AUBase*)&ACPI->mInstanceStorage, and so must every one of our methods:selfpoints at the BLOCK, and the Unit lives at an offset inside it. Open constructed it there and every other method used a bare(Unit*) self-- forty bytes early, on top of the interface pointers. auval is what finally said so out loud: the sample rate read 0 Hz, the current preset came back nameless and numbered 0, and the render test aborted, all of which are one object being written at one address and read at another. Global-scope parameters passed throughout, because those answers come from the static descriptor and never touch the instance -- which is exactly why a wrapper can look healthy while pointing at the wrong memory. - fn
inline void renderContextChanged(void* block, const void* context)[audio-thread] The OS's render-context observer: the render thread joined, left or changed its workgroup, and the DSP is told before the render that follows. Retaining the new workgroup and releasing the old one are atomic counts -- nothing here allocates or waits, which a DSP's workgroupChanged() must not do either (ThreadPool::offerAudioWorkgroup is built for it).au::renderContextChanged · au_wrapper.h:320 - fn
inline void notifyListeners(Unit* unit, AudioUnitPropertyID property)au::notifyListeners · au_wrapper.h:336 - fn
inline void notifyAllParametersChanged(Unit* unit)[main-thread] Every parameter just moved at once -- a preset, a restored session -- and the host wrote none of them. Its sliders, automation lanes and generic editor keep showing the old values until told, and AU says "all of them" with one notification on the wildcard id.au::notifyAllParametersChanged · au_wrapper.h:348 - fn
inline void notifyHostOfChanges(Unit* unit)[main-thread] Tell the host what render could only record.au::notifyHostOfChanges · au_wrapper.h:377More
The latency: a DSP whose look-ahead or oversampling is a switch moves it, and a host re-reads kAudioUnitProperty_Latency and redoes its delay compensation only when a listener on it fires. This unit never fired one, so everything in parallel with it stayed early for the rest of the session.
State no parameter carries: kAudioUnitProperty_ClassInfo firing is how an AU says its saved state is no longer what the host last saved, so the document is marked edited and a quit asks first. Without it a sample loaded in the editor was gone at the next open of a project the host thought unchanged.
And the output guard's report, which AU has no host log for: stderr, once.
AU has no way to ask to be called on the main thread, so the editor's clock delivers these, and so do the main-thread property setters that can move the latency, and Uninitialize. Never render.
- fn
inline void announceOwnStateLoad(Unit* unit)[main-thread] The plugin changed its own state -- a preset from its editor's preset bar, an undo. Every parameter moved and the host wrote none of them: the wildcard parameter notification, as for a restored session; then ClassInfo, which is how an AU says its document changed, and the latency if the preset moved it.au::announceOwnStateLoad · au_wrapper.h:397 - fn
inline void announcePresetSelected(Unit* unit)[main-thread] The preset the editor loaded becomes the unit's present preset: a host's preset menu shows its name, as it does a factory one's, numbered -1 when it is the user's own.au::announcePresetSelected · au_wrapper.h:407 - fn
inline AudioUnitParameterUnit unitFor(const ParamInfo& p)au::unitFor · au_wrapper.h:423 - fn
inline OSStatus getParameter(void* self, AudioUnitParameterID id, AudioUnitScope scope, AudioUnitElement element, AudioUnitParameterValue* value)au::getParameter · au_wrapper.h:433 - fn
inline OSStatus setParameter(void* self, AudioUnitParameterID id, AudioUnitScope scope, AudioUnitElement element, AudioUnitParameterValue value, UInt32)au::setParameter · au_wrapper.h:442 - fn
inline OSStatus scheduleParameters(void* self, const AudioUnitParameterEvent* events, UInt32 numEvents)au::scheduleParameters · au_wrapper.h:451 - fn
inline const char* auViewFactoryClassName()The Cocoa view factory's class name, unique to THIS binary.au::auViewFactoryClassName · au_wrapper.h:482More
The name is handed to the host through kAudioUnitProperty_CocoaUI and registered with the Objective-C runtime by au_view.h; both read it here so they cannot disagree. Unique per image, because the runtime has one class namespace per process and two plugins built from different SDK versions would otherwise share one class whose methods live in whichever binary registered first -- see cocoa::uniqueClassName.
- fn
inline void fillStreamFormat(AudioStreamBasicDescription* format, double sampleRate, UInt32 channels)au::fillStreamFormat · au_wrapper.h:492 - fn
inline OSStatus getPropertyInfo(void* self, AudioUnitPropertyID property, AudioUnitScope scope, AudioUnitElement element, UInt32* outSize, Boolean* outWritable)au::getPropertyInfo · au_wrapper.h:507 - fn
inline CFPropertyListRef buildClassInfo(Unit* unit)The plugin's state as a CFDictionary, which is what AU persists. The bytes inside are the SAME versionedau::buildClassInfo · au_wrapper.h:681SNRSblob the other formats write, so one product has one state format however it was loaded. - fn
inline OSStatus applyClassInfo(Unit* unit, CFPropertyListRef plist)au::applyClassInfo · au_wrapper.h:727 - fn
inline OSStatus getPropertyBody(void* self, AudioUnitPropertyID property, AudioUnitScope scope, AudioUnitElement element, void* outData)Fills the buffer. The size check is its CALLER's job -- see getProperty.au::getPropertyBody · au_wrapper.h:793 - fn
inline OSStatus getProperty(void* self, AudioUnitPropertyID property, AudioUnitScope scope, AudioUnitElement element, void* outData, UInt32* ioDataSize)kAudioUnitGetPropertySelect. The size argument is not decoration: the contract is Apple's own DoGetProperty -- ask what the property actually needs, REFUSE a buffer that cannot hold it, and report the real size back on success. Ours took five arguments where the selector passes six, so ioDataSize was never read and never written: a host asking for eight bytes of a sixteen-byte property was handed sixteen and told it had asked correctly. auval calls that "GetProperty() call accepts & returns bad data size", and it said it about Latency, Tail Time and Bypass Effect alike -- one defect, three symptoms.au::getProperty · au_wrapper.h:1070 - fn
inline OSStatus setProperty(void* self, AudioUnitPropertyID property, AudioUnitScope scope, AudioUnitElement element, const void* inData, UInt32 size)au::setProperty · au_wrapper.h:1089 - fn
inline OSStatus addPropertyListener(void* self, AudioUnitPropertyID property, AudioUnitPropertyListenerProc proc, void* userData)au::addPropertyListener · au_wrapper.h:1343 - fn
inline OSStatus removeListeners(Unit* unit, AudioUnitPropertyID property, AudioUnitPropertyListenerProc proc, void* userData, bool userDataGiven)Every listener with this property and proc -- and this user data, when the host gave one -- as AUBase's RemovePropertyListener removes them, in order. It removed the first match only, and the call without user data matched only a null one, which no host registers: a host that took its listener away that way, or had added it twice, was still called after it had freed what the user data pointed at.au::removeListeners · au_wrapper.h:1357 - fn
inline OSStatus removePropertyListenerWithUserData(void* self, AudioUnitPropertyID property, AudioUnitPropertyListenerProc proc, void* userData)au::removePropertyListenerWithUserData · au_wrapper.h:1371 - fn
inline OSStatus removePropertyListener(void* self, AudioUnitPropertyID property, AudioUnitPropertyListenerProc proc)The pre-10.5 call, which names no user data: AUBase takes that as "any".au::removePropertyListener · au_wrapper.h:1378 - fn
template <typename Edit> inline void editRenderNotifies(Unit* unit, Edit&& edit)An edit of the render-notify list, as one change render sees whole or not at all (see takeRenderNotifies). The host's calls, never render's.au::editRenderNotifies · au_wrapper.h:1386 - fn
inline OSStatus addRenderNotify(void* self, AURenderCallback proc, void* userData)au::addRenderNotify · au_wrapper.h:1394 - fn
inline OSStatus removeRenderNotify(void* self, AURenderCallback proc, void* userData)au::removeRenderNotify · au_wrapper.h:1411 - fn
inline void takeRenderNotifies(Unit* unit)[audio-thread] The host's list as this render calls it, copied once. A copy taken while the host was editing it is dropped, and this render notifies nobody -- never waits, and never calls a half-moved entry. A notification removed after the copy is still called by this render, and not by the next: AUBase's own list takes edits at the top of a render.au::takeRenderNotifies · au_wrapper.h:1441 - fn
inline void notifyRender(Unit* unit, AudioUnitRenderActionFlags phase, const AudioTimeStamp* timestamp, UInt32 bus, UInt32 frames, AudioBufferList* io)Fire the render notifications for one phase, from this render's copy. The flags word is the host's own: it tells each callback WHICH phase this is.au::notifyRender · au_wrapper.h:1458 - fn
inline TransportInfo readHostTransport(const Unit* unit)[audio-thread] The host's transport, read through the callbacks it gave.au::readHostTransport · au_wrapper.h:1486More
Every one of them is optional, and a host offering none is a host with no timeline: the defaults say so. Beats are quarter notes, as in every other format's reading (and JUCE's of these same calls); the bar start is the downbeat of the current measure, in the same units.
- fn
inline OSStatus initialize(void* self)au::initialize · au_wrapper.h:1540 - fn
inline OSStatus uninitialize(void* self)au::uninitialize · au_wrapper.h:1597 - fn
inline OSStatus reset(void* self, AudioUnitScope, AudioUnitElement)au::reset · au_wrapper.h:1605 - fn
inline void drainUiEvents(Unit* unit)Apply UI edits on the audio thread. The host was already told from the main thread (see au_view.h), so this only has to reach the DSP.au::drainUiEvents · au_wrapper.h:1620 - fn
inline void drainHostMidi(Unit* unit)[audio-thread] The host's MIDI since the last render, into this block. An event the block has no room for stays queued for the next one: a block late, which beats a note-off lost. The buffer is empty here -- render clears it -- so a SysEx no larger than its arena always fits.au::drainHostMidi · au_wrapper.h:1647 - fn
inline void queueHostMidi(Unit* unit, const MidiMessage& message, UInt32 offset)[any thread] One short message from the host, for the next render.au::queueHostMidi · au_wrapper.h:1661 - fn
inline OSStatus pullInput(Unit* unit, UInt32 el, const AudioTimeStamp* timestamp, UInt32 frames, AudioBufferList* list)Fillau::pullInput · au_wrapper.h:1673listwith elementel's input, from whichever source the host wired: a render callback, a graph connection, or nothing at all (in which case the buffer is left as the caller prepared it, which for the sidechain is silence). ONE function, because "where does the input come from" had two answers and only one of them was ever asked. - fn
inline OSStatus render(void* self, AudioUnitRenderActionFlags* flags, const AudioTimeStamp* timestamp, UInt32 outputBus, UInt32 frames, AudioBufferList* io)au::render · au_wrapper.h:1692 - fn
inline OSStatus midiEvent(void* self, UInt32 status, UInt32 data1, UInt32 data2, UInt32 offset)Notes, for instruments. AU delivers raw MIDI bytes, on the render thread or any other (see Unit::hostMidi), so they are queued for render.au::midiEvent · au_wrapper.h:1913 - fn
inline OSStatus sysExEvent(void* self, const UInt8* inData, UInt32 inLength)SysEx, which AU delivers through its OWN entry point.au::sysExEvent · au_wrapper.h:1940More
Not the MIDI callback above: MusicDeviceSysEx is a separate selector taking a pointer and a length, because a SysEx does not fit in the three UInt32s midiEvent carries. That is why this was the last format in the SDK with no SysEx path -- the bytes were never arriving anywhere the MIDI code looked.
Apple does not promise a complete message per call. A host may hand over whatever it received, so the bytes go through the same SysexAssembler the three MIDI backends use, and a DSP still only ever sees a finished message.
Compiled on macOS; still unrun against a device, because auval sends no SysEx and a CI runner has no MIDI hardware. What makes it worth having is that the risky part -- deciding what a byte MEANS -- is the assembler, which is shared with three backends and has 20 checks against it. What is new here is only where the bytes come from.
- fn
inline OSStatus startNote(void* self, MusicDeviceInstrumentID, MusicDeviceGroupID, NoteInstanceID* outNote, UInt32 offset, const MusicDeviceNoteParams* params)au::startNote · au_wrapper.h:1957 - fn
inline OSStatus stopNote(void* self, MusicDeviceGroupID, NoteInstanceID note, UInt32 offset)au::stopNote · au_wrapper.h:1970 - fn
inline OSStatus componentOpen(void* self, AudioUnit hostInstance)au::componentOpen · au_wrapper.h:1978 - fn
inline OSStatus componentClose(void* self)au::componentClose · au_wrapper.h:2019 - fn
inline AudioComponentMethod componentLookup(SInt16 selector)AU dispatch: the host asks for one selector at a time and calls what it gets back. An unknown selector must return null rather than something plausible: a wrong function pointer here is a crash inside the host.au::componentLookup · au_wrapper.h:2034
Constants 2
- const
constexpr AudioUnitPropertyID kRenderContextObserver = (AudioUnitPropertyID) ::sonore::detail::kRenderContextObserverPropertykAudioUnitProperty_RenderContextObserver, by number: macOS 11's SDK names it, older ones do not. tests/mac_host_constants.h checks it on a Mac.au::kRenderContextObserver · au_wrapper.h:333 - const
constexpr AudioUnitPropertyID kSonorePropertyUnitPointer = 64901Private property: hands the Cocoa view factory the Unit* behind a host's AudioUnit handle. That handle is an opaque wrapper owned by the AudioComponent framework; CASTING it is a crash, so the view asks the instance itself, through the one channel a host guarantees to route: AudioUnitGetProperty. Custom property IDs live at 64000 and above.au::kSonorePropertyUnitPointer · au_wrapper.h:472