Prevent sending 0-byte files. Fixes #8711.
[adiumx.git] / Source / AdiumSound.m
blobb197394015fbb4dde2d6e63658f52ac153a78e6a
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 "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;
34 @end
36 static OSStatus systemOutputDeviceDidChange(AudioHardwarePropertyID property, void *refcon);
38 @implementation AdiumSound
40 /*!
41  * @brief Init
42  */
43 - (id)init
45         if ((self = [super init])) {
46                 soundCacheDict = [[NSMutableDictionary alloc] init];
47                 soundCacheArray = [[NSMutableArray alloc] init];
48                 soundCacheCleanupTimer = nil;
49                 soundsAreMuted = NO;
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
57                                                           object:nil];
59                 [workspaceCenter addObserver:self
60                                                         selector:@selector(workspaceSessionDidResignActive:)
61                                                                 name:NSWorkspaceSessionDidResignActiveNotification
62                                                           object:nil];
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
68                                                                                                    object:nil];
69                 
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);
73         }
75         return self;
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];
86 - (void)dealloc
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;
98         [super dealloc];
101 - (void)playSoundAtPath:(NSString *)inPath
103         if (inPath && customVolume != 0.0 && !soundsAreMuted) {
104                 [self cachedPlaySound:inPath];
105         }
108 - (void)stopPlayingSoundAtPath:(NSString *)inPath
110     QTMovie *movie = [soundCacheDict objectForKey:inPath];
111     if (movie) {
112                 [movie stop];
113         }
117  * @brief Preferences changed, adjust to the new values
118  */
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];
127         }
129         //Load the new preferences
130         customVolume = newVolume;
134  * @brief Stop and release all cached sounds
135  */
136 - (void)_stopAndReleaseAllSounds
138         [[soundCacheDict allValues] makeObjectsPerformSelector:@selector(stop)];
139         [soundCacheDict removeAllObjects];
140         [soundCacheArray removeAllObjects];
144  * @brief Update the volume of all cached sounds
145  */
146 - (void)_setVolumeOfAllSoundsTo:(float)inVolume
148         NSEnumerator            *enumerator = [soundCacheDict objectEnumerator];
149         QTMovie *movie;
151         while((movie = [enumerator nextObject])){
152                 [movie setVolume:inVolume];
153         }
157  * @brief Play a QTMovie, possibly cached
158  * 
159  * @param inPath path to the sound file
160  */
161 - (void)cachedPlaySound:(NSString *)inPath
163     QTMovie *movie = [soundCacheDict objectForKey:inPath];
165         //Load the sound if necessary
166     if (!movie) {
167                 //If the cache is full, remove the least recently used cached sound
168                 if ([soundCacheDict count] >= MAX_CACHED_SOUNDS) {
169                         [self _uncacheLeastRecentlyUsedSound];
170                 }
172                 //Load and cache the sound
173                 NSError *error = nil;
174                 movie = [[QTMovie alloc] initWithFile:inPath
175                                                 error:&error];
176                 if (movie) {
177                         //Insert the player at the front of our cache
178                         [soundCacheArray insertObject:inPath atIndex:0];
179                         [soundCacheDict setObject:movie forKey:inPath];
180                         [movie release];
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);
194                 }
196     } else {
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];
200     }
202     //Engage!
203     if (movie) {
204                 //Ensure the sound is starting from the beginning; necessary for cached sounds that have already been played
205                 QTTime startOfMovie = {
206                         .timeValue = 0LL,
207                         .timeScale = [[movie attributeForKey:QTMovieTimeScaleAttribute] longValue],
208                         .flags = 0,
209                 };
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).
213                 [movie play];
214     }
218  * @brief Remove the least recently used sound from the cache
219  */
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];
229         }
232 - (QTAudioContextRef) createAudioContextWithSystemOutputDevice
234         QTAudioContextRef newAudioContext = NULL;
235         OSStatus err;
236         UInt32 dataSize;
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)
265  */
266 - (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
268         [self setSoundsAreMuted:NO];
272  * @brief Workspace resigned (Computer switched to another user)
273  */
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)
288                 soundsAreMuted--;
289         else if (mute)
290                 soundsAreMuted++;
292         if (soundsAreMuted == 1)
293                 [self _stopAndReleaseAllSounds];
296 @end
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];
307         QTMovie *movie;
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];
311                 [movie setRate:0.0];
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];
324         }
326         [pool release];
327         
328         return noErr;