Backed out changeset 8b455a7d180d - revert NSSound caching. Refs #12098
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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>
25 #define SOUND_DEFAULT_PREFS @"SoundPrefs"
26 #define MAX_CACHED_SOUNDS 4 //Max cached sounds
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;
38 @interface NSProcessInfo (AIProcessorInfoAdditions)
39 - (BOOL)processorFamilyIsG5;
42 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon);
44 @implementation AdiumSound
51 if ((self = [super init])) {
52 soundCacheDict = [[NSMutableDictionary alloc] init];
53 soundCacheArray = [[NSMutableArray alloc] init];
54 soundCacheCleanupTimer = nil;
57 //Observe workspace activity changes so we can mute sounds as necessary
58 NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
60 [workspaceCenter addObserver:self
61 selector:@selector(workspaceSessionDidBecomeActive:)
62 name:NSWorkspaceSessionDidBecomeActiveNotification
65 [workspaceCenter addObserver:self
66 selector:@selector(workspaceSessionDidResignActive:)
67 name:NSWorkspaceSessionDidResignActiveNotification
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
76 /* Sign up for notification when the user changes the system output device in the Sound pane of System Preferences.
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.
82 if (![[NSProcessInfo processInfo] processorFamilyIsG5]) {
83 OSStatus err = AudioHardwareAddPropertyListener(kAudioHardwarePropertyDefaultSystemOutputDevice, systemOutputDeviceDidChange, /*refcon*/ self);
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);
87 //We won't be updating automatically, so reconfigure before a sound is played again
88 reconfigureAudioContextBeforeEachPlay = YES;
95 - (void)controllerDidLoad
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];
105 [adium.preferenceController unregisterPreferenceObserver:self];
106 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
107 [[NSNotificationCenter defaultCenter] removeObserver:self];
109 [self _stopAndReleaseAllSounds];
111 [soundCacheDict release]; soundCacheDict = nil;
112 [soundCacheArray release]; soundCacheArray = nil;
113 [soundCacheCleanupTimer invalidate]; [soundCacheCleanupTimer release]; soundCacheCleanupTimer = nil;
118 - (void)playSoundAtPath:(NSString *)inPath
120 if (inPath && customVolume != 0.0 && !soundsAreMuted) {
121 [self cachedPlaySound:inPath];
125 - (void)stopPlayingSoundAtPath:(NSString *)inPath
127 NSSound *sound = [soundCacheDict objectForKey:inPath];
134 * @brief Preferences changed, adjust to the new values
136 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
137 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
139 CGFloat newVolume = [[prefDict objectForKey:KEY_SOUND_CUSTOM_VOLUME_LEVEL] doubleValue];
141 //If sound volume has changed, we must update all existing sounds to the new volume
142 if (customVolume != newVolume) {
143 [self _setVolumeOfAllSoundsTo:newVolume];
146 //Load the new preferences
147 customVolume = newVolume;
151 * @brief Stop and release all cached sounds
153 - (void)_stopAndReleaseAllSounds
155 [[soundCacheDict allValues] makeObjectsPerformSelector:@selector(stop)];
156 [soundCacheDict removeAllObjects];
157 [soundCacheArray removeAllObjects];
161 * @brief Update the volume of all cached sounds
163 - (void)_setVolumeOfAllSoundsTo:(CGFloat)inVolume
165 for (NSSound *sound in [soundCacheDict objectEnumerator]) {
166 [sound setVolume:inVolume];
171 * @brief Play an NSSound, possibly cached
173 * @param inPath path to the sound file
175 - (void)cachedPlaySound:(NSString *)inPath
177 NSSound *sound = [soundCacheDict objectForKey:inPath];
179 //Load the sound if necessary
181 //If the cache is full, remove the least recently used cached sound
182 if ([soundCacheDict count] >= MAX_CACHED_SOUNDS) {
183 [self _uncacheLeastRecentlyUsedSound];
186 //Load and cache the sound
187 NSError *error = nil;
188 sound = [[NSSound alloc] initWithContentsOfFile:inPath byReference:NO];
190 //Insert the player at the front of our cache
191 [soundCacheArray insertObject:inPath atIndex:0];
192 [soundCacheDict setObject:sound forKey:inPath];
195 //Set the volume (otherwise #2283 happens)
196 [sound setVolume:customVolume];
198 [self configureAudioContextForSound:sound];
200 AILogWithSignature(@"Error loading %@: %@", inPath, error);
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];
208 if (reconfigureAudioContextBeforeEachPlay) {
210 [self configureAudioContextForSound:sound];
216 [sound setCurrentTime:0.0];
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).
224 * @brief Remove the least recently used sound from the cache
226 - (void)_uncacheLeastRecentlyUsedSound
228 NSString *lastCachedPath = [soundCacheArray lastObject];
229 NSSound *sound = [soundCacheDict objectForKey:lastCachedPath];
231 //Remove it from the cache only if it is not playing.
232 if (![sound isPlaying]) {
233 [soundCacheDict removeObjectForKey:lastCachedPath];
234 [soundCacheArray removeLastObject];
238 - (NSString *)systemAudioDeviceID
243 //First, obtain the device itself.
244 AudioDeviceID systemOutputDevice = 0;
245 dataSize = sizeof(systemOutputDevice);
246 err = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultSystemOutputDevice, &dataSize, &systemOutputDevice);
248 NSLog(@"%s: Could not get the system output device: AudioHardwareGetProperty returned error %i", __PRETTY_FUNCTION__, err);
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);
257 NSLog(@"%s: Could not get the device UID for device %p: AudioDeviceGetProperty returned error %i", __PRETTY_FUNCTION__, systemOutputDevice, err);
260 [(NSString *)deviceUID autorelease];
262 return (NSString *)deviceUID;
265 - (void)configureAudioContextForSound:(NSSound *)sound
269 //Exchange the audio context for a new one with the new device.
270 NSString *deviceUID = [self systemAudioDeviceID];
272 [sound setPlaybackDeviceIdentifier:deviceUID];
274 //Resume playback, now on the new device.
278 - (NSArray *)allSounds
280 return [soundCacheDict allValues];
284 * @brief Workspace activated (Computer switched to our user)
286 - (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
288 [self setSoundsAreMuted:NO];
292 * @brief Workspace resigned (Computer switched to another user)
294 - (void)workspaceSessionDidResignActive:(NSNotification *)notification
296 [self setSoundsAreMuted:YES];
299 - (void)systemWillSleep:(NSNotification *)notification
301 [self _stopAndReleaseAllSounds];
304 - (void)setSoundsAreMuted:(BOOL)mute
306 AILog(@"setSoundsAreMuted: %i",mute);
307 if (soundsAreMuted > 0 && !mute)
312 if (soundsAreMuted == 1)
313 [self _stopAndReleaseAllSounds];
316 - (void)systemOutputDeviceDidChange
318 for (NSSound *sound in [self allSounds]) {
319 [self configureAudioContextForSound:sound];
325 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon)
327 #pragma unused(property)
328 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
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__);
333 [self performSelectorOnMainThread:@selector(systemOutputDeviceDidChange)
341 @implementation NSProcessInfo (AIProcessorInfoAdditions)
343 - (BOOL)processorFamilyIsG5
345 /* Credit to http://www.cocoadev.com/index.pl?MacintoshModels */
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];
356 if ([knownG5Macs containsObject:hardwareModel]) {
357 AILogWithSignature(@"On a G5 Mac.");