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 "AISoundController.h"
18 #import <Adium/AIPreferenceControllerProtocol.h>
19 #import "AdiumSound.h"
20 #import <AIUtilities/AIDictionaryAdditions.h>
21 #import <QTKit/QTKit.h>
23 #define SOUND_DEFAULT_PREFS @"SoundPrefs"
24 #define MAX_CACHED_SOUNDS 4 //Max cached sounds
26 @interface AdiumSound (PRIVATE)
27 - (void)_stopAndReleaseAllSounds;
28 - (void)_setVolumeOfAllSoundsTo:(float)inVolume;
29 - (void)cachedPlaySound:(NSString *)inPath;
30 - (void)_uncacheLeastRecentlyUsedSound;
31 - (QTAudioContextRef)createAudioContextWithSystemOutputDevice;
32 - (NSArray *)allSounds;
35 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon);
37 @implementation AdiumSound
44 if ((self = [super init])) {
45 soundCacheDict = [[NSMutableDictionary alloc] init];
46 soundCacheArray = [[NSMutableArray alloc] init];
47 soundCacheCleanupTimer = nil;
50 //Observe workspace activity changes so we can mute sounds as necessary
51 NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
53 [workspaceCenter addObserver:self
54 selector:@selector(workspaceSessionDidBecomeActive:)
55 name:NSWorkspaceSessionDidBecomeActiveNotification
58 [workspaceCenter addObserver:self
59 selector:@selector(workspaceSessionDidResignActive:)
60 name:NSWorkspaceSessionDidResignActiveNotification
63 //Sign up for notification when the user changes the system output device in the Sound pane of System Preferences.
64 OSStatus err = AudioHardwareAddPropertyListener(kAudioHardwarePropertyDefaultSystemOutputDevice, systemOutputDeviceDidChange, /*refcon*/ self);
65 NSAssert2(err == noErr, @"%s: Couldn't sign up for system-output-device-changed notification, because AudioHardwareAddPropertyListener returned %i", __PRETTY_FUNCTION__, err);
71 - (void)controllerDidLoad
73 //Register our default preferences and observe changes
74 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:SOUND_DEFAULT_PREFS forClass:[self class]]
75 forGroup:PREF_GROUP_SOUNDS];
76 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_SOUNDS];
81 [[adium preferenceController] unregisterPreferenceObserver:self];
82 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
84 [self _stopAndReleaseAllSounds];
86 [soundCacheDict release]; soundCacheDict = nil;
87 [soundCacheArray release]; soundCacheArray = nil;
88 [soundCacheCleanupTimer invalidate]; [soundCacheCleanupTimer release]; soundCacheCleanupTimer = nil;
93 - (void)playSoundAtPath:(NSString *)inPath
95 if (inPath && customVolume != 0.0 && !soundsAreMuted) {
96 [self cachedPlaySound:inPath];
100 - (void)stopPlayingSoundAtPath:(NSString *)inPath
102 QTMovie *movie = [soundCacheDict objectForKey:inPath];
109 * @brief Preferences changed, adjust to the new values
111 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
112 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
114 float newVolume = [[prefDict objectForKey:KEY_SOUND_CUSTOM_VOLUME_LEVEL] floatValue];
116 //If sound volume has changed, we must update all existing sounds to the new volume
117 if (customVolume != newVolume) {
118 [self _setVolumeOfAllSoundsTo:newVolume];
121 //Load the new preferences
122 customVolume = newVolume;
126 * @brief Stop and release all cached sounds
128 - (void)_stopAndReleaseAllSounds
130 [[soundCacheDict allValues] makeObjectsPerformSelector:@selector(stop)];
131 [soundCacheDict removeAllObjects];
132 [soundCacheArray removeAllObjects];
136 * @brief Update the volume of all cached sounds
138 - (void)_setVolumeOfAllSoundsTo:(float)inVolume
140 NSEnumerator *enumerator = [soundCacheDict objectEnumerator];
143 while((movie = [enumerator nextObject])){
144 [movie setVolume:inVolume];
149 * @brief Play a QTMovie, possibly cached
151 * @param inPath path to the sound file
153 - (void)cachedPlaySound:(NSString *)inPath
155 QTMovie *movie = [soundCacheDict objectForKey:inPath];
157 //Load the sound if necessary
159 //If the cache is full, remove the least recently used cached sound
160 if ([soundCacheDict count] >= MAX_CACHED_SOUNDS) {
161 [self _uncacheLeastRecentlyUsedSound];
164 //Load and cache the sound
165 NSError *error = nil;
166 movie = [[QTMovie alloc] initWithFile:inPath
169 //Insert the player at the front of our cache
170 [soundCacheArray insertObject:inPath atIndex:0];
171 [soundCacheDict setObject:movie forKey:inPath];
174 //Set the volume (otherwise #2283 happens)
175 [movie setVolume:customVolume];
177 //Create an audio context for the system output audio device.
178 //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.)
179 QTAudioContextRef newAudioContext = [self createAudioContextWithSystemOutputDevice];
181 OSStatus err = SetMovieAudioContext([movie quickTimeMovie], newAudioContext);
182 NSAssert4(err == noErr, @"%s: Could not set audio context of movie %@ to %p: SetMovieAudioContext returned error %i", __PRETTY_FUNCTION__, movie, newAudioContext, err);
184 //We created it, so we must release it.
185 QTAudioContextRelease(newAudioContext);
189 //Move this sound to the front of the cache (This will naturally move lesser used sounds to the back for removal)
190 [soundCacheArray removeObject:inPath];
191 [soundCacheArray insertObject:inPath atIndex:0];
196 //Ensure the sound is starting from the beginning; necessary for cached sounds that have already been played
197 QTTime startOfMovie = {
199 .timeScale = [[movie attributeForKey:QTMovieTimeScaleAttribute] longValue],
202 [movie setCurrentTime:startOfMovie];
204 //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).
210 * @brief Remove the least recently used sound from the cache
212 - (void)_uncacheLeastRecentlyUsedSound
214 NSString *lastCachedPath = [soundCacheArray lastObject];
215 QTMovie *movie = [soundCacheDict objectForKey:lastCachedPath];
217 //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.
218 if ([movie rate] == 0.0) {
219 [soundCacheDict removeObjectForKey:lastCachedPath];
220 [soundCacheArray removeLastObject];
224 - (QTAudioContextRef) createAudioContextWithSystemOutputDevice
226 QTAudioContextRef newAudioContext = NULL;
230 //First, obtain the device itself.
231 AudioDeviceID systemOutputDevice = 0;
232 dataSize = sizeof(systemOutputDevice);
233 err = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultSystemOutputDevice, &dataSize, &systemOutputDevice);
234 NSAssert2(err == noErr, @"%s: Could not get the system output device: AudioHardwareGetProperty returned error %i", __PRETTY_FUNCTION__, err);
236 //Now get its UID. We'll need to release this.
237 CFStringRef deviceUID = NULL;
238 dataSize = sizeof(deviceUID);
239 err = AudioDeviceGetProperty(systemOutputDevice, /*channel*/ 0, /*isInput*/ false, kAudioDevicePropertyDeviceUID, &dataSize, &deviceUID);
240 NSAssert3(err == noErr, @"%s: Could not get the device UID for device %p: AudioDeviceGetProperty returned error %i", __PRETTY_FUNCTION__, systemOutputDevice, err);
241 [(NSObject *)deviceUID autorelease];
243 //Create an audio context for this device so that our movies can play into it.
244 err = QTAudioContextCreateForAudioDevice(kCFAllocatorDefault, deviceUID, /*options*/ NULL, &newAudioContext);
245 NSAssert3(err == noErr, @"%s: QTAudioContextCreateForAudioDevice with device UID %@ returned error %i", __PRETTY_FUNCTION__, deviceUID, err);
247 return newAudioContext;
250 - (NSArray *)allSounds
252 return [soundCacheDict allValues];
256 * @brief Workspace activated (Computer switched to our user)
258 - (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
260 [self setSoundsAreMuted:NO];
264 * @brief Workspace resigned (Computer switched to another user)
266 - (void)workspaceSessionDidResignActive:(NSNotification *)notification
268 [self setSoundsAreMuted:YES];
271 - (void)setSoundsAreMuted:(BOOL)mute
273 AILog(@"setSoundsAreMuted: %i",mute);
274 if (soundsAreMuted > 0 && !mute)
279 if (soundsAreMuted == 1)
280 [self _stopAndReleaseAllSounds];
285 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon)
287 #pragma unused(property)
288 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
290 AdiumSound *self = (id)refcon;
291 NSCAssert1(self, @"AudioHardware property listener function %s called with nil refcon, which we expected to be the AdiumSound instance", __PRETTY_FUNCTION__);
293 NSEnumerator *soundsEnum = [[self allSounds] objectEnumerator];
295 while ((movie = [soundsEnum nextObject])) {
296 //QTMovie gets confused if we're playing when we do this, so pause momentarily.
297 float savedRate = [movie rate];
300 //Exchange the audio context for a new one with the new device.
301 QTAudioContextRef newAudioContext = [self createAudioContextWithSystemOutputDevice];
303 OSStatus err = SetMovieAudioContext([movie quickTimeMovie], newAudioContext);
304 NSCAssert4(err == noErr, @"%s: Could not set audio context of movie %@ to %p: SetMovieAudioContext returned error %i", __PRETTY_FUNCTION__, movie, newAudioContext, err);
306 //We created it, so we must release it.
307 QTAudioContextRelease(newAudioContext);
309 //Resume playback, now on the new device.
310 [movie setRate:savedRate];