There's no autorelease pool on the HAL thread, which is the thread that calls `system...
[adiumx.git] / Source / AdiumSound.m
blob82f7b62ca8a14d3afc7f8ce7b83de68143cd9e90
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  */
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;
33 @end
35 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon);
37 @implementation AdiumSound
39 /*!
40  * @brief Init
41  */
42 - (id)init
44         if ((self = [super init])) {
45                 soundCacheDict = [[NSMutableDictionary alloc] init];
46                 soundCacheArray = [[NSMutableArray alloc] init];
47                 soundCacheCleanupTimer = nil;
48                 soundsAreMuted = NO;
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
56                                                           object:nil];
58                 [workspaceCenter addObserver:self
59                                                         selector:@selector(workspaceSessionDidResignActive:)
60                                                                 name:NSWorkspaceSessionDidResignActiveNotification
61                                                           object:nil];
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);
66         }
68         return self;
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];
79 - (void)dealloc
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;
90         [super dealloc];
93 - (void)playSoundAtPath:(NSString *)inPath
95         if (inPath && customVolume != 0.0 && !soundsAreMuted) {
96                 [self cachedPlaySound:inPath];
97         }
100 - (void)stopPlayingSoundAtPath:(NSString *)inPath
102     QTMovie *movie = [soundCacheDict objectForKey:inPath];
103     if (movie) {
104                 [movie stop];
105         }
109  * @brief Preferences changed, adjust to the new values
110  */
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];
119         }
121         //Load the new preferences
122         customVolume = newVolume;
126  * @brief Stop and release all cached sounds
127  */
128 - (void)_stopAndReleaseAllSounds
130         [[soundCacheDict allValues] makeObjectsPerformSelector:@selector(stop)];
131         [soundCacheDict removeAllObjects];
132         [soundCacheArray removeAllObjects];
136  * @brief Update the volume of all cached sounds
137  */
138 - (void)_setVolumeOfAllSoundsTo:(float)inVolume
140         NSEnumerator            *enumerator = [soundCacheDict objectEnumerator];
141         QTMovie *movie;
143         while((movie = [enumerator nextObject])){
144                 [movie setVolume:inVolume];
145         }
149  * @brief Play a QTMovie, possibly cached
150  * 
151  * @param inPath path to the sound file
152  */
153 - (void)cachedPlaySound:(NSString *)inPath
155     QTMovie *movie = [soundCacheDict objectForKey:inPath];
157         //Load the sound if necessary
158     if (!movie) {
159                 //If the cache is full, remove the least recently used cached sound
160                 if ([soundCacheDict count] >= MAX_CACHED_SOUNDS) {
161                         [self _uncacheLeastRecentlyUsedSound];
162                 }
164                 //Load and cache the sound
165                 NSError *error = nil;
166                 movie = [[QTMovie alloc] initWithFile:inPath
167                                                 error:&error];
168                 if (movie) {
169                         //Insert the player at the front of our cache
170                         [soundCacheArray insertObject:inPath atIndex:0];
171                         [soundCacheDict setObject:movie forKey:inPath];
172                         [movie release];
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);
186                 }
188     } else {
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];
192     }
194     //Engage!
195     if (movie) {
196                 //Ensure the sound is starting from the beginning; necessary for cached sounds that have already been played
197                 QTTime startOfMovie = {
198                         .timeValue = 0LL,
199                         .timeScale = [[movie attributeForKey:QTMovieTimeScaleAttribute] longValue],
200                         .flags = 0,
201                 };
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).
205                 [movie play];
206     }
210  * @brief Remove the least recently used sound from the cache
211  */
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];
221         }
224 - (QTAudioContextRef) createAudioContextWithSystemOutputDevice
226         QTAudioContextRef newAudioContext = NULL;
227         OSStatus err;
228         UInt32 dataSize;
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)
257  */
258 - (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
260         [self setSoundsAreMuted:NO];
264  * @brief Workspace resigned (Computer switched to another user)
265  */
266 - (void)workspaceSessionDidResignActive:(NSNotification *)notification
268         [self setSoundsAreMuted:YES];
271 - (void)setSoundsAreMuted:(BOOL)mute
273         AILog(@"setSoundsAreMuted: %i",mute);
274         if (soundsAreMuted > 0 && !mute)
275                 soundsAreMuted--;
276         else if (mute)
277                 soundsAreMuted++;
279         if (soundsAreMuted == 1)
280                 [self _stopAndReleaseAllSounds];
283 @end
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];
294         QTMovie *movie;
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];
298                 [movie setRate:0.0];
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];
311         }
313         [pool release];
314         
315         return noErr;