1// 2// aulayer_cocoaui.mm 3// surge-au 4// 5// Created by Paul Walker on 12/10/18. 6// 7/* 8 OK so how do cocoa uis work in AU2? There's two things 9 10 1: You return a valid AudioUnitViewInfo which contains basically a bundle URL and class name. You 11 implement this in GetProperty in response to kAudioUnitPropert_CocoaUI 12 13 2: You make that class that you return match the AUCococUIBase protocol which requires it to be 14 able to create a frame 15 16 Now the question is how does that frame get a reference to your audio unit? Well there's trick 17 three, which is the uiForAudioUnit has to use the property mechanism to get a reference to an 18 editor. In this case I do that by sending the kVmBAudioUnitPropert_GetEditPointer which I basically 19 just made up. That returns (in this case) a pointer to a SurgeGUIEditor which uses a subset of the 20 VST api (only the public API) to give you a drawable element. 21 22 The other trick to make this all work is that Surge itself works by having parameter changes which 23 imapct other parameter changes (like when you change patch number it changes patch name) just 24 update parameters and not redraw. This makes sense for all the obvious reasons (thread locality; 25 bunched drawing; etc...). You can see the refresh_param_quuee and refresh_control_queue 26 implementation which just keep getting stacked up. But to unstack them you need to call a redraw, 27 basically, and that's what the editor ::idle method does. So the final thing to make it all work is 28 to spin up a CFRunLoopTimer (not an NSTImer, note; I tried and that's not the way to go) to call 29 ::idle every 50ms. 30 31 And that's what this code does. 32 33 */ 34 35#include <objc/runtime.h> 36#import <CoreFoundation/CoreFoundation.h> 37#import <AudioUnit/AUCocoaUIView.h> 38#import <AppKit/AppKit.h> 39#include "aulayer.h" 40#include "aulayer_cocoaui.h" 41 42#include <gui/SurgeGUIEditor.h> 43#include <gui/CScalableBitmap.h> 44 45using namespace VSTGUI; 46 47@interface SurgeNSView : NSView 48{ 49 SurgeGUIEditor *editController; 50 CFRunLoopTimerRef idleTimer; 51 float lastScale; 52 NSSize underlyingUISize; 53 bool setSizeByZoom; // use this flag to see if resize comes from here or from external 54} 55 56- (id)initWithSurge:(SurgeGUIEditor *)cont preferredSize:(NSSize)size; 57- (void)doIdle; 58- (void)dealloc; 59- (void)setFrame:(NSRect)newSize; 60 61@end 62 63@interface SurgeCocoaUI : NSObject <AUCocoaUIBase> 64{ 65} 66 67- (NSString *)description; 68@end 69 70@implementation SurgeCocoaUI 71- (NSView *)uiViewForAudioUnit:(AudioUnit)inAudioUnit withSize:(NSSize)inPreferredSize 72{ 73 // Remember we end up being called here because that's what AUCocoaUIView does in the initiation 74 // collaboration with hosts 75 SurgeGUIEditor *editController = 0; 76 UInt32 size = sizeof(SurgeGUIEditor *); 77 if (AudioUnitGetProperty(inAudioUnit, kVmbAAudioUnitProperty_GetEditPointer, 78 kAudioUnitScope_Global, 0, &editController, &size) != noErr) 79 return nil; 80 81 return [[[SurgeNSView alloc] initWithSurge:editController 82 preferredSize:inPreferredSize] autorelease]; 83 // return nil; 84} 85 86- (unsigned int)interfaceVersion 87{ 88 return 0; 89} 90 91- (NSString *)description 92{ 93 return [NSString stringWithUTF8String:"Surge View"]; 94} 95 96@end 97 98void timerCallback(CFRunLoopTimerRef timer, void *info) 99{ 100 SurgeNSView *view = (SurgeNSView *)info; 101 [view doIdle]; 102} 103 104@implementation SurgeNSView 105- (id)initWithSurge:(SurgeGUIEditor *)cont preferredSize:(NSSize)size 106{ 107 cont->setZoomCallback([](SurgeGUIEditor *ed, bool) {}); 108 self = [super initWithFrame:NSMakeRect(0, 0, size.width / 2, size.height / 2)]; 109 110 idleTimer = nil; 111 editController = cont; 112 lastScale = cont->getZoomFactor() / 100.0; 113 if (self) 114 { 115 cont->open(self); 116 117 ERect *vr; 118 if (cont->getRect(&vr)) 119 { 120 float zf = cont->getZoomFactor() / 100.0; 121 NSRect newSize = NSMakeRect(0, 0, vr->right - vr->left, vr->bottom - vr->top); 122 underlyingUISize = newSize.size; 123 newSize = NSMakeRect(0, 0, lastScale * (vr->right - vr->left), 124 lastScale * (vr->bottom - vr->top)); 125 setSizeByZoom = true; 126 [self setFrame:newSize]; 127 setSizeByZoom = false; 128 129 VSTGUI::CFrame *frame = cont->getFrame(); 130 if (frame) 131 { 132 frame->setZoom(zf); 133 134 [self.class setExtraScaleFactorForFrame:frame->getBackground() 135 withZoomFactor:cont->getZoomFactor()]; 136 137 frame->setDirty(true); 138 frame->invalid(); 139 } 140 } 141 142 cont->setZoomCallback([self](SurgeGUIEditor *ed, bool) { 143 ERect *vr; 144 if (ed->getRect(&vr)) 145 { 146 float zf = ed->getZoomFactor() / 100.0; 147 int width = (int)((vr->right - vr->left) * zf); 148 int heigth = (int)((vr->bottom - vr->top) * zf); 149 150 lastScale = zf; 151 152 setSizeByZoom = true; 153 154 NSRect newSize = NSMakeRect(0, 0, width, heigth); 155 [self setFrame:newSize]; 156 157 setSizeByZoom = false; 158 159 VSTGUI::CFrame *frame = ed->getFrame(); 160 if (frame) 161 { 162 frame->setZoom(zf); 163 frame->setSize(width, heigth); 164 165 [self.class setExtraScaleFactorForFrame:frame->getBackground() 166 withZoomFactor:ed->getZoomFactor()]; 167 168 frame->setDirty(true); 169 frame->invalid(); 170 } 171 } 172 }); 173 174 CFTimeInterval TIMER_INTERVAL = .05; // In SurgeGUISynthesizer.h it uses 50 ms 175 CFRunLoopTimerContext TimerContext = {0, self, NULL, NULL, NULL}; 176 CFAbsoluteTime FireTime = CFAbsoluteTimeGetCurrent() + TIMER_INTERVAL; 177 idleTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, FireTime, TIMER_INTERVAL, 0, 0, 178 timerCallback, &TimerContext); 179 if (idleTimer) 180 CFRunLoopAddTimer(CFRunLoopGetMain(), idleTimer, kCFRunLoopCommonModes); 181 } 182 183 return self; 184} 185 186+ (void)setExtraScaleFactorForFrame:(VSTGUI::CBitmap *)bg withZoomFactor:(float)zf 187{ 188 if (bg != NULL) 189 { 190 auto sbm = dynamic_cast<CScalableBitmap *>(bg); // dynamic casts are gross but better safe 191 if (sbm) 192 { 193 /* 194 ** VSTGUI has an error which is that the background bitmap doesn't get the frame 195 *transform 196 ** applied. Simply look at cviewcontainer::drawBackgroundRect. So we have to force the 197 *background 198 ** scale up using a backdoor API. 199 */ 200 sbm->setExtraScaleFactor(zf); 201 } 202 } 203} 204 205- (void)doIdle 206{ 207 if (editController) 208 editController->idle(); 209} 210 211- (void)dealloc 212{ 213 // You would think we want to editor->close() here, but we don't; by this point there's a good 214 // chance the AU host has killed our underlying editor object anyway. 215 editController = nullptr; 216 if (idleTimer) 217 { 218 CFRunLoopTimerInvalidate(idleTimer); 219 } 220 221 [super dealloc]; 222} 223 224- (void)setFrame:(NSRect)newSize 225{ 226 /* 227 * I override setFrame because hosts have independent views of window sizes which are saved. 228 * this needs to be found when the host resizes after creation to set the zoomFactor properly. 229 * Teensy bit gross, but works. Seems AU Lab does this but Logic Pro does not. 230 * 231 * the other option is to make zoom a parameter but then its in a patch and that seems wrong. 232 */ 233 NSSize targetSize = newSize.size; 234 235 if (!setSizeByZoom && fabs(targetSize.width - underlyingUISize.width * lastScale) > 2) 236 { 237 // so what's my apparent ratio 238 float apparentZoom = targetSize.width / (underlyingUISize.width * lastScale); 239 int azi = roundf(apparentZoom * 10) * 240 10; // this is a bit gross. I know the zoom is incremented by 10s 241 editController->setZoomFactor(azi); 242 } 243 244 [super setFrame:newSize]; 245} 246 247@end 248 249static CFBundleRef GetBundleFromExecutable(const char *filepath) 250{ 251 @autoreleasepool 252 { 253 NSString *execStr = [NSString stringWithCString:filepath encoding:NSUTF8StringEncoding]; 254 NSString *macOSStr = [execStr stringByDeletingLastPathComponent]; 255 NSString *contentsStr = [macOSStr stringByDeletingLastPathComponent]; 256 NSString *bundleStr = [contentsStr stringByDeletingLastPathComponent]; 257 return CFBundleCreate(0, (CFURLRef)[NSURL fileURLWithPath:bundleStr isDirectory:YES]); 258 } 259} 260 261//---------------------------------------------------------------------------------------------------- 262 263const char *getclamptxt(int id) 264{ 265 switch (id) 266 { 267 case 1: 268 return "Macro Parameters"; 269 case 2: 270 return "Global / FX"; 271 case 3: 272 return "Scene A Common"; 273 case 4: 274 return "Scene A Osc"; 275 case 5: 276 return "Scene A Osc Mixer"; 277 case 6: 278 return "Scene A Filters"; 279 case 7: 280 return "Scene A Envelopes"; 281 case 8: 282 return "Scene A LFOs"; 283 case 9: 284 return "Scene B Common"; 285 case 10: 286 return "Scene B Osc"; 287 case 11: 288 return "Scene B Osc Mixer"; 289 case 12: 290 return "Scene B Filters"; 291 case 13: 292 return "Scene B Envelopes"; 293 case 14: 294 return "Scene B LFOs"; 295 } 296 return ""; 297} 298 299ComponentResult aulayer::GetProperty(AudioUnitPropertyID iID, AudioUnitScope iScope, 300 AudioUnitElement iElem, void *outData) 301{ 302 if (iScope == kAudioUnitScope_Global) 303 { 304 switch (iID) 305 { 306 case kAudioUnitProperty_CocoaUI: 307 { 308 auto surgeclass = objc_getClass("SurgeCocoaUI"); 309 const char *image = class_getImageName(surgeclass); 310 CFBundleRef bundle = GetBundleFromExecutable(image); 311 CFURLRef url = CFBundleCopyBundleURL(bundle); 312 CFRetain(url); 313 CFRelease(bundle); 314 315 AudioUnitCocoaViewInfo *info = static_cast<AudioUnitCocoaViewInfo *>(outData); 316 info->mCocoaAUViewClass[0] = CFStringCreateWithCString( 317 kCFAllocatorDefault, class_getName(surgeclass), kCFStringEncodingUTF8); 318 info->mCocoaAUViewBundleLocation = url; 319 320 return noErr; 321 } 322 case kVmbAAudioUnitProperty_GetEditPointer: 323 { 324 if (editor_instance == NULL) 325 { 326 editor_instance = new SurgeGUIEditor(this, plugin_instance); 327 editor_instance->loadFromDAWExtraState(plugin_instance); 328 } 329 void **pThis = (void **)(outData); 330 *pThis = (void *)editor_instance; 331 332 return noErr; 333 } 334 case kAudioUnitProperty_ParameterValueName: 335 { 336 if (!IsInitialized()) 337 return kAudioUnitErr_Uninitialized; 338 AudioUnitParameterValueName *aup = (AudioUnitParameterValueName *)outData; 339 char tmptxt[64]; 340 float f = 0; 341 if (aup->inValue) 342 f = *(aup->inValue); 343 else 344 { 345 SurgeSynthesizer::ID iid; 346 if (plugin_instance->fromDAWSideIndex(aup->inParamID, iid)) 347 f = plugin_instance->getParameter01(iid); 348 } 349 SurgeSynthesizer::ID iid; 350 if (plugin_instance->fromDAWSideIndex(aup->inParamID, iid)) 351 { 352 plugin_instance->getParameterDisplay(iid, tmptxt, f); 353 } 354 aup->outName = CFStringCreateWithCString(NULL, tmptxt, kCFStringEncodingUTF8); 355 return noErr; 356 } 357 case kAudioUnitProperty_ParameterClumpName: 358 { 359 AudioUnitParameterNameInfo *aupn = (AudioUnitParameterNameInfo *)outData; 360 aupn->outName = 361 CFStringCreateWithCString(NULL, getclamptxt(aupn->inID), kCFStringEncodingUTF8); 362 return noErr; 363 } 364 case kVmbAAudioUnitProperty_GetPluginCPPInstance: 365 { 366 void **pThis = (void **)(outData); 367 *pThis = (void *)plugin_instance; 368 return noErr; 369 } 370 case kAudioUnitProperty_SupportsMPE: 371 { 372 uint32_t *o = (uint32_t *)outData; 373 *o = 1; 374 return noErr; 375 } 376 } 377 } 378 379 return AUInstrumentBase::GetProperty(iID, iScope, iElem, outData); 380} 381 382void aulayer::setPresetByID(int pid) 383{ 384 if (surgePatchToAUPatch.find(pid) == surgePatchToAUPatch.end()) 385 { 386 return; 387 } 388 389 AUPreset preset; 390 preset.presetNumber = surgePatchToAUPatch[pid]; 391 preset.presetName = CFStringCreateWithCString( 392 NULL, plugin_instance->storage.patch_list[pid].name.c_str(), kCFStringEncodingUTF8); 393 SetAFactoryPresetAsCurrent(preset); 394 PropertyChanged(kAudioUnitProperty_CurrentPreset, kAudioUnitScope_Global, 0); 395 PropertyChanged(kAudioUnitProperty_PresentPreset, kAudioUnitScope_Global, 0); 396} 397