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

Open
wants to merge 5 commits into
base: 2.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)) ==
Comment on lines +132 to +133
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks reasonably correct to me according to the apple docs. A QWaitCondition would also just essentially wait, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think so. Implementing the other wait with a GCD group instead of a QWaitCondition might btw be worth a try. Since the async handler is likely already invoked on some GCD queue, we'll probably run into less unexpected things there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, my bad. I thought this was the place where the earlier comments where referring to, not the QThread::sleep in AudioUnitManifest.

0) {
qDebug() << "Successfully loaded audio unit manifests";
} else {
qWarning() << "Timed out while loading audio unit manifests";
}
}
};

Expand Down
22 changes: 20 additions & 2 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,25 @@
setDescription(QString::fromNSString([component typeName]));
setAuthor(QString::fromNSString([component manufacturerName]));

// Try instantiating the unit in-process to fetch its properties quickly
// Instantiate audio unit (out-of-process) to load parameters
AudioUnitManager manager{component};

const int TIMEOUT_MS = 2000;

QElapsedTimer timer;
timer.start();

while (manager.getAudioUnit() == nil) {
if (timer.elapsed() > 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;
}
QThread::msleep(10);
}

AudioUnitManager manager{component, AudioUnitInstantiationType::Sync};
AudioUnit audioUnit = manager.getAudioUnit();

if (audioUnit) {
Expand Down
Loading