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 "AdiumSpeech.h"
18 #import "AISoundController.h"
19 #import <Adium/AIPreferenceControllerProtocol.h>
21 #import <Adium/AIListObject.h>
23 #define TEXT_TO_SPEAK @"Text"
24 #define VOICE @"Voice"
25 #define PITCH @"Pitch"
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.
34 @interface AdiumSpeech (PRIVATE)
35 - (SUSpeaker *)defaultVoice;
36 - (SUSpeaker *)variableVoice;
38 - (void)_stopSpeaking;
39 - (SUSpeaker *)_speakerForVoice:(NSString *)voiceString index:(int *)voiceIndex;
40 - (void)_setVolumeOfVoicesTo:(float)newVolume;
43 @implementation AdiumSpeech
50 if ((self = [super init])) {
51 speechArray = [[NSMutableArray alloc] init];
52 workspaceSessionIsActive = YES;
55 //Observe workspace activity changes so we can mute sounds as necessary
56 NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
58 [workspaceCenter addObserver:self
59 selector:@selector(workspaceSessionDidBecomeActive:)
60 name:NSWorkspaceSessionDidBecomeActiveNotification
63 [workspaceCenter addObserver:self
64 selector:@selector(workspaceSessionDidResignActive:)
65 name:NSWorkspaceSessionDidResignActiveNotification
73 * @brief Load the array of 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"];
92 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
93 [[adium preferenceController] unregisterPreferenceObserver:self];
97 [speechArray release]; speechArray = nil;
100 [voiceArray release];
108 * @brief Finish Initing
111 * 1) Preference controller is ready
113 - (void)controllerDidLoad
116 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_SOUNDS];
119 #pragma mark Preferences
122 * @brief Preferences changed, adjust to the new values
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];
129 //If sound volume has changed, we must update all existing sounds to the new volume
130 if (customVolume != newVolume) {
131 [self _setVolumeOfVoicesTo:newVolume];
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];
147 * @brief Speak text with the default values
149 * @param text NSString to speak
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
165 - (void)speakText:(NSString *)text withVoice:(NSString *)voiceString pitch:(float)pitch rate:(float)rate
167 if (text && [text length] && workspaceSessionIsActive) {
168 NSMutableDictionary *dict;
170 dict = [[NSMutableDictionary alloc] init];
173 [dict setObject:text forKey:TEXT_TO_SPEAK];
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];
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
194 - (void)speakDemoTextForVoice:(NSString *)voiceString withPitch:(float)pitch andRate:(float)rate
196 if(workspaceSessionIsActive){
198 SUSpeaker *theSpeaker = [self _speakerForVoice:voiceString index:&voiceIndex];
199 NSString *demoText = [theSpeaker demoTextForVoiceAtIndex:((voiceIndex != NSNotFound) ? voiceIndex : -1)];
201 [self _stopSpeaking];
202 [self speakText:demoText withVoice:voiceString pitch:pitch rate:rate];
207 //Voices ---------------------------------------------------------------------------------------------------------------
210 * @brief Returns an array of available voices
214 if(!voiceArray) [self loadVoices];
219 * @brief Returns the systemwide default rate
223 if (!_defaultRate) { //Cache this, since the calculation may be slow
224 _defaultRate = [[self defaultVoice] rate];
230 * @brief Returns the systemwide default pitch
232 - (float)defaultPitch
234 if (!_defaultPitch) { //Cache this, since the calculation may be slow
235 _defaultPitch = [[self defaultVoice] pitch];
237 return _defaultPitch;
241 * @brief Returns the default voice, creating if necessary
243 - (SUSpeaker *)defaultVoice
245 if (!_defaultVoice) {
246 _defaultVoice = [[SUSpeaker alloc] init];
247 [_defaultVoice setDelegate:self];
248 [_defaultVoice setVolume:customVolume];
250 return _defaultVoice;
254 * @brief Returns the variable voice, creating if necessary
256 - (SUSpeaker *)variableVoice
258 if (!_variableVoice) {
259 _variableVoice = [[SUSpeaker alloc] init];
260 [_variableVoice setDelegate:self];
261 [_variableVoice setVolume:customVolume];
263 return _variableVoice;
267 //Speaking -------------------------------------------------------------------------------------------------------------
268 #pragma mark Speaking
270 * @brief Attempt to speak the next item in the queue
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)
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];
302 * @brief Speaking has finished, begin speaking the next item in our queue
304 - (IBAction)didFinishSpeaking:(SUSpeaker *)theSpeaker
311 * @brief Immediately stop speaking
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.
325 - (SUSpeaker *)_speakerForVoice:(NSString *)voiceString index:(int *)voiceIndex
331 if(!voiceArray) [self loadVoices];
332 theIndex = [voiceArray indexOfObject:voiceString];
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];
345 speaker = [self defaultVoice];
352 //Misc -----------------------------------------------------------------------------------------------------------------
355 * @brief Workspace activated (Computer switched to our user)
357 - (void)workspaceSessionDidBecomeActive:(NSNotification *)notification
359 workspaceSessionIsActive = YES;
363 * @brief Workspace resigned (Computer switched to another user)
365 - (void)workspaceSessionDidResignActive:(NSNotification *)notification
367 workspaceSessionIsActive = NO;
368 [self _stopSpeaking];