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