Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
au_v2_instrument.hpp
Go to the documentation of this file.
1#pragma once
2
4
5// The AU v2 instrument adapter wraps Apple's AudioUnitSDK (Apple-only,
6// developer-supplied). The whole header is gated on __APPLE__ so it stays
7// self-contained — an empty no-op — on the Linux header-hygiene check and any
8// non-Apple TU.
9#if defined(__APPLE__)
10
11#include <AudioUnitSDK/MusicDeviceBase.h>
12
18#include <pulp/format/detail/playhead_diff.hpp>
21
22#include <array>
23#include <cstdint>
24#include <memory>
25#include <vector>
26
27namespace pulp::format::au {
28
49 const PluginDescriptor& desc) noexcept {
50 const std::size_t declared =
51 desc.output_buses.empty() ? std::size_t{1} : desc.output_buses.size();
52 return declared < BusBufferSet::kMaxBuses ? declared
54}
55
65inline std::size_t build_output_bus_infos(const PluginDescriptor& desc,
67 std::size_t cap) noexcept {
68 std::size_t n = 0;
69 for (std::size_t i = 0; i < desc.output_buses.size() && n < cap; ++i) {
70 out[n].name = desc.output_buses[i].name;
71 out[n].index = i;
72 out[n].direction = BusDirection::Output;
73 out[n].role = (i == 0) ? BusRole::Main : BusRole::Aux;
74 out[n].declared_channels = desc.output_buses[i].default_channels;
75 // Aux buses are optional; a host may leave them disconnected. The main
76 // bus (index 0) is always required.
77 out[n].optional = (i != 0);
78 out[n].active = true;
79 ++n;
80 }
81 return n;
82}
83
84class PulpAUInstrument : public ausdk::MusicDeviceBase {
85public:
86 explicit PulpAUInstrument(AudioComponentInstance ci);
87 // Multi-plugin-bundle ctor — see PulpAUEffect. The bound factory is used
88 // both for the output-element-count probe in the base-ctor argument list
89 // and to construct the processor.
90 PulpAUInstrument(AudioComponentInstance ci, ProcessorFactory factory);
91
92 OSStatus GetParameterList(AudioUnitScope inScope,
93 AudioUnitParameterID* outParameterList,
94 UInt32& outNumParameters) override;
95 OSStatus GetParameterInfo(AudioUnitScope inScope,
96 AudioUnitParameterID inParameterID,
97 AudioUnitParameterInfo& outParameterInfo) override;
98
99 // Single source of truth: the host reads/writes the plugin's StateStore
100 // directly — no separate Globals copy to reconcile per block. Mirrors the
101 // effect adapter (see au_v2_adapter). See the auv2 skill.
102 OSStatus GetParameter(AudioUnitParameterID inID, AudioUnitScope inScope,
103 AudioUnitElement inElement, Float32& outValue) override;
104 OSStatus SetParameter(AudioUnitParameterID inID, AudioUnitScope inScope,
105 AudioUnitElement inElement, Float32 inValue,
106 UInt32 inBufferOffsetInFrames) override;
107
108 // Value text for discrete parameters (value labels); continuous parameters
109 // are served through the ParameterStringFromValue / ...ValueFromString
110 // properties handled in GetProperty.
111 OSStatus GetParameterValueStrings(AudioUnitScope inScope, AudioUnitParameterID inParameterID,
112 CFArrayRef* outStrings) override;
113
114 // Serve the Pulp editor-context property (so the Cocoa view factory can
115 // reach this instance's Processor + StateStore) and advertise the Cocoa
116 // view to the host (kAudioUnitProperty_CocoaUI). Without these the host
117 // never loads the Pulp editor and shows its own generic param view —
118 // the AU-instrument editor gap ChainerSynth surfaced.
119 OSStatus GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope,
120 AudioUnitElement inElement, UInt32& outDataSize,
121 bool& outWritable) override;
122 OSStatus GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope,
123 AudioUnitElement inElement, void* outData) override;
124 // Accepts the host's kAudioUnitProperty_OfflineRender write.
125 OSStatus SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope,
126 AudioUnitElement inElement, const void* inData,
127 UInt32 inDataSize) override;
128
129 OSStatus Initialize() override;
130 void Cleanup() override;
131
132 bool StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) override;
133 bool CanScheduleParameters() const noexcept override;
134
135 // Channel capabilities for an instrument that also declares audio input:
136 // the descriptor's input/output pairs, preceded by the no-input rows when
137 // the main input is optional so the unit validates both connected and
138 // unconnected. Without inputs the table is the classic {0, N} ladder.
139 UInt32 SupportedNumChannels(const AUChannelInfo** outInfo) override;
140 // The side-chain input element (element 1) accepts 1..kBoundaryMaxChannels
141 // channels; an output sample-rate change carries over to every unconnected
142 // input element so input and output run at one rate.
143 bool ValidFormat(AudioUnitScope inScope, AudioUnitElement inElement,
144 const AudioStreamBasicDescription& inNewFormat) override;
145 OSStatus ChangeStreamFormat(AudioUnitScope inScope, AudioUnitElement inElement,
146 const AudioStreamBasicDescription& inPrevFormat,
147 const AudioStreamBasicDescription& inNewFormat) override;
148
149 OSStatus HandleNoteOn(UInt8 inChannel, UInt8 inNoteNumber,
150 UInt8 inVelocity, UInt32 inStartFrame) override;
151 OSStatus HandleNoteOff(UInt8 inChannel, UInt8 inNoteNumber,
152 UInt8 inVelocity, UInt32 inStartFrame) override;
153
161 OSStatus HandleNonNoteEvent(UInt8 inStatus, UInt8 inChannel, UInt8 inData1, UInt8 inData2,
162 UInt32 inStartFrame) override;
163
169 OSStatus HandleSysEx(const UInt8* inData, UInt32 inLength) override;
170
171 OSStatus Render(AudioUnitRenderActionFlags& ioActionFlags,
172 const AudioTimeStamp& inTimeStamp,
173 UInt32 inNumberFrames) override;
174
179 OSStatus GetPresets(CFArrayRef* outData) const override;
180
185 OSStatus NewFactoryPresetSet(const AUPreset& inNewFactoryPreset) override;
186
191 return factory_presets_;
192 }
193
194 OSStatus SaveState(CFPropertyListRef* outData) override;
195 OSStatus RestoreState(CFPropertyListRef plist) override;
196
197 bool SupportsTail() override;
198 Float64 GetTailTime() override;
199 Float64 GetLatency() override;
200
201protected:
204
205 // clang-format off: preserve the repository's four-space private-section style.
206private:
207 // AU v2 instruments are a separate adapter path from PulpAUEffect. Keep
208 // the tracing attachment on the actual host-facing object so a traced AU
209 // instrument retains a live reference to the runtime trace controller.
210 // Without this member, the traced SDK can carry its sentinel and Perfetto
211 // archive while the linker dead-strips Tracing::attach/start/stop from
212 // instrument bundles, making the advertised tracing mode inert.
214
215 // The store is declared before the Processor so it is destroyed after it.
216 // `Processor::state()` dereferences a pointer to this store, and a Processor
217 // may read it from its destructor or from a worker thread that destructor is
218 // about to join. Reversing these two lines hands that thread a freed store.
219 state::StateStore store_;
220 std::unique_ptr<Processor> processor_;
221 // Panic requests (Processor::request_panic), serviced before each block.
222 std::shared_ptr<PanicState> panic_state_;
223 // Factory presets, bound to store_ in the constructor. Declared after
224 // store_ so reverse member destruction retires the PresetManager while the
225 // store it holds a reference to is still alive.
226 FactoryPresetTable factory_presets_;
227 // kAudioUnitProperty_RenderContextObserver; bound once the processor
228 // exists, advertised only for an AudioWorkgroupClient processor.
229 RenderContextObserverProperty render_context_;
230 // Keeps a host class-info restore off the processor while Render() is
231 // inside it. Declared after processor_ so it outlives every render that
232 // consults it.
233 StateRestoreGate state_restore_gate_;
234 // Declared after processor_ so reverse member destruction retires retained
235 // editor handles before either referenced object is released.
236 runtime::AliveToken owner_alive_;
237 // Contract editors the host still holds. Declared after processor_ so its
238 // destruction invalidates them while the processor and store_ exist.
239 ContractSessionRegistry contract_sessions_;
240
241 // Immutable plugin metadata, cached once in the constructor. Owns the bus
242 // name strings that the per-block `ProcessBusBufferInfo`s view — copying it
243 // per render block would allocate (std::string members) on the audio thread.
244 PluginDescriptor descriptor_{};
245
246 // Host accommodations, resolved once at init via the runtime policy.
247 HostQuirks host_quirks_{};
248
249 // Per-output-bus channel-pointer storage. One vector per AU output element
250 // (bus); index 0 is the main bus, 1..N-1 the aux buses. Pre-reserved in
251 // Initialize() so the per-block resize to the host buffer count is a no-op
252 // realloc and the render path never grows a vector on the audio thread.
253 static constexpr std::size_t kMaxOutputBuses = BusBufferSet::kMaxBuses;
254 std::array<std::vector<float*>, kMaxOutputBuses> output_bus_ptrs_{};
255
256 // Audio input elements. `MusicDeviceBase` creates the elements the
257 // constructor asks for but never pulls them, so Render() pulls each
258 // connected element itself: element 0 is the main input, element 1 the
259 // side chain. Both are fixed at construction from the descriptor. The
260 // pointer arrays are the per-block read pointers into the elements' own
261 // preallocated buffer lists, so the pull never allocates on the audio
262 // thread. Lifetime of a pulled list is one Render() call.
263 bool has_input_element_ = false;
264 bool has_sidechain_element_ = false;
265 std::array<const float*, boundary::kBoundaryMaxChannels> input_ptrs_{};
266 std::array<const float*, boundary::kBoundaryMaxChannels> sidechain_ptrs_{};
267 // Capability table returned by SupportedNumChannels; the host reads it
268 // after the call returns, so it is instance storage rather than a local.
269 std::array<AUChannelInfo, kMaxChannelInfoPairs> channel_info_{};
270
271 // Main-thread listener that pushes editor parameter edits to the host
272 // (never from the render thread). Kept alive for the adapter's lifetime.
273 state::ListenerToken ui_push_listener_;
274 ParameterDisplayNamePublisher parameter_display_names_;
275
276 // Lock-free MIDI note input (single producer = host MIDI/render thread via
277 // HandleNoteOn/Off; single consumer = Render). No audio-thread mutex.
278 runtime::SpscQueue<midi::MidiEvent, 1024> midi_in_queue_;
279
280 // MusicDeviceSysEx has no frame offset at the AU v2 callback boundary.
281 // Keep the bounded copy shape identical to the other AU v2 adapters so a
282 // host SysEx callback never allocates or blocks.
283 static constexpr std::size_t kMaxSysexPayloadBytes = 1024;
284 struct SysexChunk {
285 std::array<uint8_t, kMaxSysexPayloadBytes> bytes{};
286 uint16_t length = 0;
287 };
288 runtime::SpscQueue<SysexChunk, 32> sysex_in_queue_;
289
290 // Per-block buffers are members so their vectors and SysEx payload pool
291 // can be reserved during Initialize(), never grown from Render().
292 static constexpr std::size_t kMaxEventsPerBlock = kMaxMidiEventsPerBlock;
293 static constexpr std::size_t kMaxSysexPerBlock = 64;
294 midi::MidiBuffer midi_in_;
295 midi::MidiBuffer midi_out_;
296
297 // MIDI the Processor emits during process() reaches the host through
298 // kAudioUnitProperty_MIDIOutputCallback, as for the effect and the MIDI
299 // processor: one output stream (kAudioUnitProperty_MIDIOutputCallbackInfo),
300 // packed allocation-free into a CoreMIDI packet list each block. Only a
301 // processor whose descriptor sets produces_midi advertises it.
302 bool plugin_produces_midi() const noexcept;
303 MidiOutputCallbackPublisher midi_output_callback_;
304 MidiOutputPacketBuilder midi_out_packet_builder_;
305
306 // Previous-block transport snapshot used to derive change flags on
307 // `ProcessContext`. Default-constructed so the first Render() call
308 // after Initialize() reports no changes.
309 detail::PlayheadSnapshot playhead_prev_{};
310
311 // Host offline-render intent (kAudioUnitProperty_OfflineRender), written by
312 // the host before a faster-than-realtime bounce and read each render block.
313 OfflineRenderProperty offline_render_;
314 // clang-format on
315};
316
317} // namespace pulp::format::au
318
319#endif // defined(__APPLE__)
The shared adapter-boundary core.
static constexpr std::size_t kMaxBuses
Definition process_block.hpp:148
The contract sessions one adapter instance opened that a host still holds.
Definition contract_editor_session.hpp:73
Definition state_restore_gate.hpp:87
The host-facing factory-preset table for one plug-in instance.
Definition au_factory_presets.hpp:56
OSStatus SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, const void *inData, UInt32 inDataSize) override
bool ValidFormat(AudioUnitScope inScope, AudioUnitElement inElement, const AudioStreamBasicDescription &inNewFormat) override
OSStatus GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void *outData) override
void publish_parameter_display_changes()
Drain host-facing name changes on the caller's control/main thread.
FactoryPresetTable & factory_preset_table() noexcept
The factory-preset table this instance serves.
Definition au_v2_instrument.hpp:190
OSStatus GetParameterList(AudioUnitScope inScope, AudioUnitParameterID *outParameterList, UInt32 &outNumParameters) override
OSStatus HandleNonNoteEvent(UInt8 inStatus, UInt8 inChannel, UInt8 inData1, UInt8 inData2, UInt32 inStartFrame) override
Every non-note channel-voice message (control change, program change, pitch bend, channel and polypho...
bool StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) override
OSStatus HandleNoteOff(UInt8 inChannel, UInt8 inNoteNumber, UInt8 inVelocity, UInt32 inStartFrame) override
PulpAUInstrument(AudioComponentInstance ci, ProcessorFactory factory)
UInt32 SupportedNumChannels(const AUChannelInfo **outInfo) override
OSStatus HandleNoteOn(UInt8 inChannel, UInt8 inNoteNumber, UInt8 inVelocity, UInt32 inStartFrame) override
OSStatus GetParameterInfo(AudioUnitScope inScope, AudioUnitParameterID inParameterID, AudioUnitParameterInfo &outParameterInfo) override
OSStatus NewFactoryPresetSet(const AUPreset &inNewFactoryPreset) override
kAudioUnitProperty_PresentPreset carrying a non-negative preset number.
PulpAUInstrument(AudioComponentInstance ci)
OSStatus GetParameter(AudioUnitParameterID inID, AudioUnitScope inScope, AudioUnitElement inElement, Float32 &outValue) override
OSStatus SetParameter(AudioUnitParameterID inID, AudioUnitScope inScope, AudioUnitElement inElement, Float32 inValue, UInt32 inBufferOffsetInFrames) override
bool CanScheduleParameters() const noexcept override
OSStatus HandleSysEx(const UInt8 *inData, UInt32 inLength) override
System-exclusive payload (F0 … F7).
OSStatus GetPresets(CFArrayRef *outData) const override
kAudioUnitProperty_FactoryPresets — the presets bundled with the plug-in, discovered from its Resourc...
OSStatus RestoreState(CFPropertyListRef plist) override
OSStatus ChangeStreamFormat(AudioUnitScope inScope, AudioUnitElement inElement, const AudioStreamBasicDescription &inPrevFormat, const AudioStreamBasicDescription &inNewFormat) override
OSStatus Render(AudioUnitRenderActionFlags &ioActionFlags, const AudioTimeStamp &inTimeStamp, UInt32 inNumberFrames) override
OSStatus GetParameterValueStrings(AudioUnitScope inScope, AudioUnitParameterID inParameterID, CFArrayRef *outStrings) override
OSStatus GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32 &outDataSize, bool &outWritable) override
OSStatus SaveState(CFPropertyListRef *outData) override
kAudioUnitProperty_RenderContextObserver, the v2 form of AUv3's renderContextObserver.
Definition au_v2_common.hpp:492
Shared, fail-closed lifetime signal for callbacks that may outlive an owner.
Definition alive_token.hpp:19
RAII attachment.
Definition trace_session.hpp:276
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
Per-host accommodation flags consumed by format adapters.
Apple-only: the whole header is gated on APPLE so it stays an empty no-op on the Linux header-hygiene...
Definition au_factory_presets.hpp:33
std::size_t instrument_output_element_count(const PluginDescriptor &desc) noexcept
Number of AU output ELEMENTS (buses) a multi-output instrument advertises: one per declared output bu...
Definition au_v2_instrument.hpp:48
constexpr std::size_t kMaxMidiEventsPerBlock
RT-safe builder for the MIDIPacketList an AU v2 plugin hands the host via kAudioUnitProperty_MIDIOutp...
Definition au_v2_common.hpp:310
std::size_t build_output_bus_infos(const PluginDescriptor &desc, ProcessBusBufferInfo *out, std::size_t cap) noexcept
Fill one ProcessBusBufferInfo per declared output bus (index 0 = Main, the rest = Aux) from the descr...
Definition au_v2_instrument.hpp:65
std::unique_ptr< Processor >(*)() ProcessorFactory
Factory function type — plugins provide this to create processor instances.
Definition processor.hpp:1416
@ Output
Definition process_block.hpp:114
@ Aux
Definition bus_role.hpp:11
@ Main
Definition bus_role.hpp:10
Always-on / host-gated defensive behaviors that the adapters consult.
Definition host_quirks.hpp:66
Plugin metadata — declared once, immutable.
Definition plugin_descriptor.hpp:245
Metadata that travels with a non-owning richer-surface process bus view.
Definition process_block.hpp:219