/**
NSSound
Load, manipulate and play sounds
Copyright (C) 2002, 2009 Free Software Foundation, Inc.
Author: Enrico Sersale ,
Stefan Bidigaray
Date: Jul 2002, Jul 2009
This file is part of the GNUstep GUI Library.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; see the file COPYING.LIB.
If not, see or write to the
Free Software Foundation, 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#import
#import "AppKit/NSPasteboard.h"
#import "AppKit/NSSound.h"
#import "GNUstepGUI/GSSoundSource.h"
#import "GNUstepGUI/GSSoundSink.h"
// Private NSConditionLock conditions used for streaming
enum
{
SOUND_SHOULD_PLAY = 1,
SOUND_SHOULD_PAUSE
};
#define BUFFER_SIZE 4096
/* Class variables and functions for class methods */
static NSMutableDictionary *nameDict = nil;
static NSDictionary *nsmapping = nil;
static NSArray *sourcePlugIns = nil;
static NSArray *sinkPlugIns = nil;
static inline void _loadNSSoundPlugIns (void)
{
NSString *path;
NSArray *paths;
NSBundle *bundle;
NSEnumerator *enumerator;
NSMutableArray *all,
*_sourcePlugIns,
*_sinkPlugIns;
Class plugInClass;
/* Gather up the paths */
paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
NSAllDomainsMask, YES);
enumerator = [paths objectEnumerator];
all = [NSMutableArray array];
while ((path = [enumerator nextObject]) != nil)
{
bundle = [NSBundle bundleWithPath: path];
paths = [bundle pathsForResourcesOfType: @"nssound"
inDirectory: @"Bundles"];
[all addObjectsFromArray: paths];
}
enumerator = [all objectEnumerator];
_sourcePlugIns = [NSMutableArray array];
_sinkPlugIns = [NSMutableArray array];
while ((path = [enumerator nextObject]) != nil)
{
NSBundle *nssoundBundle = [NSBundle bundleWithPath: path];
plugInClass = [nssoundBundle principalClass];
if ([plugInClass conformsToProtocol: @protocol(GSSoundSource)])
{
[_sourcePlugIns addObject:plugInClass];
}
else if ([plugInClass conformsToProtocol: @protocol(GSSoundSink)])
{
[_sinkPlugIns addObject:plugInClass];
}
else
{
NSLog (@"Bundle %@ does not conform to GSSoundSource or GSSoundSink",
path);
}
}
sourcePlugIns = [[NSArray alloc] initWithArray: _sourcePlugIns];
sinkPlugIns = [[NSArray alloc] initWithArray: _sinkPlugIns];
}
@implementation NSBundle (NSSoundAdditions)
- (NSString *) pathForSoundResource: (NSString *)name
{
NSString *ext = [name pathExtension];
NSString *path = nil;
if ((ext == nil) || [ext isEqualToString:@""])
{
NSArray *types = [NSSound soundUnfilteredFileTypes];
unsigned c = [types count];
unsigned i;
for (i = 0; path == nil && i < c; i++)
{
ext = [types objectAtIndex: i];
path = [self pathForResource: name ofType: ext];
}
}
else
{
name = [name stringByDeletingPathExtension];
path = [self pathForResource: name ofType: ext];
}
return path;
}
@end
@interface NSSound (PrivateMethods)
- (void)_stream;
- (void)_finished: (NSNumber *)finishedPlaying;
@end
@implementation NSSound (PrivateMethods)
- (void)_stream
{
NSUInteger bytesRead;
BOOL success = NO;
void *buffer;
// Exit with success = NO if device could not be open.
if ([_sink open])
{
// Allocate space for buffer and start writing.
buffer = NSZoneMalloc(NSDefaultMallocZone(), BUFFER_SIZE);
do
{
do
{
// If not SOUND_SHOULD_PLAY block thread
[_readLock lockWhenCondition: SOUND_SHOULD_PLAY];
if (_shouldStop)
{
[_readLock unlock];
break;
}
bytesRead = [_source readBytes: buffer
length: BUFFER_SIZE];
[_readLock unlock];
[_playbackLock lock];
success = [_sink playBytes: buffer length: bytesRead];
[_playbackLock unlock];
} while ((!_shouldStop) && (bytesRead > 0) && success);
[_source setCurrentTime: 0.0];
} while (_shouldLoop == YES && _shouldStop == NO);
[_sink close];
NSZoneFree (NSDefaultMallocZone(), buffer);
}
RETAIN(self);
[self performSelectorOnMainThread: @selector(_finished:)
withObject: [NSNumber numberWithBool: success]
waitUntilDone: YES];
RELEASE(self);
}
- (void)_finished: (NSNumber *)finishedPlaying
{
DESTROY(_readLock);
DESTROY(_playbackLock);
/* FIXME: should I call -sound:didFinishPlaying: when -stop was sent? */
if ([_delegate respondsToSelector: @selector(sound:didFinishPlaying:)])
{
[_delegate sound: self didFinishPlaying: [finishedPlaying boolValue]];
}
}
@end
@implementation NSSound
+ (void) initialize
{
if (self == [NSSound class])
{
NSString *path = [NSBundle pathForLibraryResource: @"nsmapping"
ofType: @"strings"
inDirectory: @"Sounds"];
[self setVersion: 2];
nameDict = [[NSMutableDictionary alloc] initWithCapacity: 10];
if (path)
{
nsmapping = RETAIN([[NSString stringWithContentsOfFile: path]
propertyListFromStringsFileFormat]);
}
/* FIXME: Not sure if this is the best way... */
_loadNSSoundPlugIns ();
}
}
- (void) dealloc
{
// Make sure sound is stopped before deallocating.
[self stop];
RELEASE (_data);
if (self == [nameDict objectForKey: _name])
{
[nameDict removeObjectForKey: _name];
}
RELEASE (_name);
RELEASE (_playbackDeviceIdentifier);
RELEASE (_channelMapping);
RELEASE (_source);
RELEASE (_sink);
[super dealloc];
}
//
// Creating an NSSound
//
- (id) initWithContentsOfFile: (NSString *)path byReference:(BOOL)byRef
{
NSData *fileData;
// Problem here: should every NSSound instance have a _name set?
// The Apple docs are a bit confusing here. For now, the only way
// _name will be set is if -setName: is called, or if the sound already
// exists in on of the Sounds/ directories.
_onlyReference = byRef;
fileData = [NSData dataWithContentsOfMappedFile: path];
if (!fileData)
{
NSLog (@"Could not get sound data from: %@", path);
DESTROY(self);
return nil;
}
return [self initWithData: fileData];
}
- (id) initWithContentsOfURL: (NSURL *)url byReference:(BOOL)byRef
{
_onlyReference = byRef;
return [self initWithData: [NSData dataWithContentsOfURL: url]];
}
- (id) initWithData: (NSData *)data
{
NSEnumerator *enumerator;
Class sourceClass,
sinkClass;
_data = data;
RETAIN(_data);
// Search for an GSSoundSource bundle that can play this data.
enumerator = [sourcePlugIns objectEnumerator];
while ((sourceClass = [enumerator nextObject]) != nil)
{
if ([sourceClass canInitWithData: _data])
{
_source = [[sourceClass alloc] initWithData: _data];
if (_source == nil)
{
NSLog (@"Could not read sound data!");
DESTROY(self);
return nil;
}
break;
}
}
enumerator = [sinkPlugIns objectEnumerator];
/* FIXME: Grab the first available sink/device for now. In the future
look for what is set in the GSSoundDeviceBundle default first. */
while ((sinkClass = [enumerator nextObject]) != nil)
{
if ([sinkClass canInitWithPlaybackDevice: nil])
{
_sink = [[sinkClass alloc] initWithEncoding: [_source encoding]
channels: [_source channelCount]
sampleRate: [_source sampleRate]
byteOrder: [_source byteOrder]];
if (_sink == nil)
{
NSLog (@"Could not open sound sink!");
DESTROY(self);
return nil;
}
break;
}
}
/* FIXME: There has to be a better way to do this check??? */
if (sourceClass == nil || sinkClass == nil)
{
NSLog (@"Could not find suitable sound plug-in");
DESTROY(self);
return nil;
}
return self;
}
- (id) initWithPasteboard: (NSPasteboard *)pasteboard
{
if ([object_getClass(self) canInitWithPasteboard: pasteboard] == YES)
{
/* FIXME: Should this be @"NSGeneralPboardType" or @"NSSoundPboardType"?
Apple also defines "NSString *NSSoundPboardType". */
NSData *d = [pasteboard dataForType: @"NSGeneralPboardType"];
return [self initWithData: d];
}
return nil;
}
//
// Playing and Information
//
- (BOOL) pause
{
// Do nothing if sound is already paused.
if ([_readLock condition] == SOUND_SHOULD_PAUSE)
{
return NO;
}
if ([_readLock tryLock] == NO)
{
return NO;
}
[_readLock unlockWithCondition: SOUND_SHOULD_PAUSE];
return YES;
}
- (BOOL) play
{
// If the locks exists this instance is already playing
if (_readLock != nil && _playbackLock != nil)
{
return NO;
}
_readLock = [[NSConditionLock alloc] initWithCondition: SOUND_SHOULD_PAUSE];
_playbackLock = [[NSLock alloc] init];
if ([_readLock tryLock] != YES)
{
return NO;
}
_shouldStop = NO;
[NSThread detachNewThreadSelector: @selector(_stream)
toTarget: self
withObject: nil];
[_readLock unlockWithCondition: SOUND_SHOULD_PLAY];
return YES;
}
- (BOOL) resume
{
// Do nothing if sound is already playing.
if ([_readLock condition] == SOUND_SHOULD_PLAY)
{
return NO;
}
if ([_readLock tryLock] == NO)
{
return NO;
}
[_readLock unlockWithCondition: SOUND_SHOULD_PLAY];
return YES;
}
- (BOOL) stop
{
if (_readLock == nil)
{
return NO;
}
if ([_readLock tryLock] != YES)
{
return NO;
}
_shouldStop = YES;
// Set to SOUND_SHOULD_PLAY so that thread isn't blocked.
[_readLock unlockWithCondition: SOUND_SHOULD_PLAY];
return YES;
}
- (BOOL) isPlaying
{
if (_readLock == nil)
{
return NO;
}
if ([_readLock condition] == SOUND_SHOULD_PLAY)
{
return YES;
}
return NO;
}
- (float) volume
{
return [_sink volume];
}
- (void) setVolume: (float) volume
{
[_playbackLock lock];
[_sink setVolume: volume];
[_playbackLock unlock];
}
- (NSTimeInterval) currentTime
{
return [_source currentTime];
}
- (void) setCurrentTime: (NSTimeInterval) currentTime
{
[_readLock lock];
[_source setCurrentTime: currentTime];
[_readLock unlock];
}
- (BOOL) loops
{
return _shouldLoop;
}
- (void) setLoops: (BOOL) loops
{
_shouldLoop = loops;
}
- (NSTimeInterval) duration
{
return [_source duration];
}
//
// Working with pasteboards
//
+ (BOOL) canInitWithPasteboard: (NSPasteboard *)pasteboard
{
NSArray *pbTypes = [pasteboard types];
NSArray *myTypes = [NSSound soundUnfilteredPasteboardTypes];
return ([pbTypes firstObjectCommonWithArray: myTypes] != nil);
}
+ (NSArray *) soundUnfilteredPasteboardTypes
{
return [NSArray arrayWithObjects: @"NSGeneralPboardType", nil];
}
- (void) writeToPasteboard: (NSPasteboard *)pasteboard
{
NSData *d = [NSArchiver archivedDataWithRootObject: self];
if (d != nil) {
[pasteboard declareTypes: [NSSound soundUnfilteredPasteboardTypes]
owner: nil];
[pasteboard setData: d forType: @"NSGeneralPboardType"];
}
}
//
// Working with delegates
//
- (id) delegate
{
return _delegate;
}
- (void) setDelegate: (id)aDelegate
{
_delegate = aDelegate;
}
//
// Naming Sounds
//
+ (id) soundNamed: (NSString*)name
{
NSString *realName = [nsmapping objectForKey: name];
NSSound *sound;
if (realName)
{
name = realName;
}
sound = (NSSound *)[nameDict objectForKey: name];
if (sound == nil)
{
NSString *extension;
NSString *path = nil;
NSBundle *main_bundle;
NSArray *array;
NSString *the_name = name;
// FIXME: This should use [NSBundle pathForSoundResource], but this will
// only allow soundUnfilteredFileTypes.
/* If there is no sound with that name, search in the main bundle */
main_bundle = [NSBundle mainBundle];
extension = [name pathExtension];
if (extension != nil && [extension length] == 0)
{
extension = nil;
}
/* Check if extension is one of the sound types */
array = [NSSound soundUnfilteredFileTypes];
if ([array indexOfObject: extension] != NSNotFound)
{
/* Extension is one of the sound types
So remove from the name */
the_name = [name stringByDeletingPathExtension];
}
else
{
/* Otherwise extension is not an sound type
So leave it alone */
the_name = name;
extension = nil;
}
/* First search locally */
if (extension)
{
path = [main_bundle pathForResource: the_name ofType: extension];
}
else
{
id o, e;
e = [array objectEnumerator];
while ((o = [e nextObject]))
{
path = [main_bundle pathForResource: the_name ofType: o];
if (path != nil && [path length] != 0)
{
break;
}
}
}
/* If not found then search in system */
if (!path)
{
if (extension)
{
path = [NSBundle pathForLibraryResource: the_name
ofType: extension
inDirectory: @"Sounds"];
}
else
{
id o, e;
e = [array objectEnumerator];
while ((o = [e nextObject])) {
path = [NSBundle pathForLibraryResource: the_name
ofType: o
inDirectory: @"Sounds"];
if (path != nil && [path length] != 0)
{
break;
}
}
}
}
if ([path length] != 0)
{
sound = [[self allocWithZone: NSDefaultMallocZone()]
initWithContentsOfFile: path byReference: NO];
if (sound != nil)
{
[sound setName: name];
RELEASE(sound);
sound->_onlyReference = YES;
}
return sound;
}
}
return sound;
}
+ (NSArray *) soundUnfilteredFileTypes
{
Class sourceClass;
NSMutableArray *array;
NSEnumerator *enumerator;
array = [NSMutableArray arrayWithCapacity: 10];
enumerator = [sourcePlugIns objectEnumerator];
while ((sourceClass = [enumerator nextObject]) != nil)
{
[array addObjectsFromArray: [sourceClass soundUnfilteredFileTypes]];
}
return array;
}
+ (NSArray *) soundUnfilteredTypes
{
Class sourceClass;
NSMutableArray *array;
NSEnumerator *enumerator;
array = [NSMutableArray arrayWithCapacity: 10];
enumerator = [sourcePlugIns objectEnumerator];
while ((sourceClass = [enumerator nextObject]) != nil)
{
[array addObjectsFromArray: [sourceClass soundUnfilteredTypes]];
}
return array;
}
- (NSString *) name
{
return _name;
}
- (BOOL) setName: (NSString *)aName
{
if (!aName || [nameDict objectForKey: aName])
{
return NO;
}
if ((_name != nil) && self == [nameDict objectForKey: _name])
{
[nameDict removeObjectForKey: _name];
}
ASSIGN(_name, aName);
[nameDict setObject: self forKey: _name];
return YES;
}
- (NSString *) playbackDeviceIdentifier
{
return [_sink playbackDeviceIdentifier];
}
- (void) setPlaybackDeviceIdentifier: (NSString *)playbackDeviceIdentifier
{
if ([[_sink class] canInitWithPlaybackDevice: playbackDeviceIdentifier])
{
[_playbackLock lock];
[_sink setPlaybackDeviceIdentifier: playbackDeviceIdentifier];
[_playbackLock unlock];
}
}
- (NSArray *) channelMapping
{
return [_sink channelMapping];
}
- (void) setChannelMapping: (NSArray *)channelMapping
{
[_playbackLock lock];
[_sink setChannelMapping: channelMapping];
[_playbackLock unlock];
}
//
// NSCoding
//
- (void) encodeWithCoder: (NSCoder *)coder
{
if ([coder allowsKeyedCoding])
{
// TODO_NIB: Determine keys for NSSound.
}
else
{
[coder encodeValueOfObjCType: @encode(BOOL) at: &_onlyReference];
[coder encodeObject: _name];
if (_onlyReference == YES)
{
return;
}
[coder encodeConditionalObject: _delegate];
[coder encodeObject: _data];
[coder encodeObject: _playbackDeviceIdentifier];
[coder encodeObject: _channelMapping];
}
}
- (id) initWithCoder: (NSCoder*)decoder
{
if ([decoder allowsKeyedCoding])
{
// TODO_NIB: Determine keys for NSSound.
}
else
{
[decoder decodeValueOfObjCType: @encode(BOOL) at: &_onlyReference];
if (_onlyReference == YES)
{
NSString *theName = [decoder decodeObject];
RELEASE (self);
self = RETAIN ([NSSound soundNamed: theName]);
[self setName: theName];
}
else
{
_name = RETAIN ([decoder decodeObject]);
[self setDelegate: [decoder decodeObject]];
_data = RETAIN([decoder decodeObject]);
_playbackDeviceIdentifier = RETAIN([decoder decodeObject]);
_channelMapping = RETAIN([decoder decodeObject]);
}
/* FIXME: Need to prepare the object for playback before going further. */
}
return self;
}
//
// NSCopying
//
- (id) copyWithZone: (NSZone *)zone
{
NSSound *newSound = (NSSound *)NSCopyObject(self, 0, zone);
/* FIXME: Is all this correct? And is this all that needs to be copied? */
newSound->_name = [_name copyWithZone: zone];
newSound->_data = [_data copyWithZone: zone];
newSound->_playbackDeviceIdentifier = [_playbackDeviceIdentifier
copyWithZone: zone];
newSound->_channelMapping = [_channelMapping copyWithZone: zone];
/* FIXME: Need to prepare the object for playback before going further. */
return newSound;
}
@end