Source/AdiumSound.m
author Evan Schoenberg
Mon Jul 27 15:41:10 2009 -0500 (2009-07-27)
changeset 2544 4703302ec97b
parent 2165 8b455a7d180d
child 2549 0e3b32a50a26
permissions -rw-r--r--
Backed out changeset 8b455a7d180d - revert NSSound caching. Refs #12098
David@0
     1
/* 
David@0
     2
 * Adium is the legal property of its developers, whose names are listed in the copyright file included
David@0
     3
 * with this source distribution.
David@0
     4
 * 
David@0
     5
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
David@0
     6
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
David@0
     7
 * or (at your option) any later version.
David@0
     8
 * 
David@0
     9
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
David@0
    10
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
David@0
    11
 * Public License for more details.
David@0
    12
 * 
David@0
    13
 * You should have received a copy of the GNU General Public License along with this program; if not,
David@0
    14
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
David@0
    15
 */
David@0
    16
David@0
    17
#import "AdiumSound.h"
David@0
    18
#import "AISoundController.h"
David@0
    19
#import <AIUtilities/AIDictionaryAdditions.h>
David@0
    20
#import <AIUtilities/AISleepNotification.h>
catfish@2027
    21
#import <CoreAudio/AudioHardware.h>
Evan@2544
    22
#import <CoreServices/CoreServices.h>
David@0
    23
#import <sys/sysctl.h>
David@0
    24
David@0
    25
#define SOUND_DEFAULT_PREFS				@"SoundPrefs"
Evan@2544
    26
#define MAX_CACHED_SOUNDS				4			//Max cached sounds
David@0
    27
David@84
    28
@interface AdiumSound ()
Evan@2544
    29
- (void)_stopAndReleaseAllSounds;
Evan@2544
    30
- (void)_setVolumeOfAllSoundsTo:(CGFloat)inVolume;
Evan@2544
    31
- (void)cachedPlaySound:(NSString *)inPath;
Evan@2544
    32
- (void)_uncacheLeastRecentlyUsedSound;
Evan@2544
    33
- (NSString *)systemAudioDeviceID;
Evan@2544
    34
- (void)configureAudioContextForSound:(NSSound *)sound;
Evan@2544
    35
- (NSArray *)allSounds;
David@0
    36
@end
David@0
    37
David@0
    38
@interface NSProcessInfo (AIProcessorInfoAdditions)
David@0
    39
- (BOOL)processorFamilyIsG5;
David@0
    40
@end
David@0
    41
David@0
    42
static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon);
David@0
    43
David@0
    44
@implementation AdiumSound
David@0
    45
David@0
    46
/*!
David@0
    47
 * @brief Init
David@0
    48
 */
David@0
    49
- (id)init
David@0
    50
{
David@0
    51
	if ((self = [super init])) {
Evan@2544
    52
		soundCacheDict = [[NSMutableDictionary alloc] init];
Evan@2544
    53
		soundCacheArray = [[NSMutableArray alloc] init];
Evan@2544
    54
		soundCacheCleanupTimer = nil;
David@0
    55
		soundsAreMuted = NO;
David@0
    56
David@0
    57
		//Observe workspace activity changes so we can mute sounds as necessary
David@0
    58
		NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
David@0
    59
David@0
    60
		[workspaceCenter addObserver:self
David@0
    61
							selector:@selector(workspaceSessionDidBecomeActive:)
David@0
    62
								name:NSWorkspaceSessionDidBecomeActiveNotification
David@0
    63
							  object:nil];
David@0
    64
David@0
    65
		[workspaceCenter addObserver:self
David@0
    66
							selector:@selector(workspaceSessionDidResignActive:)
David@0
    67
								name:NSWorkspaceSessionDidResignActiveNotification
David@0
    68
							  object:nil];
David@0
    69
David@0
    70
		//Monitor system sleep so we can stop sounds before sleeping; otherwise, we may crash while waking
David@0
    71
		[[NSNotificationCenter defaultCenter] addObserver:self
David@0
    72
												 selector:@selector(systemWillSleep:)
David@0
    73
													 name:AISystemWillSleep_Notification
David@0
    74
												   object:nil];
David@0
    75
		
David@0
    76
		/* Sign up for notification when the user changes the system output device in the Sound pane of System Preferences.
David@0
    77
		 *
David@0
    78
		 * However, we avoid doing this on G5 machines. G5s spew a continuous stream of
David@0
    79
		 * kAudioHardwarePropertyDefaultSystemOutputDevice notifications without the device actually changing;
David@0
    80
		 * rather than stutter our audio and eat CPU continuously, we just won't try to update.
David@0
    81
		 */
David@0
    82
		if (![[NSProcessInfo processInfo] processorFamilyIsG5]) {
David@0
    83
			OSStatus err = AudioHardwareAddPropertyListener(kAudioHardwarePropertyDefaultSystemOutputDevice, systemOutputDeviceDidChange, /*refcon*/ self);
David@0
    84
			if (err != noErr)
David@0
    85
				NSLog(@"%s: Couldn't sign up for system-output-device-changed notification, because AudioHardwareAddPropertyListener returned %i. Adium will not know when the default system audio device changes.", __PRETTY_FUNCTION__, err);			
David@0
    86
		} else {
David@0
    87
			//We won't be updating automatically, so reconfigure before a sound is played again
David@0
    88
			reconfigureAudioContextBeforeEachPlay = YES;
David@0
    89
		}
David@0
    90
	}
David@0
    91
David@0
    92
	return self;
David@0
    93
}
David@0
    94
David@0
    95
- (void)controllerDidLoad
David@0
    96
{
David@0
    97
	//Register our default preferences and observe changes
David@95
    98
	[adium.preferenceController registerDefaults:[NSDictionary dictionaryNamed:SOUND_DEFAULT_PREFS forClass:[self class]]
David@0
    99
										  forGroup:PREF_GROUP_SOUNDS];
David@95
   100
	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_SOUNDS];
David@0
   101
}
David@0
   102
David@0
   103
- (void)dealloc
David@0
   104
{
David@95
   105
	[adium.preferenceController unregisterPreferenceObserver:self];
David@0
   106
	[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
David@0
   107
	[[NSNotificationCenter defaultCenter] removeObserver:self];
David@0
   108
Evan@2544
   109
	[self _stopAndReleaseAllSounds];
Evan@2544
   110
Evan@2544
   111
	[soundCacheDict release]; soundCacheDict = nil;
Evan@2544
   112
	[soundCacheArray release]; soundCacheArray = nil;
Evan@2544
   113
	[soundCacheCleanupTimer invalidate]; [soundCacheCleanupTimer release]; soundCacheCleanupTimer = nil;
David@0
   114
David@0
   115
	[super dealloc];
David@0
   116
}
David@0
   117
Evan@2544
   118
- (void)playSoundAtPath:(NSString *)inPath
Evan@2544
   119
{
Evan@2544
   120
	if (inPath && customVolume != 0.0 && !soundsAreMuted) {
Evan@2544
   121
		[self cachedPlaySound:inPath];
Evan@2544
   122
	}
Evan@2544
   123
}
Evan@2544
   124
Evan@2544
   125
- (void)stopPlayingSoundAtPath:(NSString *)inPath
Evan@2544
   126
{
Evan@2544
   127
	NSSound *sound = [soundCacheDict objectForKey:inPath];
Evan@2544
   128
	if (sound) {
Evan@2544
   129
		[sound stop];
Evan@2544
   130
	}
Evan@2544
   131
}
Evan@2544
   132
David@0
   133
/*!
David@0
   134
 * @brief Preferences changed, adjust to the new values
David@0
   135
 */
Evan@2544
   136
- (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
Evan@2544
   137
							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
David@0
   138
{
David@3
   139
	CGFloat newVolume = [[prefDict objectForKey:KEY_SOUND_CUSTOM_VOLUME_LEVEL] doubleValue];
David@0
   140
Evan@2544
   141
	//If sound volume has changed, we must update all existing sounds to the new volume
Evan@2544
   142
	if (customVolume != newVolume) {
Evan@2544
   143
		[self _setVolumeOfAllSoundsTo:newVolume];
David@0
   144
	}
David@0
   145
David@0
   146
	//Load the new preferences
David@0
   147
	customVolume = newVolume;
David@0
   148
}
David@0
   149
David@0
   150
/*!
David@0
   151
 * @brief Stop and release all cached sounds
David@0
   152
 */
Evan@2544
   153
- (void)_stopAndReleaseAllSounds
David@0
   154
{
Evan@2544
   155
	[[soundCacheDict allValues] makeObjectsPerformSelector:@selector(stop)];
Evan@2544
   156
	[soundCacheDict removeAllObjects];
Evan@2544
   157
	[soundCacheArray removeAllObjects];
Evan@2544
   158
}
Evan@2544
   159
Evan@2544
   160
/*!
Evan@2544
   161
 * @brief Update the volume of all cached sounds
Evan@2544
   162
 */
Evan@2544
   163
- (void)_setVolumeOfAllSoundsTo:(CGFloat)inVolume
Evan@2544
   164
{
Evan@2544
   165
	for (NSSound *sound in [soundCacheDict objectEnumerator]) {
Evan@2544
   166
		[sound setVolume:inVolume];
Evan@2544
   167
	}
David@0
   168
}
David@0
   169
David@0
   170
/*!
catfish@2027
   171
 * @brief Play an NSSound, possibly cached
David@0
   172
 * 
Evan@2544
   173
 * @param inPath path to the sound file
David@0
   174
 */
Evan@2544
   175
- (void)cachedPlaySound:(NSString *)inPath
David@0
   176
{
Evan@2544
   177
	NSSound *sound = [soundCacheDict objectForKey:inPath];
David@0
   178
Evan@2544
   179
	//Load the sound if necessary
Evan@2544
   180
	if (!sound) {
Evan@2544
   181
		//If the cache is full, remove the least recently used cached sound
Evan@2544
   182
		if ([soundCacheDict count] >= MAX_CACHED_SOUNDS) {
Evan@2544
   183
			[self _uncacheLeastRecentlyUsedSound];
Evan@2544
   184
		}
Evan@2544
   185
Evan@2544
   186
		//Load and cache the sound
Evan@2544
   187
		NSError *error = nil;
Evan@2544
   188
		sound = [[NSSound alloc] initWithContentsOfFile:inPath byReference:NO];
Evan@2544
   189
		if (sound) {
Evan@2544
   190
			//Insert the player at the front of our cache
Evan@2544
   191
			[soundCacheArray insertObject:inPath atIndex:0];
Evan@2544
   192
			[soundCacheDict setObject:sound forKey:inPath];
Evan@2544
   193
			[sound release];
Evan@2544
   194
Evan@2544
   195
			//Set the volume (otherwise #2283 happens)
Evan@2544
   196
			[sound setVolume:customVolume];
Evan@2544
   197
Evan@2544
   198
			[self configureAudioContextForSound:sound];
Evan@2544
   199
		} else {
Evan@2544
   200
			AILogWithSignature(@"Error loading %@: %@", inPath, error);
Evan@2544
   201
		}
Evan@2544
   202
Evan@2544
   203
	} else {
Evan@2544
   204
		//Move this sound to the front of the cache (This will naturally move lesser used sounds to the back for removal)
Evan@2544
   205
		[soundCacheArray removeObject:inPath];
Evan@2544
   206
		[soundCacheArray insertObject:inPath atIndex:0];
Evan@2544
   207
		
Evan@2544
   208
		if (reconfigureAudioContextBeforeEachPlay) {
Evan@2544
   209
			[sound stop];
Evan@2544
   210
			[self configureAudioContextForSound:sound];
Evan@2544
   211
		}
Evan@2544
   212
	}
David@0
   213
catfish@2027
   214
	//Engage!
catfish@2027
   215
	if (sound) {
catfish@2027
   216
		[sound setCurrentTime:0.0];
Evan@2544
   217
Evan@2544
   218
		//This only has an effect if the sound is not already playing. It won't stop it, and it won't start it over (the latter is what setCurrentTime: is for).
catfish@2027
   219
		[sound play];
catfish@2027
   220
	}
David@0
   221
}
David@0
   222
Evan@2544
   223
/*!
Evan@2544
   224
 * @brief Remove the least recently used sound from the cache
Evan@2544
   225
 */
Evan@2544
   226
- (void)_uncacheLeastRecentlyUsedSound
David@0
   227
{
Evan@2544
   228
	NSString			*lastCachedPath = [soundCacheArray lastObject];
Evan@2544
   229
	NSSound *sound = [soundCacheDict objectForKey:lastCachedPath];
Evan@2544
   230
Evan@2544
   231
	//Remove it from the cache only if it is not playing.
Evan@2544
   232
	if (![sound isPlaying]) {
Evan@2544
   233
		[soundCacheDict removeObjectForKey:lastCachedPath];
Evan@2544
   234
		[soundCacheArray removeLastObject];
Evan@2544
   235
	}
David@0
   236
}
David@0
   237
catfish@2027
   238
- (NSString *)systemAudioDeviceID
David@0
   239
{
Evan@2544
   240
	OSStatus err;
Evan@2544
   241
	UInt32 dataSize;
Evan@2544
   242
Evan@2544
   243
	//First, obtain the device itself.
Evan@2544
   244
	AudioDeviceID systemOutputDevice = 0;
Evan@2544
   245
	dataSize = sizeof(systemOutputDevice);
Evan@2544
   246
	err = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultSystemOutputDevice, &dataSize, &systemOutputDevice);
Evan@2544
   247
	if (err != noErr) {
Evan@2544
   248
		NSLog(@"%s: Could not get the system output device: AudioHardwareGetProperty returned error %i", __PRETTY_FUNCTION__, err);
Evan@2544
   249
		return NULL;
David@0
   250
	}
Evan@2544
   251
Evan@2544
   252
	//Now get its UID. We'll need to release this.
Evan@2544
   253
	CFStringRef deviceUID = NULL;
Evan@2544
   254
	dataSize = sizeof(deviceUID);
Evan@2544
   255
	err = AudioDeviceGetProperty(systemOutputDevice, /*channel*/ 0, /*isInput*/ false, kAudioDevicePropertyDeviceUID, &dataSize, &deviceUID);
Evan@2544
   256
	if (err != noErr) {
Evan@2544
   257
		NSLog(@"%s: Could not get the device UID for device %p: AudioDeviceGetProperty returned error %i", __PRETTY_FUNCTION__, systemOutputDevice, err);
Evan@2544
   258
		return NULL;
David@0
   259
	}
Evan@2544
   260
	[(NSString *)deviceUID autorelease];
catfish@2027
   261
	
Evan@2544
   262
	return (NSString *)deviceUID;
Evan@2544
   263
}
Evan@2544
   264
Evan@2544
   265
- (void)configureAudioContextForSound:(NSSound *)sound
Evan@2544
   266
{
Evan@2544
   267
	[sound pause];
David@0
   268
	
Evan@2544
   269
	//Exchange the audio context for a new one with the new device.
Evan@2544
   270
	NSString *deviceUID = [self systemAudioDeviceID];
Evan@2544
   271
	
Evan@2544
   272
	[sound setPlaybackDeviceIdentifier:deviceUID];
Evan@2544
   273
	
Evan@2544
   274
	//Resume playback, now on the new device.
Evan@2544
   275
	[sound resume];
Evan@2544
   276
}
Evan@2544
   277
Evan@2544
   278
- (NSArray *)allSounds
Evan@2544
   279
{
Evan@2544
   280
	return [soundCacheDict allValues];
David@0
   281
}
David@0
   282
David@0
   283
/*!
David@0
   284
 * @brief Workspace activated (Computer switched to our user)
David@0
   285
 */
David@0
   286
- (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
David@0
   287
{
David@0
   288
	[self setSoundsAreMuted:NO];
David@0
   289
}
David@0
   290
David@0
   291
/*!
David@0
   292
 * @brief Workspace resigned (Computer switched to another user)
David@0
   293
 */
David@0
   294
- (void)workspaceSessionDidResignActive:(NSNotification *)notification
David@0
   295
{
David@0
   296
	[self setSoundsAreMuted:YES];
David@0
   297
}
David@0
   298
David@0
   299
- (void)systemWillSleep:(NSNotification *)notification
David@0
   300
{
Evan@2544
   301
	[self _stopAndReleaseAllSounds];
David@0
   302
}
David@0
   303
David@0
   304
- (void)setSoundsAreMuted:(BOOL)mute
David@0
   305
{
David@0
   306
	AILog(@"setSoundsAreMuted: %i",mute);
David@0
   307
	if (soundsAreMuted > 0 && !mute)
David@0
   308
		soundsAreMuted--;
David@0
   309
	else if (mute)
David@0
   310
		soundsAreMuted++;
David@0
   311
David@0
   312
	if (soundsAreMuted == 1)
Evan@2544
   313
		[self _stopAndReleaseAllSounds];
David@0
   314
}
David@0
   315
David@0
   316
- (void)systemOutputDeviceDidChange
David@0
   317
{
Evan@2544
   318
	for (NSSound *sound in [self allSounds]) {
Evan@2544
   319
		[self configureAudioContextForSound:sound];
David@0
   320
	}
David@0
   321
}
David@0
   322
David@0
   323
@end
David@0
   324
David@0
   325
static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon)
David@0
   326
{
David@0
   327
#pragma unused(property)
David@0
   328
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
David@0
   329
David@0
   330
	AdiumSound *self = (id)refcon;
David@0
   331
	NSCAssert1(self, @"AudioHardware property listener function %s called with nil refcon, which we expected to be the AdiumSound instance", __PRETTY_FUNCTION__);
David@0
   332
David@0
   333
	[self performSelectorOnMainThread:@selector(systemOutputDeviceDidChange)
David@0
   334
						   withObject:nil
David@0
   335
						waitUntilDone:NO];
David@0
   336
	[pool release];
David@0
   337
David@0
   338
	return noErr;
David@0
   339
}
David@0
   340
David@0
   341
@implementation NSProcessInfo (AIProcessorInfoAdditions)
David@0
   342
David@0
   343
- (BOOL)processorFamilyIsG5
David@0
   344
{
David@0
   345
	/* Credit to http://www.cocoadev.com/index.pl?MacintoshModels */
David@0
   346
	BOOL	isG5 = NO;
David@0
   347
	char	buffer[128];
David@0
   348
	size_t	length = sizeof(buffer);
David@0
   349
	if (sysctlbyname("hw.model", &buffer, &length, NULL, 0) == 0) {
David@0
   350
		NSString	*hardwareModel = [NSString stringWithUTF8String:buffer];
David@0
   351
		NSArray		*knownG5Macs = [NSArray arrayWithObjects:@"PowerMac11,2" /* G5 PCIe */, @"PowerMac12,1" /* iMac G5 (iSight) */, 
David@0
   352
									@"PowerMac7,2" /* PowerMac G5 */, @"PowerMac7,3" /* PowerMac G5 */, @"PowerMac8,1" /* iMac G5 */,
David@0
   353
									@"PowerMac8,2" /* iMac G5 Ambient Light Sensor */, @"PowerMac9,1" /* Power Mac G5 (Late 2004) */,
David@0
   354
									@"RackMac3,1" /* Xserve G5 */, nil];
David@0
   355
David@0
   356
		if ([knownG5Macs containsObject:hardwareModel]) {
David@0
   357
			AILogWithSignature(@"On a G5 Mac.");
David@0
   358
			isG5 = YES;
David@0
   359
		}
David@0
   360
	}
David@0
   361
	
David@0
   362
	return isG5;
David@0
   363
}
David@0
   364
David@0
   365
@end