forked from notahat/simplesynth
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathSynthController.m
More file actions
437 lines (347 loc) · 15.1 KB
/
Copy pathSynthController.m
File metadata and controls
437 lines (347 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
#import "SynthController.h"
#import <PYMIDI/PYMIDI.h>
#import <objc/runtime.h>
#import "InstrumentsDataSource.h"
#import "ChannelsDataSource.h"
// Lightweight NSView subclass that accepts dropped .sf2/.dls files and forwards
// them to the app delegate's application:openFile:. We don't subclass at design
// time; instead awakeFromNib runtime-swaps the window's contentView class to
// this one and registers the drag types. No ivars added — safe to swap.
@interface SoundFontDropView : NSView
@end
static BOOL SoundFontDropView_isSoundFontURL(NSURL* u)
{
NSString* ext = [[u pathExtension] lowercaseString];
return [ext isEqualToString:@"sf2"] || [ext isEqualToString:@"dls"];
}
@implementation SoundFontDropView
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
NSArray* urls = [[sender draggingPasteboard]
readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:nil];
for (NSURL* u in urls) {
if (SoundFontDropView_isSoundFontURL(u)) return NSDragOperationCopy;
}
return NSDragOperationNone;
}
- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender { return YES; }
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
{
NSArray* urls = [[sender draggingPasteboard]
readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:nil];
id delegate = [NSApp delegate];
BOOL anyOpened = NO;
for (NSURL* u in urls) {
if (!SoundFontDropView_isSoundFontURL(u)) continue;
if ([delegate respondsToSelector:@selector(application:openFile:)]) {
if ([delegate application:NSApp openFile:[u path]]) anyOpened = YES;
}
}
return anyOpened;
}
@end
@implementation SynthController
- (void)awakeFromNib
{
ChannelsDataSource* channelsDataSource;
InstrumentsDataSource* instrumentsDataSource;
NSPoint origin;
audioSystem = [[AudioSystem alloc] init];
virtualDestination = [[PYMIDIVirtualDestination alloc] initWithName:@"SimpleSynth virtual input"];
[self buildMIDIInputPopUp];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(midiSetupChanged:)
name:@"PYMIDISetupChanged" object:nil
];
// This alloc/init pair is split over 2 lines to stop silly compiler warnings.
channelsDataSource = [ChannelsDataSource alloc];
channelsDataSource = [channelsDataSource initWithAudioSystem:audioSystem];
[channelsTable setDataSource:channelsDataSource];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(audioSystemInstrumentChanged:)
name:@"instrumentChanged" object:audioSystem
];
// Catch when user picks a new MIDI channel to update Instruments selection to match
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(channelsTableSelectionChanged:)
name:@"NSTableViewSelectionDidChangeNotification" object:channelsTable
];
// The following hardcoded values should really be dynamically pulled from the
// AudioSystem, but the values you get when doing that make the slider very
// lop-sided. I'd also have to set the slider labels dynamically.
[cutoffSlider setMinValue:10.0];
[cutoffSlider setMaxValue:12000.0];
[audioSystem setFilterCutoff:12000.0];
[cutoffSlider setFloatValue:[audioSystem getFilterCutoff]];
instrumentsDataSource = [[InstrumentsDataSource alloc] initWithAudioSystem:audioSystem];
[instrumentsTable setDataSource:instrumentsDataSource];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(instrumentsTableSelectionChanged:)
name:@"NSTableViewSelectionDidChangeNotification" object:instrumentsTable
];
[self updateMIDIDetails];
// We must delay opening the drawer until the window is visible
[instrumentsDrawer performSelector:@selector(open) withObject:nil afterDelay:0];
if (![mainWindow setFrameUsingName:@"MainWindowFrame"]) {
// Center our window, taking the width of the drawer into account.
[mainWindow center];
origin = [mainWindow frame].origin;
origin = NSMakePoint (origin.x - [instrumentsDrawer contentSize].width/2.0, origin.y);
[mainWindow setFrameOrigin:origin];
}
[mainWindow setFrameAutosaveName:@"MainWindowFrame"];
uiUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateUI:) userInfo:nil repeats:YES];
[uiUpdateTimer retain];
// Enable drag-and-drop of .sf2/.dls files onto the main window. Swap the
// contentView's class at runtime so it can respond as a drag destination,
// then register the file-URL pasteboard type.
NSView* cv = [mainWindow contentView];
object_setClass(cv, [SoundFontDropView class]);
[cv registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]];
[self installVolumeControl];
}
// Insert a master-volume row above the CPU display. The nib doesn't have one
// (this app pre-dates the need: it was designed when the Apple DLS synth's
// fixed level matched system volume expectations). We make room by shrinking
// the channels table from the bottom, then add a Volume label + NSSlider
// anchored to the bottom of the content view.
- (void)installVolumeControl
{
NSView* cv = [mainWindow contentView];
const CGFloat kRowHeight = 28.0;
// Find the channels NSScrollView and the cutoff slider so we can
// calculate the inserted row's y position.
NSScrollView* scroll = nil;
for (NSView* v in [cv subviews]) {
if ([v isKindOfClass:[NSScrollView class]]) { scroll = (NSScrollView*)v; break; }
}
if (!scroll) return;
// Shrink the table from the bottom to free up vertical space for the
// new row. The scroll view keeps its widthSizable/heightSizable mask, so
// it grows correctly when the window is resized.
NSRect tf = [scroll frame];
tf.origin.y += kRowHeight;
tf.size.height -= kRowHeight;
[scroll setFrame:tf];
// Build the row at the freed space, just above the existing cutoff row.
// Match the cutoff row's column geometry so they line up vertically.
NSRect cutoffFrame = [cutoffSlider frame];
CGFloat rowY = NSMaxY(cutoffFrame) + 6.0;
NSTextField* label = [NSTextField labelWithString:@"Volume:"];
[label setFrame:NSMakeRect(9, rowY + 3, 84, 17)];
[label setAutoresizingMask:NSViewMaxXMargin | NSViewMaxYMargin];
[cv addSubview:label];
NSSlider* slider = [[[NSSlider alloc]
initWithFrame:NSMakeRect(NSMinX(cutoffFrame), rowY,
NSWidth(cutoffFrame), NSHeight(cutoffFrame))] autorelease];
[slider setAutoresizingMask:NSViewWidthSizable | NSViewMaxYMargin];
[slider setMinValue:0.0];
[slider setMaxValue:2.0];
[slider setFloatValue:[audioSystem getMasterVolume]];
[slider setContinuous:YES];
[slider setTarget:self];
[slider setAction:@selector(volumeSliderChanged:)];
[cv addSubview:slider];
volumeSlider = slider;
}
- (void)updateUI:(NSTimer*)timer
{
[self updateCPULoadGuage];
}
- (void)buildMIDIInputPopUp
{
PYMIDIManager* manager = [PYMIDIManager sharedInstance];
NSArray* sources;
NSEnumerator* enumerator;
PYMIDIEndpoint* input;
[midiInputPopup removeAllItems];
sources = [manager realSources];
enumerator = [sources objectEnumerator];
while (input = [enumerator nextObject]) {
[midiInputPopup addItemWithTitle:[input displayName]];
[[midiInputPopup lastItem] setRepresentedObject:input];
}
if ([sources count] > 0) {
[[midiInputPopup menu] addItem:[NSMenuItem separatorItem]];
}
[midiInputPopup addItemWithTitle:[virtualDestination name]];
[[midiInputPopup lastItem] setRepresentedObject:virtualDestination];
if ([audioSystem midiInput] == nil) {
if ([sources count] > 0)
[audioSystem setMIDIInput:[sources objectAtIndex:0]];
else
[audioSystem setMIDIInput:virtualDestination];
}
[midiInputPopup selectItemAtIndex:[midiInputPopup indexOfItemWithRepresentedObject:[audioSystem midiInput]]];
}
// This is called when the user selects a new MIDI input from the popup
- (IBAction)midiInputChanged:(id)sender
{
[audioSystem setMIDIInput:[[midiInputPopup selectedItem] representedObject]];
}
- (void)midiSetupChanged:(NSNotification*)notification
{
[self buildMIDIInputPopUp];
}
// Displays the standard Open panel and routes any chosen file through
// application:openFile: (the same entry point used by Finder, the dock,
// drag-and-drop, and the Open Recent menu).
- (IBAction)openDocument:(id)sender
{
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
[openPanel setAllowsMultipleSelection:YES];
[openPanel setCanChooseDirectories:NO];
[openPanel setCanChooseFiles:YES];
[openPanel setAllowedFileTypes:[NSArray arrayWithObjects:@"sf2", @"dls", nil]];
if ([openPanel runModal] != NSModalResponseOK) return;
for (NSURL* url in [openPanel URLs]) {
[self application:[NSApplication sharedApplication] openFile:[url path]];
}
}
- (IBAction)restoreAppleSounds:(id)sender
{
[audioSystem restoreAppleSounds];
[soundSetTextField setStringValue:@"Apple DLS Sound Set"];
[channelsTable reloadData];
[instrumentsTable reloadData];
[channelsTable selectRow:0 byExtendingSelection:NO];
[self updateInstrumentSelection];
[self updateMIDIDetails];
}
// Called to open a particular file, either by openDocument: above, by the Open
// Recent menu, by Finder/Dock, or by drag-and-drop. The actual bank load runs
// off the main thread (banks can be hundreds of MB), so we return YES
// immediately and report any failure via an alert once the async load returns.
- (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename
{
NSString* basename = [filename lastPathComponent];
NSString* prevText = [[[soundSetTextField stringValue] copy] autorelease];
[soundSetTextField setStringValue:
[NSString stringWithFormat:@"Loading %@…", basename]];
[audioSystem openFileAsync:filename completion:^(BOOL success, OSStatus err) {
if (success) {
[soundSetTextField setStringValue:basename];
[channelsTable reloadData];
[instrumentsTable reloadData];
[self updateMIDIDetails];
[channelsTable selectRow:0 byExtendingSelection:NO];
[self updateInstrumentSelection];
[[NSDocumentController sharedDocumentController]
noteNewRecentDocumentURL:[NSURL fileURLWithPath:filename]];
} else {
[soundSetTextField setStringValue:prevText];
[self displayOpenFailedAlert:filename withError:err];
}
}];
return YES;
}
// Best-effort interpretation of the OSStatus returned by AudioUnitSetProperty
// when loading a SoundFont/DLS bank.
static NSString* SynthOpenErrorDescription(OSStatus error)
{
switch (error) {
case kAudio_MemFullError:
return @"There isn't enough memory to load this bank. "
@"Multi-gigabyte SoundFonts can exceed what the built-in "
@"DLS synth supports.";
case kAudio_FileNotFoundError:
return @"The file could not be found.";
case kAudio_FilePermissionError:
return @"SimpleSynth doesn't have permission to read the file.";
case kAudio_BadFilePathError:
return @"The file path is not valid.";
case kAudio_ParamError:
case kAudioUnitErr_InvalidProperty:
case kAudioUnitErr_FileNotSpecified:
case kAudioUnitErr_UnknownFileType:
case kAudioUnitErr_FormatNotSupported:
return @"The file is not a SoundFont or DLS bank that SimpleSynth "
@"can read.";
default:
return [NSString stringWithFormat:
@"The bank could not be loaded (AudioUnit error %d).",
(int)error];
}
}
- (void)displayOpenFailedAlert:(NSString*)fileName withError:(OSStatus)error
{
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
[alert setAlertStyle:NSAlertStyleWarning];
[alert setMessageText:[NSString stringWithFormat:
@"Couldn’t open “%@”", [fileName lastPathComponent]]];
[alert setInformativeText:SynthOpenErrorDescription(error)];
[alert addButtonWithTitle:@"OK"];
[alert runModal];
}
- (void)audioSystemInstrumentChanged:(NSNotification*)notification
{
[channelsTable reloadData];
}
- (void)channelsTableSelectionChanged:(NSNotification*)notification
{
[self updateInstrumentSelection];
}
- (void)updateInstrumentSelection
{
UInt32 channel = [channelsTable selectedRow];
MusicDeviceInstrumentID channelInstrumentID = [audioSystem currentInstrumentOnChannel:channel];
UInt32 instrumentIndex = [audioSystem indexOfInstrumentID:channelInstrumentID];
[instrumentsTable selectRow:instrumentIndex byExtendingSelection:NO];
}
- (void)instrumentsTableSelectionChanged:(NSNotification*)notification
{
int channel = [channelsTable selectedRow];
int instrumentIndex = [instrumentsTable selectedRow];
MusicDeviceInstrumentID instrumentID = [audioSystem instrumentIDAtIndex:instrumentIndex];
[self updateMIDIDetails];
[audioSystem setInstrument:instrumentID forChannel:channel];
}
- (void)updateMIDIDetails
{
int instrumentIndex = [instrumentsTable selectedRow];
MusicDeviceInstrumentID instrumentID = [audioSystem instrumentIDAtIndex:instrumentIndex];
MIDIInstrument instrument = [AudioSystem instrumentIDToInstrument:instrumentID];
[programNumberField setIntValue:instrument.programChange + 1];
[bankSelectMSBField setIntValue:instrument.bankSelectMSB];
[bankSelectLSBField setIntValue:instrument.bankSelectLSB];
}
// This is called when the user moves the filter cutoff slider
- (IBAction)cutoffSliderChanged:(id)sender
{
[audioSystem setFilterCutoff:[cutoffSlider floatValue]];
}
// Called when the user drags the master volume slider. AUMultiChannelMixer's
// volume parameter has kAudioUnitParameterFlag_CanRamp, so AppKit hands
// successive values to the AU without zipper noise.
- (IBAction)volumeSliderChanged:(id)sender
{
[audioSystem setMasterVolume:[volumeSlider floatValue]];
}
- (void)updateCPULoadGuage
{
float load = [audioSystem getCPULoad];
[cpuLoadGuage setFloatValue:load];
}
- (IBAction)displayLicense:(id)sender
{
[[NSWorkspace sharedWorkspace]
openFile:[[NSBundle mainBundle] pathForResource:@"License" ofType:@"html"]
];
}
- (IBAction)visitWebSite:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://notahat.com/simplesynth/"]];
}
- (IBAction)sendFeedback:(id)sender
{
NSString* name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
name = [[name componentsSeparatedByString:@" "] componentsJoinedByString:@"%20"];
NSString* version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
version = [[version componentsSeparatedByString:@" "] componentsJoinedByString:@"%20"];
[[NSWorkspace sharedWorkspace] openURL:[NSURL
URLWithString:[NSString
stringWithFormat:@"mailto:simplesynth@notahat.com?subject=%@%%20%@", name, version
]
]];
}
@end