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