annotate examples/iAudioDB/AppController.m @ 726:fe2282b9bfb0

Initial querying: doesn't return results yet, but handles almost all params.
author mas01mj
date Mon, 26 Jul 2010 13:19:09 +0000
parents e3087cf8ff14
children 0d1a7e4ed6cf 040f14b5a5fc
rev   line source
mas01mj@669 1 //
mas01mj@669 2 // AppController.m
mas01mj@706 3 // CAMUS
mas01mj@669 4 //
mas01mj@669 5 // Created by Mike Jewell on 27/01/2010.
mas01mj@669 6 // Copyright 2010 __MyCompanyName__. All rights reserved.
mas01mj@669 7 //
mas01mj@699 8 #import "AppController.h"
mas01mj@701 9 #import <AudioToolbox/AudioFile.h>
mas01mj@669 10
mas01mj@669 11
mas01mj@669 12 @implementation AppController
mas01mj@669 13
mas01mj@669 14 -(id)init
mas01mj@669 15 {
mas01mj@669 16 [super init];
mas01mj@669 17
mas01mj@669 18 // A max of 100 results.
mas01mj@669 19 results = [[NSMutableArray alloc] initWithCapacity: 100];
mas01mj@669 20
mas01mj@669 21 return self;
mas01mj@669 22 }
mas01mj@669 23
mas01mj@699 24 - (void)awakeFromNib {
mas01mj@699 25 [tracksView setTarget:self];
mas01mj@699 26 [tracksView setDoubleAction:@selector(tableDoubleClick:)];
mas01mj@699 27 [self updateStatus];
mas01mj@699 28 }
mas01mj@699 29
mas01mj@699 30
mas01mj@699 31 - (IBAction)tableDoubleClick:(id)sender
mas01mj@699 32 {
mas01mj@699 33 [self playResult:Nil];
mas01mj@699 34 // NSLog(@"Table double clicked");
mas01mj@699 35 }
mas01mj@699 36
mas01mj@669 37
mas01mj@669 38 /**
mas01mj@669 39 * Create a new database, given the selected filename.
mas01mj@669 40 */
mas01mj@669 41 -(IBAction)newDatabase:(id)sender
mas01mj@669 42 {
mas01mj@699 43
mas01mj@699 44 [NSApp beginSheet:createSheet modalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil];
mas01mj@699 45 session = [NSApp beginModalSessionForWindow:createSheet];
mas01mj@699 46 [NSApp runModalSession:session];
mas01mj@699 47 }
mas01mj@699 48
mas01mj@699 49 /**
mas01mj@699 50 * Cancel the db creation (at configuration time).
mas01mj@699 51 */
mas01mj@699 52 -(IBAction)cancelCreate:(id)sender
mas01mj@699 53 {
mas01mj@699 54 [NSApp endModalSession:session];
mas01mj@699 55 [createSheet orderOut:nil];
mas01mj@699 56 [NSApp endSheet:createSheet];
mas01mj@699 57 }
mas01mj@699 58
mas01mj@699 59 -(IBAction)createDatabase:(id)sender
mas01mj@699 60 {
mas01mj@699 61 [self cancelCreate:self];
mas01mj@699 62
mas01mj@669 63 NSSavePanel* panel = [NSSavePanel savePanel];
mas01mj@709 64 [panel setCanSelectHiddenExtension:YES];
mas01mj@709 65 [panel setAllowedFileTypes:[NSArray arrayWithObjects:@"adb", nil]];
mas01mj@669 66 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@""];
mas01mj@699 67
mas01mj@669 68 [results removeAllObjects];
mas01mj@669 69 [tracksView reloadData];
mas01mj@699 70
mas01mj@669 71 if(response == NSFileHandlingPanelOKButton)
mas01mj@669 72 {
mas01mj@699 73 // Work out which extractor to use
mas01mj@699 74 NSString* extractor = @"adb_chroma";
mas01mj@699 75 // TODO: This should be stored with the n3.
mas01mj@699 76 int dim;
mas01mj@699 77 switch([extractorOptions selectedTag])
mas01mj@685 78 {
mas01mj@699 79 case 0:
mas01mj@699 80 extractor = @"adb_chroma";
mas01mj@699 81 dim = 12;
mas01mj@699 82 break;
mas01mj@704 83 /* case 1:
mas01mj@699 84 extractor = @"adb_cq";
mas01mj@699 85 dim = 48;
mas01mj@699 86 break;
mas01mj@699 87 case 2:
mas01mj@699 88 extractor = @"qm_chroma";
mas01mj@699 89 dim = 12;
mas01mj@704 90 break;*/
mas01mj@704 91 case 1:
mas01mj@699 92 extractor = @"qm_mfcc";
mas01mj@699 93 dim = 12;
mas01mj@699 94 break;
mas01mj@685 95 }
mas01mj@685 96
mas01mj@699 97 // Calculate the max DB size
mas01mj@709 98 NSLog(@"Max length: %f", [maxLengthField doubleValue]);
mas01mj@709 99 NSLog(@"hop size: %f", [hopSizeField doubleValue]);
mas01mj@709 100 int vectors = ceil([maxLengthField doubleValue] / (([hopSizeField doubleValue] / 1000.0f)));
mas01mj@709 101 NSLog(@"Max Vectors: %d", vectors);
mas01mj@699 102 int numtracks = [maxTracksField intValue];
mas01mj@699 103 int datasize = ceil((numtracks * vectors * dim * 8.0f) / 1024.0f / 1024.0f); // In MB
mas01mj@685 104
mas01mj@699 105 [self reset];
mas01mj@699 106
mas01mj@712 107
mas01mj@712 108
mas01mj@669 109 // Store useful paths.
mas01mj@669 110 dbName = [[[panel URL] relativePath] retain];
mas01mj@669 111 dbFilename = [[panel filename] retain];
mas01mj@669 112 plistFilename = [[NSString stringWithFormat:@"%@.plist", [dbFilename stringByDeletingPathExtension]] retain];
mas01mj@712 113
mas01mj@712 114 // Remove any existing files
mas01mj@712 115 NSFileManager *fileManager = [[NSFileManager alloc] init];
mas01mj@712 116
mas01mj@712 117 BOOL overwriteError = NO;
mas01mj@712 118
mas01mj@712 119 if([fileManager fileExistsAtPath:[panel filename]])
mas01mj@712 120 {
mas01mj@712 121 if(![fileManager removeItemAtPath:[panel filename] error:NULL])
mas01mj@712 122 {
mas01mj@712 123 overwriteError = YES;
mas01mj@712 124 }
mas01mj@712 125 }
mas01mj@712 126
mas01mj@712 127 if(!overwriteError && [fileManager fileExistsAtPath:plistFilename])
mas01mj@712 128 {
mas01mj@712 129 if(![fileManager removeItemAtPath:plistFilename error:NULL])
mas01mj@712 130 {
mas01mj@712 131 overwriteError = YES;
mas01mj@712 132 }
mas01mj@712 133 }
mas01mj@712 134 [fileManager release];
mas01mj@712 135
mas01mj@712 136 if(overwriteError)
mas01mj@712 137 {
mas01mj@712 138 NSAlert *alert = [[NSAlert alloc] init];
mas01mj@712 139 [alert addButtonWithTitle:@"OK"];
mas01mj@712 140 [alert setMessageText:@"Unable to create database."];
mas01mj@712 141 [alert setInformativeText:@"A database with this name already exists, and could not be overwritten."];
mas01mj@712 142 [alert setAlertStyle:NSWarningAlertStyle];
mas01mj@712 143 [alert runModal];
mas01mj@712 144 [alert release];
mas01mj@712 145 return;
mas01mj@712 146 }
mas01mj@712 147
mas01mj@712 148 // Create new db, and set flags.
mas01mj@712 149 db = audiodb_create([[panel filename] cStringUsingEncoding:NSUTF8StringEncoding], datasize, numtracks, dim);
mas01mj@712 150 audiodb_l2norm(db);
mas01mj@699 151
mas01mj@669 152 // Create the plist file (contains mapping from filename to key).
mas01mj@699 153 dbState = [[NSMutableDictionary alloc] init];
mas01mj@669 154 trackMap = [[NSMutableDictionary alloc] init];
mas01mj@699 155 [dbState setValue:trackMap forKey:@"tracks"];
mas01mj@699 156 [dbState setValue:extractor forKey:@"extractor"];
mas01mj@699 157 [dbState setValue:[hopSizeField stringValue] forKey:@"hopsize"];
mas01mj@699 158 [dbState writeToFile:plistFilename atomically:YES];
mas01mj@699 159
mas01mj@669 160 [queryKey setStringValue:@"None Selected"];
mas01mj@669 161 [self updateStatus];
mas01mj@669 162 }
mas01mj@669 163 }
mas01mj@669 164
mas01mj@699 165 -(void)reset
mas01mj@699 166 {
mas01mj@699 167 // Tidy any existing references up.
mas01mj@699 168 if(db)
mas01mj@699 169 {
mas01mj@699 170 NSLog(@"Close db");
mas01mj@699 171 audiodb_close(db);
mas01mj@699 172 }
mas01mj@699 173
mas01mj@699 174 if(dbFilename)
mas01mj@699 175 {
mas01mj@699 176 NSLog(@"Tidy up filenames");
mas01mj@699 177 [dbFilename release];
mas01mj@699 178 [dbName release];
mas01mj@699 179 [plistFilename release];
mas01mj@699 180 [trackMap release];
mas01mj@699 181 [dbState release];
mas01mj@699 182 }
mas01mj@699 183
mas01mj@699 184 if(selectedKey)
mas01mj@699 185 {
mas01mj@699 186 [selectedKey release];
mas01mj@699 187 selectedKey = Nil;
mas01mj@699 188 }
mas01mj@699 189
mas01mj@699 190 // Reset query flags
mas01mj@699 191 [queryPath setStringValue: @"No file selected"];
mas01mj@699 192 [queryLengthSeconds setDoubleValue:0];
mas01mj@699 193 [queryLengthVectors setDoubleValue:0];
mas01mj@699 194 [multipleCheckBox setState:NSOnState];
mas01mj@699 195 [queryStartSeconds setDoubleValue:0];
mas01mj@699 196 [queryStartVectors setDoubleValue:0];
mas01mj@699 197
mas01mj@699 198 [queryLengthSeconds setEnabled:NO];
mas01mj@699 199 [queryLengthVectors setEnabled:NO];
mas01mj@699 200 [queryStartSeconds setEnabled:NO];
mas01mj@699 201 [queryStartVectors setEnabled:NO];
mas01mj@699 202 [resetButton setEnabled:NO];
mas01mj@699 203 [multipleCheckBox setEnabled:NO];
mas01mj@699 204 }
mas01mj@699 205
mas01mj@669 206 /**
mas01mj@669 207 * Open an existing adb (which must have a plist)
mas01mj@669 208 */
mas01mj@669 209 -(IBAction)openDatabase:(id)sender
mas01mj@669 210 {
mas01mj@669 211 NSArray *fileTypes = [NSArray arrayWithObject:@"adb"];
mas01mj@669 212 NSOpenPanel* panel = [NSOpenPanel openPanel];
mas01mj@669 213 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@"" types:fileTypes];
mas01mj@669 214 if(response == NSFileHandlingPanelOKButton)
mas01mj@669 215 {
mas01mj@699 216 [self reset];
mas01mj@669 217
mas01mj@669 218 // Store useful paths.
mas01mj@699 219 NSLog(@"Open");
mas01mj@699 220 db = audiodb_open([[panel filename] cStringUsingEncoding:NSUTF8StringEncoding], O_RDONLY);
mas01mj@669 221 dbName = [[[panel URL] relativePath] retain];
mas01mj@669 222 dbFilename = [[panel filename] retain];
mas01mj@669 223
mas01mj@669 224 // TODO: Verify this exists!
mas01mj@669 225 plistFilename = [[NSString stringWithFormat:@"%@.plist", [dbFilename stringByDeletingPathExtension]] retain];
mas01mj@669 226
mas01mj@669 227 // Clear out any old results.
mas01mj@669 228 [results removeAllObjects];
mas01mj@669 229 [tracksView reloadData];
mas01mj@669 230
mas01mj@669 231 [queryKey setStringValue:@"None Selected"];
mas01mj@669 232
mas01mj@669 233 adb_liszt_results_t* liszt_results = audiodb_liszt(db);
mas01mj@669 234
mas01mj@669 235 for(int k=0; k<liszt_results->nresults; k++)
mas01mj@669 236 {
mas01mj@669 237 NSMutableString *trackVal = [[NSMutableString alloc] init];
mas01mj@669 238 [trackVal appendFormat:@"%s", liszt_results->entries[k].key];
mas01mj@669 239 }
mas01mj@669 240
mas01mj@669 241 audiodb_liszt_free_results(db, liszt_results);
mas01mj@699 242 dbState = [[[NSMutableDictionary alloc] initWithContentsOfFile:plistFilename] retain];
mas01mj@699 243 trackMap = [[dbState objectForKey:@"tracks"] retain];
mas01mj@699 244
mas01mj@699 245 [self updateStatus];
mas01mj@699 246
mas01mj@669 247 NSLog(@"Size: %d", [trackMap count]);
mas01mj@669 248 }
mas01mj@669 249 }
mas01mj@669 250
mas01mj@699 251 -(IBAction)pathAction:(id)sender
mas01mj@699 252 {
mas01mj@699 253 NSLog(@"Path action");
mas01mj@699 254 }
mas01mj@699 255
mas01mj@669 256 /**
mas01mj@669 257 * Update button states and status field based on current state.
mas01mj@669 258 */
mas01mj@669 259 -(void)updateStatus
mas01mj@669 260 {
mas01mj@699 261 NSLog(@"Update status");
mas01mj@669 262 if(db)
mas01mj@669 263 {
mas01mj@699 264 NSLog(@"Got a db");
mas01mj@699 265 adb_status_t *status = (adb_status_t *)malloc(sizeof(adb_status_t));
mas01mj@669 266 int flags;
mas01mj@669 267 flags = audiodb_status(db, status);
mas01mj@705 268 [statusField setStringValue: [NSString stringWithFormat:@"%@ Dim: %d Files: %d Slice: %@ms Ext: %@",
mas01mj@699 269 dbName,
mas01mj@699 270 status->dim,
mas01mj@699 271 status->numFiles,
mas01mj@699 272 [dbState objectForKey:@"hopsize"],
mas01mj@699 273 [dbState objectForKey:@"extractor"]]];
mas01mj@699 274 [performQueryButton setEnabled:YES];
mas01mj@699 275 [importAudioButton setEnabled:YES];
mas01mj@669 276 }
mas01mj@669 277 else
mas01mj@669 278 {
mas01mj@699 279 NSLog(@"No db");
mas01mj@699 280 [performQueryButton setEnabled:NO];
mas01mj@699 281 [importAudioButton setEnabled:NO];
mas01mj@699 282 [playBothButton setEnabled:NO];
mas01mj@699 283 [playResultButton setEnabled:NO];
mas01mj@699 284 [stopButton setEnabled:NO];
mas01mj@669 285 }
mas01mj@669 286 }
mas01mj@669 287
mas01mj@701 288 -(UInt64)getSampleRate:(NSString *)filename
mas01mj@701 289 {
mas01mj@701 290 AudioFileID audioFile;
mas01mj@701 291 AudioFileOpenURL((CFURLRef)[NSURL fileURLWithPath:filename], 0x01, 0, &audioFile);
mas01mj@701 292
mas01mj@701 293 UInt32 propertySize;
mas01mj@701 294 UInt32 propertyIsWritable;
mas01mj@701 295 AudioFileGetPropertyInfo(audioFile, kAudioFilePropertyDataFormat, &propertySize, &propertyIsWritable);
mas01mj@701 296
mas01mj@701 297 AudioStreamBasicDescription dataFormat;
mas01mj@701 298 AudioFileGetProperty(audioFile, kAudioFilePropertyDataFormat, &propertySize, &dataFormat);
mas01mj@701 299 Float64 sampleRate = dataFormat.mSampleRate;
mas01mj@701 300 AudioFileClose(audioFile);
mas01mj@701 301
mas01mj@701 302 return sampleRate;
mas01mj@701 303 }
mas01mj@701 304
mas01mj@702 305 -(UInt64)getHopSizeInSamples:(NSString *)filename
mas01mj@702 306 {
mas01mj@702 307 NSString* hopStr = [dbState objectForKey:@"hopsize"];
mas01mj@702 308 return round([self getSampleRate:filename] * ([hopStr doubleValue] / 1000));
mas01mj@702 309 }
mas01mj@702 310
mas01mj@702 311 -(int)nearestPow2:(int)x
mas01mj@702 312 {
mas01mj@702 313 if (x < 0)
mas01mj@702 314 return 0;
mas01mj@702 315 --x;
mas01mj@702 316 x |= x >> 1;
mas01mj@702 317 x |= x >> 2;
mas01mj@702 318 x |= x >> 4;
mas01mj@702 319 x |= x >> 8;
mas01mj@702 320 x |= x >> 16;
mas01mj@702 321 return x+1;
mas01mj@702 322 }
mas01mj@702 323
mas01mj@700 324 -(void)importFile:(NSString *)filename withExtractorConfig:(NSString *)extractorPath
mas01mj@700 325 {
mas01mj@700 326 // Create the extractor configuration
mas01mj@702 327 int hopSizeSamples = [self getHopSizeInSamples:filename];
mas01mj@702 328 int windowSizeSamples = [self nearestPow2:(hopSizeSamples*8)];
mas01mj@700 329
mas01mj@700 330 NSString* extractorContent = [NSString stringWithContentsOfFile:extractorPath];
mas01mj@702 331 NSString* newContent = [[extractorContent stringByReplacingOccurrencesOfString:@"HOP_SIZE" withString:[NSString stringWithFormat:@"%d", hopSizeSamples]]
mas01mj@702 332 stringByReplacingOccurrencesOfString:@"WINDOW_SIZE" withString:[NSString stringWithFormat:@"%d", windowSizeSamples]];
mas01mj@700 333 NSString* n3FileName = [NSTemporaryDirectory() stringByAppendingPathComponent:@"extractor_config.n3"];
mas01mj@702 334 NSLog(newContent);
mas01mj@700 335 NSError* error;
mas01mj@700 336 [newContent writeToFile:n3FileName atomically:YES encoding:NSASCIIStringEncoding error:&error];
mas01mj@700 337
mas01mj@700 338 // Create the temp file for the extracted features
mas01mj@700 339 NSString* tempFileTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"features.XXXXXX"];
mas01mj@700 340 const char* tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
mas01mj@700 341 char* tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);
mas01mj@700 342 strcpy(tempFileNameCString, tempFileTemplateCString);
mas01mj@700 343 mktemp(tempFileNameCString);
mas01mj@700 344
mas01mj@700 345 NSString* featuresFileName = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)];
mas01mj@700 346 free(tempFileNameCString);
mas01mj@700 347
mas01mj@700 348 // Extract features with sonic-annotator
mas01mj@700 349 NSTask* task = [[NSTask alloc] init];
mas01mj@709 350 NSLog(@"Resource path: %@", [ [NSBundle mainBundle] resourcePath]);
mas01mj@709 351 NSString* pluginPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Vamp"];
mas01mj@709 352 NSString* extractPath = [ [ NSBundle mainBundle ] pathForAuxiliaryExecutable: @"sonic-annotator" ];
mas01mj@705 353
mas01mj@709 354 NSLog(@"Plugin path: %@", pluginPath);
mas01mj@709 355
mas01mj@709 356 NSDictionary *defaultEnvironment = [[NSProcessInfo processInfo] environment];
mas01mj@709 357 NSMutableDictionary *environment = [[NSMutableDictionary alloc] initWithDictionary:defaultEnvironment];
mas01mj@709 358 [environment setValue:pluginPath forKey:@"VAMP_PATH"];
mas01mj@709 359 NSLog(@"Env: %@", environment);
mas01mj@705 360 [task setLaunchPath:extractPath];
mas01mj@709 361 [task setEnvironment:environment];
mas01mj@709 362
mas01mj@705 363
mas01mj@700 364 NSArray* args;
mas01mj@700 365 args = [NSArray arrayWithObjects:@"-t", n3FileName, @"-w", @"rdf", @"-r", @"--rdf-network", @"--rdf-one-file", featuresFileName, @"--rdf-force", filename, nil];
mas01mj@700 366 [task setArguments:args];
mas01mj@700 367 [task launch];
mas01mj@700 368 [task waitUntilExit];
mas01mj@700 369 [task release];
mas01mj@700 370
mas01mj@700 371 // Populate the audioDB instance
mas01mj@700 372 NSTask* importTask = [[NSTask alloc] init];
mas01mj@705 373 NSString* importPath = [ [ NSBundle mainBundle ] pathForAuxiliaryExecutable: @"populate" ];
mas01mj@705 374 [importTask setLaunchPath:importPath];
mas01mj@700 375 args = [NSArray arrayWithObjects:featuresFileName, dbFilename, nil];
mas01mj@700 376 [importTask setArguments:args];
mas01mj@700 377 [importTask launch];
mas01mj@700 378 [importTask waitUntilExit];
mas01mj@700 379 [importTask release];
mas01mj@700 380
mas01mj@700 381 NSString* val = [filename retain];
mas01mj@700 382 NSString* key = [[filename lastPathComponent] retain];
mas01mj@700 383
mas01mj@700 384 // Update the plist store.
mas01mj@700 385 [trackMap setValue:val forKey:key];
mas01mj@700 386 [dbState writeToFile:plistFilename atomically: YES];
mas01mj@700 387
mas01mj@705 388
mas01mj@700 389 }
mas01mj@700 390
mas01mj@669 391 /**
mas01mj@669 392 * Choose the file(s) to be imported.
mas01mj@669 393 */
mas01mj@699 394 -(IBAction)importAudio:(id)sender
mas01mj@669 395 {
mas01mj@669 396 [tracksView reloadData];
mas01mj@669 397
mas01mj@714 398 NSArray *fileTypes = [NSArray arrayWithObjects:@"wav", @"mp3", @"aiff", @"m4a", nil];
mas01mj@669 399 NSOpenPanel* panel = [NSOpenPanel openPanel];
mas01mj@669 400 [panel setAllowsMultipleSelection:TRUE];
mas01mj@669 401 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@"" types:fileTypes];
mas01mj@669 402 if(response == NSFileHandlingPanelOKButton)
mas01mj@669 403 {
mas01mj@699 404 [indicator startAnimation:self];
mas01mj@692 405
mas01mj@699 406 [NSApp beginSheet:importSheet modalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil];
mas01mj@699 407 session = [NSApp beginModalSessionForWindow: importSheet];
mas01mj@699 408 [NSApp runModalSession:session];
mas01mj@669 409
mas01mj@669 410 NSArray *filesToOpen = [panel filenames];
mas01mj@669 411
mas01mj@699 412 NSString* extractor = [dbState objectForKey:@"extractor"];
mas01mj@705 413
mas01mj@705 414 NSLog([NSString stringWithFormat:@"rdf/%@.n3", extractor]);
mas01mj@705 415 NSString* extractorPath = [ [ NSBundle mainBundle ] pathForResource:extractor ofType:@"n3" inDirectory:@"rdf"];
mas01mj@705 416 NSLog(@"Extractor path: %@", extractorPath);
mas01mj@705 417
mas01mj@705 418 // NSString* extractorPath = [NSString stringWithFormat:@"/Applications/iAudioDB.app/rdf/%@.n3", extractor];
mas01mj@669 419
mas01mj@669 420 for(int i=0; i<[filesToOpen count]; i++)
mas01mj@699 421 {
mas01mj@699 422 audiodb_close(db);
mas01mj@700 423
mas01mj@700 424 // Get the sample rate for the audio file
mas01mj@700 425
mas01mj@700 426 [self importFile:[filesToOpen objectAtIndex:i] withExtractorConfig:extractorPath];
mas01mj@699 427 db = audiodb_open([dbFilename cStringUsingEncoding:NSUTF8StringEncoding], O_RDONLY);
mas01mj@669 428 [self updateStatus];
mas01mj@669 429 }
mas01mj@669 430
mas01mj@669 431 [NSApp endModalSession:session];
mas01mj@669 432 [importSheet orderOut:nil];
mas01mj@669 433 [NSApp endSheet:importSheet];
mas01mj@669 434 [indicator stopAnimation:self];
mas01mj@669 435 }
mas01mj@669 436 }
mas01mj@669 437
mas01mj@669 438 /**
mas01mj@669 439 * Required table methods begin here.
mas01mj@669 440 */
mas01mj@669 441 -(int)numberOfRowsInTableView:(NSTableView *)v
mas01mj@669 442 {
mas01mj@669 443 return [results count];
mas01mj@669 444 }
mas01mj@669 445
mas01mj@669 446 /**
mas01mj@669 447 * Return appropriate values - or the distance indicator if it's the meter column.
mas01mj@669 448 */
mas01mj@669 449 -(id)tableView:(NSTableView *)v objectValueForTableColumn:(NSTableColumn *)tc row:(NSInteger)row
mas01mj@669 450 {
mas01mj@669 451 id result = [results objectAtIndex:row];
mas01mj@669 452 id value = [result objectForKey:[tc identifier]];
mas01mj@669 453
mas01mj@669 454 if([[tc identifier] isEqualToString:@"meter"])
mas01mj@669 455 {
mas01mj@669 456 NSLevelIndicatorCell *distance = [[NSLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSRelevancyLevelIndicatorStyle];
mas01mj@699 457 [distance setFloatValue:10.0f-[(NSNumber*)value floatValue]*100.0f];
mas01mj@669 458 return distance;
mas01mj@669 459 }
mas01mj@669 460 else
mas01mj@669 461 {
mas01mj@669 462 return value;
mas01mj@669 463 }
mas01mj@669 464 }
mas01mj@669 465
mas01mj@669 466 /**
mas01mj@669 467 * Handle column sorting.
mas01mj@669 468 */
mas01mj@669 469 - (void)tableView:(NSTableView *)v sortDescriptorsDidChange:(NSArray *)oldDescriptors
mas01mj@669 470 {
mas01mj@669 471 [results sortUsingDescriptors:[v sortDescriptors]];
mas01mj@669 472 [v reloadData];
mas01mj@669 473 }
mas01mj@669 474
mas01mj@669 475 /**
mas01mj@669 476 * Only enable the import menu option if a database is loaded.
mas01mj@669 477 */
mas01mj@669 478 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
mas01mj@669 479 {
mas01mj@669 480 SEL theAction = [anItem action];
mas01mj@669 481 if (theAction == @selector(importAudio:))
mas01mj@669 482 {
mas01mj@669 483 if(!db)
mas01mj@669 484 {
mas01mj@669 485 return NO;
mas01mj@669 486 }
mas01mj@669 487 }
mas01mj@669 488 return YES;
mas01mj@669 489 }
mas01mj@669 490
mas01mj@669 491 /**
mas01mj@669 492 * Ensure play buttons are only enabled if a track is selected.
mas01mj@669 493 */
mas01mj@669 494 -(IBAction)selectedChanged:(id)sender
mas01mj@669 495 {
mas01mj@669 496 if([tracksView numberOfSelectedRows] == 0)
mas01mj@669 497 {
mas01mj@699 498 [playBothButton setEnabled:NO];
mas01mj@699 499 [playResultButton setEnabled:NO];
mas01mj@669 500 }
mas01mj@669 501 else
mas01mj@669 502 {
mas01mj@699 503 [playBothButton setEnabled:YES];
mas01mj@699 504 [playResultButton setEnabled:YES];
mas01mj@669 505 }
mas01mj@669 506 }
mas01mj@669 507
mas01mj@669 508 /**
mas01mj@669 509 * Play just the result track.
mas01mj@669 510 */
mas01mj@669 511 -(IBAction)playResult:(id)sender
mas01mj@669 512 {
mas01mj@669 513
mas01mj@699 514 if([tracksView selectedRow] == -1)
mas01mj@699 515 {
mas01mj@699 516 return;
mas01mj@699 517 }
mas01mj@699 518
mas01mj@669 519 NSDictionary* selectedRow = [results objectAtIndex:[tracksView selectedRow]];
mas01mj@669 520 NSString* value = [selectedRow objectForKey:@"key"];
mas01mj@669 521 float ipos = [[selectedRow objectForKey:@"ipos"] floatValue];
mas01mj@669 522 NSString* filename = [trackMap objectForKey:value];
mas01mj@669 523 NSLog(@"Key: %@ Value: %@", value, filename);
mas01mj@669 524
mas01mj@669 525 if(queryTrack)
mas01mj@669 526 {
mas01mj@669 527 if([queryTrack isPlaying])
mas01mj@669 528 {
mas01mj@669 529 [queryTrack setDelegate:Nil];
mas01mj@669 530 [queryTrack stop];
mas01mj@669 531 }
mas01mj@669 532 [queryTrack release];
mas01mj@699 533 queryTrack = Nil;
mas01mj@669 534 }
mas01mj@669 535
mas01mj@669 536 if(resultTrack)
mas01mj@669 537 {
mas01mj@669 538 if([resultTrack isPlaying])
mas01mj@669 539 {
mas01mj@669 540 [resultTrack setDelegate:Nil];
mas01mj@669 541 [resultTrack stop];
mas01mj@669 542 }
mas01mj@669 543 [resultTrack release];
mas01mj@699 544 resultTrack = Nil;
mas01mj@669 545 }
mas01mj@669 546
mas01mj@669 547 resultTrack = [[[NSSound alloc] initWithContentsOfFile:filename byReference:YES] retain];
mas01mj@669 548 [resultTrack setCurrentTime:ipos];
mas01mj@669 549 [resultTrack setDelegate:self];
mas01mj@669 550 [resultTrack play];
mas01mj@669 551
mas01mj@669 552 [stopButton setEnabled:YES];
mas01mj@669 553 }
mas01mj@669 554
mas01mj@669 555 /**
mas01mj@669 556 * Play the result and query simultaneously.
mas01mj@669 557 */
mas01mj@669 558 -(IBAction)playBoth:(id)sender
mas01mj@669 559 {
mas01mj@669 560
mas01mj@669 561 NSDictionary* selectedRow = [results objectAtIndex:[tracksView selectedRow]];
mas01mj@669 562 NSString* value = [selectedRow objectForKey:@"key"];
mas01mj@669 563 float ipos = [[selectedRow objectForKey:@"ipos"] floatValue];
mas01mj@669 564 NSString* filename = [trackMap objectForKey:value];
mas01mj@669 565 NSLog(@"Key: %@ Value: %@", value, filename);
mas01mj@669 566
mas01mj@669 567 if(queryTrack)
mas01mj@669 568 {
mas01mj@669 569
mas01mj@669 570 if([queryTrack isPlaying])
mas01mj@669 571 {
mas01mj@669 572 [queryTrack setDelegate:Nil];
mas01mj@669 573 [queryTrack stop];
mas01mj@669 574 }
mas01mj@669 575 [queryTrack release];
mas01mj@699 576 queryTrack = Nil;
mas01mj@669 577 }
mas01mj@669 578 if(resultTrack)
mas01mj@669 579 {
mas01mj@669 580 if([resultTrack isPlaying])
mas01mj@669 581 {
mas01mj@669 582 [resultTrack setDelegate:Nil];
mas01mj@669 583 [resultTrack stop];
mas01mj@669 584 }
mas01mj@669 585 [resultTrack release];
mas01mj@699 586 resultTrack = Nil;
mas01mj@669 587 }
mas01mj@669 588
mas01mj@669 589 // Get query track and shift to start point
mas01mj@669 590 queryTrack = [[[NSSound alloc] initWithContentsOfFile:selectedFilename byReference:YES] retain];
mas01mj@669 591 [queryTrack setDelegate:self];
mas01mj@669 592
mas01mj@669 593 [queryTrack play];
mas01mj@669 594
mas01mj@669 595 resultTrack = [[[NSSound alloc] initWithContentsOfFile:filename byReference:YES] retain];
mas01mj@669 596 [resultTrack setCurrentTime:ipos];
mas01mj@669 597 [resultTrack setDelegate:self];
mas01mj@669 598 [resultTrack play];
mas01mj@669 599
mas01mj@669 600 [stopButton setEnabled:YES];
mas01mj@669 601 }
mas01mj@669 602
mas01mj@669 603 /**
mas01mj@669 604 * Disable the stop button after playback of both tracks.
mas01mj@669 605 */
mas01mj@669 606 - (void)sound:(NSSound *)sound didFinishPlaying:(BOOL)playbackSuccessful
mas01mj@669 607 {
mas01mj@669 608
mas01mj@669 609 if((queryTrack && [queryTrack isPlaying]) || (resultTrack && [resultTrack isPlaying]))
mas01mj@669 610 {
mas01mj@669 611 return;
mas01mj@669 612 }
mas01mj@669 613 else
mas01mj@669 614 {
mas01mj@669 615 [stopButton setEnabled:NO];
mas01mj@669 616 }
mas01mj@669 617 }
mas01mj@669 618
mas01mj@669 619 /**
mas01mj@669 620 * Stop playback.
mas01mj@669 621 */
mas01mj@669 622 -(IBAction)stopPlay:(id)sender
mas01mj@669 623 {
mas01mj@669 624 if(queryTrack)
mas01mj@669 625 {
mas01mj@669 626 [queryTrack stop];
mas01mj@669 627 }
mas01mj@669 628 if(resultTrack)
mas01mj@669 629 {
mas01mj@669 630 [resultTrack stop];
mas01mj@669 631 }
mas01mj@669 632 }
mas01mj@669 633
mas01mj@669 634 /**
mas01mj@669 635 * Select an audio file, determine the key, and fire off a query.
mas01mj@669 636 */
mas01mj@669 637 -(IBAction)chooseQuery:(id)sender
mas01mj@669 638 {
mas01mj@699 639 [queryButton setEnabled:(selectedKey ? YES : NO)];
mas01mj@699 640 [NSApp beginSheet:querySheet modalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil];
mas01mj@699 641 session = [NSApp beginModalSessionForWindow:querySheet];
mas01mj@699 642 [NSApp runModalSession:session];
mas01mj@699 643 }
mas01mj@699 644
mas01mj@699 645
mas01mj@699 646 -(IBAction)selectQueryFile:(id)sender
mas01mj@699 647 {
mas01mj@714 648 NSArray* fileTypes = [NSArray arrayWithObjects: @"wav", @"mp3", @"aiff",@"m4a", nil];
mas01mj@669 649 NSOpenPanel* panel = [NSOpenPanel openPanel];
mas01mj@669 650 NSInteger response = [panel runModalForDirectory:NSHomeDirectory() file:@"" types:fileTypes];
mas01mj@669 651 if(response == NSFileHandlingPanelOKButton)
mas01mj@669 652 {
mas01mj@669 653 NSArray* opts = [trackMap allKeysForObject:[panel filename]];
mas01mj@669 654 if([opts count] != 1)
mas01mj@669 655 {
mas01mj@699 656 // TODO : Needs fixing!
mas01mj@699 657
mas01mj@669 658 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
mas01mj@669 659 [alert addButtonWithTitle:@"OK"];
mas01mj@669 660 [alert setMessageText:@"Track not found"];
mas01mj@669 661 [alert setInformativeText:@"Make sure you have specified a valid track identifier."];
mas01mj@669 662 [alert setAlertStyle:NSWarningAlertStyle];
mas01mj@669 663 [alert beginSheetModalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil];
mas01mj@669 664 }
mas01mj@669 665 else
mas01mj@669 666 {
mas01mj@669 667 selectedKey = [opts objectAtIndex:0];
mas01mj@669 668 [queryKey setStringValue:selectedKey];
mas01mj@699 669 [queryPath setStringValue:selectedKey];
mas01mj@669 670 selectedFilename = [[panel filename] retain];
mas01mj@699 671 [queryButton setEnabled:YES];
mas01mj@699 672
mas01mj@699 673 [self resetLengths:self];
mas01mj@669 674 }
mas01mj@669 675 }
mas01mj@669 676 }
mas01mj@669 677
mas01mj@699 678 -(IBAction)resetLengths:(id)sender
mas01mj@699 679 {
mas01mj@699 680 queryTrack = [[NSSound alloc] initWithContentsOfFile:selectedFilename byReference:YES];
mas01mj@699 681
mas01mj@702 682 int sampleRate = [self getSampleRate:selectedFilename];
mas01mj@702 683 int hopSize = [self getHopSizeInSamples:selectedFilename];
mas01mj@702 684 int winSize = [self nearestPow2:(hopSize*8)];
mas01mj@702 685
mas01mj@702 686 double samples = ([queryTrack duration]*sampleRate);
mas01mj@699 687
mas01mj@699 688 [queryLengthSeconds setDoubleValue:[queryTrack duration]];
mas01mj@699 689 [queryLengthVectors setDoubleValue:ceil((samples-winSize)/hopSize)];
mas01mj@699 690
mas01mj@699 691 // For now, go with 0
mas01mj@699 692 [queryStartSeconds setDoubleValue:0];
mas01mj@699 693 [queryStartVectors setDoubleValue:0];
mas01mj@699 694
mas01mj@699 695 [queryLengthSeconds setEnabled:YES];
mas01mj@699 696 [queryLengthVectors setEnabled:YES];
mas01mj@699 697 [queryStartSeconds setEnabled:YES];
mas01mj@699 698 [queryStartVectors setEnabled:YES];
mas01mj@699 699 [resetButton setEnabled:YES];
mas01mj@699 700 [multipleCheckBox setEnabled:YES];
mas01mj@702 701 [queryButton setEnabled:YES];
mas01mj@699 702
mas01mj@699 703 }
mas01mj@699 704
mas01mj@699 705 - (void)controlTextDidChange:(NSNotification *)nd
mas01mj@699 706 {
mas01mj@699 707 NSTextField *ed = [nd object];
mas01mj@699 708
mas01mj@702 709 int sampleRate = [self getSampleRate:selectedFilename];
mas01mj@702 710 int hopSize = [self getHopSizeInSamples:selectedFilename];
mas01mj@702 711 int winSize = [self nearestPow2:(hopSize*8)];
mas01mj@699 712
mas01mj@699 713 if(!queryTrack)
mas01mj@699 714 {
mas01mj@699 715 queryTrack = [[NSSound alloc] initWithContentsOfFile:selectedFilename byReference:YES];
mas01mj@699 716 }
mas01mj@699 717
mas01mj@699 718 double totalDuration = [queryTrack duration];
mas01mj@702 719 double samples = totalDuration * sampleRate;
mas01mj@699 720 double totalVectors = ceil((samples-winSize)/hopSize);
mas01mj@699 721
mas01mj@702 722
mas01mj@699 723 double lengthSecs = [queryLengthSeconds doubleValue];
mas01mj@699 724 double startSecs = [queryStartSeconds doubleValue];
mas01mj@699 725 double lengthVectors = [queryLengthVectors doubleValue];
mas01mj@699 726 double startVectors = [queryStartVectors doubleValue];
mas01mj@699 727
mas01mj@699 728 // Query Length
mas01mj@699 729 if (ed == queryLengthSeconds)
mas01mj@699 730 {
mas01mj@699 731 if(lengthSecs >= 0)
mas01mj@699 732 {
mas01mj@703 733 lengthVectors = ceil((((lengthSecs*sampleRate)-winSize)/hopSize)+1);
mas01mj@699 734 if(lengthVectors < 0) {lengthVectors = 0; }
mas01mj@699 735 [queryLengthVectors setDoubleValue:lengthVectors];
mas01mj@699 736 }
mas01mj@699 737 }
mas01mj@699 738
mas01mj@699 739 if (ed == queryLengthVectors)
mas01mj@699 740 {
mas01mj@699 741 if(lengthVectors >= 0)
mas01mj@699 742 {
mas01mj@703 743 lengthSecs = ((hopSize*(lengthVectors-1))+winSize)/sampleRate;
mas01mj@699 744 if(lengthSecs < 0) { lengthSecs = 0; }
mas01mj@699 745 [queryLengthSeconds setDoubleValue:lengthSecs];
mas01mj@699 746 }
mas01mj@699 747 }
mas01mj@699 748
mas01mj@699 749 // Query start
mas01mj@699 750 if (ed == queryStartSeconds)
mas01mj@699 751 {
mas01mj@699 752 if(startSecs >= 0)
mas01mj@699 753 {
mas01mj@703 754 startVectors = ceil((startSecs*sampleRate)/hopSize);
mas01mj@699 755 if(startVectors < 0) { startVectors = 0; }
mas01mj@699 756 [queryStartVectors setDoubleValue:startVectors];
mas01mj@699 757 }
mas01mj@699 758 }
mas01mj@699 759 if (ed == queryStartVectors)
mas01mj@699 760 {
mas01mj@699 761 if(startVectors >= 0)
mas01mj@699 762 {
mas01mj@703 763 startSecs = (hopSize*startVectors)/sampleRate;
mas01mj@699 764 if(startSecs < 0) { startSecs = 0; }
mas01mj@699 765 [queryStartSeconds setDoubleValue:startSecs];
mas01mj@699 766 }
mas01mj@699 767 }
mas01mj@699 768
mas01mj@699 769 if((lengthSecs + startSecs) > totalDuration || (lengthVectors + startVectors) > totalVectors || lengthVectors == 0)
mas01mj@699 770 {
mas01mj@699 771 [queryButton setEnabled:NO];
mas01mj@699 772 }
mas01mj@699 773 else if(![queryButton isEnabled])
mas01mj@699 774 {
mas01mj@699 775 [queryButton setEnabled:YES];
mas01mj@699 776 }
mas01mj@699 777 }
mas01mj@699 778
mas01mj@699 779 -(IBAction)cancelQuery:(id)sender
mas01mj@699 780 {
mas01mj@699 781 [NSApp endModalSession:session];
mas01mj@699 782 [querySheet orderOut:nil];
mas01mj@699 783 [NSApp endSheet:querySheet];
mas01mj@699 784 }
mas01mj@699 785
mas01mj@669 786 /**
mas01mj@669 787 * Actually perform the query. TODO: Monolithic.
mas01mj@669 788 */
mas01mj@699 789 -(IBAction)performQuery:(id)sender
mas01mj@669 790 {
mas01mj@699 791 [NSApp endModalSession:session];
mas01mj@699 792 [querySheet orderOut:nil];
mas01mj@699 793 [NSApp endSheet:querySheet];
mas01mj@699 794
mas01mj@669 795 NSLog(@"Perform query! %@, %@", selectedKey, selectedFilename);
mas01mj@669 796
mas01mj@669 797 adb_query_spec_t *spec = (adb_query_spec_t *)malloc(sizeof(adb_query_spec_t));
mas01mj@669 798 spec->qid.datum = (adb_datum_t *)malloc(sizeof(adb_datum_t));
mas01mj@669 799
mas01mj@699 800 spec->qid.sequence_length = [queryLengthVectors doubleValue];
mas01mj@699 801 spec->qid.sequence_start = [queryStartVectors doubleValue];
mas01mj@699 802 spec->qid.flags = 0;
mas01mj@699 803 // spec->qid.flags = spec->qid.flags | ADB_QID_FLAG_EXHAUSTIVE;
mas01mj@692 804
mas01mj@669 805 spec->params.accumulation = ADB_ACCUMULATION_PER_TRACK;
mas01mj@699 806
mas01mj@699 807 if([multipleCheckBox state] == NSOnState)
mas01mj@699 808 {
mas01mj@699 809 spec->params.npoints = 100;
mas01mj@699 810 }
mas01mj@699 811 else
mas01mj@699 812 {
mas01mj@699 813 spec->params.npoints = 1;
mas01mj@699 814 }
mas01mj@699 815
mas01mj@669 816 spec->params.distance = ADB_DISTANCE_EUCLIDEAN_NORMED;
mas01mj@669 817
mas01mj@669 818 spec->params.ntracks = 100;
mas01mj@699 819 //spec->refine.radius = 5.0;
mas01mj@669 820 // spec->refine.absolute_threshold = -6;
mas01mj@669 821 // spec->refine.relative_threshold = 10;
mas01mj@669 822 // spec->refine.duration_ratio = 0;
mas01mj@669 823
mas01mj@669 824 spec->refine.flags = 0;
mas01mj@669 825 // spec->refine.flags |= ADB_REFINE_ABSOLUTE_THRESHOLD;
mas01mj@669 826 // spec->refine.flags |= ADB_REFINE_RELATIVE_THRESHOLD;
mas01mj@699 827 // spec->refine.flags |= ADB_REFINE_HOP_SIZE;
mas01mj@699 828 //spec->refine.flags |= ADB_REFINE_RADIUS;
mas01mj@669 829
mas01mj@669 830 adb_query_results_t *result = (adb_query_results_t *)malloc(sizeof(adb_query_results_t));
mas01mj@669 831 spec->qid.datum->data = NULL;
mas01mj@669 832 spec->qid.datum->power = NULL;
mas01mj@669 833 spec->qid.datum->times = NULL;
mas01mj@669 834
mas01mj@669 835 [results removeAllObjects];
mas01mj@669 836
mas01mj@669 837 int ok = audiodb_retrieve_datum(db, [selectedKey cStringUsingEncoding:NSUTF8StringEncoding], spec->qid.datum);
mas01mj@669 838 if(ok == 0)
mas01mj@669 839 {
mas01mj@699 840
mas01mj@699 841 float hopSize = [[dbState objectForKey:@"hopsize"] floatValue];
mas01mj@669 842 NSLog(@"Got a datum");
mas01mj@669 843 result = audiodb_query_spec(db, spec);
mas01mj@669 844 if(result == NULL)
mas01mj@669 845 {
mas01mj@669 846
mas01mj@669 847 NSLog(@"No results");
mas01mj@669 848 }
mas01mj@669 849 else
mas01mj@669 850 {
mas01mj@702 851
mas01mj@699 852 NSLog(@"Populate table: %d", result->nresults);
mas01mj@669 853 for(int i=0; i<result->nresults; i++)
mas01mj@669 854 {
mas01mj@699 855
mas01mj@702 856 NSString* filename = [trackMap objectForKey:[NSString stringWithFormat:@"%s", result->results[i].ikey]];
mas01mj@702 857 int sampleRate = [self getSampleRate:filename];
mas01mj@702 858 int hopSize = [self getHopSizeInSamples:filename];
mas01mj@702 859 float divisor = (sampleRate/hopSize);
mas01mj@702 860
mas01mj@669 861 NSMutableDictionary* dict = [[NSMutableDictionary alloc] initWithCapacity:4];
mas01mj@699 862 [dict setValue:[NSString stringWithFormat:@"%s", result->results[i].ikey] forKey:@"key"];
mas01mj@669 863 [dict setValue:[NSNumber numberWithFloat:result->results[i].dist] forKey:@"distance"];
mas01mj@669 864 [dict setValue:[NSNumber numberWithFloat:result->results[i].dist] forKey:@"meter"];
mas01mj@699 865 [dict setValue:[NSNumber numberWithFloat:result->results[i].ipos/divisor] forKey:@"ipos"];
mas01mj@699 866 NSLog(@"%s ipos: %d, dist: %f", result->results[i].ikey,result->results[i].ipos, result->results[i].dist);
mas01mj@669 867 [results addObject: dict];
mas01mj@669 868 }
mas01mj@669 869 }
mas01mj@669 870
mas01mj@669 871 NSSortDescriptor *distSort = [[NSSortDescriptor alloc]initWithKey:@"meter" ascending:YES];
mas01mj@669 872 NSArray *distDescs = [NSArray arrayWithObject:distSort];
mas01mj@669 873
mas01mj@669 874 [results sortUsingDescriptors:distDescs];
mas01mj@669 875 [tracksView setSortDescriptors:distDescs];
mas01mj@669 876 [tracksView reloadData];
mas01mj@669 877
mas01mj@669 878 }
mas01mj@669 879 else
mas01mj@669 880 {
mas01mj@669 881 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
mas01mj@669 882 [alert addButtonWithTitle:@"OK"];
mas01mj@669 883 [alert setMessageText:@"Track not found"];
mas01mj@669 884 [alert setInformativeText:@"Make sure you have specified a valid track identifier."];
mas01mj@669 885 [alert setAlertStyle:NSWarningAlertStyle];
mas01mj@669 886 [alert beginSheetModalForWindow:mainWindow modalDelegate:self didEndSelector:NULL contextInfo:nil];
mas01mj@669 887 }
mas01mj@669 888 // audiodb_query_free_results(db, spec, result);
mas01mj@669 889 }
mas01mj@669 890
mas01mj@669 891 @end