diff --git a/.gitignore b/.gitignore
index d38a0f660..8e7b07be9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@ out/*
# Ides/Agents
__pycache__/
.juggler/
+.reasonix/
.vscode/
.idea/
.vs/
@@ -51,4 +52,3 @@ __pycache__/
.pytest_cache/
.cache/
docs/superpowers/
-reasonix.toml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad30c5d2d..456a5b9eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -137,6 +137,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- SDL3 windowing: mouse move/drag was broken on touch platforms (iOS, Android). Motion was synthesized only by polling `SDL_GetGlobalMouseState`, which has no backend implementation there and falls back to window-relative coordinates, so subtracting the window position shifted every move. Touch platforms now consume the touch-synthesized `SDL_EVENT_MOUSE_MOTION` events directly; desktop keeps the global-cursor poll (needed for embedded plugin editors).
- SDL3 windowing: mouse drag events were lost inside embedded plugin editors (notably on macOS, where the host owns the native application so SDL never receives Cocoa mouse focus and suppresses drag motion). Dragging is now synthesized by polling the global cursor while a button is held, on the message thread, for all platforms.
- UBSAN and ASAN fixes throughout the codebase
+- AUv3 plugin host bypass is now connected to the processor: the wrapper-owned bypass parameter is created and drives `processBlockBypassed`, and host bypass state is persisted/restored inside the `YUPProcessorState` blob (legacy raw processor state still loads)
+- Added bypass parameter handling tests for the AU, CLAP, and VST3 plugin client wrappers (routing to `processBlockBypassed`, bypass state round-trip, and text/value conversion)
---
@@ -229,6 +231,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Standalone plugin support with improved audio parameters ([#46](https://github.com/kunitoki/yup/pull/46))
- CLAP/VST3/AU validators and code signing (`YUP_ENABLE_VST3_VALIDATOR`, etc.) ([#106](https://github.com/kunitoki/yup/pull/106))
- pluginval integration for automated VST3 validation ([#67](https://github.com/kunitoki/yup/pull/67))
+- Sidechain and multi-bus audio input support across VST3, CLAP, AUv3, AUv2, AAX, and LV2: `AudioBus` gains a `Role` (`Main`/`Auxiliary`) and `isDefaultActive`, `AudioProcessContext` exposes per-bus `inputs`/`outputs` views (`AudioBusBufferView`) with `getMainInput()`/`getAuxiliaryInput()`/`getMainOutput()` accessors, and secondary input buses are forwarded to the processor instead of being discarded
#### Audio Formats (`yup_audio_formats`)
- New `yup_audio_formats` module: `AudioFormat`, `AudioFormatManager`, `AudioFormatReader`, `AudioFormatWriter`, WAV codec ([#51](https://github.com/kunitoki/yup/pull/51))
diff --git a/cmake/platforms/mac/AudioUnitV3ContainerInfo.plist.in b/cmake/platforms/mac/AudioUnitV3ContainerInfo.plist.in
index d1830b676..4b814c925 100644
--- a/cmake/platforms/mac/AudioUnitV3ContainerInfo.plist.in
+++ b/cmake/platforms/mac/AudioUnitV3ContainerInfo.plist.in
@@ -7,7 +7,7 @@
CFBundleExecutable
${MACOSX_BUNDLE_EXECUTABLE_NAME}
CFBundleIdentifier
- @auv3_container_bundle_identifier@
+ ${MACOSX_BUNDLE_GUI_IDENTIFIER}
CFBundleInfoDictionaryVersion
6.0
CFBundleName
diff --git a/cmake/plugins/yup_plugin_auv3.cmake b/cmake/plugins/yup_plugin_auv3.cmake
index e00c8f075..f8a5cedc6 100644
--- a/cmake/plugins/yup_plugin_auv3.cmake
+++ b/cmake/plugins/yup_plugin_auv3.cmake
@@ -178,6 +178,7 @@ function (yup_plugin_auv3)
MACOSX_BUNDLE_INFO_PLIST "${auv3_container_plist_output}"
MACOSX_BUNDLE_BUNDLE_NAME "${YUP_ARG_PLUGIN_NAME}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${auv3_container_bundle_identifier}"
+ XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${auv3_container_bundle_identifier}"
FOLDER "${target_ide_group}"
XCODE_GENERATE_SCHEME ON)
diff --git a/examples/plugin/data/RobotoFlex-VariableFont.ttf b/examples/plugin/data/RobotoFlex-VariableFont.ttf
deleted file mode 100644
index 2e5c2a26a..000000000
Binary files a/examples/plugin/data/RobotoFlex-VariableFont.ttf and /dev/null differ
diff --git a/modules/yup_audio_devices/yup_audio_devices.h b/modules/yup_audio_devices/yup_audio_devices.h
index 5324bacf3..a93d6cb91 100644
--- a/modules/yup_audio_devices/yup_audio_devices.h
+++ b/modules/yup_audio_devices/yup_audio_devices.h
@@ -177,7 +177,7 @@
Enable logging of audio device events on macOS, such as device changes and errors.
*/
#ifndef YUP_ENABLE_CORE_AUDIO_LOGGING
-#define YUP_ENABLE_CORE_AUDIO_LOGGING 1
+#define YUP_ENABLE_CORE_AUDIO_LOGGING 0
#endif
//==============================================================================
diff --git a/modules/yup_audio_plugin_client/aax/yup_audio_plugin_client_AAX.cpp b/modules/yup_audio_plugin_client/aax/yup_audio_plugin_client_AAX.cpp
index 2634a4acd..ac50ebea5 100644
--- a/modules/yup_audio_plugin_client/aax/yup_audio_plugin_client_AAX.cpp
+++ b/modules/yup_audio_plugin_client/aax/yup_audio_plugin_client_AAX.cpp
@@ -77,7 +77,7 @@ YupPlugin_AAX_PlugInID_Native and YupPlugin_AAX_PlugInID_AudioSuite to be define
#endif
#endif
-extern "C" yup::AudioProcessor* createPluginProcessor();
+extern "C" yup::AudioProcessor* YUP_AUDIO_PLUGIN_CREATE_FUNCTION();
namespace yup
{
@@ -278,7 +278,7 @@ class YupAAX_Processor
YupAAX_Processor()
{
- processor.reset (createPluginProcessor());
+ processor.reset (::YUP_AUDIO_PLUGIN_CREATE_FUNCTION());
processor->addListener (this);
AAX_CEffectParameters::GetNumberOfChunks (&yupChunkIndex);
@@ -613,6 +613,63 @@ class YupAAX_Processor
// Audio processing
//==========================================================================
+ // Builds one AudioBusBufferView per audio input bus from the flat array of
+ // host-provided input channel buffers. Channels are consumed in bus
+ // declaration order; channels beyond the host-provided count (e.g. an
+ // unconnected sidechain) are exposed as null channel pointers so
+ // processors see deterministic silence.
+ void buildInputBusViews (float* const* inputChannelData, int numIn)
+ {
+ inputBusViews.clear();
+
+ int chOffset = 0;
+
+ for (const auto& bus : processor->getBusLayout().getInputBuses())
+ {
+ if (bus.getType() != AudioBus::Type::Audio)
+ continue;
+
+ const int numCh = bus.getNumChannels();
+ auto* chPtrs = inputChannelPtrStorage.data() + chOffset;
+
+ for (int ch = 0; ch < numCh; ++ch)
+ chPtrs[ch] = (inputChannelData != nullptr && chOffset + ch < numIn)
+ ? inputChannelData[chOffset + ch]
+ : nullptr;
+
+ inputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
+ chOffset += numCh;
+ }
+ }
+
+ // Builds one AudioBusBufferView per audio output bus from the flat array of
+ // host-provided output channel buffers.
+ void buildOutputBusViews (float* const* outputChannelData, int numOut)
+ {
+ outputBusViews.clear();
+
+ int chOffset = 0;
+
+ for (const auto& bus : processor->getBusLayout().getOutputBuses())
+ {
+ if (bus.getType() != AudioBus::Type::Audio)
+ continue;
+
+ const int numCh = bus.getNumChannels();
+ auto* chPtrs = outputChannelPtrStorage.data() + chOffset;
+
+ for (int ch = 0; ch < numCh; ++ch)
+ chPtrs[ch] = (outputChannelData != nullptr && chOffset + ch < numOut)
+ ? outputChannelData[chOffset + ch]
+ : nullptr;
+
+ outputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
+ chOffset += numCh;
+ }
+ }
+
void process (float* const* inputChannelData,
float* const* outputChannelData,
int bufferSize,
@@ -636,12 +693,21 @@ class YupAAX_Processor
const auto numIn = layout.getNumAudioInputChannels();
const auto numOut = layout.getNumAudioOutputChannels();
+ // Only Main-role input channels are copied into the outputs; auxiliary
+ // (sidechain) channels must never leak into the main signal path. This
+ // assumes the main input buses are declared before auxiliary buses, which
+ // is the YUP layout convention (mirrored by VST3's kMain-before-kAux rule).
+ int numMainIn = 0;
+ for (const auto& bus : layout.getInputBuses())
+ if (bus.getType() == AudioBus::Type::Audio && bus.getRole() == AudioBus::Role::Main)
+ numMainIn += bus.getNumChannels();
+
for (int ch = 0; ch < numOut; ++ch)
{
if (outputChannelData[ch] == nullptr)
continue;
- if (ch < numIn && inputChannelData[ch] != nullptr)
+ if (inputChannelData != nullptr && ch < numMainIn && inputChannelData[ch] != nullptr)
{
if (outputChannelData[ch] != inputChannelData[ch])
std::memcpy (outputChannelData[ch], inputChannelData[ch], static_cast (bufferSize) * sizeof (float));
@@ -654,10 +720,18 @@ class YupAAX_Processor
AudioBuffer audioBuffer (outputChannelData, numOut, bufferSize);
+ buildInputBusViews (inputChannelData, numIn);
+ buildOutputBusViews (outputChannelData, numOut);
+
paramChangeBuffer.clear();
AudioProcessContext context {
- audioBuffer, midiBuffer, paramChangeBuffer, nullptr
+ audioBuffer,
+ midiBuffer,
+ paramChangeBuffer,
+ nullptr,
+ { inputBusViews.data(), inputBusViews.size() },
+ { outputBusViews.data(), outputBusViews.size() }
};
processAudioBlock (*processor, context, currentlyBypassed);
@@ -712,6 +786,12 @@ class YupAAX_Processor
paramChangeBuffer.reserve (getDefaultParameterChangeCapacity (*processor));
+ // Pre-allocate per-bus view storage so the algorithm callback never allocates
+ inputBusViews.reserve (static_cast (processor->getNumAudioInputs()));
+ outputBusViews.reserve (static_cast (processor->getNumAudioOutputs()));
+ inputChannelPtrStorage.resize (static_cast (processor->getBusLayout().getNumAudioInputChannels()));
+ outputChannelPtrStorage.resize (static_cast (processor->getBusLayout().getNumAudioOutputChannels()));
+
if (auto* ctrl = Controller())
ctrl->SetSignalLatency (processor->getLatencySamples());
@@ -971,6 +1051,13 @@ class YupAAX_Processor
MidiBuffer midiBuffer;
ParameterChangeBuffer paramChangeBuffer;
+ // Per-bus view state, pre-allocated in preparePlugin so the real-time
+ // algorithm callback only clears and refills the vectors without allocating
+ std::vector> inputBusViews;
+ std::vector> outputBusViews;
+ std::vector inputChannelPtrStorage;
+ std::vector outputChannelPtrStorage;
+
std::unordered_map paramMap;
std::vector aaxMeters;
@@ -1153,7 +1240,7 @@ void AAX_CALLBACK yupAAXAlgorithmCallback (void* const instancesBegin[], const v
static void getPlugInDescription (AAX_IEffectDescriptor& descriptor)
{
- std::unique_ptr plugin (createPluginProcessor());
+ std::unique_ptr plugin (::YUP_AUDIO_PLUGIN_CREATE_FUNCTION());
descriptor.AddName (YupPlugin_Name);
descriptor.AddName (YupPlugin_Description);
diff --git a/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm
index 5a314d242..ca0e51f46 100644
--- a/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm
+++ b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm
@@ -21,6 +21,7 @@
#include "../yup_audio_plugin_client.h"
+#include "../common/yup_AudioPluginAUHelpers.h"
#include "../common/yup_AudioPluginUtilities.h"
#if ! defined(YUP_AUDIO_PLUGIN_ENABLE_AU)
@@ -50,7 +51,7 @@
//==============================================================================
-extern "C" yup::AudioProcessor* createPluginProcessor();
+extern "C" yup::AudioProcessor* YUP_AUDIO_PLUGIN_CREATE_FUNCTION();
@class AudioPluginEditorViewAU;
@@ -62,11 +63,6 @@ static String describeScopeAndElement (AudioUnitScope scope, AudioUnitElement el
return "scope=" + String (static_cast (scope)) + ", element=" + String (static_cast (element));
}
-static String describePointer (const void* value)
-{
- return "0x" + String::toHexString (static_cast (reinterpret_cast (value)));
-}
-
static String describeStatus (OSStatus status)
{
return String (static_cast (status));
@@ -74,85 +70,6 @@ static String describeStatus (OSStatus status)
//==============================================================================
-namespace
-{
-
-//==============================================================================
-
-static CFStringRef getProcessorStateKey()
-{
- return CFSTR ("YUPProcessorState");
-}
-
-//==============================================================================
-
-struct AUScopedYupInitialiser
-{
- AUScopedYupInitialiser()
- {
- if (numAUScopedInitInstances.fetch_add (1) == 0)
- {
- YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "initialising YUP GUI");
- initialiseYup_GUI();
- }
- }
-
- ~AUScopedYupInitialiser()
- {
- if (numAUScopedInitInstances.fetch_sub (1) == 1)
- {
- YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "shutting down YUP GUI");
- shutdownYup_GUI();
- }
- }
-
-private:
- static std::atomic_int numAUScopedInitInstances;
-};
-
-std::atomic_int AUScopedYupInitialiser::numAUScopedInitInstances = 0;
-
-struct AUScopedYupWindowingInitialiser
-{
- AUScopedYupWindowingInitialiser()
- {
- if (numAUScopedInitInstances.fetch_add (1) == 0)
- {
- YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "initialising YUP windowing for editor");
- initialiseYup_Windowing();
- }
- }
-
- ~AUScopedYupWindowingInitialiser()
- {
- if (numAUScopedInitInstances.fetch_sub (1) == 1)
- {
- YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "shutting down YUP windowing for editor");
- shutdownYup_Windowing();
- }
- }
-
-private:
- static std::atomic_int numAUScopedInitInstances;
-};
-
-std::atomic_int AUScopedYupWindowingInitialiser::numAUScopedInitInstances = 0;
-
-//==============================================================================
-
-static OSType osTypeFromString (const char* s)
-{
- if (s == nullptr || std::strlen (s) < 4)
- return 0;
-
- return static_cast (
- (static_cast (static_cast (s[0])) << 24) | (static_cast (static_cast (s[1])) << 16) | (static_cast (static_cast (s[2])) << 8) | static_cast (static_cast (s[3])));
-}
-
-} // namespace
-
-//==============================================================================
-
#if YupPlugin_IsSynth
using AudioPluginAUBase = ausdk::MusicDeviceBase;
#else
@@ -208,7 +125,7 @@ bool canControlTransport() override
#endif
componentInstance (component)
{
- processor.reset (::createPluginProcessor());
+ processor.reset (::YUP_AUDIO_PLUGIN_CREATE_FUNCTION());
YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "created processor instance: wrapper=" << yup::describePointer (this) << ", component=" << yup::describePointer (componentInstance) << ", processor=" << yup::describePointer (processor.get()));
@@ -251,6 +168,13 @@ OSStatus Initialize() override
return kAudioUnitErr_FailedInitialization;
}
+#if ! YupPlugin_IsSynth
+ // Expose one input element per audio input bus so hosts can route
+ // sidechain signals to auxiliary input elements (element 0 is always
+ // the main input). Processors with a single input bus keep one element.
+ SetNumberOfElements (kAudioUnitScope_Input, static_cast (processor->getNumAudioInputs()));
+#endif
+
processor->setOfflineProcessing (renderingOffline);
processor->setPlaybackConfiguration (static_cast (getCurrentSampleRate()),
static_cast (GetMaxFramesPerSlice()));
@@ -263,6 +187,12 @@ OSStatus Initialize() override
emptyParamChangeBuffer.reserve (getDefaultParameterChangeCapacity (*processor));
audioChannels.reserve (static_cast (getTotalAudioOutputChannels (*processor)));
+ // Pre-allocate per-bus view storage so the render callback never allocates
+ renderViews.prepare (*processor,
+ getTotalAudioInputChannels (*processor),
+ getTotalAudioOutputChannels (*processor));
+ auxiliaryInputValid.resize (static_cast (processor->getNumAudioInputs()), 0);
+
YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "Initialize completed: sampleRate=" << String (getCurrentSampleRate()) << ", maxFramesPerSlice=" << String (static_cast (GetMaxFramesPerSlice())));
return noErr;
@@ -316,15 +246,12 @@ OSStatus GetParameterInfo (AudioUnitScope inScope,
const auto& param = parameters[parameterIndex];
- outParameterInfo.flags = kAudioUnitParameterFlag_IsReadable | kAudioUnitParameterFlag_HasCFNameString;
-
- if (! param->isReadOnly())
- outParameterInfo.flags |= kAudioUnitParameterFlag_IsWritable;
+ outParameterInfo.flags = makeAUParameterFlags (*param) | kAudioUnitParameterFlag_HasCFNameString;
outParameterInfo.cfNameString = param->getName().toCFString();
param->getName().copyToUTF8 (outParameterInfo.name, sizeof (outParameterInfo.name));
- outParameterInfo.unit = kAudioUnitParameterUnit_Generic;
+ outParameterInfo.unit = makeAUUnit (*param);
outParameterInfo.minValue = param->getMinimumValue();
outParameterInfo.maxValue = param->getMaximumValue();
outParameterInfo.defaultValue = param->getDefaultValue();
@@ -618,6 +545,116 @@ OSStatus HandleSysEx (const UInt8* inData, UInt32 inLength) override
}
#else
+ // Effect: pull auxiliary (sidechain) input elements before the base class
+ // renders element 0, so their buffers are populated when ProcessBufferLists
+ // builds the per-bus input views.
+ OSStatus Render (AudioUnitRenderActionFlags& ioActionFlags,
+ const AudioTimeStamp& inTimeStamp,
+ UInt32 inNumberFrames) override
+ {
+ if (processor != nullptr)
+ pullAuxiliaryInputElements (ioActionFlags, inTimeStamp, inNumberFrames);
+
+ return AudioPluginAUBase::Render (ioActionFlags, inTimeStamp, inNumberFrames);
+ }
+
+ //==============================================================================
+
+ void pullAuxiliaryInputElements (AudioUnitRenderActionFlags& ioActionFlags,
+ const AudioTimeStamp& inTimeStamp,
+ UInt32 inNumberFrames)
+ {
+ const auto numInputs = static_cast (processor->getNumAudioInputs());
+ const auto numElements = Inputs().GetNumberOfElements();
+
+ for (UInt32 element = 1; element < numInputs && element < numElements; ++element)
+ {
+ auto& input = Input (element);
+
+ // Only connected elements produce audio; disconnected sidechain
+ // elements stay silent and are exposed as null-buffer views below
+ const auto pullResult = input.IsActive()
+ ? input.PullInput (ioActionFlags, inTimeStamp, element, inNumberFrames)
+ : kAudioUnitErr_NoConnection;
+
+ auxiliaryInputValid[static_cast (element)] = (pullResult == noErr);
+ }
+ }
+
+ //==============================================================================
+
+ // Builds one AudioBusBufferView per audio input bus. Element 0 (the main
+ // input) is mapped from the passed buffer list; elements 1..n (auxiliary /
+ // sidechain inputs) are mapped from their own pulled buffer lists. Views are
+ // real-time safe: they reuse pre-allocated channel-pointer storage.
+ void buildInputBusViews (const AudioBufferList& mainInBuffer)
+ {
+ renderViews.inputBusViews.clear();
+
+ int chOffset = 0;
+ int audioIdx = 0;
+
+ for (const auto& bus : processor->getBusLayout().getInputBuses())
+ {
+ if (bus.getType() != AudioBus::Type::Audio)
+ continue;
+
+ const int numCh = bus.getNumChannels();
+ auto* chPtrs = renderViews.inputChannelPtrStorage.data() + chOffset;
+
+ if (audioIdx == 0)
+ {
+ const UInt32 copyCount = std::min (mainInBuffer.mNumberBuffers, static_cast (numCh));
+ for (UInt32 ch = 0; ch < copyCount; ++ch)
+ chPtrs[ch] = static_cast (mainInBuffer.mBuffers[ch].mData);
+ }
+ else if (auxiliaryInputValid[static_cast (audioIdx)])
+ {
+ const auto& busBufferList = Input (static_cast (audioIdx)).GetBufferList();
+ const UInt32 copyCount = std::min (busBufferList.mNumberBuffers, static_cast (numCh));
+ for (UInt32 ch = 0; ch < copyCount; ++ch)
+ chPtrs[ch] = static_cast (busBufferList.mBuffers[ch].mData);
+ }
+
+ renderViews.inputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
+ chOffset += numCh;
+ ++audioIdx;
+ }
+ }
+
+ // Builds one AudioBusBufferView per audio output bus from the output buffer
+ // list, consuming channels sequentially across buses.
+ void buildOutputBusViews (const AudioBufferList& outBuffer)
+ {
+ renderViews.outputBusViews.clear();
+
+ int chOffset = 0;
+
+ for (const auto& bus : processor->getBusLayout().getOutputBuses())
+ {
+ if (bus.getType() != AudioBus::Type::Audio)
+ continue;
+
+ const int numCh = bus.getNumChannels();
+ auto* chPtrs = renderViews.outputChannelPtrStorage.data() + chOffset;
+
+ std::fill (chPtrs, chPtrs + numCh, nullptr);
+
+ const auto available = outBuffer.mNumberBuffers > static_cast (chOffset)
+ ? static_cast (numCh)
+ : 0u;
+ const UInt32 copyCount = std::min (available, outBuffer.mNumberBuffers - static_cast (chOffset));
+
+ for (UInt32 ch = 0; ch < copyCount; ++ch)
+ chPtrs[ch] = static_cast (outBuffer.mBuffers[static_cast (chOffset) + ch].mData);
+
+ renderViews.outputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
+ chOffset += numCh;
+ }
+ }
+
// Effect: copy input to output and call processBlock
OSStatus ProcessBufferLists (AudioUnitRenderActionFlags& ioActionFlags,
const AudioBufferList& inBuffer,
@@ -646,6 +683,9 @@ AudioSampleBuffer audioBuffer (audioChannels.data(),
0,
static_cast (inFramesToProcess));
+ buildInputBusViews (inBuffer);
+ buildOutputBusViews (outBuffer);
+
AudioPluginPlayHeadAU playHead (*this, nullptr);
std::unique_lock parameterLock (parameterChangeMutex, std::try_to_lock);
auto& processParamChangeBuffer = parameterLock.owns_lock() ? paramChangeBuffer : emptyParamChangeBuffer;
@@ -653,7 +693,9 @@ AudioSampleBuffer audioBuffer (audioChannels.data(),
AudioProcessContext context { audioBuffer,
midiBuffer,
processParamChangeBuffer,
- &playHead };
+ &playHead,
+ { renderViews.inputBusViews.data(), renderViews.inputBusViews.size() },
+ { renderViews.outputBusViews.data(), renderViews.outputBusViews.size() } };
processAudioBlock (*processor, context, isBypassed);
midiBuffer.clear();
processParamChangeBuffer.clear();
@@ -703,7 +745,7 @@ OSStatus SaveState (CFPropertyListRef* outData) override
auto* stateDictionary = const_cast (static_cast (*outData));
CFDictionarySetValue (stateDictionary,
- getProcessorStateKey(),
+ getAUProcessorStateKey(),
(__bridge CFDataRef) nsData);
}
@@ -728,7 +770,7 @@ OSStatus RestoreState (CFPropertyListRef inData) override
if (CFGetTypeID (inData) == CFDictionaryGetTypeID())
{
processorState = static_cast (CFDictionaryGetValue (static_cast (inData),
- getProcessorStateKey()));
+ getAUProcessorStateKey()));
if (processorState != nullptr && CFGetTypeID (processorState) != CFDataGetTypeID())
return kAudioUnitErr_InvalidPropertyValue;
@@ -1084,6 +1126,11 @@ Float64 getCurrentSampleRate()
std::vector listenedParameters;
std::vector audioChannels;
std::vector editorViews;
+
+ // Per-bus view state, pre-allocated in Initialize so the render callback
+ // only clears and refills the vectors without allocating
+ AudioPluginAURenderViews renderViews;
+ std::vector auxiliaryInputValid; // per-element pull success (effect path)
AudioUnit componentInstance = nullptr;
bool renderingOffline = false;
bool isBypassed = false;
diff --git a/modules/yup_audio_plugin_client/auv3/yup_audio_plugin_client_AUv3.mm b/modules/yup_audio_plugin_client/auv3/yup_audio_plugin_client_AUv3.mm
index d251acd3d..d8d9a43bb 100644
--- a/modules/yup_audio_plugin_client/auv3/yup_audio_plugin_client_AUv3.mm
+++ b/modules/yup_audio_plugin_client/auv3/yup_audio_plugin_client_AUv3.mm
@@ -21,6 +21,7 @@
#include "../yup_audio_plugin_client.h"
+#include "../common/yup_AudioPluginAUHelpers.h"
#include "../common/yup_AudioPluginUtilities.h"
#if ! defined(YUP_AUDIO_PLUGIN_ENABLE_AUv3)
@@ -50,55 +51,51 @@
//==============================================================================
-extern "C" yup::AudioProcessor* createPluginProcessor();
+NS_ASSUME_NONNULL_BEGIN
+
+extern "C" yup::AudioProcessor* YUP_AUDIO_PLUGIN_CREATE_FUNCTION();
namespace yup
{
//==============================================================================
-static String describePointer (const void* value)
+/** Returns value-strings for enumerated/stepped parameters so the host
+ can display discrete choices instead of a raw numeric value.
+*/
+static NSArray* _Nullable makeAUValueStrings (const AudioParameter& param)
{
- return "0x" + String::toHexString (static_cast (reinterpret_cast (value)));
-}
+ if (! param.isEnum() && ! param.isStepped())
+ return nil;
-//==============================================================================
+ const int numSteps = param.getNumSteps();
+ if (numSteps <= 0)
+ return nil;
-struct AUScopedYupInitialiser
-{
- AUScopedYupInitialiser()
- {
- if (numAUScopedInitInstances.fetch_add (1) == 0)
- {
- YUP_MODULE_DBG (PLUGIN_CLIENT_AUV3, "initialising YUP GUI");
- initialiseYup_GUI();
- }
- }
+ const int numValues = numSteps + 1; // a range with N steps has N+1 discrete values
+ auto* strings = [[NSMutableArray alloc] initWithCapacity:static_cast (numValues)];
+
+ const float stepSize = (param.getMaximumValue() - param.getMinimumValue()) / static_cast (numSteps);
- ~AUScopedYupInitialiser()
+ for (int i = 0; i < numValues; ++i)
{
- if (numAUScopedInitInstances.fetch_sub (1) == 1)
- {
- YUP_MODULE_DBG (PLUGIN_CLIENT_AUV3, "shutting down YUP GUI");
- shutdownYup_GUI();
- }
+ const auto stepValue = param.getMinimumValue() + static_cast (i) * stepSize;
+ [strings addObject:yupStringToNS (param.convertToString (stepValue))];
}
-private:
- static std::atomic_int numAUScopedInitInstances;
-};
-
-std::atomic_int AUScopedYupInitialiser::numAUScopedInitInstances = 0;
-
-//==============================================================================
-
-static float getMaximumParameterValue (const AudioParameter& p)
-{
- return p.getMaximumValue();
+ return strings;
}
//==============================================================================
+/** Magic/version used to wrap the YUP processor state blob with wrapper-owned
+ bypass state so host bypass survives preset/session restore, matching the
+ approach used by the VST3, CLAP, LV2, and AAX wrappers. Legacy raw processor
+ state (without this magic) is still loaded via readWrapperBypassState's fallback.
+*/
+constexpr int auv3WrapperStateMagic = 0x33564159; // "YAV3"
+constexpr int auv3WrapperStateVersion = 1;
+
} // namespace yup
//==============================================================================
@@ -126,7 +123,7 @@ static float getMaximumParameterValue (const AudioParameter& p)
NSError**)
: au (audioUnit)
{
- processor.reset (::createPluginProcessor());
+ processor.reset (::YUP_AUDIO_PLUGIN_CREATE_FUNCTION());
init();
}
@@ -155,6 +152,8 @@ static float getMaximumParameterValue (const AudioParameter& p)
void init()
{
+ jassert (au != nil); // The AUAudioUnit must be set before initialization
+
if (processor == nullptr)
return;
@@ -174,23 +173,17 @@ void init()
if (bus.getType() == AudioBus::Type::Audio)
totalOutChannels += bus.getNumChannels();
- // Build channel capabilities
+ // Build channel capabilities (one entry per audio bus)
{
channelCapabilities.reset ([[NSMutableArray alloc] init]);
- int maxInputCh = 0;
- int maxOutputCh = 0;
-
for (const auto& bus : busLayout.getInputBuses())
if (bus.getType() == AudioBus::Type::Audio)
- maxInputCh = std::max (maxInputCh, bus.getNumChannels());
+ [channelCapabilities.get() addObject:[NSNumber numberWithInteger:bus.getNumChannels()]];
for (const auto& bus : busLayout.getOutputBuses())
if (bus.getType() == AudioBus::Type::Audio)
- maxOutputCh = std::max (maxOutputCh, bus.getNumChannels());
-
- [channelCapabilities.get() addObject:[NSNumber numberWithInteger:maxInputCh]];
- [channelCapabilities.get() addObject:[NSNumber numberWithInteger:maxOutputCh]];
+ [channelCapabilities.get() addObject:[NSNumber numberWithInteger:bus.getNumChannels()]];
}
internalRenderBlock = CreateObjCBlock (this, &AudioPluginProcessorAUv3::renderCallback);
@@ -231,6 +224,19 @@ void addParameters()
addressForIndex[i] = address;
}
+ // Wrapper-owned bypass parameter backing the host's AUv3 bypass property.
+ // It is deliberately not added to the processor or the AU parameter tree:
+ // the host drives it through setShouldBypassEffect:, the render callback
+ // reads it directly, and it is persisted inside the YUPProcessorState blob.
+ auto bypassMetadata = AudioParameter::Metadata {};
+ bypassMetadata.name = "Bypass";
+ bypassMetadata.hostParameterID = getBypassHostParameterID (*processor);
+ bypassMetadata.valueRange = { 0.0f, 1.0f, 1.0f };
+ bypassMetadata.defaultValue = 0.0f;
+ bypassMetadata.setStepped (true);
+
+ bypassHostParam = std::make_unique ("bypass", bypassMetadata);
+
installParameterTree (createTopLevelNodes());
}
@@ -300,10 +306,12 @@ void installParameterTree (NSMutableArray* topLevelNodes)
address:address
min:minVal
max:maxVal
- unit:kAudioUnitParameterUnit_Generic
- unitName:nil
- flags:0
- valueStrings:nil
+ unit:makeAUUnit (*param)
+ unitName:(param->getUnit() == AudioParameter::ParameterUnit::Custom
+ ? yupStringToNS (param->getUnitName())
+ : nil)
+ flags:makeAUv3ParameterFlags (*param)
+ valueStrings:makeAUValueStrings (*param)
dependentParameters:nil];
if (auParam != nullptr)
@@ -325,11 +333,9 @@ void valueChangedFromHost (AUParameter* param, AUValue value)
if (yupParam == nullptr)
return;
- const auto normalisedValue = static_cast (value) / getMaximumParameterValue (*yupParam);
-
- if (! approximatelyEqual (normalisedValue, yupParam->getNormalizedValue()))
+ if (! approximatelyEqual (static_cast (value), yupParam->getValue()))
{
- yupParam->setNormalizedValue (normalisedValue);
+ yupParam->setValue (static_cast (value));
inParameterChangedCallback = true;
yupParam->beginChangeGesture();
@@ -346,7 +352,7 @@ AUValue getValueForHost (AUParameter* param) const
if (yupParam == nullptr)
return 0;
- return static_cast (yupParam->getNormalizedValue() * getMaximumParameterValue (*yupParam));
+ return static_cast (yupParam->getValue());
}
NSString* stringFromValue (AUParameter* param, const AUValue* value) const
@@ -358,8 +364,7 @@ AUValue getValueForHost (AUParameter* param) const
if (yupParam == nullptr)
return @"";
- const auto normalised = static_cast (*value) / getMaximumParameterValue (*yupParam);
- return yupStringToNS (yupParam->convertToString (normalised));
+ return yupStringToNS (yupParam->convertToString (static_cast (*value)));
}
AUValue valueFromString (AUParameter* param, NSString* str) const
@@ -371,11 +376,10 @@ AUValue valueFromString (AUParameter* param, NSString* str) const
if (yupParam == nullptr)
return 0;
- const auto normalised = yupParam->convertFromString (String::fromCFString ((__bridge CFStringRef) str));
- return static_cast (normalised * getMaximumParameterValue (*yupParam));
+ return static_cast (yupParam->convertFromString (String::fromCFString ((__bridge CFStringRef) str)));
}
- AudioParameter* getParamForAUAddress (AUParameterAddress address) const
+ AudioParameter* _Nullable getParamForAUAddress (AUParameterAddress address) const
{
for (size_t i = 0; i < addressForIndex.size(); ++i)
{
@@ -390,7 +394,7 @@ AUValue valueFromString (AUParameter* param, NSString* str) const
return nullptr;
}
- AudioParameter* getParamForIndex (int index) const
+ AudioParameter* _Nullable getParamForIndex (int index) const
{
const auto parameters = processor->getParameters();
if (isPositiveAndBelow (index, static_cast (parameters.size())))
@@ -429,7 +433,7 @@ void addPresets()
return factoryPresets.get();
}
- AUAudioUnitPreset* getCurrentPreset() const
+ AUAudioUnitPreset* _Nullable getCurrentPreset() const
{
if (processor == nullptr)
return nil;
@@ -466,10 +470,18 @@ void setCurrentPreset (AUAudioUnitPreset* preset)
if (processor != nullptr)
processor->saveStateIntoMemory (state);
- if (state.getSize() > 0)
+ // Wrap processor state together with the wrapper-owned bypass state so
+ // host bypass survives preset/session restore (see auv3WrapperStateMagic).
+ const auto wrapperState = writeWrapperBypassState (auv3WrapperStateMagic,
+ auv3WrapperStateVersion,
+ getShouldBypassEffect(),
+ state,
+ state.getSize() > 0);
+
+ if (wrapperState.getSize() > 0)
{
- [retval setObject:[[NSData alloc] initWithBytes:state.getData() length:state.getSize()]
- forKey:@"YUPProcessorState"];
+ [retval setObject:[[NSData alloc] initWithBytes:wrapperState.getData() length:wrapperState.getSize()]
+ forKey:(__bridge NSString*) getAUProcessorStateKey()];
}
return retval;
@@ -480,7 +492,7 @@ void setFullState (NSDictionary* state)
if (state == nil || processor == nullptr)
return;
- id obj = [state objectForKey:@"YUPProcessorState"];
+ id obj = [state objectForKey:(__bridge NSString*) getAUProcessorStateKey()];
if (obj == nil || ! [obj isKindOfClass:[NSData class]])
return;
@@ -494,7 +506,16 @@ void setFullState (NSDictionary* state)
}
MemoryBlock stateBlock ([data bytes], static_cast (numBytes));
- processor->loadStateFromMemory (stateBlock);
+ const auto wrapperState = readWrapperBypassState (stateBlock, auv3WrapperStateMagic, auv3WrapperStateVersion);
+
+ // Restore wrapper-owned bypass state when present, otherwise fall back to
+ // treating the whole blob as legacy raw processor state.
+ if (wrapperState.hasWrapperState)
+ setShouldBypassEffect (wrapperState.isBypassed);
+
+ const bool shouldLoadProcessorState = ! wrapperState.hasWrapperState || wrapperState.hasProcessorState;
+ if (shouldLoadProcessorState && wrapperState.processorState.getSize() > 0)
+ processor->loadStateFromMemory (wrapperState.processorState);
{
ObjCMsgSendSuper (au, @selector (didChangeValueForKey:), @"allParameterValues");
@@ -506,6 +527,8 @@ void setFullState (NSDictionary* state)
void addAudioUnitBusses (bool isInput)
{
+ jassert (au != nil); // The AUAudioUnit must be set before creating bus arrays
+
auto* array = [[NSMutableArray alloc] init];
const auto& busLayout = processor->getBusLayout();
@@ -539,7 +562,10 @@ void addAudioUnitBusses (bool isInput)
auto* auBus = [[AUAudioUnitBus alloc] initWithFormat:format error:&error];
if (auBus != nil)
+ {
+ auBus.name = yupStringToNS (bus.getName());
[array addObject:auBus];
+ }
}
if (isInput)
@@ -562,6 +588,24 @@ bool shouldChangeToFormat (AVAudioFormat* format, AUAudioUnitBus* auBus)
if (allocated)
return false;
+ // Accept Float32 always, Float64 if the processor supports it.
+ // Other sample formats (Int16, Int32, etc.) are rejected — AUv3 hosts
+ // predominantly use floating-point.
+ const auto commonFormat = [format commonFormat];
+ if (commonFormat != AVAudioPCMFormatFloat32)
+ {
+ if (commonFormat != AVAudioPCMFormatFloat64
+ || processor == nullptr
+ || ! processor->supportsDoublePrecisionProcessing())
+ {
+ return false;
+ }
+ }
+
+ // Accept both interleaved and non-interleaved. The render callback
+ // handles deinterleave / interleave internally via AudioData converters,
+ // so there is no need to reject interleaved formats here.
+
const auto isInput = ([auBus busType] == AUAudioUnitBusTypeInput);
const auto busIdx = static_cast ([auBus index]);
const auto newNumChannels = static_cast ([format channelCount]);
@@ -576,7 +620,7 @@ bool shouldChangeToFormat (AVAudioFormat* format, AUAudioUnitBus* auBus)
if (bus.getType() != AudioBus::Type::Audio)
return false;
- return newNumChannels > 0 && newNumChannels <= bus.getNumChannels();
+ return newNumChannels > 0 && newNumChannels == bus.getNumChannels();
}
//==============================================================================
@@ -584,6 +628,8 @@ bool shouldChangeToFormat (AVAudioFormat* format, AUAudioUnitBus* auBus)
bool allocateRenderResourcesAndReturnError (NSError** outError)
{
+ jassert (au != nil); // The AUAudioUnit must be set before allocating render resources
+
allocated = false;
if (processor == nullptr)
@@ -595,20 +641,94 @@ bool allocateRenderResourcesAndReturnError (NSError** outError)
const AUAudioFrameCount maxFrames = [au maximumFramesToRender];
auto sampleRate = 44100.0;
+ bool sampleRateSet = false;
+ size_t maxInterleavedBytes = 0;
+
+ // Validate bus formats and compute required scratch space.
+ // Float32 and Float64 are accepted (Float64 gated by shouldChangeToFormat).
+ // Both interleaved and non-interleaved are accepted — conversion happens in the
+ // render callback via AudioData converters.
for (auto* busses : { inputBusses.get(), outputBusses.get() })
{
- if ([busses count] > 0)
+ for (NSUInteger i = 0; i < [busses count]; ++i)
{
- sampleRate = [[[busses objectAtIndexedSubscript:0] format] sampleRate];
- break;
+ auto* auBus = [busses objectAtIndexedSubscript:i];
+ auto* fmt = [auBus format];
+
+ // Only float formats are supported (shouldChangeToFormat already gates this)
+ const auto commonFormat = [fmt commonFormat];
+ if (commonFormat != AVAudioPCMFormatFloat32 && commonFormat != AVAudioPCMFormatFloat64)
+ {
+ if (outError != nullptr)
+ *outError = [NSError errorWithDomain:NSOSStatusErrorDomain
+ code:kAudioUnitErr_FormatNotSupported
+ userInfo:@{
+ NSLocalizedDescriptionKey:
+ [NSString stringWithFormat:@"Unsupported sample format for bus %@",
+ [auBus name]]
+ }];
+ return false;
+ }
+
+ // Track the largest interleaved buffer needed for format conversion
+ const auto numCh = [fmt channelCount];
+ const auto bytesPerSample = (commonFormat == AVAudioPCMFormatFloat64) ? sizeof (double) : sizeof (float);
+ const auto interleavedBytes = static_cast (maxFrames) * static_cast (numCh) * bytesPerSample;
+ maxInterleavedBytes = jmax (maxInterleavedBytes, interleavedBytes);
+
+ // Validate consistent sample rate across all buses
+ const auto busSampleRate = [fmt sampleRate];
+ if (! sampleRateSet)
+ {
+ sampleRate = busSampleRate;
+ sampleRateSet = true;
+ }
+ else if (! approximatelyEqual (sampleRate, busSampleRate))
+ {
+ if (outError != nullptr)
+ *outError = [NSError errorWithDomain:NSOSStatusErrorDomain
+ code:kAudioUnitErr_FormatNotSupported
+ userInfo:@{
+ NSLocalizedDescriptionKey:
+ [NSString stringWithFormat:@"Inconsistent sample rate for bus %@",
+ [auBus name]]
+ }];
+ return false;
+ }
}
}
+ // Pre-allocate interleaved scratch buffer for format conversion (real-time safe)
+ if (maxInterleavedBytes > interleavedScratchSize)
+ {
+ interleavedScratchData.allocate (maxInterleavedBytes, false);
+ interleavedScratchSize = maxInterleavedBytes;
+ }
+
+ allocatedMaximumFrames = maxFrames;
+
processor->setPlaybackConfiguration (sampleRate, static_cast (maxFrames));
midiMessages.ensureSize (2048);
midiMessages.clear();
+ // Reserve parameter change buffer capacity for sample-accurate automation.
+ // Capacity = number of parameters * maxFrames per block so that per-sample
+ // ramps never exceed the pre-allocated storage.
+ {
+ const auto numParams = static_cast (processor->getParameters().size());
+ paramChangeBuffer.reserve (numParams * static_cast (maxFrames));
+ }
+
+ // Pre-allocate scratch buffers (real-time safe — no allocation in render callback)
+ scratchBuffer.setSize (totalInChannels, static_cast (maxFrames));
+ scratchBuffer.clear();
+ scratchOutputBuffer.setSize (totalOutChannels, static_cast (maxFrames));
+ scratchOutputBuffer.clear();
+
+ // Pre-allocate per-bus view storage
+ renderViews.prepare (*processor, totalInChannels, totalOutChannels);
+
hostMusicalContextCallback = [au musicalContextBlock];
hostTransportStateCallback = [au transportStateBlock];
@@ -628,6 +748,7 @@ bool allocateRenderResourcesAndReturnError (NSError** outError)
void deallocateRenderResources()
{
allocated = false;
+ allocatedMaximumFrames = 0;
midiOutputEventBlock = nullptr;
hostMusicalContextCallback = nullptr;
hostTransportStateCallback = nullptr;
@@ -659,6 +780,12 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
if (processor == nullptr)
return kAudioUnitErr_NoConnection;
+ // Reject frame counts exceeding the pre-allocated buffer capacity.
+ // The host should never request more than maximumFramesToRender, but if it
+ // does, proceeding would cause a buffer overrun in the scratch buffers.
+ if (frameCount > allocatedMaximumFrames)
+ return kAudioUnitErr_TooManyFramesToProcess;
+
const int numFrames = static_cast (frameCount);
if (! approximatelyEqual (lastTimeStamp.mSampleTime, timestamp->mSampleTime))
@@ -666,17 +793,18 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
midiMessages.clear();
// Process events (MIDI and parameters)
- processEvents (realtimeEventListHead, static_cast (timestamp->mSampleTime));
+ processEvents (realtimeEventListHead, static_cast (timestamp->mSampleTime), frameCount);
lastTimeStamp = *timestamp;
- // Prepare audio buffer
- scratchBuffer.setSize (std::max (totalInChannels, totalOutChannels), numFrames);
+ // Clear scratch buffers (already sized in allocateRenderResources)
scratchBuffer.clear();
+ scratchOutputBuffer.clear();
const auto& busLayout = processor->getBusLayout();
- // Pull inputs
+ // Build per-bus input views and pull inputs
+ renderViews.inputBusViews.clear();
{
int chIdx = 0;
const auto& inputBuses = busLayout.getInputBuses();
@@ -688,25 +816,119 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
continue;
const int numCh = bus.getNumChannels();
- AudioBufferList* pullData = nullptr;
if (pullInputBlock != nullptr)
{
- AudioBufferList localBuffer = {};
- localBuffer.mNumberBuffers = static_cast (numCh);
+ // Look up the AU bus format to determine pull-buffer layout
+ const auto auBusIdx = static_cast (renderViews.inputBusViews.size());
+ auto* auBus = [inputBusses.get() objectAtIndexedSubscript:static_cast (auBusIdx)];
+ auto* busFmt = [auBus format];
+ const bool busIsInterleaved = [busFmt isInterleaved];
+ const bool busIsFloat64 = ([busFmt commonFormat] == AVAudioPCMFormatFloat64);
+
+ if (! busIsInterleaved && ! busIsFloat64)
+ {
+ // Planar Float32 — direct pull into scratch (fast path)
+ const auto bufferListSize = offsetof (AudioBufferList, mBuffers) + static_cast (numCh) * sizeof (::AudioBuffer);
+ auto* pullBuffer = static_cast (alloca (bufferListSize));
+ pullBuffer->mNumberBuffers = static_cast (numCh);
- float* channelPtrs[16] = {};
- for (int ch = 0; ch < numCh && ch < 16; ++ch)
+ for (int ch = 0; ch < numCh; ++ch)
+ {
+ pullBuffer->mBuffers[ch].mNumberChannels = 1;
+ pullBuffer->mBuffers[ch].mData = scratchBuffer.getWritePointer (chIdx + ch);
+ pullBuffer->mBuffers[ch].mDataByteSize = static_cast (numFrames * sizeof (float));
+ }
+
+ if (pullInputBlock (actionFlags, timestamp, frameCount, auBusIdx, pullBuffer) != noErr)
+ {
+ for (int ch = 0; ch < numCh; ++ch)
+ scratchBuffer.clear (chIdx + ch, 0, numFrames);
+ }
+ }
+ else if (busIsInterleaved)
{
- localBuffer.mBuffers[ch].mNumberChannels = 1;
- localBuffer.mBuffers[ch].mData = scratchBuffer.getWritePointer (chIdx + ch);
- localBuffer.mBuffers[ch].mDataByteSize = static_cast (numFrames * sizeof (float));
+ // Interleaved pull — pull into a single interleaved buffer, then deinterleave
+ const auto bytesPerSample = busIsFloat64 ? sizeof (double) : sizeof (float);
+ const auto interleavedBytes = static_cast (numFrames) * static_cast (numCh) * bytesPerSample;
+
+ const auto bufferListSize = offsetof (AudioBufferList, mBuffers) + sizeof (::AudioBuffer);
+ auto* pullBuffer = static_cast (alloca (bufferListSize));
+ pullBuffer->mNumberBuffers = 1;
+ pullBuffer->mBuffers[0].mNumberChannels = static_cast (numCh);
+ pullBuffer->mBuffers[0].mData = interleavedScratchData.getData();
+ pullBuffer->mBuffers[0].mDataByteSize = static_cast (interleavedBytes);
+
+ const auto pullStatus = pullInputBlock (actionFlags, timestamp, frameCount, auBusIdx, pullBuffer);
+
+ // Build destination channel pointers for deinterleave.
+ // Stack allocation is real-time safe and avoids const-correctness
+ // issues with renderViews.inputChannelPtrStorage (which stores const float*
+ // for the later AudioBusBufferView build).
+ auto* chanPtrs = static_cast (
+ alloca (static_cast (numCh) * sizeof (float*)));
+ for (int ch = 0; ch < numCh; ++ch)
+ chanPtrs[ch] = scratchBuffer.getWritePointer (chIdx + ch);
+
+ if (pullStatus == noErr)
+ {
+ if (busIsFloat64)
+ {
+ using SrcFmt = AudioData::Format;
+ using DstFmt = AudioData::Format;
+ AudioData::deinterleaveSamples (
+ AudioData::InterleavedSource { reinterpret_cast (interleavedScratchData.getData()), numCh },
+ AudioData::NonInterleavedDest { chanPtrs, numCh },
+ numFrames);
+ }
+ else
+ {
+ using SrcFmt = AudioData::Format;
+ using DstFmt = AudioData::Format;
+ AudioData::deinterleaveSamples (
+ AudioData::InterleavedSource { reinterpret_cast (interleavedScratchData.getData()), numCh },
+ AudioData::NonInterleavedDest { chanPtrs, numCh },
+ numFrames);
+ }
+ }
+ else
+ {
+ for (int ch = 0; ch < numCh; ++ch)
+ scratchBuffer.clear (chIdx + ch, 0, numFrames);
+ }
}
-
- if (pullInputBlock (actionFlags, timestamp, frameCount, busIdx, &localBuffer) != noErr)
+ else
{
+ // Planar Float64 — pull into planar double, then convert double → float
+ const auto bufferListSize = offsetof (AudioBufferList, mBuffers) + static_cast (numCh) * sizeof (::AudioBuffer);
+ auto* pullBuffer = static_cast (alloca (bufferListSize));
+ pullBuffer->mNumberBuffers = static_cast (numCh);
+
+ auto* doubleScratch = reinterpret_cast (interleavedScratchData.getData());
for (int ch = 0; ch < numCh; ++ch)
- scratchBuffer.clear (chIdx + ch, 0, numFrames);
+ {
+ pullBuffer->mBuffers[ch].mNumberChannels = 1;
+ pullBuffer->mBuffers[ch].mData = doubleScratch + static_cast (ch) * static_cast (numFrames);
+ pullBuffer->mBuffers[ch].mDataByteSize = static_cast (numFrames * sizeof (double));
+ }
+
+ const auto pullStatus = pullInputBlock (actionFlags, timestamp, frameCount, auBusIdx, pullBuffer);
+
+ if (pullStatus == noErr)
+ {
+ for (int ch = 0; ch < numCh; ++ch)
+ {
+ const auto* src = doubleScratch + static_cast (ch) * static_cast (numFrames);
+ auto* dst = scratchBuffer.getWritePointer (chIdx + ch);
+ for (int s = 0; s < numFrames; ++s)
+ dst[s] = static_cast (src[s]);
+ }
+ }
+ else
+ {
+ for (int ch = 0; ch < numCh; ++ch)
+ scratchBuffer.clear (chIdx + ch, 0, numFrames);
+ }
}
}
else
@@ -715,6 +937,58 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
scratchBuffer.clear (chIdx + ch, 0, numFrames);
}
+ // Build AudioBusBufferView for this input bus (real-time safe — uses pre-allocated storage)
+ auto* chPtrs = renderViews.inputChannelPtrStorage.data() + chIdx;
+ for (int ch = 0; ch < numCh; ++ch)
+ chPtrs[ch] = scratchBuffer.getReadPointer (chIdx + ch);
+
+ renderViews.inputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
+ // Copy main inputs to the corresponding output area
+ if (bus.getRole() == AudioBus::Role::Main)
+ {
+ // Find matching output bus for this main input
+ int outputChIdx = 0;
+ for (const auto& outBus : busLayout.getOutputBuses())
+ {
+ if (outBus.getType() != AudioBus::Type::Audio)
+ continue;
+ if (outBus.getRole() == AudioBus::Role::Main
+ && outputChIdx == chIdx) // match by channel offset
+ {
+ for (int ch = 0; ch < std::min (numCh, outBus.getNumChannels()); ++ch)
+ scratchOutputBuffer.copyFrom (outputChIdx + ch, 0, scratchBuffer, chIdx + ch, 0, numFrames);
+ break;
+ }
+ outputChIdx += outBus.getNumChannels();
+ }
+ }
+
+ chIdx += numCh;
+ }
+ }
+
+ // Build per-bus output views
+ renderViews.outputBusViews.clear();
+ {
+ int chIdx = 0;
+ const auto& outputBuses = busLayout.getOutputBuses();
+
+ for (int busIdx = 0; busIdx < static_cast (outputBuses.size()); ++busIdx)
+ {
+ const auto& bus = outputBuses[busIdx];
+ if (bus.getType() != AudioBus::Type::Audio)
+ continue;
+
+ const int numCh = bus.getNumChannels();
+
+ // Build AudioBusBufferView for this output bus (real-time safe — uses pre-allocated storage)
+ auto* chPtrs = renderViews.outputChannelPtrStorage.data() + chIdx;
+ for (int ch = 0; ch < numCh; ++ch)
+ chPtrs[ch] = scratchOutputBuffer.getWritePointer (chIdx + ch);
+
+ renderViews.outputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
chIdx += numCh;
}
}
@@ -723,10 +997,14 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
{
AudioPluginPlayHeadAU playHead (*this, timestamp);
- AudioProcessContext context { scratchBuffer,
- midiMessages,
- emptyParamChangeBuffer,
- &playHead };
+ AudioProcessContext context {
+ scratchOutputBuffer,
+ midiMessages,
+ paramChangeBuffer,
+ &playHead,
+ { renderViews.inputBusViews.data(), renderViews.inputBusViews.size() },
+ { renderViews.outputBusViews.data(), renderViews.outputBusViews.size() }
+ };
if (bypassHostParam != nullptr)
processAudioBlock (*processor, context, bypassHostParam->getNormalizedValue() > 0.5f);
@@ -742,6 +1020,7 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
{
const auto& outputBuses = processor->getBusLayout().getOutputBuses();
int chIdx = 0;
+ int auOutBusIdx = 0;
for (int busIdx = 0; busIdx < static_cast (outputBuses.size()); ++busIdx)
{
@@ -751,17 +1030,94 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
const int numCh = bus.getNumChannels();
- if (busIdx == static_cast (outputBusNumber))
+ if (auOutBusIdx == static_cast (outputBusNumber))
{
- for (int ch = 0; ch < numCh && ch < static_cast (outputData->mNumberBuffers); ++ch)
+ // Look up the AU bus format to determine output layout
+ auto* auBus = [outputBusses.get() objectAtIndexedSubscript:static_cast (auOutBusIdx)];
+ auto* busFmt = [auBus format];
+ const bool busIsInterleaved = [busFmt isInterleaved];
+ const bool busIsFloat64 = ([busFmt commonFormat] == AVAudioPCMFormatFloat64);
+
+ if (! busIsInterleaved && ! busIsFloat64)
+ {
+ // Planar Float32 — direct copy (fast path)
+ for (int ch = 0; ch < numCh && ch < static_cast (outputData->mNumberBuffers); ++ch)
+ {
+ const auto* src = scratchOutputBuffer.getReadPointer (chIdx + ch);
+ auto* buf = &outputData->mBuffers[ch];
+ auto* dst = static_cast (buf->mData);
+
+ if (dst == nullptr || src == nullptr)
+ continue;
+
+ if (buf->mDataByteSize < static_cast (numFrames * static_cast (sizeof (float))))
+ continue;
+
+ std::copy (src, src + numFrames, dst);
+ }
+ }
+ else if (busIsInterleaved)
+ {
+ // Interleaved output — interleave from planar scratch.
+ // Validate the host buffer can hold the interleaved data.
+ const auto bytesPerSample = busIsFloat64 ? sizeof (double) : sizeof (float);
+ const auto requiredBytes = static_cast (numFrames * numCh * static_cast (bytesPerSample));
+
+ if (outputData->mNumberBuffers >= 1
+ && outputData->mBuffers[0].mData != nullptr
+ && outputData->mBuffers[0].mDataByteSize >= requiredBytes)
+ {
+ // Build source channel pointers from scratch buffer (stack allocation, real-time safe).
+ // NonInterleavedSource expects const float* const* — we allocate as
+ // const float** and reinterpret_cast to add the inner const qualifier.
+ auto* storage = static_cast (
+ alloca (static_cast (numCh) * sizeof (const float*)));
+ for (int ch = 0; ch < numCh; ++ch)
+ storage[ch] = scratchOutputBuffer.getReadPointer (chIdx + ch);
+ auto* chanPtrs = reinterpret_cast (storage);
+
+ if (busIsFloat64)
+ {
+ using SrcFmt = AudioData::Format;
+ using DstFmt = AudioData::Format;
+ AudioData::interleaveSamples (
+ AudioData::NonInterleavedSource { chanPtrs, numCh },
+ AudioData::InterleavedDest { reinterpret_cast (outputData->mBuffers[0].mData), numCh },
+ numFrames);
+ }
+ else
+ {
+ using SrcFmt = AudioData::Format;
+ using DstFmt = AudioData::Format;
+ AudioData::interleaveSamples (
+ AudioData::NonInterleavedSource { chanPtrs, numCh },
+ AudioData::InterleavedDest { static_cast (outputData->mBuffers[0].mData), numCh },
+ numFrames);
+ }
+ }
+ }
+ else
{
- const auto* src = scratchBuffer.getReadPointer (chIdx + ch);
- auto* dst = static_cast (outputData->mBuffers[ch].mData);
+ // Planar Float64 — convert float → double per channel
+ for (int ch = 0; ch < numCh && ch < static_cast (outputData->mNumberBuffers); ++ch)
+ {
+ const auto* src = scratchOutputBuffer.getReadPointer (chIdx + ch);
+ auto* buf = &outputData->mBuffers[ch];
+ auto* dst = static_cast (buf->mData);
+
+ if (dst == nullptr || src == nullptr)
+ continue;
- std::copy (src, src + numFrames, dst);
+ if (buf->mDataByteSize < static_cast (numFrames * static_cast (sizeof (double))))
+ continue;
+
+ for (int s = 0; s < numFrames; ++s)
+ dst[s] = static_cast (src[s]);
+ }
}
}
+ ++auOutBusIdx;
chIdx += numCh;
}
}
@@ -769,8 +1125,10 @@ AUAudioUnitStatus renderCallback (AudioUnitRenderActionFlags* actionFlags,
return noErr;
}
- void processEvents (const AURenderEvent* realtimeEventListHead, AUEventSampleTime startTime)
+ void processEvents (const AURenderEvent* realtimeEventListHead, AUEventSampleTime startTime, AUAudioFrameCount frameCount)
{
+ paramChangeBuffer.clear();
+
for (const AURenderEvent* event = realtimeEventListHead; event != nullptr; event = event->head.next)
{
switch (event->head.eventType)
@@ -786,14 +1144,48 @@ void processEvents (const AURenderEvent* realtimeEventListHead, AUEventSampleTim
break;
case AURenderEventParameter:
+ {
+ const AUParameterEvent& paramEvent = event->parameter;
+
+ if (auto* p = getParamForAUAddress (paramEvent.parameterAddress))
+ {
+ const int offset = static_cast (paramEvent.eventSampleTime - startTime);
+ const auto normalised = p->convertToNormalizedValue (static_cast (paramEvent.value));
+ p->setValue (static_cast (paramEvent.value));
+
+ if (isPositiveAndBelow (offset, static_cast (frameCount)))
+ paramChangeBuffer.addChange (p->getIndexInContainer(), normalised, offset);
+
+ inParameterChangedCallback = true;
+ }
+ }
+ break;
+
case AURenderEventParameterRamp:
{
const AUParameterEvent& paramEvent = event->parameter;
if (auto* p = getParamForAUAddress (paramEvent.parameterAddress))
{
- auto normalisedValue = static_cast (paramEvent.value) / getMaximumParameterValue (*p);
- p->setNormalizedValue (normalisedValue);
+ const int startOffset = static_cast (paramEvent.eventSampleTime - startTime);
+ const int rampFrames = static_cast (paramEvent.rampDurationSampleFrames);
+ const int rampEndOffset = startOffset + rampFrames;
+ const auto startValue = p->getValue();
+ const auto endValue = static_cast (paramEvent.value);
+ const auto startNormalised = p->convertToNormalizedValue (startValue);
+ const auto endNormalised = p->convertToNormalizedValue (endValue);
+
+ // Add ramp start event at the ramp's start offset
+ if (isPositiveAndBelow (startOffset, static_cast (frameCount)))
+ paramChangeBuffer.addChange (p->getIndexInContainer(), startNormalised, startOffset);
+
+ // Add ramp end event. If the ramp extends past this block, clamp it to the last frame.
+ const int endOffsetClamped = jmin (rampEndOffset, static_cast (frameCount) - 1);
+ if (endOffsetClamped > startOffset)
+ paramChangeBuffer.addChange (p->getIndexInContainer(), endNormalised, endOffsetClamped);
+
+ // Always set the final value so the next block picks up the correct value
+ p->setValue (endValue);
inParameterChangedCallback = true;
}
@@ -804,6 +1196,9 @@ void processEvents (const AURenderEvent* realtimeEventListHead, AUEventSampleTim
break;
}
}
+
+ // Sort changes by sample offset for binary-search lookups during processing
+ paramChangeBuffer.sort();
}
void sendMidi (int64_t baseTimeStamp, AUAudioFrameCount frameCount)
@@ -1060,7 +1455,7 @@ void sendParameterEvent (int idx, float newValue, AUParameterAutomationEventType
if (yupParam == nullptr)
return;
- const auto value = newValue * getMaximumParameterValue (*yupParam);
+ const auto value = yupParam->convertToDenormalizedValue (newValue);
if (@available (macOS 10.12, *))
{
@@ -1157,7 +1552,7 @@ void unregisterAllParameterListeners()
NSUniquePtr> channelCapabilities;
NSUniquePtr paramTree;
- AUParameterObserverToken* editorObserverToken = nullptr;
+ AUParameterObserverToken _Nullable * _Nullable editorObserverToken = nullptr;
mutable std::mutex factoryPresetsMutex;
NSUniquePtr> factoryPresets;
@@ -1166,8 +1561,17 @@ void unregisterAllParameterListeners()
ObjCBlock renderContextObserver;
MidiBuffer midiMessages;
- ParameterChangeBuffer emptyParamChangeBuffer;
+ ParameterChangeBuffer paramChangeBuffer;
AudioBuffer scratchBuffer;
+ AudioBuffer scratchOutputBuffer;
+
+ // Per-bus view state, pre-allocated in allocateRenderResources so the
+ // render callback only clears and refills the vectors without allocating
+ AudioPluginAURenderViews renderViews;
+
+ // Interleaved scratch buffer for format conversion (deinterleave/interleave)
+ HeapBlock interleavedScratchData;
+ size_t interleavedScratchSize = 0;
AUMIDIOutputEventBlock midiOutputEventBlock = nullptr;
@@ -1176,13 +1580,14 @@ void unregisterAllParameterListeners()
AudioTimeStamp lastTimeStamp;
- AudioParameter* bypassHostParam = nullptr;
+ std::unique_ptr bypassHostParam;
double viewConfigWidth = 0;
double viewConfigHeight = 0;
ThreadLocalValue inParameterChangedCallback;
bool allocated = false;
+ AUAudioFrameCount allocatedMaximumFrames = 0;
YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginProcessorAUv3)
};
@@ -1369,7 +1774,7 @@ void unregisterAllParameterListeners()
return getIvar (self, "cppObject");
}
- static void setThis (id self, AudioPluginProcessorAUv3* cpp)
+ static void setThis (id self, AudioPluginProcessorAUv3* _Nullable cpp)
{
setIvar (self, "cppObject", cpp);
}
@@ -1386,55 +1791,30 @@ explicit AudioPluginViewControllerv3 (AUViewController* vc)
: myself (vc)
{
initialiseYup_GUI();
- processor.reset (::createPluginProcessor());
}
~AudioPluginViewControllerv3() override
{
if (processor != nullptr)
{
- yup::endActiveParameterGestures (processor.get());
+ yup::endActiveParameterGestures (processor);
processor->removeListener (this);
+ }
- if (editor != nullptr)
- {
- delete editor;
- editor = nullptr;
- }
+ if (editor != nullptr)
+ {
+ delete editor;
+ editor = nullptr;
}
}
void loadView()
{
- if (processor == nullptr)
- return;
-
- if (processor->hasEditor())
- {
- editor = processor->createEditor();
-
- if (editor != nullptr)
- {
- preferredSize = { editor->getWidth(), editor->getHeight() };
-
- NSView* view = [[NSView alloc] initWithFrame:NSMakeRect (0, 0, preferredSize.getWidth(), preferredSize.getHeight())];
- [myself setView:view];
-
- editor->setVisible (true);
-
- auto options = ComponentNative::Options()
- .withFlags (ComponentNative::defaultFlags & ~ComponentNative::decoratedWindow);
-
- editor->addToDesktop (options, (__bridge void*) view);
- }
- }
+ createEditorIfNeeded();
}
void viewDidLayoutSubviews()
{
- if (processor == nullptr)
- return;
-
if ([myself view] != nil)
{
if (editor != nullptr)
@@ -1463,33 +1843,57 @@ void audioProcessorChanged (AudioProcessorBase*, const AudioProcessorBase::Chang
AUAudioUnit* createAudioUnit (const AudioComponentDescription& desc, NSError** error)
{
- if (processor == nullptr)
- return nil;
-
- auto* cpp = new AudioPluginProcessorAUv3 (nil, desc, 0, error);
-
+ // Let the ObjC init create the C++ wrapper — it correctly passes self (the
+ // real AUAudioUnit) to the AudioPluginProcessorAUv3 constructor, so au is
+ // valid when init() runs.
static AUAudioUnitSubclass auClass;
auto* au = auClass.createInstance();
au = ObjCMsgSendSuper (au, @selector (initWithComponentDescription:options:error:), desc, 0, error);
- if (au == nil)
+ if (au != nil)
{
- delete cpp;
- return nil;
- }
+ auto* cpp = AUAudioUnitSubclass::_this (au);
+ processor = cpp != nullptr ? cpp->getProcessor() : nullptr;
- AUAudioUnitSubclass::setThis (au, cpp);
- cpp->init();
+ createEditorIfNeeded();
+ }
return au;
}
- AudioProcessor* getProcessor() const { return processor.get(); }
+ AudioProcessor* getProcessor() const { return processor; }
private:
+ void createEditorIfNeeded()
+ {
+ // Already created, or no processor available yet
+ if (editor != nullptr || processor == nullptr)
+ return;
+
+ if (processor->hasEditor())
+ {
+ editor = processor->createEditor();
+
+ if (editor != nullptr)
+ {
+ preferredSize = { editor->getWidth(), editor->getHeight() };
+
+ NSView* view = [[NSView alloc] initWithFrame:NSMakeRect (0, 0, preferredSize.getWidth(), preferredSize.getHeight())];
+ [myself setView:view];
+
+ editor->setVisible (true);
+
+ auto options = ComponentNative::Options()
+ .withFlags (ComponentNative::defaultFlags & ~ComponentNative::decoratedWindow);
+
+ editor->addToDesktop (options, (__bridge void*) view);
+ }
+ }
+ }
+
AUViewController* myself = nil;
- std::unique_ptr processor;
+ AudioProcessor* processor = nullptr;
AudioProcessorEditor* editor = nullptr;
Rectangle preferredSize { 1.0f, 1.0f };
};
@@ -1519,8 +1923,8 @@ - (void) loadView
cpp->loadView();
}
-- (AUAudioUnit*) createAudioUnitWithComponentDescription:(AudioComponentDescription)desc
- error:(NSError**)error
+- (AUAudioUnit* _Nullable) createAudioUnitWithComponentDescription:(AudioComponentDescription)desc
+ error:(NSError* _Nullable* _Nullable)error
{
return cpp->createAudioUnit (desc, error);
}
@@ -1542,4 +1946,6 @@ - (void) viewDidLayout
@end
+NS_ASSUME_NONNULL_END
+
#endif // YUP_MAC
diff --git a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp
index 74bc8d3fd..080394416 100644
--- a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp
+++ b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp
@@ -34,7 +34,7 @@
#include
-extern "C" yup::AudioProcessor* createPluginProcessor();
+extern "C" yup::AudioProcessor* YUP_AUDIO_PLUGIN_CREATE_FUNCTION();
namespace yup
{
@@ -518,6 +518,7 @@ class AudioPluginProcessorCLAP final
const void* getExtension (std::string_view id);
const clap_plugin_t* getPlugin() const;
+ AudioProcessor* getProcessor() const noexcept;
void editorResized();
ScopedValueSetter scopedHostEditorResizing();
@@ -576,8 +577,22 @@ class AudioPluginProcessorCLAP final
std::vector listenedParameters;
std::vector outputChannelsFloat;
std::vector outputChannelsDouble;
+
+ std::vector> inputBusViewsFloat;
+ std::vector> outputBusViewsFloat;
+ std::vector> inputBusViewsDouble;
+ std::vector> outputBusViewsDouble;
+
bool isBypassed = false;
std::atomic isActive { false };
+
+ //==============================================================================
+ /** Returns true when the CLAP audio bus role is Main. */
+ bool isCLAPAudioBusMain (uint32_t clapAudioBusIndex, bool isInput) const noexcept
+ {
+ return audioProcessor->getBusLayout().getAudioBusRole (static_cast (clapAudioBusIndex), isInput) == AudioBus::Role::Main;
+ }
+
std::atomic isInsideProcessBlock { false };
std::atomic callLatencyChangeOnNextActivate { false };
std::atomic tailChangedPending { false };
@@ -733,9 +748,13 @@ AudioPluginProcessorCLAP::AudioPluginProcessorCLAP (const clap_host_t* host)
if (useDoublePrecision)
{
- // Copy input audio into output buffers for effect processors (double)
+ // Copy main input audio into matching main output buffers for effects.
+ // Auxiliary (sidechain) inputs are NOT copied to outputs.
for (uint32_t busIdx = 0; busIdx < std::min (process->audio_inputs_count, process->audio_outputs_count); ++busIdx)
{
+ if (! wrapper->isCLAPAudioBusMain (busIdx, true))
+ continue;
+
const auto& inBus = process->audio_inputs[busIdx];
const auto& outBus = process->audio_outputs[busIdx];
const uint32_t chCount = std::min (inBus.channel_count, outBus.channel_count);
@@ -759,7 +778,37 @@ AudioPluginProcessorCLAP::AudioPluginProcessorCLAP (const clap_host_t* host)
0,
static_cast (process->frames_count));
- AudioProcessContext context { audioBuffer, midiBuffer, wrapper->paramChangeBuffer, playHeadPtr };
+ // Build per-bus input views (all input buses)
+ wrapper->inputBusViewsDouble.clear();
+ for (uint32_t busIdx = 0; busIdx < process->audio_inputs_count; ++busIdx)
+ {
+ const auto& inBus = process->audio_inputs[busIdx];
+ const bool isSilent = inBus.constant_mask != 0; // CLAP silence flag
+ wrapper->inputBusViewsDouble.emplace_back (
+ isSilent ? nullptr : reinterpret_cast (inBus.data64),
+ static_cast (inBus.channel_count),
+ audioProcessor.getBusLayout().getAudioBusRole (static_cast (busIdx), true));
+ }
+
+ // Build per-bus output views (all output buses)
+ wrapper->outputBusViewsDouble.clear();
+ for (uint32_t busIdx = 0; busIdx < process->audio_outputs_count; ++busIdx)
+ {
+ const auto& outBus = process->audio_outputs[busIdx];
+ wrapper->outputBusViewsDouble.emplace_back (
+ reinterpret_cast (outBus.data64),
+ static_cast (outBus.channel_count),
+ audioProcessor.getBusLayout().getAudioBusRole (static_cast (busIdx), false));
+ }
+
+ AudioProcessContext context {
+ audioBuffer,
+ midiBuffer,
+ wrapper->paramChangeBuffer,
+ playHeadPtr,
+ { wrapper->inputBusViewsDouble.data(), wrapper->inputBusViewsDouble.size() },
+ { wrapper->outputBusViewsDouble.data(), wrapper->outputBusViewsDouble.size() }
+ };
wrapper->isInsideProcessBlock.store (true);
processAudioBlock (audioProcessor, context, bypassed);
@@ -767,9 +816,13 @@ AudioPluginProcessorCLAP::AudioPluginProcessorCLAP (const clap_host_t* host)
}
else
{
- // Copy input audio into output buffers for effect processors (float)
+ // Copy main input audio into matching main output buffers for effects.
+ // Auxiliary (sidechain) inputs are NOT copied to outputs.
for (uint32_t busIdx = 0; busIdx < std::min (process->audio_inputs_count, process->audio_outputs_count); ++busIdx)
{
+ if (! wrapper->isCLAPAudioBusMain (busIdx, true))
+ continue;
+
const auto& inBus = process->audio_inputs[busIdx];
const auto& outBus = process->audio_outputs[busIdx];
const uint32_t chCount = std::min (inBus.channel_count, outBus.channel_count);
@@ -793,7 +846,37 @@ AudioPluginProcessorCLAP::AudioPluginProcessorCLAP (const clap_host_t* host)
0,
static_cast (process->frames_count));
- AudioProcessContext context { audioBuffer, midiBuffer, wrapper->paramChangeBuffer, playHeadPtr };
+ // Build per-bus input views (all input buses)
+ wrapper->inputBusViewsFloat.clear();
+ for (uint32_t busIdx = 0; busIdx < process->audio_inputs_count; ++busIdx)
+ {
+ const auto& inBus = process->audio_inputs[busIdx];
+ const bool isSilent = inBus.constant_mask != 0; // CLAP silence flag
+ wrapper->inputBusViewsFloat.emplace_back (
+ isSilent ? nullptr : reinterpret_cast (inBus.data32),
+ static_cast (inBus.channel_count),
+ audioProcessor.getBusLayout().getAudioBusRole (static_cast (busIdx), true));
+ }
+
+ // Build per-bus output views (all output buses)
+ wrapper->outputBusViewsFloat.clear();
+ for (uint32_t busIdx = 0; busIdx < process->audio_outputs_count; ++busIdx)
+ {
+ const auto& outBus = process->audio_outputs[busIdx];
+ wrapper->outputBusViewsFloat.emplace_back (
+ reinterpret_cast (outBus.data32),
+ static_cast (outBus.channel_count),
+ audioProcessor.getBusLayout().getAudioBusRole (static_cast (busIdx), false));
+ }
+
+ AudioProcessContext context {
+ audioBuffer,
+ midiBuffer,
+ wrapper->paramChangeBuffer,
+ playHeadPtr,
+ { wrapper->inputBusViewsFloat.data(), wrapper->inputBusViewsFloat.size() },
+ { wrapper->outputBusViewsFloat.data(), wrapper->outputBusViewsFloat.size() }
+ };
wrapper->isInsideProcessBlock.store (true);
processAudioBlock (audioProcessor, context, bypassed);
@@ -866,7 +949,7 @@ bool AudioPluginProcessorCLAP::initialise()
{
jassert (audioProcessor == nullptr);
- audioProcessor.reset (::createPluginProcessor());
+ audioProcessor.reset (::YUP_AUDIO_PLUGIN_CREATE_FUNCTION());
if (audioProcessor == nullptr)
return false;
@@ -1126,7 +1209,9 @@ bool AudioPluginProcessorCLAP::initialise()
info->id = index;
info->channel_count = audioBus->getNumChannels();
- uint32_t flags = (index == 0) ? CLAP_AUDIO_PORT_IS_MAIN : 0;
+ uint32_t flags = audioBus->getRole() == AudioBus::Role::Main
+ ? CLAP_AUDIO_PORT_IS_MAIN
+ : 0;
if (audioProcessor->supportsDoublePrecisionProcessing())
flags |= CLAP_AUDIO_PORT_SUPPORTS_64BITS | CLAP_AUDIO_PORT_PREFERS_64BITS | CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE;
info->flags = flags;
@@ -1551,6 +1636,17 @@ bool AudioPluginProcessorCLAP::activate (float sampleRate, int samplesPerBlock)
outputChannelsFloat.reserve (static_cast (totalOutputChannels));
outputChannelsDouble.reserve (static_cast (totalOutputChannels));
+ // Pre-allocate per-bus view storage
+ {
+ const auto numAudioInputs = static_cast (audioProcessor->getNumAudioInputs());
+ const auto numAudioOutputs = static_cast (audioProcessor->getNumAudioOutputs());
+
+ inputBusViewsFloat.reserve (numAudioInputs);
+ outputBusViewsFloat.reserve (numAudioOutputs);
+ inputBusViewsDouble.reserve (numAudioInputs);
+ outputBusViewsDouble.reserve (numAudioOutputs);
+ }
+
isActive.store (true);
return true;
@@ -1637,6 +1733,13 @@ const clap_plugin_t* AudioPluginProcessorCLAP::getPlugin() const
//==============================================================================
+AudioProcessor* AudioPluginProcessorCLAP::getProcessor() const noexcept
+{
+ return audioProcessor.get();
+}
+
+//==============================================================================
+
void AudioPluginProcessorCLAP::addParameterListeners()
{
removeParameterListeners();
diff --git a/modules/yup_audio_plugin_client/common/yup_AudioPluginAUHelpers.h b/modules/yup_audio_plugin_client/common/yup_AudioPluginAUHelpers.h
new file mode 100644
index 000000000..d147e9f06
--- /dev/null
+++ b/modules/yup_audio_plugin_client/common/yup_AudioPluginAUHelpers.h
@@ -0,0 +1,152 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2026 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+#pragma once
+
+#if YUP_MAC
+
+#import
+
+#include
+
+#include
+#include
+
+//==============================================================================
+namespace yup
+{
+
+/** Returns a hex string describing a pointer, for debug logging. */
+inline String describePointer (const void* value)
+{
+ return "0x" + String::toHexString (static_cast (reinterpret_cast (value)));
+}
+
+/** Returns the dictionary key used to store the YUP processor state
+ alongside the host's own AU state (shared by the AUv2 and AUv3 clients). */
+inline CFStringRef getAUProcessorStateKey()
+{
+ return CFSTR ("YUPProcessorState");
+}
+
+/** Maps a ParameterUnit to the corresponding AudioUnitParameterUnit constant. */
+inline AudioUnitParameterUnit makeAUUnit (const AudioParameter& param)
+{
+ switch (param.getUnit())
+ {
+ case AudioParameter::ParameterUnit::Generic:
+ return kAudioUnitParameterUnit_Generic;
+ case AudioParameter::ParameterUnit::Percent:
+ return kAudioUnitParameterUnit_Percent;
+ case AudioParameter::ParameterUnit::Decibels:
+ return kAudioUnitParameterUnit_Decibels;
+ case AudioParameter::ParameterUnit::Hertz:
+ return kAudioUnitParameterUnit_Hertz;
+ case AudioParameter::ParameterUnit::Milliseconds:
+ return kAudioUnitParameterUnit_Milliseconds;
+ case AudioParameter::ParameterUnit::Seconds:
+ return kAudioUnitParameterUnit_Seconds;
+ case AudioParameter::ParameterUnit::Degrees:
+ return kAudioUnitParameterUnit_Degrees;
+ case AudioParameter::ParameterUnit::Cents:
+ return kAudioUnitParameterUnit_Cents;
+ case AudioParameter::ParameterUnit::Semitones:
+ return kAudioUnitParameterUnit_RelativeSemiTones;
+ case AudioParameter::ParameterUnit::Octaves:
+ return kAudioUnitParameterUnit_Octaves;
+ case AudioParameter::ParameterUnit::BPM:
+ return kAudioUnitParameterUnit_BPM;
+ case AudioParameter::ParameterUnit::Beats:
+ return kAudioUnitParameterUnit_Beats;
+ case AudioParameter::ParameterUnit::Ratio:
+ return kAudioUnitParameterUnit_Ratio;
+ case AudioParameter::ParameterUnit::LinearGain:
+ return kAudioUnitParameterUnit_LinearGain;
+ case AudioParameter::ParameterUnit::Pan:
+ return kAudioUnitParameterUnit_Pan;
+ case AudioParameter::ParameterUnit::MIDINoteNumber:
+ return kAudioUnitParameterUnit_MIDINoteNumber;
+ case AudioParameter::ParameterUnit::Custom:
+ return kAudioUnitParameterUnit_CustomUnit;
+ }
+
+ return kAudioUnitParameterUnit_Generic;
+}
+
+/** Returns the base AudioUnitParameterOptions shared by the AUv2 and AUv3
+ clients: readable, plus writable when the parameter is not read-only.
+
+ AUv2 callers must OR in kAudioUnitParameterFlag_HasCFNameString.
+*/
+inline AudioUnitParameterOptions makeAUParameterFlags (const AudioParameter& param)
+{
+ AudioUnitParameterOptions flags = kAudioUnitParameterFlag_IsReadable;
+
+ if (! param.isReadOnly())
+ flags |= kAudioUnitParameterFlag_IsWritable;
+
+ return flags;
+}
+
+/** Returns the AudioUnitParameterOptions used by the AUv3 client.
+
+ Extends makeAUParameterFlags with value-string and ramp capabilities.
+ This must not be used by the AUv2 client, which implements no string
+ properties and cannot schedule parameters.
+*/
+inline AudioUnitParameterOptions makeAUv3ParameterFlags (const AudioParameter& param)
+{
+ AudioUnitParameterOptions flags = makeAUParameterFlags (param)
+ | kAudioUnitParameterFlag_ValuesHaveStrings;
+
+ if (! param.isStepped())
+ flags |= kAudioUnitParameterFlag_CanRamp;
+
+ return flags;
+}
+
+//==============================================================================
+
+/** Per-bus render view storage shared by the AUv2 and AUv3 clients.
+
+ Pre-allocated once at initialization so the render callback only clears
+ and refills the vectors without allocating.
+*/
+struct AudioPluginAURenderViews
+{
+ std::vector> inputBusViews;
+ std::vector> outputBusViews;
+ std::vector inputChannelPtrStorage;
+ std::vector outputChannelPtrStorage;
+
+ /** Reserves and sizes the storage for the processor's bus layout. */
+ void prepare (const AudioProcessor& processor, int totalInputChannels, int totalOutputChannels)
+ {
+ inputBusViews.reserve (static_cast (processor.getNumAudioInputs()));
+ outputBusViews.reserve (static_cast (processor.getNumAudioOutputs()));
+ inputChannelPtrStorage.resize (static_cast (totalInputChannels));
+ outputChannelPtrStorage.resize (static_cast (totalOutputChannels));
+ }
+};
+
+} // namespace yup
+
+#endif // YUP_MAC
diff --git a/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h b/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h
index 7c453f935..f1133a0f8 100644
--- a/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h
+++ b/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h
@@ -66,6 +66,18 @@ inline int getTotalAudioOutputChannels (const AudioProcessor& processor)
return count;
}
+/** Returns the total number of input audio channels across all input audio buses. */
+inline int getTotalAudioInputChannels (const AudioProcessor& processor)
+{
+ int count = 0;
+
+ for (const auto& bus : processor.getBusLayout().getInputBuses())
+ if (bus.getType() == AudioBus::Type::Audio)
+ count += bus.getNumChannels();
+
+ return count;
+}
+
/** Returns the default automation-event capacity used by plugin wrappers. */
inline int getDefaultParameterChangeCapacity (const AudioProcessor& processor)
{
diff --git a/modules/yup_audio_plugin_client/lv2/yup_audio_plugin_client_LV2.cpp b/modules/yup_audio_plugin_client/lv2/yup_audio_plugin_client_LV2.cpp
index f4b99c98e..a6c048af7 100644
--- a/modules/yup_audio_plugin_client/lv2/yup_audio_plugin_client_LV2.cpp
+++ b/modules/yup_audio_plugin_client/lv2/yup_audio_plugin_client_LV2.cpp
@@ -50,7 +50,7 @@
//==============================================================================
-extern "C" yup::AudioProcessor* createPluginProcessor();
+extern "C" yup::AudioProcessor* YUP_AUDIO_PLUGIN_CREATE_FUNCTION();
namespace yup
{
@@ -205,7 +205,7 @@ class AudioPluginProcessorLV2 : private AudioProcessorBase::Listener
processFeatures (features);
- processor.reset (createPluginProcessor());
+ processor.reset (::YUP_AUDIO_PLUGIN_CREATE_FUNCTION());
jassert (processor != nullptr);
if (processor == nullptr)
@@ -214,6 +214,12 @@ class AudioPluginProcessorLV2 : private AudioProcessorBase::Listener
numInputChannels = processor->getBusLayout().getNumAudioInputChannels();
numOutputChannels = processor->getBusLayout().getNumAudioOutputChannels();
+ // Pre-allocate per-bus view storage so run() never allocates
+ inputBusViews.reserve (static_cast (processor->getNumAudioInputs()));
+ outputBusViews.reserve (static_cast (processor->getNumAudioOutputs()));
+ inputChannelPtrStorage.resize (static_cast (numInputChannels));
+ outputChannelPtrStorage.resize (static_cast (numOutputChannels));
+
// Build parameter Urid maps
const auto params = processor->getParameters();
lastSentValues.resize (params.size(), -1.0f);
@@ -315,6 +321,9 @@ class AudioPluginProcessorLV2 : private AudioProcessorBase::Listener
audioBuffer.copyFrom (ch, 0, src, numSamplesInt);
}
+ buildInputBusViews();
+ buildOutputBusViews();
+
// Apply offline processing state
if (freeWheelingPort != nullptr)
processor->setOfflineProcessing (*freeWheelingPort > 0.5f);
@@ -323,7 +332,12 @@ class AudioPluginProcessorLV2 : private AudioProcessorBase::Listener
isBypassed = ! isEnabled;
// Build process context and process
- AudioProcessContext context { audioBuffer, midiEvents, parameterChanges, &playHead };
+ AudioProcessContext context { audioBuffer,
+ midiEvents,
+ parameterChanges,
+ &playHead,
+ { inputBusViews.data(), inputBusViews.size() },
+ { outputBusViews.data(), outputBusViews.size() } };
{
const ScopedLock lock (processor->getProcessLock());
@@ -362,6 +376,66 @@ class AudioPluginProcessorLV2 : private AudioProcessorBase::Listener
*latencyPort = static_cast (processor->getLatencySamples());
}
+ // Builds one AudioBusBufferView per audio input bus from the host input
+ // port buffers. Channels are consumed in bus declaration order; ports not
+ // connected by the host (null) become null channel pointers so processors
+ // see deterministic silence.
+ void buildInputBusViews()
+ {
+ inputBusViews.clear();
+
+ PortIndices indices { numInputChannels, numOutputChannels };
+ int chOffset = 0;
+
+ for (const auto& bus : processor->getBusLayout().getInputBuses())
+ {
+ if (bus.getType() != AudioBus::Type::Audio)
+ continue;
+
+ const int numCh = bus.getNumChannels();
+ auto* chPtrs = inputChannelPtrStorage.data() + chOffset;
+
+ for (int ch = 0; ch < numCh; ++ch)
+ {
+ const auto port = indices.getAudioInputPort (chOffset + ch);
+ const auto* src = (chOffset + ch < numInputChannels) ? audioPorts[static_cast (port)] : nullptr;
+ chPtrs[ch] = src;
+ }
+
+ inputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
+ chOffset += numCh;
+ }
+ }
+
+ // Builds one AudioBusBufferView per audio output bus pointing into the
+ // in-place processing buffer, which is copied to the output ports after
+ // processing (the same memory the flat AudioProcessContext::audio exposes).
+ void buildOutputBusViews()
+ {
+ outputBusViews.clear();
+
+ int chOffset = 0;
+
+ for (const auto& bus : processor->getBusLayout().getOutputBuses())
+ {
+ if (bus.getType() != AudioBus::Type::Audio)
+ continue;
+
+ const int numCh = bus.getNumChannels();
+ auto* chPtrs = outputChannelPtrStorage.data() + chOffset;
+
+ for (int ch = 0; ch < numCh; ++ch)
+ chPtrs[ch] = (chOffset + ch < numOutputChannels)
+ ? audioBuffer.getWritePointer (chOffset + ch)
+ : nullptr;
+
+ outputBusViews.emplace_back (chPtrs, numCh, bus.getRole());
+
+ chOffset += numCh;
+ }
+ }
+
void deactivate()
{
}
@@ -639,6 +713,12 @@ class AudioPluginProcessorLV2 : private AudioProcessorBase::Listener
const float* freeWheelingPort = nullptr;
const float* enabledPort = nullptr;
+ // Per-bus view state, pre-allocated at construction so run() never allocates
+ std::vector> inputBusViews;
+ std::vector> outputBusViews;
+ std::vector inputChannelPtrStorage;
+ std::vector outputChannelPtrStorage;
+
LV2_Worker_Schedule* workerSchedule = nullptr;
std::map paramUridToIndex;
diff --git a/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp b/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp
index ff20dd281..a2000a480 100644
--- a/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp
+++ b/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp
@@ -29,7 +29,7 @@
#error "YUP_AUDIO_PLUGIN_ENABLE_STANDALONE must be defined"
#endif
-extern "C" yup::AudioProcessor* createPluginProcessor();
+extern "C" yup::AudioProcessor* YUP_AUDIO_PLUGIN_CREATE_FUNCTION();
namespace yup
{
@@ -73,7 +73,7 @@ class AudioProcessorApplication
{
public:
AudioProcessorApplication()
- : processor (::createPluginProcessor())
+ : processor (::YUP_AUDIO_PLUGIN_CREATE_FUNCTION())
{
}
diff --git a/modules/yup_audio_plugin_client/tools/yup_lv2_ttl_generator.cpp b/modules/yup_audio_plugin_client/tools/yup_lv2_ttl_generator.cpp
index 872c1a655..823ac1c84 100644
--- a/modules/yup_audio_plugin_client/tools/yup_lv2_ttl_generator.cpp
+++ b/modules/yup_audio_plugin_client/tools/yup_lv2_ttl_generator.cpp
@@ -36,7 +36,7 @@
//==============================================================================
-extern "C" yup::AudioProcessor* createPluginProcessor();
+extern "C" yup::AudioProcessor* YUP_AUDIO_PLUGIN_CREATE_FUNCTION();
//==============================================================================
@@ -331,7 +331,7 @@ int main (int argc, char** argv)
return 1;
}
- std::unique_ptr processor (createPluginProcessor());
+ std::unique_ptr