Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AudioUnitBackend: Initialize parameters concurrently and load music effects too #13887

Merged
merged 8 commits into from
Jan 1, 2025
66 changes: 49 additions & 17 deletions src/effects/backends/audiounit/audiounitbackend.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
#import <AVFAudio/AVFAudio.h>
#import <AudioToolbox/AudioToolbox.h>
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>

#include <QDebug>
#include <QHash>
#include <QList>
#include <QMutex>
#include <QString>
#include <memory>

#include "effects/backends/audiounit/audiounitbackend.h"
#include "effects/backends/audiounit/audiouniteffectprocessor.h"
#include "effects/backends/audiounit/audiounitmanifest.h"
#include "effects/defs.h"
#include "util/compatibility/qmutex.h"

/// An effects backend for Audio Unit (AU) plugins. macOS-only.
class AudioUnitBackend : public EffectsBackend {
public:
AudioUnitBackend() : m_componentsById([[NSDictionary alloc] init]) {
AudioUnitBackend() : m_componentsById([[NSMutableDictionary alloc] init]) {
loadAudioUnits();
}

Expand Down Expand Up @@ -59,51 +62,80 @@ bool canInstantiateEffect(const QString& effectId) const override {
}

private:
NSDictionary<NSString*, AVAudioUnitComponent*>* m_componentsById;
NSMutableDictionary<NSString*, AVAudioUnitComponent*>* m_componentsById;
QHash<QString, EffectManifestPointer> m_manifestsById;
QMutex m_mutex;

void loadAudioUnits() {
qDebug() << "Loading audio units...";

loadAudioUnitsOfType(kAudioUnitType_Effect);
loadAudioUnitsOfType(kAudioUnitType_MusicEffect);
}

void loadAudioUnitsOfType(OSType componentType) {
// 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,
.componentType = componentType,
.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<NSString*, AVAudioUnitComponent*>* componentsById =
[[NSMutableDictionary alloc] init];
QHash<QString, EffectManifestPointer> manifestsById;
// Load component manifests (parameters etc.) concurrently since this
// requires instantiating the corresponding Audio Units. We use Grand
// Central Dispatch (GCD) for this instead of Qt's threading facilities
// since GCD is a bit more lightweight and generally preferred for
// Apple API-related stuff.

dispatch_group_t group = dispatch_group_create();

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));
[component manufacturerName],
[component name],
[component versionString]]);

// Register component
m_componentsById[effectId.toNSString()] = component;

// Use a concurrent background GCD queue to load manifest
dispatch_queue_t queue = dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, queue, ^{
// Load manifest (potentially slow blocking operation)
auto manifest = EffectManifestPointer(
new AudioUnitManifest(effectId, component));

// Register manifest
auto locker = lockMutex(&m_mutex);
m_manifestsById[effectId] = manifest;
});
}

m_componentsById = componentsById;
m_manifestsById = manifestsById;
const int64_t TIMEOUT_MS = 6000;

qDebug() << "Waiting for audio unit manifests to be loaded...";
if (dispatch_group_wait(group,
dispatch_time(DISPATCH_TIME_NOW, TIMEOUT_MS * 1000000)) ==
fwcd marked this conversation as resolved.
Show resolved Hide resolved
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved
0) {
qDebug() << "Successfully loaded audio unit manifests";
} else {
qWarning() << "Timed out while loading audio unit manifests";
}
}
};

Expand Down
7 changes: 7 additions & 0 deletions src/effects/backends/audiounit/audiounitmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,16 @@ class AudioUnitManager {
/// want to e.g. block on a mutex.
AudioUnit _Nullable getAudioUnit() const;

/// Blocks until the audio unit has been instantiated.
///
/// Returns true if the audio unit was instantiated successfully and false if
/// the timeout was reached instead.
bool waitForAudioUnit(int timeoutMs) const;

private:
QString m_name;
std::atomic<bool> m_isInstantiated;
dispatch_group_t _Nonnull m_instantiationGroup;
AudioUnit _Nullable m_audioUnit;

AudioUnitManager(AVAudioUnitComponent* _Nullable component);
Expand Down
17 changes: 15 additions & 2 deletions src/effects/backends/audiounit/audiounitmanager.mm
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#import <AVFAudio/AVFAudio.h>
#import <AudioToolbox/AudioToolbox.h>
#include "util/assert.h"
#import <dispatch/dispatch.h>

#include <QString>

#include "effects/backends/audiounit/audiounitmanager.h"
#include "util/assert.h"

AudioUnitManager::AudioUnitManager(AVAudioUnitComponent* _Nullable component)
: m_name(component != nil ? QString::fromNSString([component name])
: "Unknown"),
m_isInstantiated(false) {
m_isInstantiated(false),
m_instantiationGroup(dispatch_group_create()) {
}

AudioUnitManagerPointer AudioUnitManager::create(
Expand Down Expand Up @@ -56,6 +58,14 @@
return m_audioUnit;
}

bool AudioUnitManager::waitForAudioUnit(int timeoutMs) const {
bool success =
dispatch_group_wait(m_instantiationGroup,
dispatch_time(DISPATCH_TIME_NOW, timeoutMs * 1000000)) == 0;
DEBUG_ASSERT(!success || m_isInstantiated.load());
return success;
}

void AudioUnitManager::instantiateAudioUnitAsync(
AudioUnitManagerPointer pManager,
AVAudioUnitComponent* _Nonnull component,
Expand All @@ -75,6 +85,8 @@
qDebug() << "Instantiating Audio Unit" << pManager->m_name
<< "asynchronously";

dispatch_group_enter(pManager->m_instantiationGroup);

// TODO: Fix the weird formatting of blocks
// clang-format off
AudioComponentInstantiate(component.audioComponent, options, ^(AudioUnit _Nullable audioUnit, OSStatus error) {
Expand All @@ -86,6 +98,7 @@
}

pManager->initializeWith(audioUnit);
dispatch_group_leave(pManager->m_instantiationGroup);
});
// clang-format on
}
Expand Down
16 changes: 13 additions & 3 deletions src/effects/backends/audiounit/audiounitmanifest.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#import <AudioToolbox/AudioToolbox.h>
#include "effects/backends/effectmanifestparameter.h"

#include <QElapsedTimer>
#include <QThread>
#include <memory>

#include "effects/backends/audiounit/audiounitmanager.h"
Expand All @@ -18,9 +20,17 @@
setDescription(QString::fromNSString([component typeName]));
setAuthor(QString::fromNSString([component manufacturerName]));

// Instantiate audio unit (in-process) to load parameters
AudioUnitManagerPointer pManager = AudioUnitManager::create(
component, AudioUnitInstantiationType::Sync);
// Instantiate audio unit (out-of-process) to load parameters
AudioUnitManagerPointer pManager = AudioUnitManager::create(component);

const int TIMEOUT_MS = 2000;
if (!pManager->waitForAudioUnit(TIMEOUT_MS)) {
qWarning() << name() << "took more than" << TIMEOUT_MS
<< "ms to initialize, skipping manifest initialization "
"for this effect. This means this effect will not "
"display any parameters and likely not be useful!";
return;
}

AudioUnit audioUnit = pManager->getAudioUnit();

Expand Down
Loading