SpeakingTextWindow.m

/*
     File: SpeakingTextWindow.m
 Abstract: The main window hosting all the apps speech features.
  Version: 1.5
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2013 Apple Inc. All Rights Reserved.
 
 */
 
#import "SpeakingTextWindow.h"
 
// Constants
//
NSString *  kPlainTextDataTypeString    = @"Plain Text";
NSString *  kDefaultWindowTextString    = @"Welcome to Cocoa Speech Synthesis Example.  This application provides an example of using Apple's speech synthesis technology in a Cocoa-based application.";
 
NSString *  kWordCallbackParamPosition  = @"ParamPosition";
NSString *  kWordCallbackParamLength    = @"ParamLength";
NSString *  kErrorCallbackParamPosition = @"ParamPosition";
NSString *  kErrorCallbackParamError    = @"ParamError";
 
// Prototypes
//
static pascal void  OurErrorCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, OSErr inError, long inBytePos);
static pascal void  OurTextDoneCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, const void ** nextBuf, unsigned long * byteLen, long * controlFlags);
static pascal void  OurSpeechDoneCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon);
static pascal void  OurSyncCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, OSType inSyncMessage);
static pascal void  OurPhonemeCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, short inPhonemeOpcode);
static pascal void  OurWordCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, long inWordPos, short inWordLen);
 
 
#pragma mark -
 
@implementation SpeakingTextWindow
 
/*----------------------------------------------------------------------------------------
init
 
Set the default text of the window.
----------------------------------------------------------------------------------------*/
- (id)init
{
    self = [super init];
    if (self)
    {
        // set our default window text
        const char * p = [kDefaultWindowTextString UTF8String];
        [self setTextData:[NSData dataWithBytes:p length:strlen(p)]];
        [self setTextDataType:kPlainTextDataTypeString];
    }
 
    return self;
}
 
/*----------------------------------------------------------------------------------------
 close
 
 Make sure to stop speech when closing.
 ----------------------------------------------------------------------------------------*/
- (void)close
{
    [self startStopButtonPressed:fStartStopButton];
}
 
/*----------------------------------------------------------------------------------------
setTextData:
 
Set our text data variable and update text in window if showing.
----------------------------------------------------------------------------------------*/
- (void)setTextData:(NSData *)theData
{
    fTextData = theData;
 
    // If the window is showing, update the text view.
    if (fSpokenTextView)
    {
        if ([[self textDataType] isEqualToString:@"RTF Document"])
            [fSpokenTextView replaceCharactersInRange:NSMakeRange(0,[[fSpokenTextView string] length]) withRTF:[self textData]];
        else
        {
            [fSpokenTextView replaceCharactersInRange:NSMakeRange(0,[[fSpokenTextView string] length])
                                           withString:[NSString stringWithUTF8String:[[self textData] bytes]]];
        }
    }
}
 
/*----------------------------------------------------------------------------------------
textData
 
Returns autoreleased copy of text data.
----------------------------------------------------------------------------------------*/
- (NSData *)textData
{
    return [fTextData copy];
}
 
/*----------------------------------------------------------------------------------------
setTextDataType:
 
Set our text data type variable.
----------------------------------------------------------------------------------------*/
- (void)setTextDataType:(NSString *)theType
{
    fTextDataType = theType;
}
 
/*----------------------------------------------------------------------------------------
textDataType
 
Returns autoreleased copy of text data.
----------------------------------------------------------------------------------------*/
- (NSString *)textDataType
{
    return [fTextDataType copy];
}
 
/*----------------------------------------------------------------------------------------
textDataType
 
Returns reference to character view for callbacks.
----------------------------------------------------------------------------------------*/
- (SpeakingCharacterView *)characterView
{
    return fCharacterView;
}
 
/*----------------------------------------------------------------------------------------
shouldDisplayWordCallbacks
 
Returns true if user has chosen to have words hightlight during synthesis.
----------------------------------------------------------------------------------------*/
- (BOOL)shouldDisplayWordCallbacks
{
    return [fHandleWordCallbacksCheckboxButton intValue];
}
 
/*----------------------------------------------------------------------------------------
shouldDisplayPhonemeCallbacks
 
Returns true if user has chosen to the character animate phonemes during synthesis.
----------------------------------------------------------------------------------------*/
- (BOOL)shouldDisplayPhonemeCallbacks
{
    return [fHandlePhonemeCallbacksCheckboxButton intValue];
}
 
/*----------------------------------------------------------------------------------------
shouldDisplayErrorCallbacks
 
Returns true if user has chosen to have an alert appear in response to an error callback.
----------------------------------------------------------------------------------------*/
- (BOOL)shouldDisplayErrorCallbacks
{
    return [fHandleErrorCallbacksCheckboxButton intValue];
}
 
/*----------------------------------------------------------------------------------------
shouldDisplaySyncCallbacks
 
Returns true if user has chosen to have an alert appear in response to an sync callback.
----------------------------------------------------------------------------------------*/
- (BOOL)shouldDisplaySyncCallbacks
{
    return [fHandleSyncCallbacksCheckboxButton intValue];
}
 
/*----------------------------------------------------------------------------------------
shouldDisplaySpeechDoneCallbacks
 
Returns true if user has chosen to have an alert appear when synthesis is finished.
----------------------------------------------------------------------------------------*/
- (BOOL)shouldDisplaySpeechDoneCallbacks
{
    return [fHandleSpeechDoneCallbacksCheckboxButton intValue];
}
 
/*----------------------------------------------------------------------------------------
shouldDisplayTextDoneCallbacks
 
Returns true if user has chosen to have an alert appear when text processing is finished.
----------------------------------------------------------------------------------------*/
- (BOOL)shouldDisplayTextDoneCallbacks
{
    return [fHandleTextDoneCallbacksCheckboxButton intValue];
}
 
/*----------------------------------------------------------------------------------------
awakeFromNib
 
This routine is call once right after our nib file is loaded.  We build our voices
pop-up menu, create a new speech channel and update our window using parameters from
the new speech channel.
----------------------------------------------------------------------------------------*/
- (void)awakeFromNib
{
    OSErr theErr = noErr;
 
    // Build the Voices pop-up menu
    {
        short       numOfVoices;
        long        voiceIndex;
        BOOL        voiceFoundAndSelected = NO;
        VoiceSpec   theVoiceSpec;
 
        // Delete the existing voices from the bottom of the menu.
        while([fVoicesPopUpButton numberOfItems] > 2)
            [fVoicesPopUpButton removeItemAtIndex:2];
 
        // Ask TTS API for each available voicez
        theErr = CountVoices(&numOfVoices);
        if (theErr != noErr)
            NSRunAlertPanel(@"CountVoices", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
        if (theErr == noErr)
        {
            for (voiceIndex = 1; voiceIndex <= numOfVoices; voiceIndex++)
            {
                VoiceDescription theVoiceDesc;
                theErr = GetIndVoice(voiceIndex, &theVoiceSpec);
                if (theErr != noErr)
                    NSRunAlertPanel(@"GetIndVoice", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
                if (theErr == noErr)
                    theErr = GetVoiceDescription(&theVoiceSpec, &theVoiceDesc, sizeof(theVoiceDesc));
                if (theErr != noErr)
                    NSRunAlertPanel(@"GetVoiceDescription", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
                if (theErr == noErr)
                {
                    // Get voice name and add it to the menu list
                    NSString *theNameString = [[NSString alloc] initWithUTF8String:(char *) &(theVoiceDesc.name[1])];
                    [fVoicesPopUpButton addItemWithTitle:theNameString];
 
                    // Selected this item if it matches our default voice spec.
                    if (theVoiceSpec.creator == fSelectedVoiceCreator && theVoiceSpec.id == fSelectedVoiceID)
                    {
                        [fVoicesPopUpButton selectItemAtIndex:voiceIndex-1];
                        voiceFoundAndSelected = YES;
                    }
 
                }
            }
 
            // User preference default if problems.
            if (! voiceFoundAndSelected && numOfVoices >= 1)
            {
                // Update our object fields with the first voice
                fSelectedVoiceCreator   = 0;
                fSelectedVoiceID        = 0;
 
                [fVoicesPopUpButton selectItemAtIndex:0];
            }
        }
        else
        {
            [fVoicesPopUpButton selectItemAtIndex:0];
        }
 
    }
 
    // Create Speech Channel configured with our desired options and callbacks
    [self createNewSpeechChannel:NULL];
 
    // Set editable default fields
    [self fillInEditableParameterFields];
 
    // Enable buttons appropriatelly
    [fStartStopButton setEnabled:YES];
    [fPauseContinueButton setEnabled:NO];
    [fSaveAsFileButton setEnabled:YES];
 
    // Set starting expresison on animated character
    [self phonemeCallbacksButtonPressed:fHandlePhonemeCallbacksCheckboxButton];
 
}
 
/*----------------------------------------------------------------------------------------
updateSpeakingControlState
 
This routine is called when appropriate to update the Start/Stop Speaking,
Pause/Continue Speaking buttons.
----------------------------------------------------------------------------------------*/
- (void)updateSpeakingControlState
{
    // Update controls based on speaking state
    //
    [fSaveAsFileButton setEnabled:!fCurrentlySpeaking];
    [fPauseContinueButton setEnabled:fCurrentlySpeaking];
    [fStartStopButton setEnabled:!fCurrentlyPaused];
 
    if (fCurrentlySpeaking)
    {
        [fStartStopButton setTitle:NSLocalizedString(@"Stop Speaking", @"Stop Speaking")];
        [fPauseContinueButton setTitle:NSLocalizedString(@"Pause Speaking", @"Pause Speaking")];
    }
    else
    {
        [fStartStopButton setTitle:NSLocalizedString(@"Start Speaking", @"Start Speaking")];
        [fSpokenTextView setSelectedRange:fOrgSelectionRange];  // Set selection length to zero.
    }
 
    if (fCurrentlyPaused)
        [fPauseContinueButton setTitle:NSLocalizedString(@"Continue Speaking", @"Continue Speaking")];
    else
        [fPauseContinueButton setTitle:NSLocalizedString(@"Pause Speaking", @"Pause Speaking")];
 
    [self enableOptionsForSpeakingState:fCurrentlySpeaking];
 
    // update parameter fields
    Fixed tempFixedValue = 0;
 
    GetSpeechInfo(fCurSpeechChannel, soRate, &tempFixedValue);
    [fRateCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
 
    GetSpeechPitch(fCurSpeechChannel, &tempFixedValue);
    [fPitchBaseCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
 
    GetSpeechInfo(fCurSpeechChannel, soPitchMod, &tempFixedValue);
    [fPitchModCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
 
    GetSpeechInfo(fCurSpeechChannel, soVolume, &tempFixedValue);
    [fVolumeCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
}
 
/*----------------------------------------------------------------------------------------
highlightWordWithParams:
 
Highlights the word currently being spoken based on text position and text length
provided in the word callback routine.
----------------------------------------------------------------------------------------*/
- (void)highlightWordWithParams:(NSDictionary *)params
{
    UInt32  selectionPosition = [params[kWordCallbackParamPosition] longValue] + fOffsetToSpokenText;
    UInt32  wordLength = [params[kWordCallbackParamLength] longValue];
 
    [fSpokenTextView scrollRangeToVisible:NSMakeRange(selectionPosition, wordLength)];
    [fSpokenTextView setSelectedRange:NSMakeRange(selectionPosition, wordLength)];
    [fSpokenTextView display];
}
 
/*----------------------------------------------------------------------------------------
displayErrorAlertWithParams:
 
Displays an alert describing a text processing error provided in the error callback.
----------------------------------------------------------------------------------------*/
- (void)displayErrorAlertWithParams:(NSDictionary *)params
{
    UInt32  errorPosition = [params[kErrorCallbackParamPosition] longValue] + fOffsetToSpokenText;
    UInt32  errorCode = [params[kErrorCallbackParamError] longValue];
 
    if (errorCode != fLastErrorCode)
    {
        OSErr theErr = noErr;
        unsigned long alertButtonClicked;
        NSString *theMessageStr = NULL;
 
        // Tell engine to pause while we display this dialog.
        theErr = PauseSpeechAt(fCurSpeechChannel, kImmediate);
        if (theErr != noErr)
            NSRunAlertPanel(@"PauseSpeechAt", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
        // Select offending character
        [fSpokenTextView setSelectedRange:NSMakeRange(errorPosition, 1)];
        [fSpokenTextView display];
 
        // Display error alert, and stop or continue based on user's desires
        theMessageStr = [NSString stringWithFormat:@"Error #%ld occurred at position %ld in the text.", (long) errorCode, (long) errorPosition];
        alertButtonClicked = NSRunAlertPanel(@"Text Processing Error", theMessageStr, @"Stop", NULL, @"Continue");
        if (alertButtonClicked == 1)
            [self startStopButtonPressed:fStartStopButton];
        else
        {
            theErr = ContinueSpeech(fCurSpeechChannel);
            if (theErr != noErr)
                NSRunAlertPanel(@"ContinueSpeech", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
        }
 
        fLastErrorCode = errorCode;
    }
}
 
/*----------------------------------------------------------------------------------------
displaySyncAlertWithMessage:
 
Displays an alert with information about a sync command in response to a sync callback.
----------------------------------------------------------------------------------------*/
- (void)displaySyncAlertWithMessage:(NSNumber *)messageNumber
{
    OSErr theErr = noErr;
    unsigned long alertButtonClicked;
    NSString *theMessageStr = NULL;
 
    // Tell engine to pause while we display this dialog.
    theErr = PauseSpeechAt(fCurSpeechChannel, kImmediate);
    if (theErr != noErr)
        NSRunAlertPanel(@"PauseSpeechAt", [NSString stringWithFormat:@"Error #%i returned.", theErr], @"Oh?", NULL, NULL);
 
    // Display error alert, and stop or continue based on user's desires
    UInt32  theMessageValue = [messageNumber longValue];
    theMessageStr = [NSString stringWithFormat:@"Sync embedded command was discovered containing message %ld ('%4s').", (long) theMessageValue, (char*) &theMessageValue];
    alertButtonClicked = NSRunAlertPanel(@"Sync Callback", theMessageStr, @"Stop", NULL, @"Continue");
    if (alertButtonClicked == 1)
        [self startStopButtonPressed:fStartStopButton];
    else
    {
        theErr = ContinueSpeech(fCurSpeechChannel);
        if (theErr != noErr)
            NSRunAlertPanel(@"ContinueSpeech", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
 
}
 
/*----------------------------------------------------------------------------------------
speechIsDone
 
Updates user interface and optionally displays an alert when generation of speech is
finish.
----------------------------------------------------------------------------------------*/
- (void)speechIsDone
{
    fCurrentlySpeaking = NO;
    [self updateSpeakingControlState];
    [self enableCallbackControlsBasedOnSavingToFileFlag:NO];
 
    if ([self shouldDisplaySpeechDoneCallbacks])
        NSRunAlertPanel(@"Speech Done", @"Generation of synthesized speech is finished.", @"OK", NULL, NULL);
}
 
/*----------------------------------------------------------------------------------------
displayTextDoneAlert
 
Displays an alert in response to a text done callback.
----------------------------------------------------------------------------------------*/
- (void)displayTextDoneAlert
{
    OSErr theErr = noErr;
    unsigned long alertButtonClicked;
 
    // Tell engine to pause while we display this dialog.
    theErr = PauseSpeechAt(fCurSpeechChannel, kImmediate);
    if (theErr != noErr)
        NSRunAlertPanel(@"PauseSpeechAt", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
    // Display error alert, and stop or continue based on user's desires
    alertButtonClicked = NSRunAlertPanel(@"Text Done Callback", @"Processing of the text has completed.", @"Stop", NULL, @"Continue");
    if (alertButtonClicked == 1)
        [self startStopButtonPressed:fStartStopButton];
    else
    {
        theErr = ContinueSpeech(fCurSpeechChannel);
        if (theErr != noErr)
            NSRunAlertPanel(@"ContinueSpeech", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
}
 
 
/*----------------------------------------------------------------------------------------
startStopButtonPressed:
 
An action method called when the user clicks the "Start Speaking"/"Stop Speaking"
button.  We either start or stop speaking based on the current speaking state.
----------------------------------------------------------------------------------------*/
- (IBAction)startStopButtonPressed:(id)sender
{
    OSErr theErr = noErr;
 
    if (fCurrentlySpeaking)
    {
        long whereToStop;
 
        // Grab where to stop at value from radio buttons
        if ([fAfterWordRadioButton intValue])
            whereToStop = kEndOfWord;
        else if ([fAfterSentenceRadioButton intValue])
            whereToStop = kEndOfSentence;
        else
            whereToStop = kImmediate;
 
        if (whereToStop == kImmediate)
        {
            // NOTE:    We could just call StopSpeechAt with kImmediate, but for test purposes
            //          we exercise the StopSpeech routine.
            theErr = StopSpeech(fCurSpeechChannel);
            if (theErr != noErr)
                NSRunAlertPanel(@"StopSpeech", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
        }
        else
        {
            theErr = StopSpeechAt(fCurSpeechChannel, whereToStop);
            if (theErr != noErr)
                NSRunAlertPanel(@"StopSpeechAt", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
        }
 
        fCurrentlySpeaking = NO;
        [self updateSpeakingControlState];
    }
    else
    {
        [self startSpeakingTextViewToURL:NULL];
    }
}
 
/*----------------------------------------------------------------------------------------
saveAsButtonPressed:
 
An action method called when the user clicks the "Save As File" button.  We ask user
to specify where to save the file, then start speaking to this file.
----------------------------------------------------------------------------------------*/
- (IBAction)saveAsButtonPressed:(id)sender
{
    NSURL *selectedFileURL = NULL;
 
    NSSavePanel *theSavePanel = [NSSavePanel savePanel];
    [theSavePanel setPrompt:NSLocalizedString(@"Save", @"Save")];
    [theSavePanel setNameFieldStringValue:@"Synthesized Speech.aiff"];
 
    if (NSFileHandlingPanelOKButton == [theSavePanel runModal])
    {
        selectedFileURL = [theSavePanel URL];
        [self startSpeakingTextViewToURL:selectedFileURL];
    }
}
 
/*----------------------------------------------------------------------------------------
startSpeakingTextViewToURL:
 
This method sets up the speech channel and begins the speech synthesis
process, optionally speaking to a file instead playing through the speakers.
----------------------------------------------------------------------------------------*/
- (void)startSpeakingTextViewToURL:(NSURL *)url
{
    OSErr theErr = noErr;
    NSString *theViewText;
 
    // Grab the selection substring, or if no selection then grab entire text.
    fOrgSelectionRange = [fSpokenTextView selectedRange];
 
    if (fOrgSelectionRange.length == 0)
    {
        theViewText = [fSpokenTextView string];
        fOffsetToSpokenText = 0;
    }
    else
    {
        theViewText = [[fSpokenTextView string] substringWithRange:fOrgSelectionRange];
        fOffsetToSpokenText = fOrgSelectionRange.location;
    }
 
    // Setup our callbacks
    fSavingToFile = (url != NULL);
    if (theErr == noErr)
    {
        theErr = SetSpeechInfo(fCurSpeechChannel, soErrorCallBack, fSavingToFile?NULL:OurErrorCallBackProc);
        if (theErr != noErr)
            NSRunAlertPanel(@"SetSpeechInfo(soErrorCallBack)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
    if (theErr == noErr)
    {
        theErr = SetSpeechInfo(fCurSpeechChannel, soPhonemeCallBack, fSavingToFile?NULL:OurPhonemeCallBackProc);
        if (theErr != noErr)
            NSRunAlertPanel(@"SetSpeechInfo(soPhonemeCallBack)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
    if (theErr == noErr)
    {
        theErr = SetSpeechInfo(fCurSpeechChannel, soSpeechDoneCallBack, OurSpeechDoneCallBackProc);
        if (theErr != noErr)
            NSRunAlertPanel(@"SetSpeechInfo(soSpeechDoneCallBack)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
    if (theErr == noErr)
    {
        theErr = SetSpeechInfo(fCurSpeechChannel, soSyncCallBack, fSavingToFile?NULL:OurSyncCallBackProc);
        if (theErr != noErr)
            NSRunAlertPanel(@"SetSpeechInfo(soSyncCallBack)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
 
    if (theErr == noErr)
    {
        theErr = SetSpeechInfo(fCurSpeechChannel, soTextDoneCallBack, fSavingToFile?NULL:OurTextDoneCallBackProc);
        if (theErr != noErr)
            NSRunAlertPanel(@"SetSpeechInfo(soTextDoneCallBack)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
    if (theErr == noErr)
    {
        theErr = SetSpeechInfo(fCurSpeechChannel, soWordCallBack, fSavingToFile?NULL:OurWordCallBackProc);
        if (theErr != noErr)
            NSRunAlertPanel(@"SetSpeechInfo(soWordCallBack)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
 
    // Set URL to save file to disk
    SetSpeechInfo(fCurSpeechChannel, soOutputToFileWithCFURL, (__bridge const void *)(url));
 
    // Convert NSString to cString.
    char *theTextToSpeak = (char *)[theViewText UTF8String];
    if (theTextToSpeak)
    {
        // We want the text view the active view.  Also saves any parameters currently being edited.
        [fWindow makeFirstResponder:fSpokenTextView];
 
        theErr = SpeakText(fCurSpeechChannel, theTextToSpeak, strlen(theTextToSpeak));
        if (theErr == noErr)
        {
            // Update our vars
            fLastErrorCode = 0;
            fLastSpeakingValue = NO;
            fLastPausedValue = NO;
            fCurrentlySpeaking = YES;
            fCurrentlyPaused = NO;
            [self updateSpeakingControlState];
        }
        else
        {
            NSRunAlertPanel(@"SpeakText", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
        }
    }
 
    [self enableCallbackControlsBasedOnSavingToFileFlag:fSavingToFile];
}
 
/*----------------------------------------------------------------------------------------
pauseContinueButtonPressed:
 
An action method called when the user clicks the "Pause Speaking"/"Continue Speaking"
button.  We either pause or continue speaking based on the current speaking state.
----------------------------------------------------------------------------------------*/
- (IBAction)pauseContinueButtonPressed:(id)sender
{
    OSErr theErr = noErr;
 
    if (fCurrentlyPaused)
    {
        // We want the text view the active view.  Also saves any parameters currently being edited.
        [fWindow makeFirstResponder:fSpokenTextView];
 
        theErr = ContinueSpeech(fCurSpeechChannel);
        if (theErr != noErr)
            NSRunAlertPanel(@"ContinueSpeech", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
        fCurrentlyPaused = NO;
        [self updateSpeakingControlState];
    }
    else
    {
        long whereToPause;
 
        // Figure out where to stop from radio buttons
        if ([fAfterWordRadioButton intValue])
            whereToPause = kEndOfWord;
        else if ([fAfterSentenceRadioButton intValue])
            whereToPause = kEndOfSentence;
        else
            whereToPause = kImmediate;
 
        theErr = PauseSpeechAt(fCurSpeechChannel, whereToPause);
        if (theErr != noErr)
            NSRunAlertPanel(@"PauseSpeechAt", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
        fCurrentlyPaused = YES;
        [self updateSpeakingControlState];
    }
}
 
/*----------------------------------------------------------------------------------------
voicePopupSelected:
 
An action method called when the user selects a new voice from the Voices pop-up
menu.  We ask the speech channel to use the selected voice.  If the current
speech channel cannot use the selected voice, we close and open new speech
channel with the selecte voice.
----------------------------------------------------------------------------------------*/
- (IBAction) voicePopupSelected:(id)sender
{
    OSErr theErr = noErr;
    VoiceSpec theVoiceSpec;
    long theSelectedMenuIndex = [sender indexOfSelectedItem];
 
    if (theSelectedMenuIndex == 0)
    {
        // Use the default voice from preferences.
        //
        // Our only choice is to close and reopen the speech channel to get the default voice.
        fSelectedVoiceCreator = 0;
        theErr = [self createNewSpeechChannel:NULL];
        if (theErr != noErr)
            NSRunAlertPanel(@"createNewSpeechChannel", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
    else
    {
        // Use the voice the user selected.
        //
        theErr = GetIndVoice([sender indexOfSelectedItem] - 1, &theVoiceSpec);
        if (theErr != noErr)
            NSRunAlertPanel(@"GetIndVoice", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
        if (theErr == noErr)
        {
            // Update our object fields with the selection
            fSelectedVoiceCreator   = theVoiceSpec.creator;
            fSelectedVoiceID        = theVoiceSpec.id;
 
            // Change the current voice.  If it needs another engine, then dispose the current channel and open another
            if (SetSpeechInfo(fCurSpeechChannel, soCurrentVoice, &theVoiceSpec) == incompatibleVoice)
            {
                theErr = [self createNewSpeechChannel:&theVoiceSpec];
                if (theErr != noErr)
                    NSRunAlertPanel(@"createNewSpeechChannel", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
            }
        }
    }
 
    // Set editable default fields
    if (fCurSpeechChannel)
        [self fillInEditableParameterFields];
}
 
/*----------------------------------------------------------------------------------------
charByCharCheckboxSelected:
 
An action method called when the user checks/unchecks the Character-By-Character
mode checkbox.  We tell the speech channel to use this setting.
----------------------------------------------------------------------------------------*/
- (IBAction)charByCharCheckboxSelected:(id)sender
{
    OSErr theErr = noErr;
 
    OSType theMode;
    if ([fCharByCharCheckboxButton intValue])
        theMode = modeLiteral;
    else
        theMode = modeNormal;
 
    theErr = SetSpeechInfo(fCurSpeechChannel, soCharacterMode, &theMode);
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechInfo(soCharacterMode)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
}
 
/*----------------------------------------------------------------------------------------
digitByDigitCheckboxSelected:
 
An action method called when the user checks/unchecks the Digit-By-Digit
mode checkbox.  We tell the speech channel to use this setting.
----------------------------------------------------------------------------------------*/
- (IBAction)digitByDigitCheckboxSelected:(id)sender
{
    OSErr theErr = noErr;
 
    OSType theMode;
    if ([fDigitByDigitCheckboxButton intValue])
        theMode = modeLiteral;
    else
        theMode = modeNormal;
 
    theErr = SetSpeechInfo(fCurSpeechChannel, soNumberMode, &theMode);
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechInfo(soNumberMode)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
}
 
/*----------------------------------------------------------------------------------------
digitByDigitCheckboxSelected:
 
An action method called when the user checks/unchecks the Phoneme input
mode checkbox.  We tell the speech channel to use this setting.
----------------------------------------------------------------------------------------*/
- (IBAction)phonemeModeCheckboxSelected:(id)sender
{
    OSErr theErr = noErr;
 
    OSType theMode;
    if ([fPhonemeModeCheckboxButton intValue])
        theMode = modePhonemes;
    else
        theMode = modeText;
 
    theErr = SetSpeechInfo(fCurSpeechChannel, soInputMode, &theMode);
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechInfo(soInputMode)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
}
 
/*----------------------------------------------------------------------------------------
dumpPhonemesSelected:
 
An action method called when the user clicks the Dump Phonemes button.  We ask
the speech channel for a phoneme representation of the window text then save the
result to a text file at a location determined by the user.
----------------------------------------------------------------------------------------*/
- (IBAction)dumpPhonemesSelected:(id)sender
{
    NSSavePanel *panel = [NSSavePanel savePanel];
 
    if ([panel runModal])
    {
        long theNumOfPhonBytes  = 0;
 
        // Get and speech text
        char *  theTextToSpeak = (char *)[[fSpokenTextView string] UTF8String];
        if (theTextToSpeak)
        {
            Handle thePhonemeHandle = NewHandle(0);
            if (thePhonemeHandle)
            {
                OSErr theErr = TextToPhonemes(fCurSpeechChannel, (Ptr)theTextToSpeak, (long)strlen(theTextToSpeak), thePhonemeHandle, (long *)&theNumOfPhonBytes);
                if (theErr != noErr)
                    NSRunAlertPanel(@"TextToPhonemes", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
                else
                {
                    // Create NSData object with resulting handle and then write to disk.
                    NSData *    thePhonemeText = [NSData dataWithBytesNoCopy:*thePhonemeHandle length:theNumOfPhonBytes freeWhenDone:NO];
                    if(! [thePhonemeText writeToURL:[panel URL] atomically:NO])
                        NSRunAlertPanel(@"TextToPhonemes", @"Phoneme file could not be written to disk", @"Oh?", NULL, NULL);
                }
 
                DisposeHandle(thePhonemeHandle);
            }
            else
                NSRunAlertPanel(@"TextToPhonemes", @"Could not allocate handle for phonemes", @"Oh?", NULL, NULL);
        }
    }
 
}
 
/*----------------------------------------------------------------------------------------
useDictionarySelected:
 
An action method called when the user clicks the Use Dictionary button.
----------------------------------------------------------------------------------------*/
- (IBAction)useDictionarySelected:(id)sender
{
    // Open file.
    NSOpenPanel *panel = [NSOpenPanel openPanel];
 
    [panel setAllowsMultipleSelection:YES];
    if ([panel runModal])
    {
        NSEnumerator *fileURLsEnumerator = [[panel URLs]objectEnumerator];
        NSURL *fileURL = NULL;
        while (fileURL = [fileURLsEnumerator nextObject])
        {
            // Read dictionary file into NSData object.
            NSData *speechSynthesisDictionaryData = [NSData dataWithContentsOfURL:fileURL];
            if (speechSynthesisDictionaryData)
            {
                // Allocate handle, then copy the data into the handle and pass it to UseDictionary.
                Handle  theFileDataHandle = NewHandle([speechSynthesisDictionaryData length]);
                if (theFileDataHandle)
                {
                    memcpy(*theFileDataHandle, [speechSynthesisDictionaryData bytes], [speechSynthesisDictionaryData length]);
 
                    OSErr theErr = UseDictionary(fCurSpeechChannel, theFileDataHandle);
                    DisposeHandle(theFileDataHandle);
 
                    if (theErr != noErr)
                        NSRunAlertPanel(@"UseDictionary", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
                }
                else
                    NSRunAlertPanel(@"TextToPhonemes", @"Could not allocate handle for dictionary", @"Oh?", NULL, NULL);
            }
            else
                NSRunAlertPanel(@"TextToPhonemes", [NSString stringWithFormat:@"Error occurred reading %@.", [fileURL path]], @"Oh?", NULL, NULL);
        }
    }
}
 
/*----------------------------------------------------------------------------------------
rateChanged:
 
An action method called when the user changes the rate field.  We tell the speech
channel to use this setting.
----------------------------------------------------------------------------------------*/
- (IBAction)rateChanged:(id)sender
{
    OSErr theErr = noErr;
 
    Fixed theNewValue = [fRateDefaultEditableField doubleValue] * 65536.0;
    theErr = SetSpeechInfo(fCurSpeechChannel, soRate, &theNewValue);
 
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechInfo(soRate)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    else
        [fRateCurrentStaticField setDoubleValue:[fRateDefaultEditableField doubleValue]];
}
 
/*----------------------------------------------------------------------------------------
pitchBaseChanged:
 
An action method called when the user changes the pitch base field.  We tell the speech
channel to use this setting.
----------------------------------------------------------------------------------------*/
- (IBAction)pitchBaseChanged:(id)sender
{
    OSErr theErr = noErr;
 
    Fixed theNewValue = [fPitchBaseDefaultEditableField doubleValue] * 65536.0;
    theErr = SetSpeechPitch(fCurSpeechChannel, theNewValue);
 
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechPitch", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    else
        [fPitchBaseCurrentStaticField setDoubleValue:[fPitchBaseDefaultEditableField doubleValue]];
}
 
/*----------------------------------------------------------------------------------------
pitchModChanged:
 
An action method called when the user changes the pitch modulation field.  We tell
the speech channel to use this setting.
----------------------------------------------------------------------------------------*/
- (IBAction)pitchModChanged:(id)sender
{
    OSErr theErr = noErr;
 
    Fixed theNewValue = [fPitchModDefaultEditableField doubleValue] * 65536.0;
    theErr = SetSpeechInfo(fCurSpeechChannel, soPitchMod, &theNewValue);
 
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechInfo(soPitchMod)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    else
        [fPitchModCurrentStaticField setDoubleValue:[fPitchModDefaultEditableField doubleValue]];
}
 
/*----------------------------------------------------------------------------------------
volumeChanged:
 
An action method called when the user changes the volume field.  We tell
the speech channel to use this setting.
----------------------------------------------------------------------------------------*/
- (IBAction)volumeChanged:(id)sender
{
    OSErr theErr = noErr;
 
    Fixed theNewValue = [fVolumeDefaultEditableField doubleValue] * 65536.0;
    theErr = SetSpeechInfo(fCurSpeechChannel, soVolume, &theNewValue);
 
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechInfo(soVolume)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    else
        [fVolumeCurrentStaticField setDoubleValue:[fVolumeDefaultEditableField doubleValue]];
}
 
/*----------------------------------------------------------------------------------------
resetSelected:
 
An action method called when the user clicks the Use Defaults button.  We tell
the speech channel to use this the default settings.
----------------------------------------------------------------------------------------*/
- (IBAction)resetSelected:(id)sender
{
    OSErr theErr = noErr;
 
    theErr = SetSpeechInfo(fCurSpeechChannel, soReset, NULL);
 
    [self fillInEditableParameterFields];
 
    if (theErr != noErr)
        NSRunAlertPanel(@"SetSpeechInfo(soReset)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
}
 
- (IBAction)wordCallbacksButtonPressed:(id)sender
{
    if (![fHandleWordCallbacksCheckboxButton intValue])
        [fSpokenTextView setSelectedRange:fOrgSelectionRange];
}
 
- (IBAction)phonemeCallbacksButtonPressed:(id)sender
{
    if ([fHandlePhonemeCallbacksCheckboxButton intValue])
        [fCharacterView setExpression:kCharacterExpressionIdentifierIdle];
    else
        [fCharacterView setExpression:kCharacterExpressionIdentifierSleep];
}
 
/*----------------------------------------------------------------------------------------
enableOptionsForSpeakingState:
 
Updates controls in the Option tab panel based on the passed speakingNow flag.
----------------------------------------------------------------------------------------*/
- (void)enableOptionsForSpeakingState:(BOOL)speakingNow
{
    [fVoicesPopUpButton setEnabled:!speakingNow];
    [fCharByCharCheckboxButton setEnabled:!speakingNow];
    [fDigitByDigitCheckboxButton setEnabled:!speakingNow];
    [fPhonemeModeCheckboxButton setEnabled:!speakingNow];
    [fDumpPhonemesButton setEnabled:!speakingNow];
    [fUseDictionaryButton setEnabled:!speakingNow];
}
 
/*----------------------------------------------------------------------------------------
enableCallbackControlsForSavingToFile:
 
Updates controls in the Callback tab panel based on the passed savingToFile flag.
----------------------------------------------------------------------------------------*/
- (void)enableCallbackControlsBasedOnSavingToFileFlag:(BOOL)savingToFile
{
    [fHandleWordCallbacksCheckboxButton setEnabled:!savingToFile];
    [fHandlePhonemeCallbacksCheckboxButton setEnabled:!savingToFile];
    [fHandleSyncCallbacksCheckboxButton setEnabled:!savingToFile];
    [fHandleErrorCallbacksCheckboxButton setEnabled:!savingToFile];
    [fHandleTextDoneCallbacksCheckboxButton setEnabled:!savingToFile];
 
    if (savingToFile || [fHandlePhonemeCallbacksCheckboxButton intValue] == 0)
        [fCharacterView setExpression:kCharacterExpressionIdentifierSleep];
    else
        [fCharacterView setExpression:kCharacterExpressionIdentifierIdle];
}
 
/*----------------------------------------------------------------------------------------
fillInEditableParameterFields
 
Updates "Current" fields in the Parameters tab panel based on the current state of the
speech channel.
----------------------------------------------------------------------------------------*/
- (void)fillInEditableParameterFields
{
    Fixed tempFixedValue = 0;
 
    GetSpeechInfo(fCurSpeechChannel, soRate, &tempFixedValue);
    [fRateDefaultEditableField setDoubleValue:(tempFixedValue / 65536.0)];
    [fRateCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
 
    GetSpeechPitch(fCurSpeechChannel, &tempFixedValue);
    [fPitchBaseDefaultEditableField setDoubleValue:(tempFixedValue / 65536.0)];
    [fPitchBaseCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
 
    GetSpeechInfo(fCurSpeechChannel, soPitchMod, &tempFixedValue);
    [fPitchModDefaultEditableField setDoubleValue:(tempFixedValue / 65536.0)];
    [fPitchModCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
 
    GetSpeechInfo(fCurSpeechChannel, soVolume, &tempFixedValue);
    [fVolumeDefaultEditableField setDoubleValue:(tempFixedValue / 65536.0)];
    [fVolumeCurrentStaticField setDoubleValue:(tempFixedValue / 65536.0)];
}
 
/*----------------------------------------------------------------------------------------
createNewSpeechChannel:
 
Create a new speech channel for the given voice spec.  A nil voice spec pointer
causes the speech channel to use the default voice.  Any existing speech channel
for this window is closed first.
----------------------------------------------------------------------------------------*/
- (OSErr)createNewSpeechChannel:(VoiceSpec *)voiceSpec
{
    OSErr theErr = noErr;
 
    // Dispose of the current one, if present.
    if (fCurSpeechChannel)
    {
        theErr = DisposeSpeechChannel(fCurSpeechChannel);
        if (theErr != noErr)
            NSRunAlertPanel(@"DisposeSpeechChannel", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
 
        fCurSpeechChannel = NULL;
    }
 
    // Create a speech channel
    if (theErr == noErr)
    {
        theErr = NewSpeechChannel(voiceSpec, &fCurSpeechChannel);
        if (theErr != noErr)
            NSRunAlertPanel(@"NewSpeechChannel", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
 
    // Setup our refcon to the document controller object so we have access within our Speech callbacks
    if (theErr == noErr)
    {
        theErr = SetSpeechInfo(fCurSpeechChannel, soRefCon, (__bridge const void *)(self));
        if (theErr != noErr)
            NSRunAlertPanel(@"SetSpeechInfo(soRefCon)", [NSString stringWithFormat:@"Error #%d returned.", theErr], @"Oh?", NULL, NULL);
    }
 
    return theErr;
}
 
/*----------------------------------------------------------------------------------------
windowNibName
 
Part of the NSDocument support.  Called by NSDocument to return the nib file name of
the document.
----------------------------------------------------------------------------------------*/
- (NSString *)windowNibName
{
    return @"SpeakingTextWindow";
}
 
/*----------------------------------------------------------------------------------------
windowControllerDidLoadNib:
 
Part of the NSDocument support.  Called by NSDocument after the nib has been loaded
to udpate window as appropriate.
----------------------------------------------------------------------------------------*/
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
    [super windowControllerDidLoadNib:aController];
 
    // Update the window text from data
    if ([[self textDataType] isEqualToString:@"RTF Document"])
        [fSpokenTextView replaceCharactersInRange:NSMakeRange(0,[[fSpokenTextView string] length]) withRTF:[self textData]];
    else
        [fSpokenTextView replaceCharactersInRange:NSMakeRange(0,[[fSpokenTextView string] length]) withString:[NSString stringWithUTF8String:[[self textData] bytes]]];
}
 
/*----------------------------------------------------------------------------------------
dataRepresentationOfType:
 
Part of the NSDocument support.  Called by NSDocument to read data from file.
----------------------------------------------------------------------------------------*/
- (NSData *)dataRepresentationOfType:(NSString *)aType
{
    // Write text to file.
    if ([aType isEqualToString:@"RTF Document"])
        [self setTextData:[fSpokenTextView RTFFromRange:NSMakeRange(0,[[fSpokenTextView string] length])]];
    else
        [self setTextData:[NSData dataWithBytes:[[fSpokenTextView string] cString] length:[[fSpokenTextView string] cStringLength]]];
 
    return [self textData];
}
 
/*----------------------------------------------------------------------------------------
loadDataRepresentation: ofType:
 
Part of the NSDocument support.  Called by NSDocument to write data to file.
----------------------------------------------------------------------------------------*/
- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
    // Read the opened file.
    [self setTextData:data];
    [self setTextDataType:aType];
 
    return YES;
}
 
@end
 
#pragma mark - Callback routines
 
//
// AN IMPORTANT NOTE ABOUT CALLBACKS AND THREADS
//
// All speech synthesis callbacks, except for the Text Done callback, call their specified routine on a
// thread other than the main thread.  Performing certain actions directly from a speech synthesis callback
// routine may cause your program to crash without certain safe gaurds.  In this example, we use the NSThread
// method performSelectorOnMainThread:withObject:waitUntilDone: to safely update the user interface and
// interact with our objects using only the main thread.
//
// Depending on your needs you may be able to specify your Cocoa application is multiple threaded
// then preform actions directly from the speech synthesis callback routines.  To indicate your Cocoa
// application is mulitthreaded, call the following line before calling speech synthesis routines for
// the first time:
//
//    [NSThread detachNewThreadSelector:@selector(self) toTarget:self withObject:nil];
//
 
 
/*----------------------------------------------------------------------------------------
OurErrorCallBackProc
 
Called by speech channel when an error occurs during processing of text to speak.
----------------------------------------------------------------------------------------*/
pascal void OurErrorCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, OSErr inError, long inBytePos)
{
    @autoreleasepool {
        SpeakingTextWindow *stw = (__bridge SpeakingTextWindow *)inRefCon;
 
    if ([stw shouldDisplayTextDoneCallbacks])
        [stw performSelectorOnMainThread:@selector(displayErrorAlertWithParams:)
                              withObject:@{kErrorCallbackParamPosition: @(inBytePos), kErrorCallbackParamError:[NSNumber numberWithLong:inError]}
                           waitUntilDone:NO];
 
    }
}
 
/*----------------------------------------------------------------------------------------
OurTextDoneCallBackProc
 
Called by speech channel when all text has been processed.  Additional text can be
passed back to continue processing.
----------------------------------------------------------------------------------------*/
pascal void OurTextDoneCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, const void **nextBuf, unsigned long *byteLen, long *controlFlags)
{
    @autoreleasepool {
        SpeakingTextWindow *stw = (__bridge SpeakingTextWindow *)inRefCon;
 
    *nextBuf = NULL;
 
    if ([stw shouldDisplayTextDoneCallbacks])
        [stw performSelectorOnMainThread:@selector(displayTextDoneAlert)
                              withObject:NULL
                           waitUntilDone:NO];
 
    }
}
 
/*----------------------------------------------------------------------------------------
OurSpeechDoneCallBackProc
 
Called by speech channel when all speech has been generated.
----------------------------------------------------------------------------------------*/
pascal void OurSpeechDoneCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon)
{
    @autoreleasepool {
        SpeakingTextWindow *stw = (__bridge SpeakingTextWindow *)inRefCon;
 
    [stw performSelectorOnMainThread:@selector(speechIsDone)
                          withObject:NULL
                       waitUntilDone:NO];
    }
}
 
/*----------------------------------------------------------------------------------------
OurSyncCallBackProc
 
Called by speech channel when it encouters a synchronization command within an
embedded speech comand in text being processed.
----------------------------------------------------------------------------------------*/
pascal void OurSyncCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, OSType inSyncMessage)
{
    @autoreleasepool {
        SpeakingTextWindow *stw = (__bridge SpeakingTextWindow *)inRefCon;
 
    if ([stw shouldDisplaySyncCallbacks])
        [stw performSelectorOnMainThread:@selector(displaySyncAlertWithMessage:)
                              withObject:[NSNumber numberWithLong:inSyncMessage]
                           waitUntilDone:NO];
 
    }
}
 
/*----------------------------------------------------------------------------------------
OurPhonemeCallBackProc
 
Called by speech channel every time a phoneme is about to be generated.  You might use
this to animate a speaking character.
----------------------------------------------------------------------------------------*/
pascal void OurPhonemeCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, short inPhonemeOpcode)
{
    @autoreleasepool {
        SpeakingTextWindow *stw = (__bridge SpeakingTextWindow *)inRefCon;
 
    if ([stw shouldDisplayPhonemeCallbacks])
        [[stw characterView] performSelectorOnMainThread:@selector(setExpressionForPhoneme:)
                                              withObject:@(inPhonemeOpcode)
                                           waitUntilDone:NO];
 
    }
}
 
/*----------------------------------------------------------------------------------------
OurWordCallBackProc
 
Called by speech channel every time a word is about to be generated.  This program
uses this callback to highlight the currently spoken word.
----------------------------------------------------------------------------------------*/
pascal void OurWordCallBackProc(SpeechChannel inSpeechChannel, SRefCon inRefCon, long inWordPos, short inWordLen)
{
    @autoreleasepool {
        SpeakingTextWindow *stw = (__bridge SpeakingTextWindow *)inRefCon;
 
    if ([stw shouldDisplayWordCallbacks])
        [stw performSelectorOnMainThread:@selector(highlightWordWithParams:)
                              withObject:@{kWordCallbackParamPosition: @(inWordPos), kWordCallbackParamLength: [NSNumber numberWithLong:inWordLen]}
                           waitUntilDone:NO];
    }
}