Merged [15028]: Remove all pending speech objects in _stopSpeaking
[adiumx.git] / Source / AISoundController.m
blobe00bd9f599918bb7afe99e30e15a12a2067489e7
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 // $Id$
19 #import "AIPreferenceController.h"
20 #import "AISoundController.h"
21 #import <Adium/AIObject.h>
22 #import <Adium/AIAccount.h>
23 #import <Adium/SUSpeaker.h>
24 #import <Adium/QTSoundFilePlayer.h>
25 #import <AIUtilities/CBApplicationAdditions.h>
26 #import <AIUtilities/AIDictionaryAdditions.h>
27 #import <AIUtilities/AIWorkspaceAdditions.h>
28 #include <float.h>
30 #define PATH_SOUNDS                                     @"/Sounds"
31 #define PATH_INTERNAL_SOUNDS            @"/Contents/Resources/Sounds/"
32 #define SOUND_SET_PATH_EXTENSION        @"txt"
33 #define SOUND_DEFAULT_PREFS                     @"SoundPrefs"
34 #define MAX_CACHED_SOUNDS                       4                                       //Max cached sounds
36 #define TEXT_TO_SPEAK                           @"Text"
37 #define VOICE                                           @"Voice"
38 #define PITCH                                           @"Pitch"
39 #define RATE                                            @"Rate"
41 #define SOUND_CACHE_CLEANUP_INTERVAL    60.0
43 #define SOUND_LOCATION                                  @"Location"
44 #define SOUND_LOCATION_SEPARATOR                @"////"
45 #define SOUND_PACK_PATHNAME                             @"AdiumSetPathname_Private"
46 #define SOUND_PACK_VERSION                              @"AdiumSetVersion"
47 #define SOUND_NAMES                                             @"Sounds"
49 @interface AISoundController (PRIVATE)
50 - (void)_removeSystemAlertIDs;
51 - (void)_coreAudioPlaySound:(NSString *)inPath;
52 - (void)_scanSoundSetsFromPath:(NSString *)soundFolderPath intoArray:(NSMutableArray *)soundSetArray;
53 - (void)_addSet:(NSString *)inSet withSounds:(NSArray *)inSounds toArray:(NSMutableArray *)inArray;
54 - (void)addSoundsIndicatedByDictionary:(NSDictionary *)infoDict toArray:(NSMutableArray *)soundSetContents;
56 - (NSArray *)voiceArray;
57 - (SUSpeaker *)_speakerForVoice:(NSString *)voiceString index:(int *)voiceIndex;
58 - (void)speakNext;
59 - (void)initDefaultVoiceIfNecessary;
60 - (void)_stopSpeakingNow;
62 - (void)uncacheLastPlayer;
63 @end
65 @implementation AISoundController
67 - (void)initController
69     soundCacheDict = [[NSMutableDictionary alloc] init];
70     soundCacheArray = [[NSMutableArray alloc] init];
71         soundCacheCleanupTimer = nil;
73     speechArray = [[NSMutableArray alloc] init];
74     resetNextTime = NO;
75     speaking = NO;
77     //Create a custom sounds directory ~/Library/Application Support/Adium 2.0/Sounds
78     [[AIObject sharedAdiumInstance] createResourcePathForName:PATH_SOUNDS];
79     
80     AIPreferenceController *preferenceController = [adium preferenceController];
82     //Register our default preferences
83     [preferenceController registerDefaults:[NSDictionary dictionaryNamed:SOUND_DEFAULT_PREFS forClass:[self class]]
84                                                                                   forGroup:PREF_GROUP_SOUNDS];
85     
86     //Ensure the temporary mute is off
87         if([[preferenceController preferenceForKey:KEY_SOUND_TEMPORARY_MUTE
88                                              group:PREF_GROUP_SOUNDS] boolValue])
89         {
90                 [preferenceController setPreference:nil
91                                              forKey:KEY_SOUND_TEMPORARY_MUTE
92                                               group:PREF_GROUP_SOUNDS];
93         }
95     //observe pref changes
96         [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_SOUNDS];
99 //close
100 - (void)closeController
102         [[adium preferenceController] unregisterPreferenceObserver:self];
104         //Stop speaking
105         [self _stopSpeakingNow];
107         //Stop all sounds from playing
108         NSEnumerator            *enumerator = [soundCacheDict objectEnumerator];
109         QTSoundFilePlayer   *soundFilePlayer;
110         while (soundFilePlayer = [enumerator nextObject]){
111                 [soundFilePlayer stop];
112         }
115 - (void)dealloc
117         [speechArray release]; speechArray = nil;
118         [soundCacheDict release]; soundCacheDict = nil;
119         [soundCacheArray release]; soundCacheArray = nil;
121         [super dealloc];
125 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
126                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
128         NSEnumerator            *enumerator;
129         QTSoundFilePlayer   *soundFilePlayer;
130         SoundDeviceType         oldSoundDeviceType;
131         
132         useCustomVolume = YES;
133         customVolume = ([[prefDict objectForKey:KEY_SOUND_CUSTOM_VOLUME_LEVEL] floatValue]);
134                                 
135         muteSounds = ([[prefDict objectForKey:KEY_SOUND_MUTE] intValue] ||
136                                   [[prefDict objectForKey:KEY_SOUND_TEMPORARY_MUTE] intValue] ||
137                                   [[prefDict objectForKey:KEY_SOUND_STATUS_MUTE] intValue]);
138         
139         oldSoundDeviceType = soundDeviceType;
140         soundDeviceType = [[prefDict objectForKey:KEY_SOUND_SOUND_DEVICE_TYPE] intValue];
141         
142         
143         //Clear out our cached sounds and our speech aray if either
144         // -We're probably not going to be using them for a while
145         // -We've changed output device types so will want to recreate our sound output objects
146         //
147         //If neither of these things happened, we need to update our currently playing songs
148         //to the new volume setting.
149         
150         BOOL needToStopAndRelease = (muteSounds || (soundDeviceType != oldSoundDeviceType));
151         
152         enumerator = [soundCacheDict objectEnumerator];
153         while (soundFilePlayer = [enumerator nextObject]){
154                 if (needToStopAndRelease){
155                         [soundFilePlayer stop];
156                 }else{
157                         [soundFilePlayer setVolume:customVolume];
158                 }
159         }
160         
161         if (speaker_defaultVoice) [speaker_defaultVoice setVolume:customVolume]; 
162         if (speaker_variableVoice) [speaker_variableVoice setVolume:customVolume];  
163         
164         if (needToStopAndRelease){
165                 [speechArray removeAllObjects];
166                 [soundCacheDict removeAllObjects];
167                 [soundCacheArray removeAllObjects];
168         }
172 //Sound Playing --------------------------------------------------------------------------------------------------------
173 #pragma mark Sound Playing
174 //Play a sound by name
175 - (void)playSoundNamed:(NSString *)inName
177     NSString      *path;
178     NSArray       *soundsFolders = [adium resourcePathsForName:PATH_SOUNDS];
179     NSEnumerator  *folderEnum    = [soundsFolders objectEnumerator];
180     NSFileManager *mgr           = [NSFileManager defaultManager];
181     BOOL           isDir         = NO;
183     while(path = [folderEnum nextObject]) {
184         path = [path stringByAppendingPathComponent:inName];
185         if([mgr fileExistsAtPath:path isDirectory:&isDir]) {
186             if(!isDir) {
187                 break;
188             }
189         }
190     }
192     if(path) {
193         [self playSoundAtPath:path];
194     }else{
195                 //They wanted a sound.  We can't find the one they wanted.  At least give 'em something.
196                 NSBeep();
197         }
200 //Play a sound by path
201 - (void)playSoundAtPath:(NSString *)inPath
203     if(!muteSounds){
204                 if (inPath){
205                         [self _coreAudioPlaySound:inPath];
206                 }
207         }
212 //Quicktime ------------------------------------------------------------------------------------------------------------
213 #pragma mark CoreAudio
214 // - Sound loading routine is not incredibly cheap (though not bad), so we should cache manually
215 // + CoreAudio offers volume control, including in real time as the sound plays
216 // + CoreAudio offers control over the output device (system events versus default audio, for example)
217 // + CoreAudio is present and functional on OS X 10.2.7 and above
218 // + QTSoundFilePlayer utilizes Quicktime for conversion so can play basically anything
219 //Play a sound using CoreAudio via QTSoundFilePlayer.
220 - (void)_coreAudioPlaySound:(NSString *)inPath
222     QTSoundFilePlayer   *justCrushAlot;
224     //Search for this sound in our cache
225     justCrushAlot = [soundCacheDict objectForKey:inPath];
226         
227     //If the sound is not cached, load it
228     if(!justCrushAlot){
229                 //If the cache is full, remove the least recently used cached sound
230                 if([soundCacheDict count] >= MAX_CACHED_SOUNDS){
231                         [self uncacheLastPlayer];
232                 }
233                 
234                 //Load and cache the sound
235                 justCrushAlot = [[QTSoundFilePlayer alloc] initWithContentsOfFile:inPath
236                                                                                                    usingSystemAlertDevice:(soundDeviceType == SOUND_SYTEM_ALERT_DEVICE)];
237                 if(justCrushAlot){
238                         /*
239                          It's important that we are caching, not so much because of the overhead but because:
240                                 1) we don't want to leak QTSoundFilePlayer objects but
241                                 2) we don't want to release them immediately as then they would crash while playing and
242                                 3) we don't want to wait here until they finish playing as then Adium would beachball during each sound
243                          So we cache them and release them at some point in the future.  We could accomplish the same using a
244                          non-autoreleasing QTSoundFilePlayer and the provided delegate methods, however:
245                                 4) we don't want to play the same sound more than once at a time - we would rather reset to the beginning.
246                                         this implies having one QTSoundFilePlayer per path, which requires caching into a lookup dict.
247                         */
248                         [soundCacheDict setObject:justCrushAlot forKey:inPath];
249                         [justCrushAlot release];
251                         [soundCacheArray insertObject:inPath atIndex:0];
252                 }
253                 
254     }else{
256                 //Move this sound to the front of the cache (This will naturally move lesser used sounds to the back for removal)
257                 [soundCacheArray removeObject:inPath];
258                 [soundCacheArray insertObject:inPath atIndex:0];
259     }
260         
261     //Set the volume and play sound
262     if(justCrushAlot){
263                 //Reset the cached sound back to the beginning and set its volume; if it is currently playing,
264                 //this will make it restart.
265                 [justCrushAlot setVolume:customVolume];
266                 [justCrushAlot setPlaybackPosition:0];
267                 
268                 //QTSoundFilePlayer won't play if the sound is already playing, but that's fine since we
269                 //reset the playback position and it will start playing there in the next run loop.
270                 [justCrushAlot play];
271     }
272         
273         if (!soundCacheCleanupTimer){
274                 soundCacheCleanupTimer = [[NSTimer scheduledTimerWithTimeInterval:SOUND_CACHE_CLEANUP_INTERVAL
275                                                                                                                                    target:self
276                                                                                                                                  selector:@selector(soundCacheCleanup:)
277                                                                                                                                  userInfo:nil
278                                                                                                                                   repeats:YES] retain];
279         }else{
280                 [soundCacheCleanupTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:SOUND_CACHE_CLEANUP_INTERVAL]];
281         }
284 //If sounds are cached when this fires, dealloc the one used least recently;
285 //If none are cached, stop the timer that got us here.
286 - (void)soundCacheCleanup:(NSTimer *)inTimer
288         if ([soundCacheArray count]){
289                 [self uncacheLastPlayer];
290         }else{
291                 [soundCacheCleanupTimer invalidate]; [soundCacheCleanupTimer release]; soundCacheCleanupTimer = nil;
292         }
295 - (void)uncacheLastPlayer
297         NSString                        *lastCachedPath = [soundCacheArray lastObject];
298         QTSoundFilePlayer   *gangstaPlaya = [soundCacheDict objectForKey:lastCachedPath];
299         
300         if (![gangstaPlaya isPlaying]){
301                 [gangstaPlaya stop];
302                 [soundCacheDict removeObjectForKey:lastCachedPath];
303                 [soundCacheArray removeLastObject];     
304         }
307 //Sound Sets -----------------------------------------------------------------------------------------------------------
308 #pragma mark Sound Sets
309 //Returns an array of dictionaries, each representing a soundset with the following keys:
310 // (NString *)"Set" - The path of the soundset (name is the last component)
311 // (NSArray *)"Sounds" - An array of sound paths (name is the last component) (NSString *'s)
312 - (NSArray *)soundSetArray
314     NSString            *path;
315     NSMutableArray      *soundSetArray;
316         NSEnumerator    *enumerator;
317         
318     //Setup
319     soundSetArray = [[NSMutableArray alloc] init];
320     
321     //Scan sounds
322         enumerator = [[adium resourcePathsForName:@"Sounds"] objectEnumerator];
323         while (path = [enumerator nextObject]){
324                 [self _scanSoundSetsFromPath:path intoArray:soundSetArray];
325         }
326     
327     return [soundSetArray autorelease];
330 - (void)_scanSoundSetsFromPath:(NSString *)soundFolderPath intoArray:(NSMutableArray *)soundSetArray
332     NSDirectoryEnumerator       *enumerator;            //Sound folder directory enumerator
333     NSString                            *file;                          //Current Path (relative to sound folder)
334     NSString                            *soundSetPath;          //Name of the set
335     NSMutableArray                      *soundSetContents;  //Array of sounds in the set
337     //Start things off with a valid set path and contents, incase any sounds aren't in subfolders
338     soundSetPath = soundFolderPath;
339     soundSetContents = [[NSMutableArray alloc] init];
341     //Scan the directory
342     enumerator = [[NSFileManager defaultManager] enumeratorAtPath:soundFolderPath];
343     while((file = [enumerator nextObject])){
344         BOOL                    isDirectory;
345         NSString                *fullPath;
346                 NSString                *fileName = [file lastPathComponent];
348                 //Skip .*, *.txt, and .svn
349         if([fileName characterAtIndex:0] != '.' &&
350            [[file pathExtension] caseInsensitiveCompare:SOUND_SET_PATH_EXTENSION] != NSOrderedSame &&
351            ![[file pathComponents] containsObject:@".svn"]){ //Ignore certain files
353             //Determine if this is a file or a directory
354             fullPath = [soundFolderPath stringByAppendingPathComponent:file];
355             [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
356             if(isDirectory){
357                                 //Only add the soundset if it contains sounds
358                 if([soundSetContents count] != 0){
359                     //Close the current soundset, adding it to our sound set array
360                     [self _addSet:soundSetPath withSounds:soundSetContents toArray:soundSetArray];
361                 }
363                 //Open a new soundset for this directory
364                 soundSetPath = fullPath;
366                                 [soundSetContents release];
367                 soundSetContents = [[NSMutableArray alloc] init];
369             }else{
370                                 if([fileName isEqualToString:@"Info.plist"]){
371                                         NSMutableDictionary *infoDict = [NSMutableDictionary dictionaryWithContentsOfFile:fullPath];
372                                         [infoDict setObject:soundSetPath forKey:SOUND_PACK_PATHNAME];
373                                         [self addSoundsIndicatedByDictionary:infoDict
374                                                                                                  toArray:soundSetContents];
375                                         
376                                 }else{
377                                         //Add the sound
378                                         [soundSetContents addObject:fullPath];
379                                 }
380             }
381         }
382     }
384     //Close the last soundset, adding it to our sound set array
385     [self _addSet:soundSetPath withSounds:soundSetContents toArray:soundSetArray];
386         [soundSetContents release];
389 - (void)_addSet:(NSString *)inSet withSounds:(NSArray *)inSounds toArray:(NSMutableArray *)inArray
391         if(inSet && inSounds && inArray){
392                 [inArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:inSet, KEY_SOUND_SET, inSounds, KEY_SOUND_SET_CONTENTS, nil]];
393         }
397  * @brief Add sounds indicated dynamically by a dictionary to an array
399  * Handle optional location key, which allows emoticons to be loaded from arbitrary directories.
400  * This is currently only used by the iChat sound pack.
401  */
402 - (void)addSoundsIndicatedByDictionary:(NSDictionary *)infoDict toArray:(NSMutableArray *)soundSetContents
404         int version = [[infoDict objectForKey:SOUND_PACK_VERSION] intValue];
406         switch(version){
407                 case 1:
408                 {
409                         NSDictionary    *sounds;
410                         NSEnumerator    *enumerator;
411                         NSString                *soundName, *soundLocation = nil;
413                         sounds = [self soundsDictionaryFromDictionary:infoDict usingLocation:&soundLocation];
414                         
415                         //If we don't have a sound location, return
416                         if(!sounds || !soundLocation) return;
418                         enumerator = [sounds objectEnumerator];
419                         while(soundName = [enumerator nextObject]){
420                                 [soundSetContents addObject:[soundLocation stringByAppendingPathComponent:soundName]];
421                         }
422                         
423                         break;  
424                 }
426                 default:
427                         NSRunAlertPanel(AILocalizedString(@"Cannot open sound set", nil),
428                                         AILocalizedString(@"The sound set at %@ is version %i, and this version of Adium does not know how to handle that; perhaps try a later version of Adium.", nil),
429                                         /*defaultButton*/ nil, /*alternateButton*/ nil, /*otherButton*/ nil,
430                                         [infoDict objectForKey:SOUND_PACK_PATHNAME], version);
431                         break;
432         }       
435 - (NSDictionary *)soundsDictionaryFromDictionary:(NSDictionary *)infoDict usingLocation:(NSString **)outSoundLocation
437         NSString                *soundLocation = nil, *fullSoundLocation = nil;
438         NSDictionary    *sounds;
440         id                      possiblePaths = [infoDict objectForKey:SOUND_LOCATION];
442         if(possiblePaths){
443                 if([possiblePaths isKindOfClass:[NSString class]]){
444                         possiblePaths = [NSArray arrayWithObjects:possiblePaths, nil];
445                 }
446                 
447                 NSEnumerator    *pathEnumerator = [possiblePaths objectEnumerator];
448                 NSString                *aPath;
449                 
450                 while((aPath = [pathEnumerator nextObject])){
451                         NSString        *possiblePath;
452                         NSArray         *splitPath = [aPath componentsSeparatedByString:SOUND_LOCATION_SEPARATOR];
453                         
454                         /* Two possible formats:
455                                 *
456                                 * <string>/absolute/path/to/directory</string>
457                                 * <string>CFBundleIdentifier////relative/path/from/bundle/to/directory</string>
458                                 *
459                                 * The separator in the latter is ////, defined as SOUND_LOCATION_SEPARATOR.
460                                 */
461                         if([splitPath count] == 1){
462                                 possiblePath = [splitPath objectAtIndex:0];
463                         }else{
464                                 NSArray *components = [NSArray arrayWithObjects:
465                                         [[NSWorkspace sharedWorkspace] compatibleAbsolutePathForAppBundleWithIdentifier:[splitPath objectAtIndex:0]],
466                                         [splitPath objectAtIndex:1],
467                                         nil];
468                                 possiblePath = [NSString pathWithComponents:components];
469                         }
470                         
471                         /* If the directory exists, then we've found the location. If we
472                                 * make it all the way through the list without finding a valid
473                                 * directory, then the standard location will be used.
474                                 */
475                         BOOL isDir;
476                         if([[NSFileManager defaultManager] fileExistsAtPath:possiblePath isDirectory:&isDir] && isDir){
477                                 soundLocation = possiblePath;
478                                 
479                                 /* Keep the 'full sound location', which is what was indicated in the dictionary, for generation of
480                                  * the SOUND_NAMES key on a by-location basis later on.
481                                  */
482                                 fullSoundLocation = aPath;
483                                 break;
484                         }
485                 }
486         }
487                 
488         sounds = [infoDict objectForKey:[NSString stringWithFormat:@"%@:%@",SOUND_NAMES,fullSoundLocation]];
489         if(!sounds) sounds = [infoDict objectForKey:SOUND_NAMES];
490         
491         if(outSoundLocation) *outSoundLocation = soundLocation;
492         
493         return sounds;
496 //Text to Speech -------------------------------------------------------------------------------------------------------
497 #pragma mark Text to Speech
498 /* Text to Speech
499  * We use SUSpeaker to provide maximum flexibility over speech.  NSSpeechSynthesizer does not gives us pitch/rate controls,
500  * and is not compatible with 10.2, as well.
501  * The only significant bug in SUSpeaker is that it does not reset to the system default voice when it is asked to. We
502  * therefore use 2 instances of SUSpeaker: one for default settings, and one for custom settings.
503  */
505 //Convenience method: speak the given text with default values
506 - (void)speakText:(NSString *)text
508     [self speakText:text withVoice:nil pitch:0.0 rate:0.0];
511 //Speak a voice-specific sample text at the passed settings
512 - (void)speakDemoTextForVoice:(NSString *)voiceString withPitch:(float)pitch andRate:(float)rate
513 {               
514         NSString        *demoText;      
515         int                     voiceIndex;
516         SUSpeaker       *theSpeaker;
518         [self _stopSpeakingNow];
519         theSpeaker = [self _speakerForVoice:voiceString index:&voiceIndex];
520         demoText = [theSpeaker demoTextForVoiceAtIndex:((voiceIndex != NSNotFound) ? voiceIndex : -1)];
522         [self speakText:demoText
523                   withVoice:voiceString
524                           pitch:pitch
525                            rate:rate];
528 //Return an array of voices in the same order as expected by SUSpeaker
529 - (NSArray *)voices
531     return [self voiceArray];
534 //The systemwide default rate. This is cached when first used; it does not update if the systemwide default updates.
535 - (float)defaultRate
537     [self initDefaultVoiceIfNecessary];
538     return defaultRate;
541 //The systemwide default pitch. This is cached when first used; it does not update if the systemwide default updates.
542 - (float)defaultPitch
544     [self initDefaultVoiceIfNecessary];
545     return defaultPitch;
548 //add text & voiceString to the speech queue and attempt to speak text now
549 //pass voice as nil to use default voice
550 //pass pitch as 0 to use default pitch
551 //pass rate as 0 to use default rate
552 - (void)speakText:(NSString *)text withVoice:(NSString *)voiceString pitch:(float)pitch rate:(float)rate
554     if(text && [text length]){
555                 if(!muteSounds){
556                         NSMutableDictionary *dict;
557                         
558                         dict = [[NSMutableDictionary alloc] init];
559                         
560                         if(text){
561                                 [dict setObject:text forKey:TEXT_TO_SPEAK];
562                         }
563                         
564                         if(voiceString) [dict setObject:voiceString forKey:VOICE];                      
565                         if(pitch > FLT_EPSILON) [dict setObject:[NSNumber numberWithFloat:pitch] forKey:PITCH];
566                         if(rate  > FLT_EPSILON) [dict setObject:[NSNumber numberWithFloat:rate]  forKey:RATE];
568                         [speechArray addObject:dict];
569                         [dict release];
570                         
571                         [self speakNext];
572                 }
573     }
576 //attempt to speak the next item in the queue
577 - (void)speakNext
579     //we have items left to speak and aren't already speaking
580     if([speechArray count] && !speaking){
581                 //don't speak on top of other apps; instead, wait 1 second and try again
582                 if(SpeechBusySystemWide() > 0){
583                         [self performSelector:@selector(speakNext)
584                                            withObject:nil
585                                            afterDelay:1.0];
586                         return;
587                 }
589                 speaking = YES;
590                 NSMutableDictionary *dict = [speechArray objectAtIndex:0];
591                 NSString                        *text = [dict objectForKey:TEXT_TO_SPEAK];
592                 NSNumber                        *pitchNumber = [dict objectForKey:PITCH];
593                 NSNumber                        *rateNumber = [dict objectForKey:RATE];
594                 SUSpeaker                       *theSpeaker = [self _speakerForVoice:[dict objectForKey:VOICE] index:NULL];
596                 [theSpeaker setPitch:(pitchNumber ? [pitchNumber floatValue] : defaultPitch)];
597                 [theSpeaker setRate:(rateNumber ?  [rateNumber floatValue] : defaultRate)];
598                 [theSpeaker setVolume:customVolume];
600                 [theSpeaker speakText:text];
601                 [speechArray removeObjectAtIndex:0];
602     }
605 - (IBAction)didFinishSpeaking:(SUSpeaker *)theSpeaker
607     speaking = NO;
608     [self speakNext];
611 //Immediately stop speaking
612 - (void)_stopSpeakingNow
614         [speechArray removeAllObjects];
616         [speaker_defaultVoice stopSpeaking];
617         [speaker_variableVoice stopSpeaking];
620 //INitialize the default voice if it has not yet been done
621 - (void)initDefaultVoiceIfNecessary
623     if(!speaker_defaultVoice){
624                 speaker_defaultVoice = [[SUSpeaker alloc] init];
625                 [speaker_defaultVoice setDelegate:self];
626                 defaultRate = [speaker_defaultVoice rate];
627                 defaultPitch = [speaker_defaultVoice pitch];
628     }
631 //Return the SUSpeaker which should be used for a given voice name, configured for that voice. Optionally, return
632 //the index of that voice in our array by reference.
633 - (SUSpeaker *)_speakerForVoice:(NSString *)voiceString index:(int *)voiceIndex;
635         int theIndex = (voiceIndex ? *voiceIndex : 0);
636         SUSpeaker       *theSpeaker;
638         if(voiceString){
639                 theIndex = [[self voiceArray] indexOfObject:voiceString];
640         }else{
641                 theIndex = NSNotFound;
642         }
644         if(theIndex != NSNotFound){
645                 if(!speaker_variableVoice){ //initVariableVoiceifNecessary
646                         speaker_variableVoice = [[SUSpeaker alloc] init];
647                         [speaker_variableVoice setDelegate:self];
648                 }
649                 theSpeaker = speaker_variableVoice;
650                 [theSpeaker setVoiceUsingIndex:theIndex];
652         }else{
653                 [self initDefaultVoiceIfNecessary];
654                 theSpeaker = speaker_defaultVoice;
655         }
657         if (voiceIndex) *voiceIndex = theIndex;
658                 
659         return theSpeaker;
662 - (NSArray *)voiceArray
664     static NSArray      *_voiceArray = nil;
665         
666         if (!_voiceArray) {
667                 NSArray                 *originalVoiceArray = [SUSpeaker voiceNames];
668                 NSMutableArray  *ourVoiceArray = [originalVoiceArray mutableCopy];
669                 int messedUpIndex;
670                 
671                 //Vicki, a new voice in 10.3, returns an invalid name to SUSpeaker, Vicki3Smallurrent. If we see that name,
672                 //replace it with just Vicki.  If this gets fixed in a future release of OS X, this code will simply do nothing.
673                 messedUpIndex = [ourVoiceArray indexOfObject:@"Vicki3Smallurrent"];
674                 if(messedUpIndex != NSNotFound){
675                         [ourVoiceArray replaceObjectAtIndex:messedUpIndex
676                                                                          withObject:@"Vicki"];
677                 }
678                 
679                 //ourVoiceArray is retained, so just assign it
680                 _voiceArray = ourVoiceArray;  //voiceArray will be in the same order that SUSpeaker expects
681         }
682         
683         return _voiceArray;
686 @end