DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
DSParkAu.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
59#define DSPARK_PLUGIN_AU_INCLUDED 1
60
61#if defined(__APPLE__)
62
63#include "../DSParkPlugin.h"
64
65#include <AudioToolbox/AudioToolbox.h>
66#include <CoreFoundation/CoreFoundation.h>
67
68#if defined(DSPARK_PLUGIN_WEBVIEW)
69#include <AudioToolbox/AudioUnitUtilities.h> // AUParameterSet, AUEventListenerNotify
70#include <dlfcn.h>
71#include <objc/message.h>
72#include <objc/runtime.h>
73#endif
74
75#include <atomic>
76#include <cstring>
77#include <new>
78#include <vector>
79
80namespace dspark::plugin::au {
81
82inline constexpr uint32_t kBypassParamId = 0x42595053u; // matches VST3/CLAP
83inline constexpr int kBypassRampSamples = 256;
84
85inline OSType fourCC(const char* s) noexcept
86{
87 return (static_cast<OSType>(static_cast<unsigned char>(s[0])) << 24)
88 | (static_cast<OSType>(static_cast<unsigned char>(s[1])) << 16)
89 | (static_cast<OSType>(static_cast<unsigned char>(s[2])) << 8)
90 | static_cast<OSType>(static_cast<unsigned char>(s[3]));
91}
92
93#if defined(DSPARK_PLUGIN_WEBVIEW)
94
95// -- WebView editor glue (Cocoa view factory) -----------------------------------
96//
97// AUv2 has no IPlugView: the host reads kAudioUnitProperty_CocoaUI, loads the
98// announced bundle, instantiates the named factory class and asks it for an
99// NSView. Both classes here (factory + container view) are registered through
100// the Objective-C runtime on first use - no Objective-C sources, no AppKit
101// link dependency in the plugin. The factory is stateless: it recovers the
102// C++ plugin instance through a PRIVATE property on the AudioUnit handle
103// (IDs >= 64000 are reserved for third parties), so one process-wide class
104// serves every DSPark plugin loaded in the host, whichever binary won the
105// registration race (same policy as the WebView message relay).
106
108inline constexpr AudioUnitPropertyID kDsparkEditorHookProperty = 64537;
109
113struct EditorHook
114{
115 void* context = nullptr;
116 void* (*createView)(void* context, double width, double height) = nullptr;
117};
118
120struct ViewSize
121{
122 double width = 0, height = 0;
123};
124
127struct EditorViewSink
128{
129 void (*onDealloc)(void* context) = nullptr;
130 void* context = nullptr;
131};
132
133inline void editorContainerDealloc(void* self, SEL) noexcept
134{
135 // Instances exist only after the class registered, so the lookup cannot
136 // miss; the superclass is resolved from the registered class (NOT from
137 // object_getClass) so a future subclass would not recurse.
138 Class cls = objc_getClass("DSParkAUEditorContainer");
139 if (cls == nullptr) return;
140 if (Ivar ivar = class_getInstanceVariable(cls, "dsparkSink"))
141 {
142 auto* sink = reinterpret_cast<EditorViewSink*>(
143 object_getIvar(static_cast<id>(self), ivar));
144 if (sink != nullptr && sink->onDealloc != nullptr)
145 sink->onDealloc(sink->context);
146 }
147 objc_super super { static_cast<id>(self), class_getSuperclass(cls) };
148 reinterpret_cast<void (*)(objc_super*, SEL)>(&objc_msgSendSuper)(
149 &super, sel_registerName("dealloc"));
150}
151
154inline Class editorContainerClass() noexcept
155{
156 static Class registered = []() -> Class {
157 Class viewCls = objc_getClass("NSView");
158 if (viewCls == nullptr) return nullptr;
159 Class c = objc_allocateClassPair(viewCls, "DSParkAUEditorContainer", 0);
160 if (c == nullptr) // another DSPark plugin in this process won the race
161 return objc_getClass("DSParkAUEditorContainer");
162 class_addIvar(c, "dsparkSink", sizeof(void*), alignof(void*) == 8 ? 3 : 2, "^v");
163 class_addMethod(c, sel_registerName("dealloc"),
164 reinterpret_cast<IMP>(&editorContainerDealloc), "v@:");
165 objc_registerClassPair(c);
166 return c;
167 }();
168 return registered;
169}
170
171inline unsigned int editorFactoryInterfaceVersion(void*, SEL) noexcept
172{
173 return 0; // AUCocoaUIBase protocol version
174}
175
176inline void* editorFactoryUiView(void*, SEL, void* audioUnit, ViewSize size) noexcept
177{
178 EditorHook hook {};
179 UInt32 ioSize = sizeof(hook);
180 if (audioUnit == nullptr
181 || AudioUnitGetProperty(static_cast<AudioUnit>(audioUnit),
182 kDsparkEditorHookProperty, kAudioUnitScope_Global,
183 0, &hook, &ioSize) != noErr
184 || hook.createView == nullptr)
185 return nullptr;
186 return hook.createView(hook.context, size.width, size.height);
187}
188
190inline Class editorFactoryClass() noexcept
191{
192 static Class registered = []() -> Class {
193 Class c = objc_allocateClassPair(objc_getClass("NSObject"),
194 "DSParkAUCocoaViewFactory", 0);
195 if (c == nullptr)
196 return objc_getClass("DSParkAUCocoaViewFactory");
197 class_addMethod(c, sel_registerName("interfaceVersion"),
198 reinterpret_cast<IMP>(&editorFactoryInterfaceVersion), "I@:");
199 class_addMethod(c, sel_registerName("uiViewForAudioUnit:withSize:"),
200 reinterpret_cast<IMP>(&editorFactoryUiView),
201 "@@:^{ComponentInstanceRecord=}{CGSize=dd}");
202 // Hosts overwhelmingly probe by respondsToSelector; declare the formal
203 // protocol too when some loaded image has registered it.
204 if (Protocol* proto = objc_getProtocol("AUCocoaUIBase"))
205 class_addProtocol(c, proto);
206 objc_registerClassPair(c);
207 return c;
208 }();
209 return registered;
210}
211
215inline CFURLRef copyOwningBundleUrl() noexcept
216{
217 static const int anchor = 0; // any address inside THIS image
218 Dl_info info {};
219 if (dladdr(&anchor, &info) == 0 || info.dli_fname == nullptr) return nullptr;
220 const char* path = info.dli_fname;
221 const char* marker = nullptr;
222 for (const char* p = std::strstr(path, "/Contents/MacOS/"); p != nullptr;
223 p = std::strstr(p + 1, "/Contents/MacOS/"))
224 marker = p; // last occurrence wins (nested bundle layouts)
225 size_t length = marker != nullptr ? static_cast<size_t>(marker - path) : 0;
226 if (length == 0)
227 if (const char* slash = std::strrchr(path, '/'))
228 length = static_cast<size_t>(slash - path);
229 if (length == 0) return nullptr;
230 return CFURLCreateFromFileSystemRepresentation(
231 kCFAllocatorDefault, reinterpret_cast<const UInt8*>(path),
232 static_cast<CFIndex>(length), true);
233}
234
235#endif // DSPARK_PLUGIN_WEBVIEW
236
237template <typename P>
238struct Plugin
239{
240 static constexpr size_t kNumParams = P::parameters.size();
241 static constexpr bool kIsInstrument =
242 P::descriptor.category == Category::Instrument;
243 static constexpr int kNumPresets = factoryPresetCountOf<P>();
244 static_assert(!(kIsInstrument && HasSidechain<P>),
245 "an Instrument has no audio inputs; remove the sidechain "
246 "processBlock or use Category::Fx");
247 static_assert(!kIsInstrument || HasMidi<P>,
248 "an Instrument needs handleMidiEvent (see HasMidi): it has "
249 "no audio input to process");
250 static_assert(paramIdsUnique<P>(),
251 "two parameter ids share a hash32 (or collide with the "
252 "reserved PRGM/BYPS state ids): automation and state would "
253 "cross-wire. Rename one id.");
254
256 static OSType expectedType() noexcept
257 {
258 if (kIsInstrument) return fourCC("aumu");
259 if (HasMidi<P>) return fourCC("aumf");
260 return fourCC("aufx");
261 }
262
263 AudioComponentInstance instance = nullptr;
264 OSType subtype = 0, manufacturer = 0;
265
266 double sampleRate = 44100.0;
267 UInt32 maxFrames = 1156; // CoreAudio's historical default
268 bool initialized = false;
269 int currentChannels = defaultChannelCount<P>(); // negotiated width
270 UInt32 offlineRender = 0;
271
272 P user {};
273 std::atomic<double> shadow[kNumParams == 0 ? 1 : kNumParams] {};
274 std::atomic<bool> bypass { false };
275 float bypassMix = 0.0f;
276 int cachedLatency = 0;
277
278 // Input elements: 0 = main, 1 = sidechain. An instrument has none.
279 static constexpr UInt32 kNumInputElements =
280 kIsInstrument ? 0 : (HasSidechain<P> ? 2 : 1);
281 AURenderCallbackStruct inputCallback[2] {};
282 AudioUnit inputConnection[2] = { nullptr, nullptr };
283 UInt32 inputConnectionBus[2] = { 0, 0 };
284
285 std::vector<float> pullL, pullR, dryL, dryR, scL, scR;
286
287 // Host transport callbacks (kAudioUnitProperty_HostCallbacks).
288 HostCallbackInfo hostCallbacks {};
289
290 // Incoming MIDI: the MusicDevice selectors push, render() drains.
291 // Single producer (host MIDI thread), single consumer (render thread).
292 static constexpr uint32_t kMidiRingSize = 256; // power of two
293 MidiEvent midiRing[HasMidi<P> ? kMidiRingSize : 1] {};
294 std::atomic<uint32_t> midiHead { 0 }, midiTail { 0 };
295
296 // Sample-accurate parameter events (kAudioUnitScheduleParametersSelect
297 // arrives on the render thread, right before the matching render call).
298 BlockEvent scheduled[kMaxBlockEvents];
299 int scheduledCount = 0;
300
301 // Factory presets: AUPreset records plus the lazily built CFArray the
302 // property hands out (retained per get; the strings live here).
303 std::vector<AUPreset> presetStorage;
304 CFArrayRef presetArray = nullptr;
305 SInt32 currentPresetNumber = -1;
306
307 // Property listeners (registered/fired on the host's main thread) and
308 // the user-preset name AU hosts expect through kAudioUnitProperty_PresentPreset.
309 struct Listener
310 {
311 AudioUnitPropertyID id;
312 AudioUnitPropertyListenerProc proc;
313 void* user;
314 };
315 std::vector<Listener> listeners;
316 CFStringRef presetName = nullptr;
317
318#if defined(DSPARK_PLUGIN_WEBVIEW)
319 webview_ui::Editor<P> editor;
320 void* editorContainer = nullptr; // NSView* the HOST owns; weak here
321 EditorViewSink editorSink {};
322 bool editActive[kNumParams == 0 ? 1 : kNumParams] {};
323#endif
324
325 ~Plugin() noexcept
326 {
327#if defined(DSPARK_PLUGIN_WEBVIEW)
328 teardownEditor();
329#endif
330 if (presetName != nullptr) CFRelease(presetName);
331 for (auto& preset : presetStorage)
332 if (preset.presetName != nullptr) CFRelease(preset.presetName);
333 if (presetArray != nullptr) CFRelease(presetArray);
334 }
335
337 void applyFactoryPresetIdx(int idx) noexcept
338 {
339 if constexpr (kNumPresets > 0)
340 {
341 if (idx < 0 || idx >= kNumPresets) return;
342 for (size_t i = 0; i < kNumParams; ++i)
343 applyNormalized(static_cast<int>(i), presetNormalized<P>(idx, i));
344 currentPresetNumber = idx;
345 }
346 else
347 (void) idx;
348 }
349
352 void refreshLatency() noexcept
353 {
354 if constexpr (HasLatency<P>)
355 {
356 const int now = user.getLatency();
357 if (initialized && now != cachedLatency)
358 {
359 cachedLatency = now;
360 notifyProperty(kAudioUnitProperty_Latency,
361 kAudioUnitScope_Global, 0);
362 }
363 }
364 }
365
366 void notifyProperty(AudioUnitPropertyID id, AudioUnitScope scope,
367 AudioUnitElement element) noexcept
368 {
369 for (const auto& l : listeners)
370 if (l.id == id && l.proc != nullptr)
371 l.proc(l.user, instance, id, scope, element);
372 }
373
374 Plugin() noexcept
375 {
376 for (size_t i = 0; i < kNumParams; ++i)
377 shadow[i].store(toNormalized(P::parameters[i], P::parameters[i].defValue),
378 std::memory_order_relaxed);
379 }
380
381 static int indexOfParamId(AudioUnitParameterID id) noexcept
382 {
383 for (size_t i = 0; i < kNumParams; ++i)
384 if (hash32(P::parameters[i].id) == id) return static_cast<int>(i);
385 return -1;
386 }
387
388 void applyNormalized(int index, double normalized) noexcept
389 {
390 // Host values are untrusted doubles: a NaN would pass both range
391 // clamps, poison the shadow and reach the user's setter. Ignore it.
392 if (normalized != normalized) return;
393 const auto& spec = P::parameters[static_cast<size_t>(index)];
394 shadow[static_cast<size_t>(index)].store(normalized, std::memory_order_relaxed);
395 user.setParameter(index, static_cast<float>(toPlain(spec, normalized)));
396 }
397
398 void applyAllShadows() noexcept
399 {
400 for (size_t i = 0; i < kNumParams; ++i)
401 user.setParameter(static_cast<int>(i),
402 static_cast<float>(toPlain(P::parameters[i],
403 shadow[i].load(std::memory_order_relaxed))));
404 }
405
406#if defined(DSPARK_PLUGIN_WEBVIEW)
407
408 // --- WebView editor (all on the host's main thread) -----------------------------
409
410 static constexpr bool kHasWebEditor =
411 HasEditor<P> && webview_ui::Editor<P>::kAvailable;
412
413 void sendGesture(int index, AudioUnitEventType type) noexcept
414 {
415 AudioUnitEvent event {};
416 event.mEventType = type;
417 event.mArgument.mParameter = AudioUnitParameter {
418 instance, hash32(P::parameters[static_cast<size_t>(index)].id),
419 kAudioUnitScope_Global, 0 };
420 AUEventListenerNotify(nullptr, nullptr, &event);
421 }
422
423 // Editor -> host/DSP bridge. AUParameterSet routes through our own
424 // SetParameter (shadow + user setter) AND notifies host listeners, which
425 // is how AU hosts observe UI edits for automation recording.
426 static void cbSetParam(void* context, int index, double plain) noexcept
427 {
428 auto* self = static_cast<Plugin*>(context);
429 const AudioUnitParameter parameter {
430 self->instance, hash32(P::parameters[static_cast<size_t>(index)].id),
431 kAudioUnitScope_Global, 0 };
432 const bool inGesture = self->editActive[static_cast<size_t>(index)];
433 if (!inGesture) // edits outside a drag still need a gesture for undo
434 self->sendGesture(index, kAudioUnitEvent_BeginParameterChangeGesture);
435 AUParameterSet(nullptr, nullptr, &parameter,
436 static_cast<AudioUnitParameterValue>(plain), 0);
437 if (!inGesture)
438 self->sendGesture(index, kAudioUnitEvent_EndParameterChangeGesture);
439 }
440
441 static void cbBeginEdit(void* context, int index) noexcept
442 {
443 auto* self = static_cast<Plugin*>(context);
444 self->editActive[static_cast<size_t>(index)] = true;
445 self->sendGesture(index, kAudioUnitEvent_BeginParameterChangeGesture);
446 }
447
448 static void cbEndEdit(void* context, int index) noexcept
449 {
450 auto* self = static_cast<Plugin*>(context);
451 self->editActive[static_cast<size_t>(index)] = false;
452 self->sendGesture(index, kAudioUnitEvent_EndParameterChangeGesture);
453 }
454
455 static void sOnContainerDealloc(void* context) noexcept
456 {
457 auto* self = static_cast<Plugin*>(context);
458 self->editorContainer = nullptr; // the host is destroying the view
459 self->editor.destroy();
460 }
461
464 void teardownEditor() noexcept
465 {
466 if (editorContainer != nullptr)
467 {
468 Class cls = editorContainerClass();
469 if (Ivar ivar = cls != nullptr
470 ? class_getInstanceVariable(cls, "dsparkSink") : nullptr)
471 object_setIvar(static_cast<id>(editorContainer), ivar, nullptr);
472 editorContainer = nullptr;
473 }
474 editor.destroy();
475 }
476
477 static void* sCreateEditorView(void* context, double width, double height) noexcept
478 {
479 return static_cast<Plugin*>(context)->createEditorView(width, height);
480 }
481
487 void* createEditorView(double, double) noexcept
488 {
489 namespace og = webview_ui::objc_glue;
490 teardownEditor(); // hosts may ask again without releasing the first view
491 Class cls = editorContainerClass();
492 if (cls == nullptr) return nullptr; // AppKit absent: faceless host
493 const EditorSize logical = editorSizeOf<P>();
494 const og::Rect frame { 0.0, 0.0, static_cast<double>(logical.width),
495 static_cast<double>(logical.height) };
496 og::ObjId view = og::call(og::call(reinterpret_cast<og::ObjId>(cls), "alloc"),
497 "initWithFrame:", frame);
498 if (view == nullptr) return nullptr;
499 const webview_ui::HostCallbacks callbacks {
500 this, &cbSetParam, &cbBeginEdit, &cbEndEdit
501 };
502 if (!editor.create(view, shadow, callbacks))
503 {
504 og::call<void>(view, "release");
505 return nullptr;
506 }
507 editorSink = { &sOnContainerDealloc, this };
508 if (Ivar ivar = class_getInstanceVariable(cls, "dsparkSink"))
509 object_setIvar(static_cast<id>(view),
510 ivar, reinterpret_cast<id>(&editorSink));
511 editorContainer = view;
512 editor.setBounds(logical.width, logical.height);
513 editor.setVisible(true);
514 webview_ui::debugLog("au createEditorView %dx%d container=%p",
515 logical.width, logical.height, view);
516 return og::call(view, "autorelease");
517 }
518
519#endif // DSPARK_PLUGIN_WEBVIEW
520
521 // --- lifecycle ---------------------------------------------------------------
522
523 OSStatus initialize() noexcept
524 {
525 dspark::AudioSpec spec { sampleRate, static_cast<int>(maxFrames),
526 currentChannels };
527 user.prepare(spec);
528 if constexpr (HasOfflineMode<P>)
529 user.setOfflineRendering(offlineRender != 0);
530 applyAllShadows();
531 pullL.assign(maxFrames, 0.0f);
532 pullR.assign(maxFrames, 0.0f);
533 dryL.assign(maxFrames, 0.0f);
534 dryR.assign(maxFrames, 0.0f);
535 if constexpr (HasSidechain<P>)
536 {
537 scL.assign(maxFrames, 0.0f);
538 scR.assign(maxFrames, 0.0f);
539 }
540 bypassMix = bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
541 if constexpr (HasLatency<P>)
542 cachedLatency = user.getLatency();
543 scheduledCount = 0;
544 initialized = true;
545 return noErr;
546 }
547
548 OSStatus uninitialize() noexcept
549 {
550 initialized = false;
551 return noErr;
552 }
553
554 OSStatus reset() noexcept
555 {
556 if constexpr (HasReset<P>)
557 user.reset();
558 bypassMix = bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
559 return noErr;
560 }
561
562 // --- stream format helpers -----------------------------------------------------
563
564 void fillStreamFormat(AudioStreamBasicDescription& f) const noexcept
565 {
566 std::memset(&f, 0, sizeof(f));
567 f.mSampleRate = sampleRate;
568 f.mFormatID = kAudioFormatLinearPCM;
569 f.mFormatFlags = static_cast<AudioFormatFlags>(kAudioFormatFlagsNativeFloatPacked)
570 | static_cast<AudioFormatFlags>(kAudioFormatFlagIsNonInterleaved);
571 f.mBytesPerPacket = sizeof(float);
572 f.mFramesPerPacket = 1;
573 f.mBytesPerFrame = sizeof(float);
574 f.mChannelsPerFrame = static_cast<UInt32>(currentChannels);
575 f.mBitsPerChannel = 32;
576 }
577
578 bool acceptableFormat(const AudioStreamBasicDescription& f) const noexcept
579 {
580 return f.mFormatID == kAudioFormatLinearPCM
581 && (f.mFormatFlags & kAudioFormatFlagIsFloat) != 0
582 && (f.mFormatFlags & kAudioFormatFlagIsNonInterleaved) != 0
583 && supportsChannelCount<P>(static_cast<int>(f.mChannelsPerFrame))
584 && f.mBitsPerChannel == 32
585 && f.mSampleRate > 0.0;
586 }
587
588 // --- properties -------------------------------------------------------------------
589
590 static constexpr int numChannelConfigs() noexcept
591 {
592 return channelSupportOf<P>() == ChannelSupport::MonoAndStereo ? 2 : 1;
593 }
594
597 void buildPresetArray() noexcept
598 {
599 if constexpr (kNumPresets > 0)
600 {
601 if (presetArray != nullptr) return;
602 presetStorage.resize(static_cast<size_t>(kNumPresets));
603 const void* pointers[kNumPresets == 0 ? 1 : kNumPresets];
604 for (int i = 0; i < kNumPresets; ++i)
605 {
606 presetStorage[static_cast<size_t>(i)].presetNumber = i;
607 presetStorage[static_cast<size_t>(i)].presetName =
608 CFStringCreateWithCString(
609 nullptr, P::factoryPresets[static_cast<size_t>(i)].name,
610 kCFStringEncodingUTF8);
611 pointers[i] = &presetStorage[static_cast<size_t>(i)];
612 }
613 presetArray = CFArrayCreate(nullptr, pointers,
614 static_cast<CFIndex>(kNumPresets), nullptr);
615 }
616 }
617
618 OSStatus getPropertyInfo(AudioUnitPropertyID id, AudioUnitScope scope,
619 AudioUnitElement element, UInt32* outSize,
620 Boolean* outWritable) noexcept
621 {
622 UInt32 size = 0;
623 Boolean writable = false;
624 switch (id)
625 {
626 case kAudioUnitProperty_StreamFormat:
627 size = sizeof(AudioStreamBasicDescription); writable = true; break;
628 case kAudioUnitProperty_SampleRate:
629 size = sizeof(Float64); writable = true; break;
630 case kAudioUnitProperty_MaximumFramesPerSlice:
631 size = sizeof(UInt32); writable = true; break;
632 case kAudioUnitProperty_ElementCount:
633 size = sizeof(UInt32); break;
634 case kAudioUnitProperty_SupportedNumChannels:
635 size = static_cast<UInt32>(numChannelConfigs() * sizeof(AUChannelInfo));
636 break;
637 case kAudioUnitProperty_HostCallbacks:
638 size = sizeof(HostCallbackInfo); writable = true; break;
639 case kAudioUnitProperty_OfflineRender:
640 size = sizeof(UInt32); writable = true; break;
641 case kAudioUnitProperty_FactoryPresets:
642 if (kNumPresets == 0) return kAudioUnitErr_InvalidProperty;
643 size = sizeof(CFArrayRef);
644 break;
645 case kAudioUnitProperty_ParameterList:
646 size = scope == kAudioUnitScope_Global
647 ? static_cast<UInt32>((kNumParams + 1) * sizeof(AudioUnitParameterID))
648 : 0;
649 break;
650 case kAudioUnitProperty_ParameterInfo:
651 size = sizeof(AudioUnitParameterInfo); break;
652 case kAudioUnitProperty_Latency:
653 case kAudioUnitProperty_TailTime:
654 size = sizeof(Float64); break;
655 case kAudioUnitProperty_BypassEffect:
656 size = sizeof(UInt32); writable = true; break;
657 case kAudioUnitProperty_ClassInfo:
658 size = sizeof(CFPropertyListRef); writable = true; break;
659 case kAudioUnitProperty_SetRenderCallback:
660 size = sizeof(AURenderCallbackStruct); writable = true; break;
661 case kAudioUnitProperty_MakeConnection:
662 size = sizeof(AudioUnitConnection); writable = true; break;
663 case kAudioUnitProperty_InPlaceProcessing:
664 size = sizeof(UInt32); break;
665 case kAudioUnitProperty_PresentPreset:
666 case kAudioUnitProperty_CurrentPreset:
667 size = sizeof(AUPreset); writable = true; break;
668#if defined(DSPARK_PLUGIN_WEBVIEW)
669 case kAudioUnitProperty_CocoaUI:
670 if constexpr (!kHasWebEditor) return kAudioUnitErr_InvalidProperty;
671 size = sizeof(AudioUnitCocoaViewInfo);
672 break;
673 case kDsparkEditorHookProperty:
674 if constexpr (!kHasWebEditor) return kAudioUnitErr_InvalidProperty;
675 size = sizeof(EditorHook);
676 break;
677#endif
678 default:
679 (void) element;
680 return kAudioUnitErr_InvalidProperty;
681 }
682 if (outSize) *outSize = size;
683 if (outWritable) *outWritable = writable;
684 return noErr;
685 }
686
687 OSStatus getProperty(AudioUnitPropertyID id, AudioUnitScope scope,
688 AudioUnitElement element, void* outData,
689 UInt32* ioSize) noexcept
690 {
691 if (ioSize == nullptr) return kAudio_ParamError;
692 if (outData == nullptr)
693 {
694 Boolean w = false;
695 return getPropertyInfo(id, scope, element, ioSize, &w);
696 }
697 switch (id)
698 {
699 case kAudioUnitProperty_StreamFormat:
700 {
701 if (*ioSize < sizeof(AudioStreamBasicDescription))
702 return kAudioUnitErr_InvalidPropertyValue;
703 fillStreamFormat(*static_cast<AudioStreamBasicDescription*>(outData));
704 *ioSize = sizeof(AudioStreamBasicDescription);
705 return noErr;
706 }
707 case kAudioUnitProperty_SampleRate:
708 if (*ioSize < sizeof(Float64)) return kAudioUnitErr_InvalidPropertyValue;
709 *static_cast<Float64*>(outData) = sampleRate;
710 *ioSize = sizeof(Float64);
711 return noErr;
712 case kAudioUnitProperty_MaximumFramesPerSlice:
713 if (*ioSize < sizeof(UInt32)) return kAudioUnitErr_InvalidPropertyValue;
714 *static_cast<UInt32*>(outData) = maxFrames;
715 *ioSize = sizeof(UInt32);
716 return noErr;
717 case kAudioUnitProperty_ElementCount:
718 if (*ioSize < sizeof(UInt32)) return kAudioUnitErr_InvalidPropertyValue;
719 *static_cast<UInt32*>(outData) =
720 scope == kAudioUnitScope_Input ? kNumInputElements : 1;
721 *ioSize = sizeof(UInt32);
722 return noErr;
723 case kAudioUnitProperty_SupportedNumChannels:
724 {
725 const UInt32 want =
726 static_cast<UInt32>(numChannelConfigs() * sizeof(AUChannelInfo));
727 if (*ioSize < want) return kAudioUnitErr_InvalidPropertyValue;
728 auto* info = static_cast<AUChannelInfo*>(outData);
729 int slot = 0;
730 // An instrument declares 0 inputs; effects run symmetric widths.
731 if (supportsChannelCount<P>(1))
732 {
733 info[slot].inChannels = kIsInstrument ? 0 : 1;
734 info[slot].outChannels = 1;
735 ++slot;
736 }
737 if (supportsChannelCount<P>(2))
738 {
739 info[slot].inChannels = kIsInstrument ? 0 : 2;
740 info[slot].outChannels = 2;
741 ++slot;
742 }
743 *ioSize = want;
744 return noErr;
745 }
746 case kAudioUnitProperty_HostCallbacks:
747 if (*ioSize < sizeof(HostCallbackInfo))
748 return kAudioUnitErr_InvalidPropertyValue;
749 *static_cast<HostCallbackInfo*>(outData) = hostCallbacks;
750 *ioSize = sizeof(HostCallbackInfo);
751 return noErr;
752 case kAudioUnitProperty_OfflineRender:
753 if (*ioSize < sizeof(UInt32)) return kAudioUnitErr_InvalidPropertyValue;
754 *static_cast<UInt32*>(outData) = offlineRender;
755 *ioSize = sizeof(UInt32);
756 return noErr;
757 case kAudioUnitProperty_FactoryPresets:
758 {
759 if (kNumPresets == 0) return kAudioUnitErr_InvalidProperty;
760 if (*ioSize < sizeof(CFArrayRef))
761 return kAudioUnitErr_InvalidPropertyValue;
762 buildPresetArray();
763 if (presetArray == nullptr) return kAudioUnitErr_InvalidProperty;
764 *static_cast<CFArrayRef*>(outData) =
765 static_cast<CFArrayRef>(CFRetain(presetArray)); // caller releases
766 *ioSize = sizeof(CFArrayRef);
767 return noErr;
768 }
769 case kAudioUnitProperty_ParameterList:
770 {
771 if (scope != kAudioUnitScope_Global)
772 {
773 *ioSize = 0;
774 return noErr;
775 }
776 const UInt32 want =
777 static_cast<UInt32>((kNumParams + 1) * sizeof(AudioUnitParameterID));
778 if (*ioSize < want) return kAudioUnitErr_InvalidPropertyValue;
779 auto* ids = static_cast<AudioUnitParameterID*>(outData);
780 for (size_t i = 0; i < kNumParams; ++i)
781 ids[i] = hash32(P::parameters[i].id);
782 ids[kNumParams] = kBypassParamId;
783 *ioSize = want;
784 return noErr;
785 }
786 case kAudioUnitProperty_ParameterInfo:
787 {
788 if (*ioSize < sizeof(AudioUnitParameterInfo))
789 return kAudioUnitErr_InvalidPropertyValue;
790 auto* info = static_cast<AudioUnitParameterInfo*>(outData);
791 std::memset(info, 0, sizeof(*info));
792 info->flags = kAudioUnitParameterFlag_IsReadable
793 | kAudioUnitParameterFlag_IsWritable
794 | kAudioUnitParameterFlag_HasCFNameString
795 | kAudioUnitParameterFlag_CFNameRelease;
796 if (element == kBypassParamId)
797 {
798 std::snprintf(info->name, sizeof(info->name), "Bypass");
799 info->cfNameString = CFStringCreateWithCString(
800 nullptr, "Bypass", kCFStringEncodingUTF8);
801 info->unit = kAudioUnitParameterUnit_Boolean;
802 info->minValue = 0.0f;
803 info->maxValue = 1.0f;
804 info->defaultValue = 0.0f;
805 *ioSize = sizeof(AudioUnitParameterInfo);
806 return noErr;
807 }
808 const int idx = indexOfParamId(element);
809 if (idx < 0) return kAudioUnitErr_InvalidParameter;
810 const auto& spec = P::parameters[static_cast<size_t>(idx)];
811 std::snprintf(info->name, sizeof(info->name), "%s", spec.name);
812 info->cfNameString = CFStringCreateWithCString(
813 nullptr, spec.name, kCFStringEncodingUTF8);
814 info->unit = spec.steps == 1 ? kAudioUnitParameterUnit_Boolean
815 : (std::strcmp(spec.unit, "dB") == 0
816 ? kAudioUnitParameterUnit_Decibels
817 : kAudioUnitParameterUnit_Generic);
818 info->minValue = spec.minValue;
819 info->maxValue = spec.maxValue;
820 info->defaultValue = spec.defValue;
821 *ioSize = sizeof(AudioUnitParameterInfo);
822 return noErr;
823 }
824 case kAudioUnitProperty_Latency:
825 if (*ioSize < sizeof(Float64)) return kAudioUnitErr_InvalidPropertyValue;
826 *static_cast<Float64*>(outData) =
827 sampleRate > 0.0 ? cachedLatency / sampleRate : 0.0;
828 *ioSize = sizeof(Float64);
829 return noErr;
830 case kAudioUnitProperty_TailTime:
831 {
832 if (*ioSize < sizeof(Float64)) return kAudioUnitErr_InvalidPropertyValue;
833 double tail = 0.0;
834 if constexpr (HasTail<P>)
835 tail = user.getTailSeconds();
836 *static_cast<Float64*>(outData) = tail;
837 *ioSize = sizeof(Float64);
838 return noErr;
839 }
840 case kAudioUnitProperty_BypassEffect:
841 if (*ioSize < sizeof(UInt32)) return kAudioUnitErr_InvalidPropertyValue;
842 *static_cast<UInt32*>(outData) =
843 bypass.load(std::memory_order_relaxed) ? 1 : 0;
844 *ioSize = sizeof(UInt32);
845 return noErr;
846 case kAudioUnitProperty_InPlaceProcessing:
847 if (*ioSize < sizeof(UInt32)) return kAudioUnitErr_InvalidPropertyValue;
848 *static_cast<UInt32*>(outData) = 1;
849 *ioSize = sizeof(UInt32);
850 return noErr;
851 case kAudioUnitProperty_PresentPreset:
852 case kAudioUnitProperty_CurrentPreset:
853 {
854 if (*ioSize < sizeof(AUPreset)) return kAudioUnitErr_InvalidPropertyValue;
855 auto* preset = static_cast<AUPreset*>(outData);
856 preset->presetNumber = currentPresetNumber;
857 preset->presetName = presetName != nullptr
858 ? static_cast<CFStringRef>(CFRetain(presetName))
859 : CFStringCreateWithCString(nullptr, "Untitled",
860 kCFStringEncodingUTF8);
861 *ioSize = sizeof(AUPreset);
862 return noErr;
863 }
864 case kAudioUnitProperty_ClassInfo:
865 {
866 if (*ioSize < sizeof(CFPropertyListRef))
867 return kAudioUnitErr_InvalidPropertyValue;
868 double norm[kNumParams == 0 ? 1 : kNumParams];
869 for (size_t i = 0; i < kNumParams; ++i)
870 norm[i] = shadow[i].load(std::memory_order_relaxed);
871 const std::vector<uint8_t> blob = buildState(
872 user, norm, kNumParams,
873 kNumPresets > 0 ? currentPresetNumber : -1,
874 bypass.load(std::memory_order_relaxed) ? 1 : 0);
875
876 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
877 nullptr, 0, &kCFTypeDictionaryKeyCallBacks,
878 &kCFTypeDictionaryValueCallBacks);
879 auto putInt = [&](const char* key, SInt32 v) {
880 CFNumberRef n = CFNumberCreate(nullptr, kCFNumberSInt32Type, &v);
881 CFStringRef k = CFStringCreateWithCString(nullptr, key,
882 kCFStringEncodingUTF8);
883 CFDictionarySetValue(dict, k, n);
884 CFRelease(k);
885 CFRelease(n);
886 };
887 putInt("version", 0);
888 putInt("type", static_cast<SInt32>(expectedType()));
889 putInt("subtype", static_cast<SInt32>(subtype));
890 putInt("manufacturer", static_cast<SInt32>(manufacturer));
891 CFStringRef nameKey = CFSTR("name");
892 CFStringRef nameVal = CFStringCreateWithCString(
893 nullptr, P::descriptor.name, kCFStringEncodingUTF8);
894 CFDictionarySetValue(dict, nameKey, nameVal);
895 CFRelease(nameVal);
896 CFDataRef data = CFDataCreate(nullptr, blob.data(),
897 static_cast<CFIndex>(blob.size()));
898 CFDictionarySetValue(dict, CFSTR("dspark-state"), data);
899 CFRelease(data);
900
901 *static_cast<CFPropertyListRef*>(outData) = dict; // caller releases
902 *ioSize = sizeof(CFPropertyListRef);
903 return noErr;
904 }
905#if defined(DSPARK_PLUGIN_WEBVIEW)
906 case kAudioUnitProperty_CocoaUI:
907 {
908 if constexpr (!kHasWebEditor) return kAudioUnitErr_InvalidProperty;
909 if (*ioSize < sizeof(AudioUnitCocoaViewInfo))
910 return kAudioUnitErr_InvalidPropertyValue;
911 Class factory = editorFactoryClass(); // registered before the host
912 CFURLRef url = copyOwningBundleUrl(); // looks the class name up
913 if (factory == nullptr || url == nullptr)
914 {
915 if (url != nullptr) CFRelease(url);
916 return kAudioUnitErr_InvalidProperty;
917 }
918 auto* viewInfo = static_cast<AudioUnitCocoaViewInfo*>(outData);
919 viewInfo->mCocoaAUViewBundleLocation = url; // caller releases both
920 viewInfo->mCocoaAUViewClass[0] = CFStringCreateWithCString(
921 nullptr, class_getName(factory), kCFStringEncodingUTF8);
922 *ioSize = sizeof(AudioUnitCocoaViewInfo);
923 return noErr;
924 }
925 case kDsparkEditorHookProperty:
926 {
927 if constexpr (!kHasWebEditor) return kAudioUnitErr_InvalidProperty;
928 if (*ioSize < sizeof(EditorHook))
929 return kAudioUnitErr_InvalidPropertyValue;
930 *static_cast<EditorHook*>(outData) = EditorHook { this, &sCreateEditorView };
931 *ioSize = sizeof(EditorHook);
932 return noErr;
933 }
934#endif
935 default:
936 return kAudioUnitErr_InvalidProperty;
937 }
938 }
939
940 OSStatus setProperty(AudioUnitPropertyID id, AudioUnitScope scope,
941 AudioUnitElement element, const void* inData,
942 UInt32 inSize) noexcept
943 {
944 switch (id)
945 {
946 case kAudioUnitProperty_StreamFormat:
947 {
948 if (inData == nullptr || inSize < sizeof(AudioStreamBasicDescription))
949 return kAudioUnitErr_InvalidPropertyValue;
950 const auto* f = static_cast<const AudioStreamBasicDescription*>(inData);
951 if (!acceptableFormat(*f)) return kAudioUnitErr_FormatNotSupported;
952 sampleRate = f->mSampleRate;
953 // The negotiated width: every element of the instance follows
954 // (initialize() prepares the plugin at this count).
955 currentChannels = static_cast<int>(f->mChannelsPerFrame);
956 return noErr;
957 }
958 case kAudioUnitProperty_SampleRate:
959 if (inData == nullptr || inSize < sizeof(Float64))
960 return kAudioUnitErr_InvalidPropertyValue;
961 sampleRate = *static_cast<const Float64*>(inData);
962 return noErr;
963 case kAudioUnitProperty_MaximumFramesPerSlice:
964 if (inData == nullptr || inSize < sizeof(UInt32))
965 return kAudioUnitErr_InvalidPropertyValue;
966 maxFrames = *static_cast<const UInt32*>(inData);
967 notifyProperty(id, scope, element);
968 return noErr;
969 case kAudioUnitProperty_PresentPreset:
970 case kAudioUnitProperty_CurrentPreset:
971 {
972 if (inData == nullptr || inSize < sizeof(AUPreset))
973 return kAudioUnitErr_InvalidPropertyValue;
974 const auto* preset = static_cast<const AUPreset*>(inData);
975 if (preset->presetNumber >= 0)
976 {
977 // A factory preset by number: apply its parameter values.
978 if (preset->presetNumber >= kNumPresets)
979 return kAudioUnitErr_InvalidPropertyValue;
980 applyFactoryPresetIdx(preset->presetNumber);
981 refreshLatency();
982 if (presetName != nullptr) CFRelease(presetName);
983 presetName = preset->presetName != nullptr
984 ? static_cast<CFStringRef>(CFRetain(preset->presetName))
985 : CFStringCreateWithCString(
986 nullptr,
987 [&]() -> const char* {
988 if constexpr (kNumPresets > 0)
989 return P::factoryPresets[static_cast<size_t>(
990 preset->presetNumber)].name;
991 else
992 return "";
993 }(),
994 kCFStringEncodingUTF8);
995 notifyProperty(id, scope, element);
996 return noErr;
997 }
998 currentPresetNumber = -1; // back to a user preset
999 if (presetName != nullptr) CFRelease(presetName);
1000 presetName = preset->presetName != nullptr
1001 ? static_cast<CFStringRef>(CFRetain(preset->presetName))
1002 : nullptr;
1003 notifyProperty(id, scope, element);
1004 return noErr;
1005 }
1006 case kAudioUnitProperty_HostCallbacks:
1007 {
1008 if (inData == nullptr) return kAudioUnitErr_InvalidPropertyValue;
1009 // Hosts may pass a truncated struct (older callback sets).
1010 std::memset(&hostCallbacks, 0, sizeof(hostCallbacks));
1011 std::memcpy(&hostCallbacks, inData,
1012 inSize < sizeof(hostCallbacks) ? inSize
1013 : sizeof(hostCallbacks));
1014 return noErr;
1015 }
1016 case kAudioUnitProperty_OfflineRender:
1017 if (inData == nullptr || inSize < sizeof(UInt32))
1018 return kAudioUnitErr_InvalidPropertyValue;
1019 offlineRender = *static_cast<const UInt32*>(inData);
1020 if constexpr (HasOfflineMode<P>)
1021 user.setOfflineRendering(offlineRender != 0);
1022 return noErr;
1023 case kAudioUnitProperty_BypassEffect:
1024 if (inData == nullptr || inSize < sizeof(UInt32))
1025 return kAudioUnitErr_InvalidPropertyValue;
1026 bypass.store(*static_cast<const UInt32*>(inData) != 0,
1027 std::memory_order_relaxed);
1028 return noErr;
1029 case kAudioUnitProperty_SetRenderCallback:
1030 {
1031 // Instruments have no input elements yet must still ACCEPT a
1032 // render callback (auval and some hosts set one regardless);
1033 // it is stored and never pulled.
1034 constexpr UInt32 storable = kIsInstrument ? 2 : kNumInputElements;
1035 if (scope != kAudioUnitScope_Input || element >= storable
1036 || inData == nullptr || inSize < sizeof(AURenderCallbackStruct))
1037 return kAudioUnitErr_InvalidPropertyValue;
1038 inputCallback[element] = *static_cast<const AURenderCallbackStruct*>(inData);
1039 inputConnection[element] = nullptr;
1040 return noErr;
1041 }
1042 case kAudioUnitProperty_MakeConnection:
1043 {
1044 if (inData == nullptr || inSize < sizeof(AudioUnitConnection))
1045 return kAudioUnitErr_InvalidPropertyValue;
1046 const auto* c = static_cast<const AudioUnitConnection*>(inData);
1047 constexpr UInt32 storable = kIsInstrument ? 2 : kNumInputElements;
1048 if (c->destInputNumber >= storable)
1049 return kAudioUnitErr_InvalidPropertyValue;
1050 inputConnection[c->destInputNumber] = c->sourceAudioUnit;
1051 inputConnectionBus[c->destInputNumber] = c->sourceOutputNumber;
1052 inputCallback[c->destInputNumber] = {};
1053 return noErr;
1054 }
1055 case kAudioUnitProperty_ClassInfo:
1056 {
1057 if (inData == nullptr || inSize < sizeof(CFPropertyListRef))
1058 return kAudioUnitErr_InvalidPropertyValue;
1059 CFPropertyListRef plist = *static_cast<const CFPropertyListRef*>(inData);
1060 if (plist == nullptr || CFGetTypeID(plist) != CFDictionaryGetTypeID())
1061 return kAudioUnitErr_InvalidPropertyValue;
1062 auto dict = static_cast<CFDictionaryRef>(plist);
1063 auto data = static_cast<CFDataRef>(
1064 CFDictionaryGetValue(dict, CFSTR("dspark-state")));
1065 if (data == nullptr || CFGetTypeID(data) != CFDataGetTypeID())
1066 return noErr; // foreign preset: keep current state
1067 double norm[kNumParams == 0 ? 1 : kNumParams];
1068 for (size_t i = 0; i < kNumParams; ++i)
1069 norm[i] = shadow[i].load(std::memory_order_relaxed);
1070 int program = -1;
1071 int bypassState = -1;
1072 if (applyState(user,
1073 CFDataGetBytePtr(data),
1074 static_cast<size_t>(CFDataGetLength(data)), norm,
1075 &program, &bypassState))
1076 {
1077 for (size_t i = 0; i < kNumParams; ++i)
1078 applyNormalized(static_cast<int>(i), norm[i]);
1079 if (kNumPresets > 0 && program >= 0 && program < kNumPresets)
1080 currentPresetNumber = program;
1081 if (bypassState >= 0)
1082 bypass.store(bypassState != 0, std::memory_order_relaxed);
1083 refreshLatency(); // restored state may imply a new lookahead
1084 }
1085 return noErr;
1086 }
1087 default:
1088 return kAudioUnitErr_InvalidProperty;
1089 }
1090 }
1091
1092 // --- parameters ----------------------------------------------------------------
1093
1094 OSStatus getParameter(AudioUnitParameterID id, AudioUnitScope scope,
1095 AudioUnitParameterValue* outValue) noexcept
1096 {
1097 if (scope != kAudioUnitScope_Global || outValue == nullptr)
1098 return kAudioUnitErr_InvalidParameter;
1099 if (id == kBypassParamId)
1100 {
1101 *outValue = bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
1102 return noErr;
1103 }
1104 const int idx = indexOfParamId(id);
1105 if (idx < 0) return kAudioUnitErr_InvalidParameter;
1106 *outValue = static_cast<AudioUnitParameterValue>(
1107 toPlain(P::parameters[static_cast<size_t>(idx)],
1108 shadow[static_cast<size_t>(idx)].load(std::memory_order_relaxed)));
1109 return noErr;
1110 }
1111
1112 OSStatus setParameter(AudioUnitParameterID id, AudioUnitScope scope,
1113 AudioUnitParameterValue value) noexcept
1114 {
1115 if (scope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidParameter;
1116 if (id == kBypassParamId)
1117 {
1118 bypass.store(value >= 0.5f, std::memory_order_relaxed);
1119 return noErr;
1120 }
1121 const int idx = indexOfParamId(id);
1122 if (idx < 0) return kAudioUnitErr_InvalidParameter;
1123 applyNormalized(idx,
1124 toNormalized(P::parameters[static_cast<size_t>(idx)], value));
1125 refreshLatency();
1126 return noErr;
1127 }
1128
1129 // --- MIDI (MusicDevice selectors -> lock-free ring -> render) ---------------------
1130
1132 OSStatus pushMidi(UInt32 status, UInt32 data1, UInt32 data2,
1133 UInt32 offsetFrame) noexcept
1134 {
1135 if constexpr (HasMidi<P>)
1136 {
1137 MidiEvent ev {};
1138 ev.channel = static_cast<uint8_t>(status & 0x0Fu);
1139 ev.sampleOffset = static_cast<int>(offsetFrame);
1140 const uint8_t d1 = static_cast<uint8_t>(data1 & 0x7Fu);
1141 const uint8_t d2 = static_cast<uint8_t>(data2 & 0x7Fu);
1142 switch (status & 0xF0u)
1143 {
1144 case 0x90: // wire convention: velocity 0 means note off
1145 ev.type = d2 > 0 ? MidiEvent::Type::NoteOn : MidiEvent::Type::NoteOff;
1146 ev.note = d1;
1147 ev.value = static_cast<float>(d2) / 127.0f;
1148 break;
1149 case 0x80:
1150 ev.type = MidiEvent::Type::NoteOff;
1151 ev.note = d1;
1152 ev.value = static_cast<float>(d2) / 127.0f;
1153 break;
1154 case 0xA0:
1155 ev.type = MidiEvent::Type::PolyPressure;
1156 ev.note = d1;
1157 ev.value = static_cast<float>(d2) / 127.0f;
1158 break;
1159 case 0xB0:
1160 ev.type = MidiEvent::Type::ControlChange;
1161 ev.note = d1;
1162 ev.value = static_cast<float>(d2) / 127.0f;
1163 break;
1164 case 0xD0:
1165 ev.type = MidiEvent::Type::ChannelPressure;
1166 ev.value = static_cast<float>(d1) / 127.0f;
1167 break;
1168 case 0xE0:
1169 ev.type = MidiEvent::Type::PitchBend;
1170 ev.value = (static_cast<float>((d2 << 7) | d1) - 8192.0f) / 8192.0f;
1171 break;
1172 default:
1173 return noErr; // system messages: accepted, ignored
1174 }
1175 const uint32_t tail = midiTail.load(std::memory_order_relaxed);
1176 const uint32_t next = (tail + 1) & (kMidiRingSize - 1);
1177 if (next == midiHead.load(std::memory_order_acquire))
1178 return noErr; // ring full: drop (never block the MIDI thread)
1179 midiRing[tail] = ev;
1180 midiTail.store(next, std::memory_order_release);
1181 return noErr;
1182 }
1183 else
1184 {
1185 (void) status;
1186 (void) data1;
1187 (void) data2;
1188 (void) offsetFrame;
1189 return kAudioUnitErr_InvalidProperty;
1190 }
1191 }
1192
1194 void forwardTransport() noexcept
1195 {
1196 if constexpr (HasTransport<P>)
1197 {
1198 TransportInfo info {};
1199 bool any = false;
1200 if (hostCallbacks.beatAndTempoProc != nullptr)
1201 {
1202 Float64 beat = 0.0, tempo = 0.0;
1203 if (hostCallbacks.beatAndTempoProc(hostCallbacks.hostUserData,
1204 &beat, &tempo) == noErr)
1205 {
1206 if (tempo > 0.0)
1207 {
1208 info.tempoBpm = tempo;
1209 info.tempoValid = true;
1210 }
1211 info.ppqPosition = beat;
1212 info.positionValid = true;
1213 any = true;
1214 }
1215 }
1216 if (hostCallbacks.musicalTimeLocationProc != nullptr)
1217 {
1218 UInt32 deltaToNextBeat = 0;
1219 Float32 num = 4.0f;
1220 UInt32 den = 4;
1221 Float64 downbeat = 0.0;
1222 if (hostCallbacks.musicalTimeLocationProc(
1223 hostCallbacks.hostUserData, &deltaToNextBeat, &num, &den,
1224 &downbeat) == noErr)
1225 {
1226 info.timeSigNumerator = static_cast<int>(num);
1227 info.timeSigDenominator = static_cast<int>(den);
1228 info.timeSigValid = true;
1229 info.barStartPpq = downbeat;
1230 any = true;
1231 }
1232 }
1233 if (hostCallbacks.transportStateProc != nullptr)
1234 {
1235 Boolean isPlaying = false, changed = false, isCycling = false;
1236 Float64 samplePos = 0.0, cycleStart = 0.0, cycleEnd = 0.0;
1237 if (hostCallbacks.transportStateProc(
1238 hostCallbacks.hostUserData, &isPlaying, &changed,
1239 &samplePos, &isCycling, &cycleStart, &cycleEnd) == noErr)
1240 {
1241 info.playing = isPlaying;
1242 info.looping = isCycling;
1243 info.loopStartPpq = cycleStart;
1244 info.loopEndPpq = cycleEnd;
1245 info.loopValid = isCycling;
1246 any = true;
1247 }
1248 }
1249 if (any)
1250 user.setTransport(info);
1251 }
1252 }
1253
1254 // --- render ----------------------------------------------------------------------
1255
1259 OSStatus pullInput(UInt32 element, float* dst[2],
1260 const AudioTimeStamp* timeStamp, UInt32 frames) noexcept
1261 {
1262 // AudioBufferList declares mBuffers[1] (variable length): a stack
1263 // instance has storage for ONE AudioBuffer, so writing mBuffers[1]
1264 // corrupts the frame. Give the list real storage and lend it out
1265 // as AudioBufferList* - the canonical CoreAudio pattern.
1266 struct StereoBufferList
1267 {
1268 UInt32 mNumberBuffers;
1269 ::AudioBuffer mBuffers[2]; // CoreAudio's, not dspark::AudioBuffer
1270 } list {};
1271 const UInt32 width = static_cast<UInt32>(currentChannels) < 2u
1272 ? static_cast<UInt32>(currentChannels) : 2u;
1273 list.mNumberBuffers = width;
1274 for (UInt32 ch = 0; ch < width; ++ch)
1275 {
1276 list.mBuffers[ch].mNumberChannels = 1;
1277 list.mBuffers[ch].mDataByteSize = frames * sizeof(float);
1278 list.mBuffers[ch].mData = dst[ch];
1279 }
1280 auto* abl = reinterpret_cast<AudioBufferList*>(&list);
1281 OSStatus status = noErr;
1282 AudioUnitRenderActionFlags flags = 0;
1283 if (inputCallback[element].inputProc != nullptr)
1284 status = inputCallback[element].inputProc(
1285 inputCallback[element].inputProcRefCon, &flags, timeStamp,
1286 element, frames, abl);
1287 else if (inputConnection[element] != nullptr)
1288 status = AudioUnitRender(inputConnection[element], &flags, timeStamp,
1289 inputConnectionBus[element], frames, abl);
1290 else
1291 for (UInt32 ch = 0; ch < width; ++ch)
1292 std::memset(dst[ch], 0, frames * sizeof(float));
1293 if (status != noErr) return status;
1294 for (UInt32 ch = 0; ch < width && ch < list.mNumberBuffers; ++ch)
1295 if (list.mBuffers[ch].mData != nullptr)
1296 dst[ch] = static_cast<float*>(list.mBuffers[ch].mData);
1297 return noErr;
1298 }
1299
1300 OSStatus render(AudioUnitRenderActionFlags* ioFlags,
1301 const AudioTimeStamp* timeStamp, UInt32 busNumber,
1302 UInt32 frames, AudioBufferList* ioData) noexcept
1303 {
1304 (void) busNumber;
1305 if (ioData == nullptr || frames == 0) return noErr;
1306 if (!initialized) return kAudioUnitErr_Uninitialized;
1307 if (frames > maxFrames) return kAudioUnitErr_TooManyFramesToProcess;
1308
1309 // FTZ/DAZ for the whole callback: DSPark processors guard their own
1310 // hot loops, this covers user DSP in hosts that do not set it.
1311 dspark::DenormalGuard denormalGuard;
1312
1313 forwardTransport();
1314
1315 // One timestamped stream: scheduled parameter events (already on
1316 // this thread) plus the queued MIDI from the MusicDevice selectors.
1317 BlockEvent events[kMaxBlockEvents];
1318 int eventCount = 0;
1319 for (int i = 0; i < scheduledCount; ++i)
1320 {
1321 BlockEvent ev = scheduled[i];
1322 if (ev.offset >= static_cast<int32_t>(frames))
1323 ev.offset = static_cast<int32_t>(frames) - 1;
1324 if (ev.offset < 0) ev.offset = 0;
1325 events[eventCount++] = ev;
1326 }
1327 scheduledCount = 0;
1328 if constexpr (HasMidi<P>)
1329 {
1330 uint32_t head = midiHead.load(std::memory_order_relaxed);
1331 const uint32_t tail = midiTail.load(std::memory_order_acquire);
1332 while (head != tail)
1333 {
1334 BlockEvent ev {};
1335 ev.kind = BlockEvent::Kind::Midi;
1336 ev.midi = midiRing[head];
1337 ev.offset = ev.midi.sampleOffset;
1338 if (ev.offset >= static_cast<int32_t>(frames))
1339 ev.offset = static_cast<int32_t>(frames) - 1;
1340 if (ev.offset < 0) ev.offset = 0;
1341 if (eventCount < kMaxBlockEvents) events[eventCount++] = ev;
1342 else events[kMaxBlockEvents - 1] = ev;
1343 head = (head + 1) & (kMidiRingSize - 1);
1344 }
1345 midiHead.store(head, std::memory_order_release);
1346 }
1347 sortBlockEvents(events, eventCount);
1348
1349 const UInt32 width = static_cast<UInt32>(currentChannels);
1350 const UInt32 outCh = ioData->mNumberBuffers < width
1351 ? ioData->mNumberBuffers : width;
1352
1353 // Pull the main input through the registered callback or connection
1354 // into our own buffers (instruments have no input: silence), then
1355 // process and write to the host's buffers.
1356 float* pull[2] = { pullL.data(), pullR.data() };
1357 if constexpr (!kIsInstrument)
1358 {
1359 const OSStatus pullStatus = pullInput(0, pull, timeStamp, frames);
1360 if (pullStatus != noErr) return pullStatus;
1361 }
1362
1363 float* out[2] = { nullptr, nullptr };
1364 for (UInt32 ch = 0; ch < outCh; ++ch)
1365 {
1366 if (ioData->mBuffers[ch].mData == nullptr)
1367 ioData->mBuffers[ch].mData = pull[ch];
1368 out[ch] = static_cast<float*>(ioData->mBuffers[ch].mData);
1369 ioData->mBuffers[ch].mDataByteSize = frames * sizeof(float);
1370 }
1371
1372 // Dry copy for the bypass blend; an instrument starts cleared
1373 // (voices ADD) and its bypass blends toward silence (dry stays 0).
1374 float* dry[2] = { dryL.data(), dryR.data() };
1375 for (UInt32 ch = 0; ch < outCh; ++ch)
1376 {
1377 if (kIsInstrument)
1378 {
1379 std::memset(out[ch], 0, frames * sizeof(float));
1380 continue;
1381 }
1382 std::memcpy(dry[ch], pull[ch], frames * sizeof(float));
1383 if (out[ch] != pull[ch])
1384 std::memcpy(out[ch], pull[ch], frames * sizeof(float));
1385 }
1386
1387 // Sidechain: input element 1; a missing or failing source must
1388 // never take the main path down - fall back to silence.
1389 float* sc[2] = { nullptr, nullptr };
1390 if constexpr (HasSidechain<P>)
1391 {
1392 sc[0] = scL.data();
1393 sc[1] = scR.data();
1394 if (pullInput(1, sc, timeStamp, frames) != noErr)
1395 {
1396 sc[0] = scL.data();
1397 sc[1] = scR.data();
1398 std::memset(sc[0], 0, frames * sizeof(float));
1399 std::memset(sc[1], 0, frames * sizeof(float));
1400 }
1401 }
1402
1403 // Sub-block processing at quantum-aligned event positions (the
1404 // sample-accurate default); opted out, everything applies up front.
1405 auto applyEvent = [&](const BlockEvent& ev, int blockStart) noexcept -> bool {
1406 switch (ev.kind)
1407 {
1408 case BlockEvent::Kind::Midi:
1409 if constexpr (HasMidi<P>)
1410 {
1411 MidiEvent midi = ev.midi;
1412 midi.sampleOffset = ev.offset - blockStart;
1413 if (midi.sampleOffset < 0) midi.sampleOffset = 0;
1414 user.handleMidiEvent(midi);
1415 }
1416 return false;
1417 case BlockEvent::Kind::Param:
1418 default:
1419 if (const int idx = indexOfParamId(ev.paramId); idx >= 0)
1420 {
1421 applyNormalized(idx, ev.value);
1422 return true;
1423 }
1424 return false;
1425 }
1426 };
1427 auto processSegment = [&](int start, int length) noexcept {
1428 float* sub[2] = { out[0] + start,
1429 outCh > 1 ? out[1] + start : out[0] + start };
1430 dspark::AudioBufferView<float> view(sub, static_cast<int>(outCh),
1431 length);
1432 if constexpr (HasSidechain<P>)
1433 {
1434 // The key view mirrors the main width (mono main, mono key).
1435 float* scSub[2] = { sc[0] + start, sc[1] + start };
1436 dspark::AudioBufferView<float> scView(scSub,
1437 static_cast<int>(outCh), length);
1438 user.processBlock(view, scView);
1439 }
1440 else
1441 user.processBlock(view);
1442 };
1443
1444 bool paramsChanged = false;
1445 const int total = static_cast<int>(frames);
1446 int evIdx = 0;
1447 if (!sampleAccurateOf<P>())
1448 {
1449 for (; evIdx < eventCount; ++evIdx)
1450 paramsChanged |= applyEvent(events[evIdx], 0);
1451 processSegment(0, total);
1452 }
1453 else
1454 {
1455 int pos = 0;
1456 while (pos < total)
1457 {
1458 while (evIdx < eventCount
1459 && (events[evIdx].offset / kAutomationQuantum)
1460 * kAutomationQuantum <= pos)
1461 paramsChanged |= applyEvent(events[evIdx++], pos);
1462 int next = total;
1463 if (evIdx < eventCount)
1464 {
1465 const int snapped = (events[evIdx].offset / kAutomationQuantum)
1466 * kAutomationQuantum;
1467 if (snapped < next) next = snapped;
1468 }
1469 if (next <= pos) next = pos + kAutomationQuantum < total
1470 ? pos + kAutomationQuantum : total;
1471 processSegment(pos, next - pos);
1472 pos = next;
1473 }
1474 for (; evIdx < eventCount; ++evIdx)
1475 paramsChanged |= applyEvent(events[evIdx], total);
1476 }
1477
1478 const float target = bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
1479 if (bypassMix != target || target > 0.0f)
1480 {
1481 const float step = 1.0f / static_cast<float>(kBypassRampSamples);
1482 float mix = bypassMix;
1483 for (UInt32 i = 0; i < frames; ++i)
1484 {
1485 mix += (target > mix) ? step : ((target < mix) ? -step : 0.0f);
1486 mix = mix < 0.0f ? 0.0f : (mix > 1.0f ? 1.0f : mix);
1487 for (UInt32 ch = 0; ch < outCh; ++ch)
1488 out[ch][i] += (dry[ch][i] - out[ch][i]) * mix;
1489 }
1490 bypassMix = mix;
1491 }
1492
1493 if (paramsChanged) refreshLatency();
1494
1495 (void) ioFlags;
1496 return noErr;
1497 }
1498};
1499
1500// -- component plug-in interface -----------------------------------------------------
1501
1502template <typename P>
1503struct Component
1504{
1505 AudioComponentPlugInInterface iface {};
1506 Plugin<P>* state = nullptr;
1507
1508 static Component* fromSelf(void* self) noexcept
1509 {
1510 return static_cast<Component*>(self);
1511 }
1512
1513 static OSStatus sOpen(void* self, AudioComponentInstance instance) noexcept
1514 {
1515 auto* c = fromSelf(self);
1516 c->state = new (std::nothrow) Plugin<P>();
1517 if (c->state == nullptr) return kAudio_MemFullError;
1518 c->state->instance = instance;
1519 c->state->subtype = gSubtype;
1520 c->state->manufacturer = gManufacturer;
1521 return noErr;
1522 }
1523
1524 static OSStatus sClose(void* self) noexcept
1525 {
1526 auto* c = fromSelf(self);
1527 delete c->state;
1528 delete c;
1529 return noErr;
1530 }
1531
1532 // Selector implementations (signatures from AUComponent.h).
1533
1534 static OSStatus sInitialize(void* self) noexcept
1535 { return fromSelf(self)->state->initialize(); }
1536
1537 static OSStatus sUninitialize(void* self) noexcept
1538 { return fromSelf(self)->state->uninitialize(); }
1539
1540 static OSStatus sGetPropertyInfo(void* self, AudioUnitPropertyID id,
1541 AudioUnitScope scope, AudioUnitElement element,
1542 UInt32* outSize, Boolean* outWritable) noexcept
1543 { return fromSelf(self)->state->getPropertyInfo(id, scope, element, outSize, outWritable); }
1544
1545 static OSStatus sGetProperty(void* self, AudioUnitPropertyID id,
1546 AudioUnitScope scope, AudioUnitElement element,
1547 void* outData, UInt32* ioSize) noexcept
1548 { return fromSelf(self)->state->getProperty(id, scope, element, outData, ioSize); }
1549
1550 static OSStatus sSetProperty(void* self, AudioUnitPropertyID id,
1551 AudioUnitScope scope, AudioUnitElement element,
1552 const void* inData, UInt32 inSize) noexcept
1553 { return fromSelf(self)->state->setProperty(id, scope, element, inData, inSize); }
1554
1555 static OSStatus sGetParameter(void* self, AudioUnitParameterID id,
1556 AudioUnitScope scope, AudioUnitElement element,
1557 AudioUnitParameterValue* outValue) noexcept
1558 {
1559 (void) element;
1560 return fromSelf(self)->state->getParameter(id, scope, outValue);
1561 }
1562
1563 static OSStatus sSetParameter(void* self, AudioUnitParameterID id,
1564 AudioUnitScope scope, AudioUnitElement element,
1565 AudioUnitParameterValue value,
1566 UInt32 bufferOffset) noexcept
1567 {
1568 (void) element;
1569 (void) bufferOffset;
1570 return fromSelf(self)->state->setParameter(id, scope, value);
1571 }
1572
1573 static OSStatus sReset(void* self, AudioUnitScope scope,
1574 AudioUnitElement element) noexcept
1575 {
1576 (void) scope;
1577 (void) element;
1578 return fromSelf(self)->state->reset();
1579 }
1580
1581 static OSStatus sRender(void* self, AudioUnitRenderActionFlags* ioFlags,
1582 const AudioTimeStamp* timeStamp, UInt32 busNumber,
1583 UInt32 frames, AudioBufferList* ioData) noexcept
1584 { return fromSelf(self)->state->render(ioFlags, timeStamp, busNumber, frames, ioData); }
1585
1586 static OSStatus sAddPropertyListener(void* self, AudioUnitPropertyID id,
1587 AudioUnitPropertyListenerProc proc,
1588 void* user) noexcept
1589 {
1590 auto* state = fromSelf(self)->state;
1591 state->listeners.push_back({ id, proc, user });
1592 return noErr;
1593 }
1594
1595 static OSStatus sRemovePropertyListener(void* self, AudioUnitPropertyID id,
1596 AudioUnitPropertyListenerProc proc) noexcept
1597 {
1598 auto& ls = fromSelf(self)->state->listeners;
1599 for (size_t i = ls.size(); i > 0; --i)
1600 if (ls[i - 1].id == id && ls[i - 1].proc == proc)
1601 ls.erase(ls.begin() + static_cast<ptrdiff_t>(i - 1));
1602 return noErr;
1603 }
1604
1605 static OSStatus sRemovePropertyListenerWithUserData(void* self, AudioUnitPropertyID id,
1606 AudioUnitPropertyListenerProc proc,
1607 void* user) noexcept
1608 {
1609 auto& ls = fromSelf(self)->state->listeners;
1610 for (size_t i = ls.size(); i > 0; --i)
1611 if (ls[i - 1].id == id && ls[i - 1].proc == proc && ls[i - 1].user == user)
1612 ls.erase(ls.begin() + static_cast<ptrdiff_t>(i - 1));
1613 return noErr;
1614 }
1615
1616 static OSStatus sAddRenderNotify(void*, AURenderCallback, void*) noexcept
1617 { return noErr; }
1618
1619 static OSStatus sRemoveRenderNotify(void*, AURenderCallback, void*) noexcept
1620 { return noErr; }
1621
1626 static OSStatus sScheduleParameters(void* self,
1627 const AudioUnitParameterEvent* events,
1628 UInt32 numEvents) noexcept
1629 {
1630 auto* state = fromSelf(self)->state;
1631 auto push = [&](AudioUnitParameterID id, AudioUnitScope scope,
1632 float plain, SInt32 offset) {
1633 if (scope != kAudioUnitScope_Global) return;
1634 if (id == kBypassParamId)
1635 {
1636 state->bypass.store(plain >= 0.5f, std::memory_order_relaxed);
1637 return;
1638 }
1639 const int idx = Plugin<P>::indexOfParamId(id);
1640 if (idx < 0 || state->scheduledCount >= kMaxBlockEvents) return;
1641 BlockEvent ev {};
1642 ev.offset = offset < 0 ? 0 : offset;
1643 ev.kind = BlockEvent::Kind::Param;
1644 ev.paramId = id;
1645 ev.value = toNormalized(P::parameters[static_cast<size_t>(idx)], plain);
1646 state->scheduled[state->scheduledCount++] = ev;
1647 };
1648 for (UInt32 i = 0; events != nullptr && i < numEvents; ++i)
1649 {
1650 const auto& ev = events[i];
1651 if (ev.eventType == kParameterEvent_Immediate)
1652 push(ev.parameter, ev.scope, ev.eventValues.immediate.value,
1653 static_cast<SInt32>(ev.eventValues.immediate.bufferOffset));
1654 else if (ev.eventType == kParameterEvent_Ramped)
1655 {
1656 push(ev.parameter, ev.scope, ev.eventValues.ramp.startValue,
1657 ev.eventValues.ramp.startBufferOffset);
1658 push(ev.parameter, ev.scope, ev.eventValues.ramp.endValue,
1659 ev.eventValues.ramp.startBufferOffset
1660 + static_cast<SInt32>(ev.eventValues.ramp.durationInFrames));
1661 }
1662 }
1663 return noErr;
1664 }
1665
1666 // MusicDevice selectors (aumu / aumf): raw MIDI in, queued lock-free.
1667
1668 static OSStatus sMidiEvent(void* self, UInt32 status, UInt32 data1,
1669 UInt32 data2, UInt32 offsetSampleFrame) noexcept
1670 {
1671 return fromSelf(self)->state->pushMidi(status, data1, data2,
1672 offsetSampleFrame);
1673 }
1674
1675 static OSStatus sSysEx(void* self, const UInt8*, UInt32) noexcept
1676 {
1677 (void) self;
1678 return noErr; // accepted, ignored (no sysex contract in v1)
1679 }
1680
1681 static AudioComponentMethod sLookup(SInt16 selector) noexcept
1682 {
1683 if constexpr (HasMidi<P>)
1684 {
1685 if (selector == kMusicDeviceMIDIEventSelect)
1686 return reinterpret_cast<AudioComponentMethod>(&sMidiEvent);
1687 if (selector == kMusicDeviceSysExSelect)
1688 return reinterpret_cast<AudioComponentMethod>(&sSysEx);
1689 }
1690 switch (selector)
1691 {
1692 case kAudioUnitInitializeSelect:
1693 return reinterpret_cast<AudioComponentMethod>(&sInitialize);
1694 case kAudioUnitUninitializeSelect:
1695 return reinterpret_cast<AudioComponentMethod>(&sUninitialize);
1696 case kAudioUnitGetPropertyInfoSelect:
1697 return reinterpret_cast<AudioComponentMethod>(&sGetPropertyInfo);
1698 case kAudioUnitGetPropertySelect:
1699 return reinterpret_cast<AudioComponentMethod>(&sGetProperty);
1700 case kAudioUnitSetPropertySelect:
1701 return reinterpret_cast<AudioComponentMethod>(&sSetProperty);
1702 case kAudioUnitGetParameterSelect:
1703 return reinterpret_cast<AudioComponentMethod>(&sGetParameter);
1704 case kAudioUnitSetParameterSelect:
1705 return reinterpret_cast<AudioComponentMethod>(&sSetParameter);
1706 case kAudioUnitResetSelect:
1707 return reinterpret_cast<AudioComponentMethod>(&sReset);
1708 case kAudioUnitRenderSelect:
1709 return reinterpret_cast<AudioComponentMethod>(&sRender);
1710 case kAudioUnitAddPropertyListenerSelect:
1711 return reinterpret_cast<AudioComponentMethod>(&sAddPropertyListener);
1712 case kAudioUnitRemovePropertyListenerSelect:
1713 return reinterpret_cast<AudioComponentMethod>(&sRemovePropertyListener);
1714 case kAudioUnitRemovePropertyListenerWithUserDataSelect:
1715 return reinterpret_cast<AudioComponentMethod>(&sRemovePropertyListenerWithUserData);
1716 case kAudioUnitAddRenderNotifySelect:
1717 return reinterpret_cast<AudioComponentMethod>(&sAddRenderNotify);
1718 case kAudioUnitRemoveRenderNotifySelect:
1719 return reinterpret_cast<AudioComponentMethod>(&sRemoveRenderNotify);
1720 case kAudioUnitScheduleParametersSelect:
1721 return reinterpret_cast<AudioComponentMethod>(&sScheduleParameters);
1722 default:
1723 return nullptr;
1724 }
1725 }
1726
1727 inline static OSType gSubtype = 0;
1728 inline static OSType gManufacturer = 0;
1729
1730 static void* factory(const AudioComponentDescription* desc) noexcept
1731 {
1732 // The Info.plist entry must declare the type the class implies:
1733 // aumu (Instrument), aumf (HasMidi effect) or aufx.
1734 if (desc != nullptr && desc->componentType != Plugin<P>::expectedType())
1735 return nullptr;
1736 auto* c = new (std::nothrow) Component();
1737 if (c == nullptr) return nullptr;
1738 c->iface.Open = &sOpen;
1739 c->iface.Close = &sClose;
1740 c->iface.Lookup = &sLookup;
1741 c->iface.reserved = nullptr;
1742 return c;
1743 }
1744};
1745
1746} // namespace dspark::plugin::au
1747
1754#define DSPARK_AU_PLUGIN(PluginClass, subtype4, manufacturer4) \
1755 extern "C" __attribute__((visibility("default"))) \
1756 void* DSParkAuFactory(const AudioComponentDescription* desc) \
1757 { \
1758 using Comp = dspark::plugin::au::Component<PluginClass>; \
1759 Comp::gSubtype = dspark::plugin::au::fourCC(subtype4); \
1760 Comp::gManufacturer = dspark::plugin::au::fourCC(manufacturer4); \
1761 return Comp::factory(desc); \
1762 }
1763
1764#else // !__APPLE__
1765
1766// Self-disabling off macOS so one translation unit can target every format.
1767#define DSPARK_AU_PLUGIN(PluginClass, subtype4, manufacturer4)
1768
1769#endif // __APPLE__
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35