#include "rack_au.h"
#import <AudioToolbox/AudioToolbox.h>
#import <CoreAudioKit/CoreAudioKit.h>
#import <AppKit/AppKit.h>
#import <CoreFoundation/CoreFoundation.h>
#include <cstring>
#include <dispatch/dispatch.h>
struct RackAUGui {
AudioComponentInstance audio_unit;
NSViewController* view_controller; NSView* view; NSWindow* window; NSMutableArray* slider_targets; bool owns_view_controller; bool owns_view;
char error_message[256];
};
@interface RackAUSliderTarget : NSObject
@property (assign) AudioComponentInstance audioUnit;
@property (assign) AudioUnitParameterID parameterID;
@property (weak) NSTextField* valueLabel;
- (void)sliderChanged:(NSSlider*)slider;
@end
@implementation RackAUSliderTarget
- (void)sliderChanged:(NSSlider*)slider {
AudioUnitParameterValue value = [slider doubleValue];
AudioUnitSetParameter(self.audioUnit, self.parameterID, kAudioUnitScope_Global, 0, value, 0);
if (self.valueLabel) {
[self.valueLabel setStringValue:[NSString stringWithFormat:@"%.2f", value]];
}
}
@end
typedef void (*RackAUGuiCallback)(void* user_data, RackAUGui* gui, int error_code);
static UInt32 get_parameter_count(AudioComponentInstance audio_unit) {
UInt32 param_count = 0;
UInt32 size = 0;
OSStatus status = AudioUnitGetPropertyInfo(
audio_unit,
kAudioUnitProperty_ParameterList,
kAudioUnitScope_Global,
0,
&size,
NULL
);
if (status == noErr && size > 0) {
param_count = size / sizeof(AudioUnitParameterID);
}
return param_count;
}
static bool get_parameter_info(
AudioComponentInstance audio_unit,
AudioUnitParameterID param_id,
AudioUnitParameterInfo* info,
char* name_buffer,
size_t name_buffer_size
) {
UInt32 size = sizeof(AudioUnitParameterInfo);
OSStatus status = AudioUnitGetProperty(
audio_unit,
kAudioUnitProperty_ParameterInfo,
kAudioUnitScope_Global,
param_id,
info,
&size
);
if (status != noErr) {
return false;
}
if (info->cfNameString != NULL) {
CFStringGetCString(
info->cfNameString,
name_buffer,
name_buffer_size,
kCFStringEncodingUTF8
);
} else {
snprintf(name_buffer, name_buffer_size, "Parameter %u", (unsigned)param_id);
}
return true;
}
static NSView* create_generic_ui(AudioComponentInstance audio_unit, NSMutableArray** out_targets) {
@autoreleasepool {
NSMutableArray* targets = [[NSMutableArray alloc] init];
UInt32 param_count = get_parameter_count(audio_unit);
if (param_count == 0) {
NSTextField* label = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 400, 40)];
[label setStringValue:@"This plugin has no parameters"];
[label setBezeled:NO];
[label setDrawsBackground:NO];
[label setEditable:NO];
[label setSelectable:NO];
[label setAlignment:NSTextAlignmentCenter];
return label;
}
NSStackView* stackView = [[NSStackView alloc] init];
[stackView setOrientation:NSUserInterfaceLayoutOrientationVertical];
[stackView setAlignment:NSLayoutAttributeLeading];
[stackView setSpacing:10];
UInt32 size = param_count * sizeof(AudioUnitParameterID);
AudioUnitParameterID* param_ids = (AudioUnitParameterID*)malloc(size);
if (!param_ids) {
return stackView;
}
OSStatus status = AudioUnitGetProperty(
audio_unit,
kAudioUnitProperty_ParameterList,
kAudioUnitScope_Global,
0,
param_ids,
&size
);
if (status != noErr) {
free(param_ids);
return stackView; }
UInt32 display_count = param_count > 20 ? 20 : param_count;
for (UInt32 i = 0; i < display_count; i++) {
AudioUnitParameterID param_id = param_ids[i];
AudioUnitParameterInfo info;
char name_buffer[256];
if (!get_parameter_info(audio_unit, param_id, &info, name_buffer, sizeof(name_buffer))) {
continue;
}
NSStackView* rowView = [[NSStackView alloc] init];
[rowView setOrientation:NSUserInterfaceLayoutOrientationHorizontal];
[rowView setSpacing:10];
NSTextField* nameLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)];
[nameLabel setStringValue:[NSString stringWithUTF8String:name_buffer]];
[nameLabel setBezeled:NO];
[nameLabel setDrawsBackground:NO];
[nameLabel setEditable:NO];
[nameLabel setSelectable:NO];
[nameLabel setAlignment:NSTextAlignmentRight];
NSSlider* slider = [[NSSlider alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)];
[slider setMinValue:info.minValue];
[slider setMaxValue:info.maxValue];
[slider setDoubleValue:info.defaultValue];
AudioUnitParameterValue currentValue = info.defaultValue;
AudioUnitGetParameter(audio_unit, param_id, kAudioUnitScope_Global, 0, ¤tValue);
[slider setDoubleValue:currentValue];
NSTextField* valueLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 80, 24)];
[valueLabel setStringValue:[NSString stringWithFormat:@"%.2f", currentValue]];
[valueLabel setBezeled:NO];
[valueLabel setDrawsBackground:NO];
[valueLabel setEditable:NO];
[valueLabel setSelectable:NO];
[valueLabel setAlignment:NSTextAlignmentLeft];
RackAUSliderTarget* target = [[RackAUSliderTarget alloc] init];
target.audioUnit = audio_unit;
target.parameterID = param_id;
target.valueLabel = valueLabel;
[slider setTarget:target];
[slider setAction:@selector(sliderChanged:)];
[targets addObject:target];
[rowView addView:nameLabel inGravity:NSStackViewGravityLeading];
[rowView addView:slider inGravity:NSStackViewGravityLeading];
[rowView addView:valueLabel inGravity:NSStackViewGravityLeading];
[stackView addView:rowView inGravity:NSStackViewGravityTop];
}
free(param_ids);
NSSize contentSize = [stackView fittingSize];
[stackView setFrameSize:contentSize];
if (out_targets) {
*out_targets = targets;
}
return stackView;
}
}
static void try_load_auv3_gui(
AudioComponentInstance audio_unit,
void (^completion)(AUViewControllerBase* viewController)
) {
@autoreleasepool {
AudioComponent component = AudioComponentInstanceGetComponent(audio_unit);
if (component == NULL) {
completion(nil);
return;
}
AudioComponentDescription desc;
if (AudioComponentGetDescription(component, &desc) != noErr) {
completion(nil);
return;
}
[AUAudioUnit instantiateWithComponentDescription:desc
options:0
completionHandler:^(AUAudioUnit* _Nullable auAudioUnit, NSError* _Nullable error) {
if (error != nil || auAudioUnit == nil) {
completion(nil);
return;
}
[auAudioUnit requestViewControllerWithCompletionHandler:^(AUViewControllerBase* _Nullable viewController) {
completion(viewController);
}];
}];
}
}
static NSView* try_load_auv2_gui(AudioComponentInstance audio_unit) {
@autoreleasepool {
AudioUnitCocoaViewInfo viewInfo;
UInt32 dataSize = sizeof(AudioUnitCocoaViewInfo);
OSStatus status = AudioUnitGetProperty(
audio_unit,
kAudioUnitProperty_CocoaUI,
kAudioUnitScope_Global,
0,
&viewInfo,
&dataSize
);
if (status != noErr || viewInfo.mCocoaAUViewBundleLocation == NULL) {
return nil;
}
NSView* auView = nil;
@try {
NSURL* bundleURL = (__bridge NSURL*)viewInfo.mCocoaAUViewBundleLocation;
NSBundle* viewBundle = [NSBundle bundleWithURL:bundleURL];
if (viewBundle == nil) {
return nil;
}
NSString* viewClassName = (__bridge NSString*)viewInfo.mCocoaAUViewClass[0];
Class viewClass = [viewBundle classNamed:viewClassName];
if (viewClass == nil) {
return nil;
}
if ([viewClass instancesRespondToSelector:@selector(initWithAudioUnit:)]) {
SEL selector = @selector(initWithAudioUnit:);
NSMethodSignature *signature = [viewClass instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
id instance = [[viewClass alloc] init];
[invocation setTarget:instance];
[invocation setArgument:&audio_unit atIndex:2]; [invocation invoke];
[invocation getReturnValue:&auView];
}
}
@finally {
CFRelease(viewInfo.mCocoaAUViewBundleLocation);
if (viewInfo.mCocoaAUViewClass[0] != NULL) {
CFRelease(viewInfo.mCocoaAUViewClass[0]);
}
}
return auView;
}
}
extern "C" AudioComponentInstance rack_au_plugin_get_audio_unit(RackAUPlugin* plugin);
extern "C" {
void rack_au_gui_create_async(
RackAUPlugin* plugin,
RackAUGuiCallback callback,
void* user_data
) {
if (!plugin || !callback) {
if (callback) {
callback(user_data, NULL, RACK_AU_ERROR_INVALID_PARAM);
}
return;
}
AudioComponentInstance audio_unit = rack_au_plugin_get_audio_unit(plugin);
if (audio_unit == NULL) {
callback(user_data, NULL, RACK_AU_ERROR_INVALID_PARAM);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
try_load_auv3_gui(audio_unit, ^(AUViewControllerBase* viewController) {
if (viewController != nil) {
RackAUGui* gui = new RackAUGui();
gui->audio_unit = audio_unit;
gui->view_controller = viewController;
gui->view = viewController.view;
gui->window = nil;
gui->slider_targets = nil; gui->owns_view_controller = true;
gui->owns_view = false; gui->error_message[0] = '\0';
callback(user_data, gui, RACK_AU_OK);
} else {
NSView* auv2_view = try_load_auv2_gui(audio_unit);
if (auv2_view != nil) {
RackAUGui* gui = new RackAUGui();
gui->audio_unit = audio_unit;
gui->view_controller = nil;
gui->view = auv2_view;
gui->window = nil;
gui->slider_targets = nil; gui->owns_view_controller = false;
gui->owns_view = true;
gui->error_message[0] = '\0';
callback(user_data, gui, RACK_AU_OK);
} else {
RackAUGui* gui = new RackAUGui();
NSMutableArray* targets = nil;
NSView* generic_view = create_generic_ui(audio_unit, &targets);
gui->audio_unit = audio_unit;
gui->view_controller = nil;
gui->view = generic_view;
gui->window = nil;
gui->slider_targets = targets;
gui->owns_view_controller = false;
gui->owns_view = true;
gui->error_message[0] = '\0';
callback(user_data, gui, RACK_AU_OK);
}
}
});
}
});
}
void rack_au_gui_destroy(RackAUGui* gui) {
if (!gui) {
return;
}
auto cleanup = ^{
@autoreleasepool {
if (gui->window != nil) {
[gui->window close];
gui->window = nil;
}
if (gui->owns_view_controller && gui->view_controller != nil) {
gui->view_controller = nil; }
if (gui->owns_view && gui->view != nil) {
[gui->view removeFromSuperview];
gui->view = nil; }
if (gui->slider_targets != nil) {
gui->slider_targets = nil; }
delete gui;
}
};
if ([NSThread isMainThread]) {
cleanup();
} else {
dispatch_sync(dispatch_get_main_queue(), cleanup);
}
}
void* rack_au_gui_get_view(RackAUGui* gui) {
if (!gui) {
return NULL;
}
return (__bridge void*)gui->view;
}
int rack_au_gui_get_size(RackAUGui* gui, float* width, float* height) {
if (!gui || !gui->view || !width || !height) {
return RACK_AU_ERROR_INVALID_PARAM;
}
NSSize size;
if ([NSThread isMainThread]) {
size = [gui->view frame].size;
} else {
__block NSSize blockSize;
dispatch_sync(dispatch_get_main_queue(), ^{
blockSize = [gui->view frame].size;
});
size = blockSize;
}
*width = size.width;
*height = size.height;
return RACK_AU_OK;
}
int rack_au_gui_show_window(RackAUGui* gui, const char* title) {
if (!gui || !gui->view) {
return RACK_AU_ERROR_INVALID_PARAM;
}
NSString* windowTitle = nil;
if (title != NULL) {
windowTitle = [NSString stringWithUTF8String:title];
} else {
windowTitle = @"AudioUnit GUI";
}
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
NSSize viewSize = [gui->view frame].size;
if (gui->window == nil) {
NSRect frame = NSMakeRect(100, 100, viewSize.width, viewSize.height);
gui->window = [[NSWindow alloc] initWithContentRect:frame
styleMask:(NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable)
backing:NSBackingStoreBuffered
defer:NO];
[gui->window setContentView:gui->view];
[gui->window setTitle:windowTitle];
}
[NSApp activateIgnoringOtherApps:YES];
[gui->window makeKeyAndOrderFront:nil];
[gui->window center];
[gui->window setLevel:NSFloatingWindowLevel]; }
});
return RACK_AU_OK;
}
int rack_au_gui_hide_window(RackAUGui* gui) {
if (!gui) {
return RACK_AU_ERROR_INVALID_PARAM;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (gui->window != nil) {
[gui->window orderOut:nil];
}
});
return RACK_AU_OK;
}
}