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