1/** <title>NSSound</title> 2 3 <abstract>Load, manipulate and play sounds</abstract> 4 5 Copyright (C) 2002, 2009 Free Software Foundation, Inc. 6 7 Author: Enrico Sersale <enrico@imago.ro>, 8 Stefan Bidigaray <stefanbidi@gmail.com> 9 Date: Jul 2002, Jul 2009 10 11 This file is part of the GNUstep GUI Library. 12 13 This library is free software; you can redistribute it and/or 14 modify it under the terms of the GNU Lesser General Public 15 License as published by the Free Software Foundation; either 16 version 2 of the License, or (at your option) any later version. 17 18 This library is distributed in the hope that it will be useful, 19 but WITHOUT ANY WARRANTY; without even the implied warranty of 20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21 Lesser General Public License for more details. 22 23 You should have received a copy of the GNU Lesser General Public 24 License along with this library; see the file COPYING.LIB. 25 If not, see <http://www.gnu.org/licenses/> or write to the 26 Free Software Foundation, 51 Franklin Street, Fifth Floor, 27 Boston, MA 02110-1301, USA. 28*/ 29 30#import <Foundation/Foundation.h> 31#import "AppKit/NSPasteboard.h" 32#import "AppKit/NSSound.h" 33 34#import "GNUstepGUI/GSSoundSource.h" 35#import "GNUstepGUI/GSSoundSink.h" 36 37// Private NSConditionLock conditions used for streaming 38enum 39{ 40 SOUND_SHOULD_PLAY = 1, 41 SOUND_SHOULD_PAUSE 42}; 43 44#define BUFFER_SIZE 4096 45 46/* Class variables and functions for class methods */ 47static NSMutableDictionary *nameDict = nil; 48static NSDictionary *nsmapping = nil; 49static NSArray *sourcePlugIns = nil; 50static NSArray *sinkPlugIns = nil; 51 52static inline void _loadNSSoundPlugIns (void) 53{ 54 NSString *path; 55 NSArray *paths; 56 NSBundle *bundle; 57 NSEnumerator *enumerator; 58 NSMutableArray *all, 59 *_sourcePlugIns, 60 *_sinkPlugIns; 61 Class plugInClass; 62 63 /* Gather up the paths */ 64 paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, 65 NSAllDomainsMask, YES); 66 67 enumerator = [paths objectEnumerator]; 68 all = [NSMutableArray array]; 69 while ((path = [enumerator nextObject]) != nil) 70 { 71 bundle = [NSBundle bundleWithPath: path]; 72 paths = [bundle pathsForResourcesOfType: @"nssound" 73 inDirectory: @"Bundles"]; 74 [all addObjectsFromArray: paths]; 75 } 76 77 enumerator = [all objectEnumerator]; 78 _sourcePlugIns = [NSMutableArray array]; 79 _sinkPlugIns = [NSMutableArray array]; 80 while ((path = [enumerator nextObject]) != nil) 81 { 82 NSBundle *nssoundBundle = [NSBundle bundleWithPath: path]; 83 plugInClass = [nssoundBundle principalClass]; 84 if ([plugInClass conformsToProtocol: @protocol(GSSoundSource)]) 85 { 86 [_sourcePlugIns addObject:plugInClass]; 87 } 88 else if ([plugInClass conformsToProtocol: @protocol(GSSoundSink)]) 89 { 90 [_sinkPlugIns addObject:plugInClass]; 91 } 92 else 93 { 94 NSLog (@"Bundle %@ does not conform to GSSoundSource or GSSoundSink", 95 path); 96 } 97 } 98 99 sourcePlugIns = [[NSArray alloc] initWithArray: _sourcePlugIns]; 100 sinkPlugIns = [[NSArray alloc] initWithArray: _sinkPlugIns]; 101} 102 103 104 105@implementation NSBundle (NSSoundAdditions) 106 107- (NSString *) pathForSoundResource: (NSString *)name 108{ 109 NSString *ext = [name pathExtension]; 110 NSString *path = nil; 111 112 if ((ext == nil) || [ext isEqualToString:@""]) 113 { 114 NSArray *types = [NSSound soundUnfilteredFileTypes]; 115 unsigned c = [types count]; 116 unsigned i; 117 118 for (i = 0; path == nil && i < c; i++) 119 { 120 ext = [types objectAtIndex: i]; 121 path = [self pathForResource: name ofType: ext]; 122 } 123 } 124 else 125 { 126 name = [name stringByDeletingPathExtension]; 127 path = [self pathForResource: name ofType: ext]; 128 } 129 return path; 130} 131 132@end 133 134@interface NSSound (PrivateMethods) 135 136- (void)_stream; 137- (void)_finished: (NSNumber *)finishedPlaying; 138 139@end 140 141@implementation NSSound (PrivateMethods) 142 143- (void)_stream 144{ 145 NSUInteger bytesRead; 146 BOOL success = NO; 147 void *buffer; 148 149 // Exit with success = NO if device could not be open. 150 if ([_sink open]) 151 { 152 // Allocate space for buffer and start writing. 153 buffer = NSZoneMalloc(NSDefaultMallocZone(), BUFFER_SIZE); 154 do 155 { 156 do 157 { 158 // If not SOUND_SHOULD_PLAY block thread 159 [_readLock lockWhenCondition: SOUND_SHOULD_PLAY]; 160 if (_shouldStop) 161 { 162 [_readLock unlock]; 163 break; 164 } 165 bytesRead = [_source readBytes: buffer 166 length: BUFFER_SIZE]; 167 [_readLock unlock]; 168 [_playbackLock lock]; 169 success = [_sink playBytes: buffer length: bytesRead]; 170 [_playbackLock unlock]; 171 } while ((!_shouldStop) && (bytesRead > 0) && success); 172 173 [_source setCurrentTime: 0.0]; 174 } while (_shouldLoop == YES && _shouldStop == NO); 175 176 [_sink close]; 177 NSZoneFree (NSDefaultMallocZone(), buffer); 178 } 179 180 RETAIN(self); 181 [self performSelectorOnMainThread: @selector(_finished:) 182 withObject: [NSNumber numberWithBool: success] 183 waitUntilDone: YES]; 184 RELEASE(self); 185} 186 187- (void)_finished: (NSNumber *)finishedPlaying 188{ 189 DESTROY(_readLock); 190 DESTROY(_playbackLock); 191 192 /* FIXME: should I call -sound:didFinishPlaying: when -stop was sent? */ 193 if ([_delegate respondsToSelector: @selector(sound:didFinishPlaying:)]) 194 { 195 [_delegate sound: self didFinishPlaying: [finishedPlaying boolValue]]; 196 } 197} 198 199@end 200 201@implementation NSSound 202 203+ (void) initialize 204{ 205 if (self == [NSSound class]) 206 { 207 NSString *path = [NSBundle pathForLibraryResource: @"nsmapping" 208 ofType: @"strings" 209 inDirectory: @"Sounds"]; 210 [self setVersion: 2]; 211 212 nameDict = [[NSMutableDictionary alloc] initWithCapacity: 10]; 213 214 if (path) 215 { 216 nsmapping = RETAIN([[NSString stringWithContentsOfFile: path] 217 propertyListFromStringsFileFormat]); 218 } 219 220 /* FIXME: Not sure if this is the best way... */ 221 _loadNSSoundPlugIns (); 222 } 223} 224 225- (void) dealloc 226{ 227 // Make sure sound is stopped before deallocating. 228 [self stop]; 229 230 RELEASE (_data); 231 if (self == [nameDict objectForKey: _name]) 232 { 233 [nameDict removeObjectForKey: _name]; 234 } 235 RELEASE (_name); 236 RELEASE (_playbackDeviceIdentifier); 237 RELEASE (_channelMapping); 238 RELEASE (_source); 239 RELEASE (_sink); 240 241 [super dealloc]; 242} 243 244// 245// Creating an NSSound 246// 247- (id) initWithContentsOfFile: (NSString *)path byReference:(BOOL)byRef 248{ 249 NSData *fileData; 250 251 // Problem here: should every NSSound instance have a _name set? 252 // The Apple docs are a bit confusing here. For now, the only way 253 // _name will be set is if -setName: is called, or if the sound already 254 // exists in on of the Sounds/ directories. 255 _onlyReference = byRef; 256 257 258 fileData = [NSData dataWithContentsOfMappedFile: path]; 259 if (!fileData) 260 { 261 NSLog (@"Could not get sound data from: %@", path); 262 DESTROY(self); 263 return nil; 264 } 265 266 return [self initWithData: fileData]; 267} 268 269- (id) initWithContentsOfURL: (NSURL *)url byReference:(BOOL)byRef 270{ 271 _onlyReference = byRef; 272 return [self initWithData: [NSData dataWithContentsOfURL: url]]; 273} 274 275- (id) initWithData: (NSData *)data 276{ 277 NSEnumerator *enumerator; 278 Class sourceClass, 279 sinkClass; 280 281 _data = data; 282 RETAIN(_data); 283 284 // Search for an GSSoundSource bundle that can play this data. 285 enumerator = [sourcePlugIns objectEnumerator]; 286 while ((sourceClass = [enumerator nextObject]) != nil) 287 { 288 if ([sourceClass canInitWithData: _data]) 289 { 290 _source = [[sourceClass alloc] initWithData: _data]; 291 if (_source == nil) 292 { 293 NSLog (@"Could not read sound data!"); 294 DESTROY(self); 295 return nil; 296 } 297 break; 298 } 299 } 300 301 enumerator = [sinkPlugIns objectEnumerator]; 302 /* FIXME: Grab the first available sink/device for now. In the future 303 look for what is set in the GSSoundDeviceBundle default first. */ 304 while ((sinkClass = [enumerator nextObject]) != nil) 305 { 306 if ([sinkClass canInitWithPlaybackDevice: nil]) 307 { 308 _sink = [[sinkClass alloc] initWithEncoding: [_source encoding] 309 channels: [_source channelCount] 310 sampleRate: [_source sampleRate] 311 byteOrder: [_source byteOrder]]; 312 if (_sink == nil) 313 { 314 NSLog (@"Could not open sound sink!"); 315 DESTROY(self); 316 return nil; 317 } 318 break; 319 } 320 } 321 322 /* FIXME: There has to be a better way to do this check??? */ 323 if (sourceClass == nil || sinkClass == nil) 324 { 325 NSLog (@"Could not find suitable sound plug-in"); 326 DESTROY(self); 327 return nil; 328 } 329 330 return self; 331} 332 333- (id) initWithPasteboard: (NSPasteboard *)pasteboard 334{ 335 if ([object_getClass(self) canInitWithPasteboard: pasteboard] == YES) 336 { 337 /* FIXME: Should this be @"NSGeneralPboardType" or @"NSSoundPboardType"? 338 Apple also defines "NSString *NSSoundPboardType". */ 339 NSData *d = [pasteboard dataForType: @"NSGeneralPboardType"]; 340 return [self initWithData: d]; 341 } 342 return nil; 343} 344 345// 346// Playing and Information 347// 348- (BOOL) pause 349{ 350 // Do nothing if sound is already paused. 351 if ([_readLock condition] == SOUND_SHOULD_PAUSE) 352 { 353 return NO; 354 } 355 356 if ([_readLock tryLock] == NO) 357 { 358 return NO; 359 } 360 [_readLock unlockWithCondition: SOUND_SHOULD_PAUSE]; 361 return YES; 362} 363 364- (BOOL) play 365{ 366 // If the locks exists this instance is already playing 367 if (_readLock != nil && _playbackLock != nil) 368 { 369 return NO; 370 } 371 372 _readLock = [[NSConditionLock alloc] initWithCondition: SOUND_SHOULD_PAUSE]; 373 _playbackLock = [[NSLock alloc] init]; 374 375 if ([_readLock tryLock] != YES) 376 { 377 return NO; 378 } 379 _shouldStop = NO; 380 [NSThread detachNewThreadSelector: @selector(_stream) 381 toTarget: self 382 withObject: nil]; 383 [_readLock unlockWithCondition: SOUND_SHOULD_PLAY]; 384 385 return YES; 386} 387 388- (BOOL) resume 389{ 390 // Do nothing if sound is already playing. 391 if ([_readLock condition] == SOUND_SHOULD_PLAY) 392 { 393 return NO; 394 } 395 396 if ([_readLock tryLock] == NO) 397 { 398 return NO; 399 } 400 [_readLock unlockWithCondition: SOUND_SHOULD_PLAY]; 401 return YES; 402} 403 404- (BOOL) stop 405{ 406 if (_readLock == nil) 407 { 408 return NO; 409 } 410 411 if ([_readLock tryLock] != YES) 412 { 413 return NO; 414 } 415 _shouldStop = YES; 416 // Set to SOUND_SHOULD_PLAY so that thread isn't blocked. 417 [_readLock unlockWithCondition: SOUND_SHOULD_PLAY]; 418 419 return YES; 420} 421 422- (BOOL) isPlaying 423{ 424 if (_readLock == nil) 425 { 426 return NO; 427 } 428 if ([_readLock condition] == SOUND_SHOULD_PLAY) 429 { 430 return YES; 431 } 432 return NO; 433} 434 435- (float) volume 436{ 437 return [_sink volume]; 438} 439 440- (void) setVolume: (float) volume 441{ 442 [_playbackLock lock]; 443 [_sink setVolume: volume]; 444 [_playbackLock unlock]; 445} 446 447- (NSTimeInterval) currentTime 448{ 449 return [_source currentTime]; 450} 451 452- (void) setCurrentTime: (NSTimeInterval) currentTime 453{ 454 [_readLock lock]; 455 [_source setCurrentTime: currentTime]; 456 [_readLock unlock]; 457} 458 459- (BOOL) loops 460{ 461 return _shouldLoop; 462} 463 464- (void) setLoops: (BOOL) loops 465{ 466 _shouldLoop = loops; 467} 468 469- (NSTimeInterval) duration 470{ 471 return [_source duration]; 472} 473 474// 475// Working with pasteboards 476// 477+ (BOOL) canInitWithPasteboard: (NSPasteboard *)pasteboard 478{ 479 NSArray *pbTypes = [pasteboard types]; 480 NSArray *myTypes = [NSSound soundUnfilteredPasteboardTypes]; 481 482 return ([pbTypes firstObjectCommonWithArray: myTypes] != nil); 483} 484 485+ (NSArray *) soundUnfilteredPasteboardTypes 486{ 487 return [NSArray arrayWithObjects: @"NSGeneralPboardType", nil]; 488} 489 490- (void) writeToPasteboard: (NSPasteboard *)pasteboard 491{ 492 NSData *d = [NSArchiver archivedDataWithRootObject: self]; 493 494 if (d != nil) { 495 [pasteboard declareTypes: [NSSound soundUnfilteredPasteboardTypes] 496 owner: nil]; 497 [pasteboard setData: d forType: @"NSGeneralPboardType"]; 498 } 499} 500 501// 502// Working with delegates 503// 504- (id) delegate 505{ 506 return _delegate; 507} 508 509- (void) setDelegate: (id)aDelegate 510{ 511 _delegate = aDelegate; 512} 513 514// 515// Naming Sounds 516// 517+ (id) soundNamed: (NSString*)name 518{ 519 NSString *realName = [nsmapping objectForKey: name]; 520 NSSound *sound; 521 522 if (realName) 523 { 524 name = realName; 525 } 526 527 sound = (NSSound *)[nameDict objectForKey: name]; 528 529 if (sound == nil) 530 { 531 NSString *extension; 532 NSString *path = nil; 533 NSBundle *main_bundle; 534 NSArray *array; 535 NSString *the_name = name; 536 537 // FIXME: This should use [NSBundle pathForSoundResource], but this will 538 // only allow soundUnfilteredFileTypes. 539 /* If there is no sound with that name, search in the main bundle */ 540 541 main_bundle = [NSBundle mainBundle]; 542 extension = [name pathExtension]; 543 544 if (extension != nil && [extension length] == 0) 545 { 546 extension = nil; 547 } 548 549 /* Check if extension is one of the sound types */ 550 array = [NSSound soundUnfilteredFileTypes]; 551 552 if ([array indexOfObject: extension] != NSNotFound) 553 { 554 /* Extension is one of the sound types 555 So remove from the name */ 556 the_name = [name stringByDeletingPathExtension]; 557 } 558 else 559 { 560 /* Otherwise extension is not an sound type 561 So leave it alone */ 562 the_name = name; 563 extension = nil; 564 } 565 566 /* First search locally */ 567 if (extension) 568 { 569 path = [main_bundle pathForResource: the_name ofType: extension]; 570 } 571 else 572 { 573 id o, e; 574 575 e = [array objectEnumerator]; 576 while ((o = [e nextObject])) 577 { 578 path = [main_bundle pathForResource: the_name ofType: o]; 579 if (path != nil && [path length] != 0) 580 { 581 break; 582 } 583 } 584 } 585 586 /* If not found then search in system */ 587 if (!path) 588 { 589 if (extension) 590 { 591 path = [NSBundle pathForLibraryResource: the_name 592 ofType: extension 593 inDirectory: @"Sounds"]; 594 } 595 else 596 { 597 id o, e; 598 599 e = [array objectEnumerator]; 600 while ((o = [e nextObject])) { 601 path = [NSBundle pathForLibraryResource: the_name 602 ofType: o 603 inDirectory: @"Sounds"]; 604 if (path != nil && [path length] != 0) 605 { 606 break; 607 } 608 } 609 } 610 } 611 612 if ([path length] != 0) 613 { 614 sound = [[self allocWithZone: NSDefaultMallocZone()] 615 initWithContentsOfFile: path byReference: NO]; 616 617 if (sound != nil) 618 { 619 [sound setName: name]; 620 RELEASE(sound); 621 sound->_onlyReference = YES; 622 } 623 624 return sound; 625 } 626 } 627 628 return sound; 629} 630 631+ (NSArray *) soundUnfilteredFileTypes 632{ 633 Class sourceClass; 634 NSMutableArray *array; 635 NSEnumerator *enumerator; 636 637 array = [NSMutableArray arrayWithCapacity: 10]; 638 enumerator = [sourcePlugIns objectEnumerator]; 639 while ((sourceClass = [enumerator nextObject]) != nil) 640 { 641 [array addObjectsFromArray: [sourceClass soundUnfilteredFileTypes]]; 642 } 643 644 return array; 645} 646 647+ (NSArray *) soundUnfilteredTypes 648{ 649 Class sourceClass; 650 NSMutableArray *array; 651 NSEnumerator *enumerator; 652 653 array = [NSMutableArray arrayWithCapacity: 10]; 654 enumerator = [sourcePlugIns objectEnumerator]; 655 while ((sourceClass = [enumerator nextObject]) != nil) 656 { 657 [array addObjectsFromArray: [sourceClass soundUnfilteredTypes]]; 658 } 659 660 return array; 661} 662 663- (NSString *) name 664{ 665 return _name; 666} 667 668- (BOOL) setName: (NSString *)aName 669{ 670 if (!aName || [nameDict objectForKey: aName]) 671 { 672 return NO; 673 } 674 675 if ((_name != nil) && self == [nameDict objectForKey: _name]) 676 { 677 [nameDict removeObjectForKey: _name]; 678 } 679 680 ASSIGN(_name, aName); 681 682 [nameDict setObject: self forKey: _name]; 683 684 return YES; 685} 686 687- (NSString *) playbackDeviceIdentifier 688{ 689 return [_sink playbackDeviceIdentifier]; 690} 691 692- (void) setPlaybackDeviceIdentifier: (NSString *)playbackDeviceIdentifier 693{ 694 if ([[_sink class] canInitWithPlaybackDevice: playbackDeviceIdentifier]) 695 { 696 [_playbackLock lock]; 697 [_sink setPlaybackDeviceIdentifier: playbackDeviceIdentifier]; 698 [_playbackLock unlock]; 699 } 700} 701 702- (NSArray *) channelMapping 703{ 704 return [_sink channelMapping]; 705} 706 707- (void) setChannelMapping: (NSArray *)channelMapping 708{ 709 [_playbackLock lock]; 710 [_sink setChannelMapping: channelMapping]; 711 [_playbackLock unlock]; 712} 713 714// 715// NSCoding 716// 717- (void) encodeWithCoder: (NSCoder *)coder 718{ 719 if ([coder allowsKeyedCoding]) 720 { 721 // TODO_NIB: Determine keys for NSSound. 722 } 723 else 724 { 725 [coder encodeValueOfObjCType: @encode(BOOL) at: &_onlyReference]; 726 [coder encodeObject: _name]; 727 728 if (_onlyReference == YES) 729 { 730 return; 731 } 732 [coder encodeConditionalObject: _delegate]; 733 [coder encodeObject: _data]; 734 [coder encodeObject: _playbackDeviceIdentifier]; 735 [coder encodeObject: _channelMapping]; 736 } 737} 738 739- (id) initWithCoder: (NSCoder*)decoder 740{ 741 if ([decoder allowsKeyedCoding]) 742 { 743 // TODO_NIB: Determine keys for NSSound. 744 } 745 else 746 { 747 [decoder decodeValueOfObjCType: @encode(BOOL) at: &_onlyReference]; 748 749 if (_onlyReference == YES) 750 { 751 NSString *theName = [decoder decodeObject]; 752 RELEASE (self); 753 self = RETAIN ([NSSound soundNamed: theName]); 754 [self setName: theName]; 755 } 756 else 757 { 758 _name = RETAIN ([decoder decodeObject]); 759 [self setDelegate: [decoder decodeObject]]; 760 _data = RETAIN([decoder decodeObject]); 761 _playbackDeviceIdentifier = RETAIN([decoder decodeObject]); 762 _channelMapping = RETAIN([decoder decodeObject]); 763 } 764 765 /* FIXME: Need to prepare the object for playback before going further. */ 766 } 767 return self; 768} 769 770// 771// NSCopying 772// 773- (id) copyWithZone: (NSZone *)zone 774{ 775 NSSound *newSound = (NSSound *)NSCopyObject(self, 0, zone); 776 777 /* FIXME: Is all this correct? And is this all that needs to be copied? */ 778 newSound->_name = [_name copyWithZone: zone]; 779 newSound->_data = [_data copyWithZone: zone]; 780 newSound->_playbackDeviceIdentifier = [_playbackDeviceIdentifier 781 copyWithZone: zone]; 782 newSound->_channelMapping = [_channelMapping copyWithZone: zone]; 783 784 /* FIXME: Need to prepare the object for playback before going further. */ 785 return newSound; 786} 787 788@end 789