Added a debug log to investigate #8685, as it worksforme. Refs #8685
[adiumx.git] / Source / AdiumSpeech.m
blobfbc5a8d6698bbb7c2416c584be5bbee49f764dab
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 "AdiumSpeech.h"
18 #import "AISoundController.h"
19 #import <Adium/AIPreferenceControllerProtocol.h>
20 #import "SUSpeaker.h"
21 #import <Adium/AIListObject.h>
23 #define TEXT_TO_SPEAK                   @"Text"
24 #define VOICE                                   @"Voice"
25 #define PITCH                                   @"Pitch"
26 #define RATE                                    @"Rate"
28 /* Text to Speech  
29  * We use SUSpeaker to provide maximum flexibility over speech.  NSSpeechSynthesizer does not gives us pitch/rate controls.  
30  * The only significant bug in SUSpeaker is that it does not reset to the system default voice when it is asked to. We  
31  * therefore use 2 instances of SUSpeaker: one for default settings, and one for custom settings.  
32  */  
34 @interface AdiumSpeech (PRIVATE)
35 - (SUSpeaker *)defaultVoice;
36 - (SUSpeaker *)variableVoice;
37 - (void)_speakNext;
38 - (void)_stopSpeaking;
39 - (SUSpeaker *)_speakerForVoice:(NSString *)voiceString index:(int *)voiceIndex;
40 - (void)_setVolumeOfVoicesTo:(float)newVolume;
41 @end
43 @implementation AdiumSpeech
45 /*!
46  * @brief Init
47  */
48 - (id)init
50         if ((self = [super init])) {
51                 speechArray = [[NSMutableArray alloc] init];
52                 workspaceSessionIsActive = YES;
53                 speaking = NO;
55                 //Observe workspace activity changes so we can mute sounds as necessary
56                 NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
57                 
58                 [workspaceCenter addObserver:self
59                                                         selector:@selector(workspaceSessionDidBecomeActive:)
60                                                                 name:NSWorkspaceSessionDidBecomeActiveNotification
61                                                           object:nil];
62                 
63                 [workspaceCenter addObserver:self
64                                                         selector:@selector(workspaceSessionDidResignActive:)
65                                                                 name:NSWorkspaceSessionDidResignActiveNotification
66                                                           object:nil];
67         }
68         
69         return self;
72 /*!
73  * @brief Load the array of voices
74  */
75 - (void)loadVoices
77         //Load voices
78         //Vicki, a new voice in 10.3, returns an invalid name to SUSpeaker, Vicki3Smallurrent. If we see that name,
79         //replace it with just Vicki.  If this gets fixed in a future release of OS X, this code will simply do nothing.
80         voiceArray = [[SUSpeaker voiceNames] mutableCopy];
81         int messedUpIndex = [voiceArray indexOfObject:@"Vicki3Smallurrent"];
82         if (messedUpIndex != NSNotFound) {
83                 [voiceArray replaceObjectAtIndex:messedUpIndex withObject:@"Vicki"];
84         }
87 /*!
88  * @brief Close
89  */
90 - (void)dealloc
92         [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
93         [[adium preferenceController] unregisterPreferenceObserver:self];
95         [self _stopSpeaking];
97         [speechArray release]; speechArray = nil;
98         if(voiceArray)
99         {
100                 [voiceArray release]; 
101                 voiceArray = nil;
102         }
103         
104         [super dealloc];
108 * @brief Finish Initing
110  * Requires:
111  * 1) Preference controller is ready
112  */
113 - (void)controllerDidLoad
115         //Observe changes
116         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_SOUNDS];      
119 #pragma mark Preferences
122 * @brief Preferences changed, adjust to the new values
123  */
124 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
125                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
127         float newVolume = [[prefDict objectForKey:KEY_SOUND_CUSTOM_VOLUME_LEVEL] floatValue];
128         
129         //If sound volume has changed, we must update all existing sounds to the new volume
130         if (customVolume != newVolume) {
131                 [self _setVolumeOfVoicesTo:newVolume];
132         }
133         
134         //Load the new preferences
135         customVolume = newVolume;
138 - (void)_setVolumeOfVoicesTo:(float)newVolume
140         if (_defaultVoice) [_defaultVoice setVolume:newVolume];
141         if (_variableVoice) [_variableVoice setVolume:newVolume]; 
144 #pragma mark Speech
147  * @brief Speak text with the default values
149  * @param text NSString to speak
150  */
151 - (void)speakText:(NSString *)text
153     [self speakText:text withVoice:nil pitch:0 rate:0];
157  * @brief Speak text with a specific voice, pitch, and rate
159  * If text is already being spoken, this text will be queued and spoken at the next available opportunity
160  * @param text NSString to speak
161  * @param voiceString NSString voice identifier
162  * @param pitch Speaking pitch
163  * @param rate Speaking rate
164  */
165 - (void)speakText:(NSString *)text withVoice:(NSString *)voiceString pitch:(float)pitch rate:(float)rate
167     if (text && [text length] && workspaceSessionIsActive) {
168                 NSMutableDictionary *dict;
169                 
170                 dict = [[NSMutableDictionary alloc] init];
171                 
172                 if (text) {
173                         [dict setObject:text forKey:TEXT_TO_SPEAK];
174                 }
175                 
176                 if (voiceString) [dict setObject:voiceString forKey:VOICE];                     
177                 if (pitch > FLT_EPSILON) [dict setObject:[NSNumber numberWithFloat:pitch] forKey:PITCH];
178                 if (rate  > FLT_EPSILON) [dict setObject:[NSNumber numberWithFloat:rate]  forKey:RATE];
179                 AILog(@"AdiumSpeech: %@",dict);
180                 [speechArray addObject:dict];
181                 [dict release];
182                 
183                 [self _speakNext];
184     }
188  * @brief Speak a voice-specific sample text at the passed settings
190  * @param voiceString NSString voice identifier
191  * @param pitch Speaking pitch
192  * @param rate Speaking rate
193  */
194 - (void)speakDemoTextForVoice:(NSString *)voiceString withPitch:(float)pitch andRate:(float)rate
196         if(workspaceSessionIsActive){
197                 int                     voiceIndex;
198                 SUSpeaker       *theSpeaker = [self _speakerForVoice:voiceString index:&voiceIndex];
199                 NSString        *demoText = [theSpeaker demoTextForVoiceAtIndex:((voiceIndex != NSNotFound) ? voiceIndex : -1)];
200                 
201                 [self _stopSpeaking];
202                 [self speakText:demoText withVoice:voiceString pitch:pitch rate:rate];
203         }
207 //Voices ---------------------------------------------------------------------------------------------------------------
208 #pragma mark Voices
210  * @brief Returns an array of available voices
211  */
212 - (NSArray *)voices
214         if(!voiceArray) [self loadVoices];
215     return voiceArray;
219  * @brief Returns the systemwide default rate
220  */
221 - (float)defaultRate
223         if (!_defaultRate) { //Cache this, since the calculation may be slow
224                 _defaultRate = [[self defaultVoice] rate];
225         }
226         return _defaultRate;
230  * @brief Returns the systemwide default pitch
231  */
232 - (float)defaultPitch
234         if (!_defaultPitch) { //Cache this, since the calculation may be slow
235                 _defaultPitch = [[self defaultVoice] pitch];
236         }
237         return _defaultPitch;
241  * @brief Returns the default voice, creating if necessary
242  */
243 - (SUSpeaker *)defaultVoice
245     if (!_defaultVoice) {
246                 _defaultVoice = [[SUSpeaker alloc] init];
247                 [_defaultVoice setDelegate:self];
248                 [_defaultVoice setVolume:customVolume];
249     }
250         return _defaultVoice;
254  * @brief Returns the variable voice, creating if necessary
255  */
256 - (SUSpeaker *)variableVoice
258     if (!_variableVoice) {
259                 _variableVoice = [[SUSpeaker alloc] init];
260                 [_variableVoice setDelegate:self];
261                 [_variableVoice setVolume:customVolume];
262     }
263         return _variableVoice;
267 //Speaking -------------------------------------------------------------------------------------------------------------
268 #pragma mark Speaking
270  * @brief Attempt to speak the next item in the queue
271  */
272 - (void)_speakNext
274     //we have items left to speak and aren't already speaking
275     if ([speechArray count] && !speaking) {
276                 //Don't speak on top of other apps; instead, wait 1 second and try again
277                 if (SpeechBusySystemWide() > 0) {
278                         [self performSelector:@selector(_speakNext)
279                                            withObject:nil
280                                            afterDelay:1.0];
281                 } else {                        
282                         speaking = YES;
284                         //Speak the next entry in our queue
285                         NSMutableDictionary *dict = [speechArray objectAtIndex:0];
286                         NSString                        *text = [dict objectForKey:TEXT_TO_SPEAK];
287                         NSNumber                        *pitchNumber = [dict objectForKey:PITCH];
288                         NSNumber                        *rateNumber = [dict objectForKey:RATE];
289                         SUSpeaker                       *theSpeaker = [self _speakerForVoice:[dict objectForKey:VOICE] index:NULL];
291                         [theSpeaker setPitch:(pitchNumber ? [pitchNumber floatValue] : [self defaultPitch])];
292                         [theSpeaker setRate:  (rateNumber ?  [rateNumber floatValue] : [self defaultRate])];
293                         [theSpeaker setVolume:customVolume];
295                         [theSpeaker speakText:text];
296                         [speechArray removeObjectAtIndex:0];
297                 }
298         }
302  * @brief Speaking has finished, begin speaking the next item in our queue
303  */
304 - (IBAction)didFinishSpeaking:(SUSpeaker *)theSpeaker
306         speaking = NO;
307     [self _speakNext];
311  * @brief Immediately stop speaking
312  */
313 - (void)_stopSpeaking
315         [speechArray removeAllObjects];
317         [_defaultVoice stopSpeaking];
318         [_variableVoice stopSpeaking];
322  * @brief Return the SUSpeaker which should be used for a given voice name, configured for that voice.
323  * Optionally, return the index of that voice in our array by reference.
324  */
325 - (SUSpeaker *)_speakerForVoice:(NSString *)voiceString index:(int *)voiceIndex
327         SUSpeaker       *speaker;
328         int             theIndex;
329         if(voiceString)
330         {
331                 if(!voiceArray) [self loadVoices];
332                 theIndex = [voiceArray indexOfObject:voiceString];
333         }
334         else
335                 theIndex = NSNotFound;
337         //Return the voice index by reference
338         if (voiceIndex) *voiceIndex = theIndex;
340         //Configure and return the voice
341         if (theIndex != NSNotFound) {
342                 speaker = [self variableVoice];
343                 [speaker setVoiceUsingIndex:theIndex];          
344         } else {
345                 speaker = [self defaultVoice];
346         }
347         
348         return speaker;
352 //Misc -----------------------------------------------------------------------------------------------------------------
353 #pragma mark Misc
355  * @brief Workspace activated (Computer switched to our user)
356  */
357 - (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
359         workspaceSessionIsActive = YES;
363  * @brief Workspace resigned (Computer switched to another user)
364  */
365 - (void)workspaceSessionDidResignActive:(NSNotification *)notification
367         workspaceSessionIsActive = NO;
368         [self _stopSpeaking];   
371 @end