Mercurial > hg > audiodb
comparison examples/iAudioDB/AppController.m @ 669:780ebab29268
Added initial increment of OSX audioDB interface
author | mas01mj |
---|---|
date | Wed, 03 Mar 2010 17:17:08 +0000 |
parents | |
children | 15e71890b584 |
comparison
equal
deleted
inserted
replaced
668:ce5ff00168e1 | 669:780ebab29268 |
---|---|
1 // | |
2 // AppController.m | |
3 // iAudioDB | |
4 // | |
5 // Created by Mike Jewell on 27/01/2010. | |
6 // Copyright 2010 __MyCompanyName__. All rights reserved. | |
7 // | |
8 | |
9 #import "AppController.h" | |
10 | |
11 | |
12 @implementation AppController | |
13 | |
14 -(id)init | |
15 { | |
16 [super init]; | |
17 | |
18 // A max of 100 results. | |
19 results = [[NSMutableArray alloc] initWithCapacity: 100]; | |
20 | |
21 | |
22 return self; | |
23 } | |
24 | |
25 | |
26 /** | |
27 * Create a new database, given the selected filename. | |
28 */ | |
29 -(IBAction)newDatabase:(id)sender | |
30 { | |
31 NSSavePanel* panel = [NSSavePanel savePanel]; | |
32 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@""]; | |
33 | |
34 [results removeAllObjects]; | |
35 [tracksView reloadData]; | |
36 | |
37 if(response == NSFileHandlingPanelOKButton) | |
38 { | |
39 // TODO: Refactor this into a 'tidy' method. | |
40 // Tidy any existing references up. | |
41 if(db) | |
42 { | |
43 audiodb_close(db); | |
44 } | |
45 | |
46 if(dbFilename) | |
47 { | |
48 [dbFilename release]; | |
49 [dbName release]; | |
50 [plistFilename release]; | |
51 } | |
52 | |
53 // Create new db, and set flags. | |
54 db = audiodb_create([[panel filename] cStringUsingEncoding:NSUTF8StringEncoding], 0, 0, 0); | |
55 audiodb_l2norm(db); | |
56 audiodb_power(db); | |
57 | |
58 // Store useful paths. | |
59 dbName = [[[panel URL] relativePath] retain]; | |
60 dbFilename = [[panel filename] retain]; | |
61 plistFilename = [[NSString stringWithFormat:@"%@.plist", [dbFilename stringByDeletingPathExtension]] retain]; | |
62 | |
63 // Create the plist file (contains mapping from filename to key). | |
64 trackMap = [[NSMutableDictionary alloc] init]; | |
65 [trackMap writeToFile:plistFilename atomically:YES]; | |
66 | |
67 [queryKey setStringValue:@"None Selected"]; | |
68 [self updateStatus]; | |
69 } | |
70 } | |
71 | |
72 /** | |
73 * Open an existing adb (which must have a plist) | |
74 */ | |
75 -(IBAction)openDatabase:(id)sender | |
76 { | |
77 NSArray *fileTypes = [NSArray arrayWithObject:@"adb"]; | |
78 NSOpenPanel* panel = [NSOpenPanel openPanel]; | |
79 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@"" types:fileTypes]; | |
80 if(response == NSFileHandlingPanelOKButton) | |
81 { | |
82 // Tidy any existing references up. | |
83 if(db) | |
84 { | |
85 audiodb_close(db); | |
86 } | |
87 | |
88 if(dbFilename) | |
89 { | |
90 [dbFilename release]; | |
91 [dbName release]; | |
92 [plistFilename release]; | |
93 } | |
94 | |
95 // Store useful paths. | |
96 db = audiodb_open([[panel filename] cStringUsingEncoding:NSUTF8StringEncoding], O_RDWR); | |
97 dbName = [[[panel URL] relativePath] retain]; | |
98 dbFilename = [[panel filename] retain]; | |
99 | |
100 // TODO: Verify this exists! | |
101 plistFilename = [[NSString stringWithFormat:@"%@.plist", [dbFilename stringByDeletingPathExtension]] retain]; | |
102 | |
103 // Clear out any old results. | |
104 [results removeAllObjects]; | |
105 [tracksView reloadData]; | |
106 | |
107 [queryKey setStringValue:@"None Selected"]; | |
108 [self updateStatus]; | |
109 | |
110 adb_liszt_results_t* liszt_results = audiodb_liszt(db); | |
111 | |
112 for(int k=0; k<liszt_results->nresults; k++) | |
113 { | |
114 NSMutableString *trackVal = [[NSMutableString alloc] init]; | |
115 [trackVal appendFormat:@"%s", liszt_results->entries[k].key]; | |
116 } | |
117 | |
118 audiodb_liszt_free_results(db, liszt_results); | |
119 trackMap = [[[NSMutableDictionary alloc] initWithContentsOfFile:plistFilename] retain]; | |
120 NSLog(@"Size: %d", [trackMap count]); | |
121 } | |
122 } | |
123 | |
124 /** | |
125 * Update button states and status field based on current state. | |
126 */ | |
127 -(void)updateStatus | |
128 { | |
129 if(db) | |
130 { | |
131 adb_status_ptr status = (adb_status_ptr)malloc(sizeof(struct adbstatus)); | |
132 int flags; | |
133 flags = audiodb_status(db, status); | |
134 [statusField setStringValue: [NSString stringWithFormat:@"Database: %@ Dimensions: %d Files: %d", dbName, status->dim, status->numFiles]]; | |
135 [chooseButton setEnabled:YES]; | |
136 } | |
137 else | |
138 { | |
139 [chooseButton setEnabled:NO]; | |
140 [playBothButton setEnabled:FALSE]; | |
141 [playResultButton setEnabled:FALSE]; | |
142 } | |
143 } | |
144 | |
145 /** | |
146 * Get user's import choices. | |
147 */ | |
148 -(IBAction)importAudio:(id)sender | |
149 { | |
150 [NSApp beginSheet:importSheet modalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil]; | |
151 session = [NSApp beginModalSessionForWindow: importSheet]; | |
152 [NSApp runModalSession:session]; | |
153 } | |
154 | |
155 /** | |
156 * Cancel the import (at configuration time). | |
157 */ | |
158 -(IBAction)cancelImport:(id)sender; | |
159 { | |
160 [NSApp endModalSession:session]; | |
161 [importSheet orderOut:nil]; | |
162 [NSApp endSheet:importSheet]; | |
163 } | |
164 | |
165 /** | |
166 * Choose the file(s) to be imported. | |
167 * TODO: Currently handles the import process too - split this off. | |
168 */ | |
169 -(IBAction)selectFiles:(id)sender | |
170 { | |
171 [tracksView reloadData]; | |
172 | |
173 NSArray *fileTypes = [NSArray arrayWithObject:@"wav"]; | |
174 NSOpenPanel* panel = [NSOpenPanel openPanel]; | |
175 [panel setAllowsMultipleSelection:TRUE]; | |
176 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@"" types:fileTypes]; | |
177 if(response == NSFileHandlingPanelOKButton) | |
178 { | |
179 NSRect newFrame; | |
180 | |
181 [extractingBox setHidden:FALSE]; | |
182 newFrame.origin.x = [importSheet frame].origin.x; | |
183 newFrame.origin.y = [importSheet frame].origin.y - [extractingBox frame].size.height; | |
184 newFrame.size.width = [importSheet frame].size.width; | |
185 newFrame.size.height = [importSheet frame].size.height + [extractingBox frame].size.height; | |
186 | |
187 [indicator startAnimation:self]; | |
188 [importSheet setFrame:newFrame display:YES animate:YES]; | |
189 | |
190 NSArray *filesToOpen = [panel filenames]; | |
191 | |
192 NSLog(@"Begin import"); | |
193 | |
194 // Work out which extractor to use | |
195 NSString* extractor = @"chromagram"; | |
196 switch([extractorOptions selectedTag]) | |
197 { | |
198 case 0: | |
199 extractor = @"mfcc"; | |
200 break; | |
201 case 1: | |
202 extractor = @"chromagram"; | |
203 break; | |
204 } | |
205 | |
206 for(int i=0; i<[filesToOpen count]; i++) | |
207 { | |
208 // First extract powers | |
209 | |
210 NSString *tempFileTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"powers.XXXXXX"]; | |
211 const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation]; | |
212 char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1); | |
213 strcpy(tempFileNameCString, tempFileTemplateCString); | |
214 mktemp(tempFileNameCString); | |
215 | |
216 NSString* powersFileName = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)]; | |
217 free(tempFileNameCString); | |
218 | |
219 NSTask *task = [[NSTask alloc] init]; | |
220 [task setLaunchPath:@"/usr/local/bin/fftExtract2"]; | |
221 NSArray *args = [NSArray arrayWithObjects:@"-P", @"-h", @"11025", @"-w", @"16384", @"-n", @"32768", @"-i", @"1000", [filesToOpen objectAtIndex:i], powersFileName, nil]; | |
222 [task setArguments:args]; | |
223 [task launch]; | |
224 [task waitUntilExit]; | |
225 [task release]; | |
226 | |
227 // Then features | |
228 | |
229 tempFileTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"features.XXXXXX"]; | |
230 tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation]; | |
231 tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1); | |
232 strcpy(tempFileNameCString, tempFileTemplateCString); | |
233 mktemp(tempFileNameCString); | |
234 | |
235 NSString* featuresFileName = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)]; | |
236 free(tempFileNameCString); | |
237 | |
238 task = [[NSTask alloc] init]; | |
239 | |
240 [task setLaunchPath:@"/usr/local/bin/fftExtract2"]; | |
241 | |
242 NSArray *args2; | |
243 | |
244 // Choose the args (TODO: This should use sonic annotator eventually) | |
245 if([extractor isEqualToString:@"chromagram"]) | |
246 { | |
247 args2 = [NSArray arrayWithObjects:@"-p",@"/Users/moj/planfile",@"-c", @"36", @"-h", @"11025", @"-w", @"16384", @"-n", @"32768", @"-i", @"1000", [filesToOpen objectAtIndex:i], featuresFileName, nil]; | |
248 } | |
249 else | |
250 { | |
251 args2 = [NSArray arrayWithObjects:@"-p",@"/Users/moj/planfile",@"-m", @"13", @"-h", @"11025", @"-w", @"16384", @"-n ", @"32768", @"-i", @"1000", [filesToOpen objectAtIndex:i], featuresFileName, nil]; | |
252 } | |
253 [task setArguments:args2]; | |
254 [task launch]; | |
255 [task waitUntilExit]; | |
256 [task release]; | |
257 | |
258 NSString* val = [[filesToOpen objectAtIndex:i] retain]; | |
259 NSString* key = [[[filesToOpen objectAtIndex:i] lastPathComponent] retain]; | |
260 | |
261 adb_insert_t insert; | |
262 insert.features = [featuresFileName cStringUsingEncoding:NSUTF8StringEncoding]; | |
263 insert.power = [powersFileName cStringUsingEncoding:NSUTF8StringEncoding]; | |
264 insert.times = NULL; | |
265 insert.key = [key cStringUsingEncoding:NSUTF8StringEncoding]; | |
266 | |
267 // Insert into db. | |
268 if(audiodb_insert(db, &insert)) | |
269 { | |
270 // TODO: Show an error message. | |
271 NSLog(@"Weep: %@ %@ %@", featuresFileName, powersFileName, key); | |
272 continue; | |
273 } | |
274 | |
275 // Update the plist store. | |
276 [trackMap setValue:val forKey:key]; | |
277 [trackMap writeToFile:plistFilename atomically: YES]; | |
278 | |
279 [self updateStatus]; | |
280 } | |
281 | |
282 newFrame.origin.x = [importSheet frame].origin.x; | |
283 newFrame.origin.y = [importSheet frame].origin.y + [extractingBox frame].size.height; | |
284 newFrame.size.width = [importSheet frame].size.width; | |
285 newFrame.size.height = [importSheet frame].size.height - [extractingBox frame].size.height; | |
286 | |
287 [importSheet setFrame:newFrame display:YES animate:YES]; | |
288 | |
289 [NSApp endModalSession:session]; | |
290 [importSheet orderOut:nil]; | |
291 [NSApp endSheet:importSheet]; | |
292 [indicator stopAnimation:self]; | |
293 [extractingBox setHidden:TRUE]; | |
294 } | |
295 } | |
296 | |
297 /** | |
298 * Required table methods begin here. | |
299 */ | |
300 -(int)numberOfRowsInTableView:(NSTableView *)v | |
301 { | |
302 return [results count]; | |
303 } | |
304 | |
305 /** | |
306 * Return appropriate values - or the distance indicator if it's the meter column. | |
307 */ | |
308 -(id)tableView:(NSTableView *)v objectValueForTableColumn:(NSTableColumn *)tc row:(NSInteger)row | |
309 { | |
310 id result = [results objectAtIndex:row]; | |
311 id value = [result objectForKey:[tc identifier]]; | |
312 | |
313 if([[tc identifier] isEqualToString:@"meter"]) | |
314 { | |
315 NSLevelIndicatorCell *distance = [[NSLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSRelevancyLevelIndicatorStyle]; | |
316 [distance setFloatValue:10-[(NSNumber*)value floatValue]*100]; | |
317 return distance; | |
318 } | |
319 else | |
320 { | |
321 return value; | |
322 } | |
323 } | |
324 | |
325 /** | |
326 * Handle column sorting. | |
327 */ | |
328 - (void)tableView:(NSTableView *)v sortDescriptorsDidChange:(NSArray *)oldDescriptors | |
329 { | |
330 [results sortUsingDescriptors:[v sortDescriptors]]; | |
331 [v reloadData]; | |
332 } | |
333 | |
334 /** | |
335 * Only enable the import menu option if a database is loaded. | |
336 */ | |
337 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem | |
338 { | |
339 SEL theAction = [anItem action]; | |
340 if (theAction == @selector(importAudio:)) | |
341 { | |
342 if(!db) | |
343 { | |
344 return NO; | |
345 } | |
346 } | |
347 return YES; | |
348 } | |
349 | |
350 /** | |
351 * Ensure play buttons are only enabled if a track is selected. | |
352 */ | |
353 -(IBAction)selectedChanged:(id)sender | |
354 { | |
355 if([tracksView numberOfSelectedRows] == 0) | |
356 { | |
357 [playBothButton setEnabled:FALSE]; | |
358 [playResultButton setEnabled:FALSE]; | |
359 } | |
360 else | |
361 { | |
362 [playBothButton setEnabled:TRUE]; | |
363 [playResultButton setEnabled:TRUE]; | |
364 } | |
365 } | |
366 | |
367 /** | |
368 * Play just the result track. | |
369 */ | |
370 -(IBAction)playResult:(id)sender | |
371 { | |
372 | |
373 NSDictionary* selectedRow = [results objectAtIndex:[tracksView selectedRow]]; | |
374 NSString* value = [selectedRow objectForKey:@"key"]; | |
375 float ipos = [[selectedRow objectForKey:@"ipos"] floatValue]; | |
376 NSString* filename = [trackMap objectForKey:value]; | |
377 NSLog(@"Key: %@ Value: %@", value, filename); | |
378 | |
379 if(queryTrack) | |
380 { | |
381 if([queryTrack isPlaying]) | |
382 { | |
383 [queryTrack setDelegate:Nil]; | |
384 [queryTrack stop]; | |
385 } | |
386 [queryTrack release]; | |
387 } | |
388 | |
389 if(resultTrack) | |
390 { | |
391 if([resultTrack isPlaying]) | |
392 { | |
393 [resultTrack setDelegate:Nil]; | |
394 [resultTrack stop]; | |
395 } | |
396 [resultTrack release]; | |
397 } | |
398 | |
399 resultTrack = [[[NSSound alloc] initWithContentsOfFile:filename byReference:YES] retain]; | |
400 [resultTrack setCurrentTime:ipos]; | |
401 [resultTrack setDelegate:self]; | |
402 [resultTrack play]; | |
403 | |
404 [stopButton setEnabled:YES]; | |
405 } | |
406 | |
407 /** | |
408 * Play the result and query simultaneously. | |
409 */ | |
410 -(IBAction)playBoth:(id)sender | |
411 { | |
412 | |
413 NSDictionary* selectedRow = [results objectAtIndex:[tracksView selectedRow]]; | |
414 NSString* value = [selectedRow objectForKey:@"key"]; | |
415 float ipos = [[selectedRow objectForKey:@"ipos"] floatValue]; | |
416 float qpos = [[selectedRow objectForKey:@"qpos"] floatValue]; | |
417 NSString* filename = [trackMap objectForKey:value]; | |
418 NSLog(@"Key: %@ Value: %@", value, filename); | |
419 | |
420 if(queryTrack) | |
421 { | |
422 | |
423 if([queryTrack isPlaying]) | |
424 { | |
425 [queryTrack setDelegate:Nil]; | |
426 [queryTrack stop]; | |
427 } | |
428 [queryTrack release]; | |
429 } | |
430 if(resultTrack) | |
431 { | |
432 if([resultTrack isPlaying]) | |
433 { | |
434 [resultTrack setDelegate:Nil]; | |
435 [resultTrack stop]; | |
436 } | |
437 [resultTrack release]; | |
438 } | |
439 | |
440 // Get query track and shift to start point | |
441 queryTrack = [[[NSSound alloc] initWithContentsOfFile:selectedFilename byReference:YES] retain]; | |
442 [queryTrack setCurrentTime:qpos]; | |
443 [queryTrack setDelegate:self]; | |
444 | |
445 [queryTrack play]; | |
446 | |
447 resultTrack = [[[NSSound alloc] initWithContentsOfFile:filename byReference:YES] retain]; | |
448 [resultTrack setCurrentTime:ipos]; | |
449 [resultTrack setDelegate:self]; | |
450 [resultTrack play]; | |
451 | |
452 [stopButton setEnabled:YES]; | |
453 } | |
454 | |
455 /** | |
456 * Disable the stop button after playback of both tracks. | |
457 */ | |
458 - (void)sound:(NSSound *)sound didFinishPlaying:(BOOL)playbackSuccessful | |
459 { | |
460 | |
461 if((queryTrack && [queryTrack isPlaying]) || (resultTrack && [resultTrack isPlaying])) | |
462 { | |
463 return; | |
464 } | |
465 else | |
466 { | |
467 [stopButton setEnabled:NO]; | |
468 } | |
469 } | |
470 | |
471 /** | |
472 * Stop playback. | |
473 */ | |
474 -(IBAction)stopPlay:(id)sender | |
475 { | |
476 if(queryTrack) | |
477 { | |
478 [queryTrack stop]; | |
479 } | |
480 if(resultTrack) | |
481 { | |
482 [resultTrack stop]; | |
483 } | |
484 } | |
485 | |
486 /** | |
487 * Select an audio file, determine the key, and fire off a query. | |
488 */ | |
489 -(IBAction)chooseQuery:(id)sender | |
490 { | |
491 NSArray* fileTypes = [NSArray arrayWithObject:@"wav"]; | |
492 NSOpenPanel* panel = [NSOpenPanel openPanel]; | |
493 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@"" types:fileTypes]; | |
494 if(response == NSFileHandlingPanelOKButton) | |
495 { | |
496 NSLog(@"%@", [panel filename]); | |
497 // Grab key | |
498 NSArray* opts = [trackMap allKeysForObject:[panel filename]]; | |
499 if([opts count] != 1) | |
500 { | |
501 NSAlert *alert = [[[NSAlert alloc] init] autorelease]; | |
502 [alert addButtonWithTitle:@"OK"]; | |
503 [alert setMessageText:@"Track not found"]; | |
504 [alert setInformativeText:@"Make sure you have specified a valid track identifier."]; | |
505 [alert setAlertStyle:NSWarningAlertStyle]; | |
506 [alert beginSheetModalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil]; | |
507 } | |
508 else | |
509 { | |
510 selectedKey = [opts objectAtIndex:0]; | |
511 [queryKey setStringValue:selectedKey]; | |
512 selectedFilename = [[panel filename] retain]; | |
513 [self performQuery]; | |
514 } | |
515 } | |
516 } | |
517 | |
518 /** | |
519 * Actually perform the query. TODO: Monolithic. | |
520 */ | |
521 -(void)performQuery | |
522 { | |
523 NSLog(@"Perform query! %@, %@", selectedKey, selectedFilename); | |
524 | |
525 adb_query_spec_t *spec = (adb_query_spec_t *)malloc(sizeof(adb_query_spec_t)); | |
526 spec->qid.datum = (adb_datum_t *)malloc(sizeof(adb_datum_t)); | |
527 | |
528 spec->qid.sequence_length = 20; | |
529 spec->qid.sequence_start = 0; | |
530 spec->qid.flags = 0; | |
531 | |
532 // spec->qid.flags = spec->qid.flags | ADB_QID_FLAG_EXHAUSTIVE; | |
533 spec->params.accumulation = ADB_ACCUMULATION_PER_TRACK; | |
534 spec->params.distance = ADB_DISTANCE_EUCLIDEAN_NORMED; | |
535 | |
536 spec->params.npoints = 1; | |
537 spec->params.ntracks = 100; | |
538 //spec->refine.radius = 5.0; | |
539 spec->refine.hopsize = 1; | |
540 // spec->refine.absolute_threshold = -6; | |
541 // spec->refine.relative_threshold = 10; | |
542 // spec->refine.duration_ratio = 0; | |
543 | |
544 spec->refine.flags = 0; | |
545 // spec->refine.flags |= ADB_REFINE_ABSOLUTE_THRESHOLD; | |
546 // spec->refine.flags |= ADB_REFINE_RELATIVE_THRESHOLD; | |
547 spec->refine.flags |= ADB_REFINE_HOP_SIZE; | |
548 //spec->refine.flags |= ADB_REFINE_RADIUS; | |
549 | |
550 adb_query_results_t *result = (adb_query_results_t *)malloc(sizeof(adb_query_results_t)); | |
551 spec->qid.datum->data = NULL; | |
552 spec->qid.datum->power = NULL; | |
553 spec->qid.datum->times = NULL; | |
554 | |
555 [results removeAllObjects]; | |
556 | |
557 int ok = audiodb_retrieve_datum(db, [selectedKey cStringUsingEncoding:NSUTF8StringEncoding], spec->qid.datum); | |
558 if(ok == 0) | |
559 { | |
560 NSLog(@"Got a datum"); | |
561 result = audiodb_query_spec(db, spec); | |
562 if(result == NULL) | |
563 { | |
564 | |
565 NSLog(@"No results"); | |
566 } | |
567 else | |
568 { | |
569 for(int i=0; i<result->nresults; i++) | |
570 { | |
571 NSMutableDictionary* dict = [[NSMutableDictionary alloc] initWithCapacity:4]; | |
572 [dict setValue:[NSString stringWithFormat:@"%s", result->results[i].key] forKey:@"key"]; | |
573 [dict setValue:[NSNumber numberWithFloat:result->results[i].dist] forKey:@"distance"]; | |
574 [dict setValue:[NSNumber numberWithFloat:result->results[i].dist] forKey:@"meter"]; | |
575 [dict setValue:[NSNumber numberWithFloat:result->results[i].qpos/4] forKey:@"qpos"]; | |
576 [dict setValue:[NSNumber numberWithFloat:result->results[i].ipos/4] forKey:@"ipos"]; | |
577 NSLog(@"%s qpos %d ipos %d", result->results[i].key, result->results[i].qpos/4, result->results[i].ipos/4); | |
578 [results addObject: dict]; | |
579 } | |
580 } | |
581 | |
582 NSSortDescriptor *distSort = [[NSSortDescriptor alloc]initWithKey:@"meter" ascending:YES]; | |
583 NSArray *distDescs = [NSArray arrayWithObject:distSort]; | |
584 | |
585 [results sortUsingDescriptors:distDescs]; | |
586 [tracksView setSortDescriptors:distDescs]; | |
587 [tracksView reloadData]; | |
588 | |
589 } | |
590 else | |
591 { | |
592 NSAlert *alert = [[[NSAlert alloc] init] autorelease]; | |
593 [alert addButtonWithTitle:@"OK"]; | |
594 [alert setMessageText:@"Track not found"]; | |
595 [alert setInformativeText:@"Make sure you have specified a valid track identifier."]; | |
596 [alert setAlertStyle:NSWarningAlertStyle]; | |
597 [alert beginSheetModalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil]; | |
598 } | |
599 // audiodb_query_free_results(db, spec, result); | |
600 } | |
601 | |
602 @end |