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 <Adium/AIPreferenceControllerProtocol.h>
20 #import <AIUtilities/AIDictionaryAdditions.h>
21 #import <AIUtilities/AISleepNotification.h>
22 #import <QTKit/QTKit.h>
24 #define SOUND_DEFAULT_PREFS @"SoundPrefs"
25 #define MAX_CACHED_SOUNDS 4 //Max cached sounds
27 @interface AdiumSound (PRIVATE)
28 - (void)_stopAndReleaseAllSounds;
29 - (void)_setVolumeOfAllSoundsTo:(float)inVolume;
30 - (void)cachedPlaySound:(NSString *)inPath;
31 - (void)_uncacheLeastRecentlyUsedSound;
32 - (QTAudioContextRef)createAudioContextWithSystemOutputDevice;
33 - (NSArray *)allSounds;
36 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon);
38 @implementation AdiumSound
45 if ((self = [super init])) {
46 soundCacheDict = [[NSMutableDictionary alloc] init];
47 soundCacheArray = [[NSMutableArray alloc] init];
48 soundCacheCleanupTimer = nil;
51 //Observe workspace activity changes so we can mute sounds as necessary
52 NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
54 [workspaceCenter addObserver:self
55 selector:@selector(workspaceSessionDidBecomeActive:)
56 name:NSWorkspaceSessionDidBecomeActiveNotification
59 [workspaceCenter addObserver:self
60 selector:@selector(workspaceSessionDidResignActive:)
61 name:NSWorkspaceSessionDidResignActiveNotification
64 //Monitor system sleep so we can stop sounds before sleeping; otherwise, we may crash while waking
65 [[NSNotificationCenter defaultCenter] addObserver:self
66 selector:@selector(systemWillSleep:)
67 name:AISystemWillSleep_Notification
70 //Sign up for notification when the user changes the system output device in the Sound pane of System Preferences.
71 OSStatus err = AudioHardwareAddPropertyListener(kAudioHardwarePropertyDefaultSystemOutputDevice, systemOutputDeviceDidChange, /*refcon*/ self);
72 NSAssert2(err == noErr, @"%s: Couldn't sign up for system-output-device-changed notification, because AudioHardwareAddPropertyListener returned %i", __PRETTY_FUNCTION__, err);
78 - (void)controllerDidLoad
80 //Register our default preferences and observe changes
81 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:SOUND_DEFAULT_PREFS forClass:[self class]]
82 forGroup:PREF_GROUP_SOUNDS];
83 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_SOUNDS];
88 [[adium preferenceController] unregisterPreferenceObserver:self];
89 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
90 [[NSNotificationCenter defaultCenter] removeObserver:self];
92 [self _stopAndReleaseAllSounds];
94 [soundCacheDict release]; soundCacheDict = nil;
95 [soundCacheArray release]; soundCacheArray = nil;
96 [soundCacheCleanupTimer invalidate]; [soundCacheCleanupTimer release]; soundCacheCleanupTimer = nil;
101 - (void)playSoundAtPath:(NSString *)inPath
103 if (inPath && customVolume != 0.0 && !soundsAreMuted) {
104 [self cachedPlaySound:inPath];
108 - (void)stopPlayingSoundAtPath:(NSString *)inPath
110 QTMovie *movie = [soundCacheDict objectForKey:inPath];
117 * @brief Preferences changed, adjust to the new values
119 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
120 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
122 float newVolume = [[prefDict objectForKey:KEY_SOUND_CUSTOM_VOLUME_LEVEL] floatValue];
124 //If sound volume has changed, we must update all existing sounds to the new volume
125 if (customVolume != newVolume) {
126 [self _setVolumeOfAllSoundsTo:newVolume];
129 //Load the new preferences
130 customVolume = newVolume;
134 * @brief Stop and release all cached sounds
136 - (void)_stopAndReleaseAllSounds
138 [[soundCacheDict allValues] makeObjectsPerformSelector:@selector(stop)];
139 [soundCacheDict removeAllObjects];
140 [soundCacheArray removeAllObjects];
144 * @brief Update the volume of all cached sounds
146 - (void)_setVolumeOfAllSoundsTo:(float)inVolume
148 NSEnumerator *enumerator = [soundCacheDict objectEnumerator];
151 while((movie = [enumerator nextObject])){
152 [movie setVolume:inVolume];
157 * @brief Play a QTMovie, possibly cached
159 * @param inPath path to the sound file
161 - (void)cachedPlaySound:(NSString *)inPath
163 QTMovie *movie = [soundCacheDict objectForKey:inPath];
165 //Load the sound if necessary
167 //If the cache is full, remove the least recently used cached sound
168 if ([soundCacheDict count] >= MAX_CACHED_SOUNDS) {
169 [self _uncacheLeastRecentlyUsedSound];
172 //Load and cache the sound
173 NSError *error = nil;
174 movie = [[QTMovie alloc] initWithFile:inPath
177 //Insert the player at the front of our cache
178 [soundCacheArray insertObject:inPath atIndex:0];
179 [soundCacheDict setObject:movie forKey:inPath];
182 //Set the volume (otherwise #2283 happens)
183 [movie setVolume:customVolume];
185 //Create an audio context for the system output audio device.
186 //We'd reuse one context for all the movies, but that doesn't work; movies can't share a context, apparently. (You get paramErr when you try to give the second movie a context already in use by the first.)
187 QTAudioContextRef newAudioContext = [self createAudioContextWithSystemOutputDevice];
189 OSStatus err = SetMovieAudioContext([movie quickTimeMovie], newAudioContext);
190 NSAssert4(err == noErr, @"%s: Could not set audio context of movie %@ to %p: SetMovieAudioContext returned error %i", __PRETTY_FUNCTION__, movie, newAudioContext, err);
192 //We created it, so we must release it.
193 QTAudioContextRelease(newAudioContext);
197 //Move this sound to the front of the cache (This will naturally move lesser used sounds to the back for removal)
198 [soundCacheArray removeObject:inPath];
199 [soundCacheArray insertObject:inPath atIndex:0];
204 //Ensure the sound is starting from the beginning; necessary for cached sounds that have already been played
205 QTTime startOfMovie = {
207 .timeScale = [[movie attributeForKey:QTMovieTimeScaleAttribute] longValue],
210 [movie setCurrentTime:startOfMovie];
212 //This only has an effect if the movie is not already playing. It won't stop it, and it won't start it over (the latter is what setCurrentTime: is for).
218 * @brief Remove the least recently used sound from the cache
220 - (void)_uncacheLeastRecentlyUsedSound
222 NSString *lastCachedPath = [soundCacheArray lastObject];
223 QTMovie *movie = [soundCacheDict objectForKey:lastCachedPath];
225 //If a movie is stopped, then its rate is zero. Thus, this tests whether the movie is playing. We remove it from the cache only if it is not playing.
226 if ([movie rate] == 0.0) {
227 [soundCacheDict removeObjectForKey:lastCachedPath];
228 [soundCacheArray removeLastObject];
232 - (QTAudioContextRef) createAudioContextWithSystemOutputDevice
234 QTAudioContextRef newAudioContext = NULL;
238 //First, obtain the device itself.
239 AudioDeviceID systemOutputDevice = 0;
240 dataSize = sizeof(systemOutputDevice);
241 err = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultSystemOutputDevice, &dataSize, &systemOutputDevice);
242 NSAssert2(err == noErr, @"%s: Could not get the system output device: AudioHardwareGetProperty returned error %i", __PRETTY_FUNCTION__, err);
244 //Now get its UID. We'll need to release this.
245 CFStringRef deviceUID = NULL;
246 dataSize = sizeof(deviceUID);
247 err = AudioDeviceGetProperty(systemOutputDevice, /*channel*/ 0, /*isInput*/ false, kAudioDevicePropertyDeviceUID, &dataSize, &deviceUID);
248 NSAssert3(err == noErr, @"%s: Could not get the device UID for device %p: AudioDeviceGetProperty returned error %i", __PRETTY_FUNCTION__, systemOutputDevice, err);
249 [(NSObject *)deviceUID autorelease];
251 //Create an audio context for this device so that our movies can play into it.
252 err = QTAudioContextCreateForAudioDevice(kCFAllocatorDefault, deviceUID, /*options*/ NULL, &newAudioContext);
253 NSAssert3(err == noErr, @"%s: QTAudioContextCreateForAudioDevice with device UID %@ returned error %i", __PRETTY_FUNCTION__, deviceUID, err);
255 return newAudioContext;
258 - (NSArray *)allSounds
260 return [soundCacheDict allValues];
264 * @brief Workspace activated (Computer switched to our user)
266 - (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
268 [self setSoundsAreMuted:NO];
272 * @brief Workspace resigned (Computer switched to another user)
274 - (void)workspaceSessionDidResignActive:(NSNotification *)notification
276 [self setSoundsAreMuted:YES];
279 - (void)systemWillSleep:(NSNotification *)notification
281 [self _stopAndReleaseAllSounds];
284 - (void)setSoundsAreMuted:(BOOL)mute
286 AILog(@"setSoundsAreMuted: %i",mute);
287 if (soundsAreMuted > 0 && !mute)
292 if (soundsAreMuted == 1)
293 [self _stopAndReleaseAllSounds];
298 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon)
300 #pragma unused(property)
301 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
303 AdiumSound *self = (id)refcon;
304 NSCAssert1(self, @"AudioHardware property listener function %s called with nil refcon, which we expected to be the AdiumSound instance", __PRETTY_FUNCTION__);
306 NSEnumerator *soundsEnum = [[self allSounds] objectEnumerator];
308 while ((movie = [soundsEnum nextObject])) {
309 //QTMovie gets confused if we're playing when we do this, so pause momentarily.
310 float savedRate = [movie rate];
313 //Exchange the audio context for a new one with the new device.
314 QTAudioContextRef newAudioContext = [self createAudioContextWithSystemOutputDevice];
316 OSStatus err = SetMovieAudioContext([movie quickTimeMovie], newAudioContext);
317 NSCAssert4(err == noErr, @"%s: Could not set audio context of movie %@ to %p: SetMovieAudioContext returned error %i", __PRETTY_FUNCTION__, movie, newAudioContext, err);
319 //We created it, so we must release it.
320 QTAudioContextRelease(newAudioContext);
322 //Resume playback, now on the new device.
323 [movie setRate:savedRate];