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