diff --git a/CMakeLists.txt b/CMakeLists.txt index 80797bf6036..d4e2ead5180 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1595,6 +1595,21 @@ if(APPLE) target_compile_definitions(mixxx-lib PUBLIC __MACOS_ITUNES_LIBRARY__) endif() endif() + + option(AU_EFFECTS "Audio Unit (AU) effects integration" ON) + if(AU_EFFECTS) + target_sources(mixxx-lib PRIVATE + src/effects/backends/audiounit/audiounitbackend.mm + src/effects/backends/audiounit/audiounitmanager.mm + src/effects/backends/audiounit/audiouniteffectprocessor.mm + src/effects/backends/audiounit/audiounitmanifest.mm + ) + target_link_libraries(mixxx-lib PRIVATE + "-weak_framework AudioToolbox" + "-weak_framework AVFAudio" + ) + target_compile_definitions(mixxx-lib PUBLIC __AU_EFFECTS__) + endif() endif() if(EMSCRIPTEN) diff --git a/src/effects/backends/audiounit/audiounitbackend.h b/src/effects/backends/audiounit/audiounitbackend.h new file mode 100644 index 00000000000..03e34872e2d --- /dev/null +++ b/src/effects/backends/audiounit/audiounitbackend.h @@ -0,0 +1,5 @@ +#pragma once + +#include "effects/defs.h" + +EffectsBackendPointer createAudioUnitBackend(); diff --git a/src/effects/backends/audiounit/audiounitbackend.mm b/src/effects/backends/audiounit/audiounitbackend.mm new file mode 100644 index 00000000000..8e7b839b259 --- /dev/null +++ b/src/effects/backends/audiounit/audiounitbackend.mm @@ -0,0 +1,112 @@ +#include "effects/backends/audiounit/audiounitbackend.h" + +#import +#import +#import + +#include +#include +#include +#include +#include + +#include "effects/backends/audiounit/audiounitbackend.h" +#include "effects/backends/audiounit/audiouniteffectprocessor.h" +#include "effects/backends/audiounit/audiounitmanifest.h" +#include "effects/defs.h" + +/// An effects backend for Audio Unit (AU) plugins. macOS-only. +class AudioUnitBackend : public EffectsBackend { + public: + AudioUnitBackend() : m_componentsById([[NSDictionary alloc] init]) { + loadAudioUnits(); + } + + ~AudioUnitBackend() override { + } + + EffectBackendType getType() const override { + return EffectBackendType::AudioUnit; + }; + + const QList getEffectIds() const override { + QList effectIds; + + for (NSString* effectId in m_componentsById) { + effectIds.append(QString::fromNSString(effectId)); + } + + return effectIds; + } + + EffectManifestPointer getManifest(const QString& effectId) const override { + return m_manifestsById[effectId]; + } + + const QList getManifests() const override { + return m_manifestsById.values(); + } + + bool canInstantiateEffect(const QString& effectId) const override { + return [m_componentsById objectForKey:effectId.toNSString()] != nil; + } + + std::unique_ptr createProcessor( + const EffectManifestPointer pManifest) const override { + AVAudioUnitComponent* component = + m_componentsById[pManifest->id().toNSString()]; + return std::make_unique(component); + } + + private: + NSDictionary* m_componentsById; + QHash m_manifestsById; + + void loadAudioUnits() { + qDebug() << "Loading audio units..."; + + // See + // https://developer.apple.com/documentation/audiotoolbox/audio_unit_v3_plug-ins/incorporating_audio_effects_and_instruments?language=objc + + // Create a query for audio components + AudioComponentDescription description = { + .componentType = kAudioUnitType_Effect, + .componentSubType = 0, + .componentManufacturer = 0, + .componentFlags = 0, + .componentFlagsMask = 0, + }; + + // Find the audio units + // TODO: Should we perform this asynchronously (e.g. using Qt's + // threading or GCD)? + auto manager = + [AVAudioUnitComponentManager sharedAudioUnitComponentManager]; + auto components = [manager componentsMatchingDescription:description]; + + // Assign ids to the components + NSMutableDictionary* componentsById = + [[NSMutableDictionary alloc] init]; + QHash manifestsById; + + for (AVAudioUnitComponent* component in components) { + qDebug() << "Found audio unit" << [component name]; + + QString effectId = QString::fromNSString( + [NSString stringWithFormat:@"%@~%@~%@", + [component manufacturerName], + [component name], + [component versionString]]); + componentsById[effectId.toNSString()] = component; + manifestsById[effectId] = EffectManifestPointer( + new AudioUnitManifest(effectId, component)); + } + + m_componentsById = componentsById; + m_manifestsById = manifestsById; + } +}; + +EffectsBackendPointer createAudioUnitBackend() { + return EffectsBackendPointer(new AudioUnitBackend()); +} diff --git a/src/effects/backends/audiounit/audiouniteffectprocessor.h b/src/effects/backends/audiounit/audiouniteffectprocessor.h new file mode 100644 index 00000000000..3bbf1d0c933 --- /dev/null +++ b/src/effects/backends/audiounit/audiouniteffectprocessor.h @@ -0,0 +1,67 @@ +#pragma once + +#import +#import +#import + +#include +#include + +#include "audio/types.h" +#include "effects/backends/audiounit/audiounitmanager.h" +#include "effects/backends/effectprocessor.h" +#include "engine/engine.h" + +class AudioUnitEffectGroupState final : public EffectState { + public: + AudioUnitEffectGroupState(const mixxx::EngineParameters& engineParameters); + + void render(AudioUnit _Nonnull audioUnit, + const mixxx::EngineParameters& engineParameters, + const CSAMPLE* _Nonnull pInput, + CSAMPLE* _Nonnull pOutput); + + private: + AudioTimeStamp m_timestamp; + AudioBufferList m_inputBuffers; + AudioBufferList m_outputBuffers; + + static OSStatus renderCallbackUntyped(void* _Nonnull rawThis, + AudioUnitRenderActionFlags* _Nonnull inActionFlags, + const AudioTimeStamp* _Nonnull inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumFrames, + AudioBufferList* _Nonnull ioData); + OSStatus renderCallback(AudioUnitRenderActionFlags* _Nonnull inActionFlags, + const AudioTimeStamp* _Nonnull inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumFrames, + AudioBufferList* _Nonnull ioData) const; +}; + +class AudioUnitEffectProcessor final : public EffectProcessorImpl { + public: + AudioUnitEffectProcessor(AVAudioUnitComponent* _Nullable component = nil); + + void loadEngineEffectParameters( + const QMap& parameters) + override; + + void processChannel(AudioUnitEffectGroupState* _Nonnull channelState, + const CSAMPLE* _Nonnull pInput, + CSAMPLE* _Nonnull pOutput, + const mixxx::EngineParameters& engineParameters, + const EffectEnableState enableState, + const GroupFeatureState& groupFeatures) override; + + private: + AudioUnitManager m_manager; + + QList m_parameters; + QList m_lastValues; + mixxx::audio::ChannelCount m_lastChannelCount; + mixxx::audio::SampleRate m_lastSampleRate; + + void syncParameters(); + void syncStreamFormat(const mixxx::EngineParameters& engineParameters); +}; diff --git a/src/effects/backends/audiounit/audiouniteffectprocessor.mm b/src/effects/backends/audiounit/audiouniteffectprocessor.mm new file mode 100644 index 00000000000..621e119d69e --- /dev/null +++ b/src/effects/backends/audiounit/audiouniteffectprocessor.mm @@ -0,0 +1,253 @@ +#import +#import +#import +#include + +#include +#include +#include + +#include "effects/backends/audiounit/audiouniteffectprocessor.h" +#include "engine/effects/engineeffectparameter.h" +#include "engine/engine.h" +#include "util/assert.h" + +AudioUnitEffectGroupState::AudioUnitEffectGroupState( + const mixxx::EngineParameters& engineParameters) + : EffectState(engineParameters), + m_timestamp{ + .mSampleTime = 0, + .mFlags = kAudioTimeStampSampleTimeValid, + } { + m_inputBuffers.mNumberBuffers = 1; + m_outputBuffers.mNumberBuffers = 1; +} + +// static +OSStatus AudioUnitEffectGroupState::renderCallbackUntyped( + void* _Nonnull rawThis, + AudioUnitRenderActionFlags* _Nonnull inActionFlags, + const AudioTimeStamp* _Nonnull inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumFrames, + AudioBufferList* _Nonnull ioData) { + return static_cast(rawThis)->renderCallback( + inActionFlags, inTimeStamp, inBusNumber, inNumFrames, ioData); +} + +OSStatus AudioUnitEffectGroupState::renderCallback(AudioUnitRenderActionFlags*, + const AudioTimeStamp*, + UInt32, + UInt32, + AudioBufferList* ioData) const { + if (ioData->mNumberBuffers == 0) { + qWarning() << "Audio unit render callback failed, no buffers available " + "to write to."; + return noErr; + } + VERIFY_OR_DEBUG_ASSERT(m_inputBuffers.mNumberBuffers > 0) { + qWarning() << "Audio unit render callback failed, no buffers available " + "to read from."; + return noErr; + } + ioData->mBuffers[0].mData = m_inputBuffers.mBuffers[0].mData; + return noErr; +} + +void AudioUnitEffectGroupState::render(AudioUnit _Nonnull audioUnit, + const mixxx::EngineParameters& engineParameters, + const CSAMPLE* _Nonnull pInput, + CSAMPLE* _Nonnull pOutput) { + // Find the max number of samples per buffer that the Audio Unit can handle. + // + // Note that (confusingly) the Apple API refers to this count as "frames" + // even though empirical tests show that this has to be interpreted as a + // sample count, otherwise rendering the Audio Unit will fail with status + // -10874 = kAudioUnitErr_TooManyFramesToProcess. + // + // For documentation on this property, see + // https://developer.apple.com/documentation/audiotoolbox/kaudiounitproperty_maximumframesperslice + // + // TODO: Should we cache this? + UInt32 maxSamplesPerChunk = 0; + UInt32 maxSamplesPerChunkSize = sizeof(UInt32); + OSStatus maxSamplesPerSliceStatus = AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_MaximumFramesPerSlice, + kAudioUnitScope_Global, + 0, + &maxSamplesPerChunk, + &maxSamplesPerChunkSize); + if (maxSamplesPerSliceStatus != noErr) { + qWarning() << "Fetching the maximum samples per slice for Audio Unit " + "failed " + "with status" + << maxSamplesPerSliceStatus; + return; + } + + UInt32 totalSamples = engineParameters.samplesPerBuffer(); + + // Process the buffer in chunks + for (UInt32 offset = 0; offset < totalSamples; + offset += maxSamplesPerChunk) { + // Compute the size of the current slice. + UInt32 remainingSamples = totalSamples - offset; + UInt32 chunkSamples = std::min(remainingSamples, maxSamplesPerChunk); + UInt32 chunkSize = sizeof(CSAMPLE) * chunkSamples; + + // Fill the input and output buffers. + m_inputBuffers.mBuffers[0].mData = + const_cast(pInput) + offset; + m_inputBuffers.mBuffers[0].mDataByteSize = chunkSize; + m_outputBuffers.mBuffers[0].mData = pOutput + offset; + m_outputBuffers.mBuffers[0].mDataByteSize = chunkSize; + + // Set the render callback + AURenderCallbackStruct callback; + callback.inputProc = AudioUnitEffectGroupState::renderCallbackUntyped; + callback.inputProcRefCon = this; + + OSStatus setCallbackStatus = AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + 0, + &callback, + sizeof(AURenderCallbackStruct)); + if (setCallbackStatus != noErr) { + qWarning() + << "Setting Audio Unit render callback failed with status" + << setCallbackStatus; + return; + } + + // Apply the actual effect to the sample. + AudioUnitRenderActionFlags flags = 0; + NSInteger outputBusNumber = 0; + OSStatus renderStatus = AudioUnitRender(audioUnit, + &flags, + &m_timestamp, + outputBusNumber, + chunkSamples, + &m_outputBuffers); + if (renderStatus != noErr) { + qWarning() << "Rendering Audio Unit failed with status" + << renderStatus; + return; + } + + // Increment the timestamp + m_timestamp.mSampleTime += chunkSamples; + } +} + +AudioUnitEffectProcessor::AudioUnitEffectProcessor( + AVAudioUnitComponent* _Nullable component) + : m_manager(component) { +} + +void AudioUnitEffectProcessor::loadEngineEffectParameters( + const QMap& parameters) { + m_parameters = parameters.values(); +} + +void AudioUnitEffectProcessor::processChannel( + AudioUnitEffectGroupState* _Nonnull channelState, + const CSAMPLE* _Nonnull pInput, + CSAMPLE* _Nonnull pOutput, + const mixxx::EngineParameters& engineParameters, + const EffectEnableState, + const GroupFeatureState&) { + AudioUnit _Nullable audioUnit = m_manager.getAudioUnit(); + if (!audioUnit) { + qWarning() + << "Cannot process channel before Audio Unit is instantiated"; + return; + } + + // Sync engine parameters with Audio Unit + syncStreamFormat(engineParameters); + + // Sync effect parameters with Audio Unit + syncParameters(); + + // Render the effect into the output buffer + channelState->render(audioUnit, engineParameters, pInput, pOutput); +} + +void AudioUnitEffectProcessor::syncParameters() { + AudioUnit _Nullable audioUnit = m_manager.getAudioUnit(); + DEBUG_ASSERT(audioUnit != nil); + + m_lastValues.reserve(m_parameters.size()); + + int i = 0; + for (auto parameter : m_parameters) { + if (m_lastValues.size() < i) { + m_lastValues.push_back(NAN); + } + DEBUG_ASSERT(m_lastValues.size() >= i); + + AudioUnitParameterID id = parameter->id().toInt(); + auto value = static_cast(parameter->value()); + + // Update parameter iff changed since the last sync + if (m_lastValues[i] != value) { + m_lastValues[i] = value; + + OSStatus status = AudioUnitSetParameter( + audioUnit, id, kAudioUnitScope_Global, 0, value, 0); + if (status != noErr) { + qWarning() + << "Could not set Audio Unit parameter" << id << ":" + << status + << "(Check https://www.osstatus.com for a description)"; + } + } + + i++; + } +} + +void AudioUnitEffectProcessor::syncStreamFormat( + const mixxx::EngineParameters& parameters) { + AudioUnit _Nullable audioUnit = m_manager.getAudioUnit(); + DEBUG_ASSERT(audioUnit != nil); + + if (parameters.sampleRate() != m_lastSampleRate || + parameters.channelCount() != m_lastChannelCount) { + auto sampleRate = parameters.sampleRate(); + auto channelCount = parameters.channelCount(); + + m_lastSampleRate = sampleRate; + m_lastChannelCount = channelCount; + + AVAudioFormat* audioFormat = [[AVAudioFormat alloc] + initWithCommonFormat:AVAudioPCMFormatFloat32 + sampleRate:sampleRate + channels:channelCount + interleaved:false]; + + const auto* streamFormat = [audioFormat streamDescription]; + + qDebug() << "Updating Audio Unit stream format to sample rate" + << sampleRate << "and channel count" << channelCount; + + for (auto scope : {kAudioUnitScope_Input, kAudioUnitScope_Output}) { + OSStatus status = AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_StreamFormat, + scope, + 0, + streamFormat, + sizeof(AudioStreamBasicDescription)); + + if (status != noErr) { + qWarning() + << "Could not set Audio Unit stream format to sample " + "rate" + << sampleRate << "and channel count" << channelCount + << ":" << status + << "(Check https://www.osstatus.com for a description)"; + } + } + } +} diff --git a/src/effects/backends/audiounit/audiounitmanager.h b/src/effects/backends/audiounit/audiounitmanager.h new file mode 100644 index 00000000000..47de73f9e3d --- /dev/null +++ b/src/effects/backends/audiounit/audiounitmanager.h @@ -0,0 +1,42 @@ +#pragma once + +#import +#import +#import + +#include + +enum AudioUnitInstantiationType { + Sync, + AsyncInProcess, + AsyncOutOfProcess, +}; + +/// A RAII wrapper around an `AudioUnit`. +class AudioUnitManager { + public: + AudioUnitManager(AVAudioUnitComponent* _Nullable component = nil, + AudioUnitInstantiationType instantiationType = + AudioUnitInstantiationType::AsyncOutOfProcess); + ~AudioUnitManager(); + + AudioUnitManager(const AudioUnitManager&) = delete; + AudioUnitManager& operator=(const AudioUnitManager&) = delete; + + /// Fetches the audio unit if already instantiated. + /// + /// Non-blocking and thread-safe, since this method is intended to (also) be + /// called in a real-time context, e.g. from an audio thread, where we don't + /// want to e.g. block on a mutex. + AudioUnit _Nullable getAudioUnit(); + + private: + QString m_name; + std::atomic m_isInstantiated; + AudioUnit _Nullable m_audioUnit; + + void instantiateAudioUnitAsync(AVAudioUnitComponent* _Nonnull component, bool inProcess); + void instantiateAudioUnitSync(AVAudioUnitComponent* _Nonnull component); + + void initializeWith(AudioUnit _Nonnull audioUnit); +}; diff --git a/src/effects/backends/audiounit/audiounitmanager.mm b/src/effects/backends/audiounit/audiounitmanager.mm new file mode 100644 index 00000000000..71793cae175 --- /dev/null +++ b/src/effects/backends/audiounit/audiounitmanager.mm @@ -0,0 +1,109 @@ +#import +#import +#include "util/assert.h" + +#include + +#include "effects/backends/audiounit/audiounitmanager.h" + +AudioUnitManager::AudioUnitManager(AVAudioUnitComponent* _Nullable component, + AudioUnitInstantiationType instantiationType) + : m_name(QString::fromNSString([component name])) { + // NOTE: The component can be null if the lookup failed in + // `AudioUnitBackend::createProcessor`, in which case the effect simply acts + // as an identity function on the audio. Same applies when + // `AudioUnitManager` is default-initialized. + if (!component) { + return; + } + + switch (instantiationType) { + case Sync: + instantiateAudioUnitSync(component); + break; + case AsyncInProcess: + case AsyncOutOfProcess: + instantiateAudioUnitAsync( + component, instantiationType == AsyncInProcess); + break; + } +} + +AudioUnitManager::~AudioUnitManager() { + if (m_isInstantiated.load()) { + qDebug() << "Uninitializing and disposing of Audio Unit" << m_name; + AudioUnitUninitialize(m_audioUnit); + AudioComponentInstanceDispose(m_audioUnit); + } +} + +AudioUnit _Nullable AudioUnitManager::getAudioUnit() { + // We need to load this atomic flag to ensure that we don't get a partial + // read of the audio unit pointer (probably extremely uncommon, but not + // impossible: https://belkadan.com/blog/2023/10/Implicity-Atomic) + if (!m_isInstantiated.load()) { + return nil; + } + return m_audioUnit; +} + +void AudioUnitManager::instantiateAudioUnitAsync( + AVAudioUnitComponent* _Nonnull component, bool inProcess) { + auto options = inProcess ? kAudioComponentInstantiation_LoadInProcess + : kAudioComponentInstantiation_LoadOutOfProcess; + + // Instantiate the audio unit asynchronously. + qDebug() << "Instantiating Audio Unit" << m_name << "asynchronously"; + + // TODO: Fix the weird formatting of blocks + // clang-format off + AudioComponentInstantiate(component.audioComponent, options, ^(AudioUnit _Nullable audioUnit, OSStatus error) { + if (error != noErr) { + qWarning() << "Could not instantiate Audio Unit" << m_name << ":" << error << "(Check https://www.osstatus.com for a description)"; + return; + } + + VERIFY_OR_DEBUG_ASSERT(audioUnit != nil) { + qWarning() << "Could not instantiate Audio Unit" << m_name << "...but the error is noErr, what's going on?"; + return; + } + + initializeWith(audioUnit); + }); + // clang-format on +} + +void AudioUnitManager::instantiateAudioUnitSync( + AVAudioUnitComponent* _Nonnull component) { + AudioUnit _Nullable audioUnit = nil; + OSStatus error = + AudioComponentInstanceNew(component.audioComponent, &audioUnit); + if (error != noErr) { + qWarning() << "Audio Unit" << m_name + << "could not be instantiated:" << error + << "(Check https://www.osstatus.com for a description)"; + } + + initializeWith(audioUnit); +} + +void AudioUnitManager::initializeWith(AudioUnit _Nonnull audioUnit) { + VERIFY_OR_DEBUG_ASSERT(!m_isInstantiated.load()) { + qWarning() << "Audio Unit" << m_name + << "cannot be initialized after already having been " + "instantiated"; + return; + } + + OSStatus initError = AudioUnitInitialize(audioUnit); + if (initError != noErr) { + qWarning() << "Audio Unit" << m_name + << "failed to initialize, i.e. allocate render resources:" + << initError + << "(Check https://www.osstatus.com for a description)"; + return; + } + + m_audioUnit = audioUnit; + m_isInstantiated.store(true); +} diff --git a/src/effects/backends/audiounit/audiounitmanifest.h b/src/effects/backends/audiounit/audiounitmanifest.h new file mode 100644 index 00000000000..25cb3f64976 --- /dev/null +++ b/src/effects/backends/audiounit/audiounitmanifest.h @@ -0,0 +1,11 @@ +#pragma once + +#import + +#include "effects/backends/effectmanifest.h" +#include "effects/defs.h" + +class AudioUnitManifest : public EffectManifest { + public: + AudioUnitManifest(const QString& id, AVAudioUnitComponent* component); +}; diff --git a/src/effects/backends/audiounit/audiounitmanifest.mm b/src/effects/backends/audiounit/audiounitmanifest.mm new file mode 100644 index 00000000000..1ee8fd2c8f2 --- /dev/null +++ b/src/effects/backends/audiounit/audiounitmanifest.mm @@ -0,0 +1,91 @@ +#import +#include "effects/backends/effectmanifestparameter.h" + +#include + +#include "effects/backends/audiounit/audiounitmanager.h" +#include "effects/backends/audiounit/audiounitmanifest.h" +#include "effects/backends/audiounit/audiounitutils.h" +#include "effects/defs.h" + +AudioUnitManifest::AudioUnitManifest( + const QString& id, AVAudioUnitComponent* component) { + setBackendType(EffectBackendType::AudioUnit); + + setId(id); + setName(QString::fromNSString([component name])); + setVersion(QString::fromNSString([component versionString])); + setDescription(QString::fromNSString([component typeName])); + setAuthor(QString::fromNSString([component manufacturerName])); + + // Try instantiating the unit in-process to fetch its properties quickly + + AudioUnitManager manager{component, AudioUnitInstantiationType::Sync}; + AudioUnit audioUnit = manager.getAudioUnit(); + + if (audioUnit) { + // Fetch number of parameters + UInt32 paramListBytes = 0; + AUDIO_UNIT_INFO(audioUnit, ParameterList, Global, 0, ¶mListBytes); + + // Fetch parameter ids + UInt32 paramCount = paramListBytes / sizeof(AudioUnitParameterID); + std::unique_ptr paramIds{ + new AudioUnitParameterID[paramCount]}; + AUDIO_UNIT_GET(audioUnit, + ParameterList, + Global, + 0, + paramIds.get(), + ¶mListBytes); + + // Resolve parameters + AudioUnitParameterInfo paramInfo; + UInt32 paramInfoSize = sizeof(AudioUnitParameterInfo); + bool hasLinkedParam = false; + for (UInt32 i = 0; i < paramCount; i++) { + AudioUnitParameterID paramId = paramIds[i]; + + AUDIO_UNIT_GET(audioUnit, + ParameterInfo, + Global, + paramId, + ¶mInfo, + ¶mInfoSize); + + QString paramName = QString::fromUtf8(paramInfo.name); + auto paramFlags = paramInfo.flags; + + qDebug() << QString::fromNSString([component name]) + << "has parameter" << paramName; + + // TODO: Check CanRamp too? + if (paramFlags & kAudioUnitParameterFlag_IsWritable) { + EffectManifestParameterPointer manifestParam = addParameter(); + manifestParam->setId(QString::number(paramId)); + manifestParam->setName(paramName); + manifestParam->setRange(paramInfo.minValue, + paramInfo.defaultValue, + paramInfo.maxValue); + + // Link the first parameter + // TODO: Figure out if AU plugins provide a better way to figure + // out the "default" parameter + if (!hasLinkedParam) { + manifestParam->setDefaultLinkType( + EffectManifestParameter::LinkType::Linked); + hasLinkedParam = true; + } + + // TODO: Support more modes, e.g. squared, square root in Mixxx + if (paramFlags & kAudioUnitParameterFlag_DisplayLogarithmic) { + manifestParam->setValueScaler( + EffectManifestParameter::ValueScaler::Logarithmic); + } else { + manifestParam->setValueScaler( + EffectManifestParameter::ValueScaler::Linear); + } + } + } + } +} diff --git a/src/effects/backends/audiounit/audiounitutils.h b/src/effects/backends/audiounit/audiounitutils.h new file mode 100644 index 00000000000..9a8c8923352 --- /dev/null +++ b/src/effects/backends/audiounit/audiounitutils.h @@ -0,0 +1,18 @@ +#pragma once + +#define AUDIO_UNIT_GET( \ + INSTANCE, PROPERTY, SCOPE, ELEMENT, OUT_VALUE, OUT_SIZE) \ + AudioUnitGetProperty(INSTANCE, \ + kAudioUnitProperty_##PROPERTY, \ + kAudioUnitScope_##SCOPE, \ + ELEMENT, \ + OUT_VALUE, \ + OUT_SIZE) + +#define AUDIO_UNIT_INFO(INSTANCE, PROPERTY, SCOPE, ELEMENT, OUT_VALUE) \ + AudioUnitGetPropertyInfo(INSTANCE, \ + kAudioUnitProperty_##PROPERTY, \ + kAudioUnitScope_##SCOPE, \ + ELEMENT, \ + OUT_VALUE, \ + nullptr) diff --git a/src/effects/backends/effectsbackend.cpp b/src/effects/backends/effectsbackend.cpp index 583814a3330..1af9f35aef1 100644 --- a/src/effects/backends/effectsbackend.cpp +++ b/src/effects/backends/effectsbackend.cpp @@ -3,6 +3,7 @@ #include namespace { +const QString backendTypeNameAudioUnit = QStringLiteral("AudioUnit"); const QString backendTypeNameLV2 = QStringLiteral("LV2"); // QString::tr requires const char[] rather than QString //: Backend type for effects that are built into Mixxx. @@ -14,6 +15,8 @@ constexpr char backendTypeNameUnknown[] = QT_TRANSLATE_NOOP("EffectsBackend", "U EffectBackendType EffectsBackend::backendTypeFromString(const QString& typeName) { if (typeName == backendTypeNameLV2) { return EffectBackendType::LV2; + } else if (typeName == backendTypeNameAudioUnit) { + return EffectBackendType::AudioUnit; } else { return EffectBackendType::BuiltIn; } @@ -23,6 +26,8 @@ QString EffectsBackend::backendTypeToString(EffectBackendType backendType) { switch (backendType) { case EffectBackendType::BuiltIn: return backendTypeNameBuiltIn; + case EffectBackendType::AudioUnit: + return backendTypeNameAudioUnit; case EffectBackendType::LV2: return backendTypeNameLV2; default: @@ -36,6 +41,8 @@ QString EffectsBackend::translatedBackendName(EffectBackendType backendType) { // Clazy's `tr-non-literal` check is a false positive, because the // source string has been marked `QT_TR_NOOP`. return QObject::tr(backendTypeNameBuiltIn); // clazy:exclude=tr-non-literal + case EffectBackendType::AudioUnit: + return backendTypeNameAudioUnit; case EffectBackendType::LV2: return backendTypeNameLV2; default: diff --git a/src/effects/backends/effectsbackendmanager.cpp b/src/effects/backends/effectsbackendmanager.cpp index abc464b5f87..d92fc049649 100644 --- a/src/effects/backends/effectsbackendmanager.cpp +++ b/src/effects/backends/effectsbackendmanager.cpp @@ -4,6 +4,9 @@ #include "effects/backends/builtin/builtinbackend.h" #include "effects/backends/effectmanifest.h" #include "effects/backends/effectprocessor.h" +#ifdef __AU_EFFECTS__ +#include "effects/backends/audiounit/audiounitbackend.h" +#endif #ifdef __LILV__ #include "effects/backends/lv2/lv2backend.h" #endif @@ -15,6 +18,9 @@ EffectsBackendManager::EffectsBackendManager() { m_pNumEffectsAvailable->setReadOnly(); addBackend(EffectsBackendPointer(new BuiltInBackend())); +#ifdef __AU_EFFECTS__ + addBackend(createAudioUnitBackend()); +#endif #ifdef __LILV__ addBackend(EffectsBackendPointer(new LV2Backend())); #endif diff --git a/src/effects/defs.h b/src/effects/defs.h index c333739ab98..ac26e2b6d2f 100644 --- a/src/effects/defs.h +++ b/src/effects/defs.h @@ -17,6 +17,7 @@ enum class EffectEnableState { // The order of the enum values here is used to determine sort order in EffectManifest::alphabetize enum class EffectBackendType { BuiltIn, + AudioUnit, LV2, Unknown };