eudora-mac/speechutil.c

1 line
54 KiB
C
Executable File
Raw Permalink Blame History

/* Copyright (c) 2017, Computer History Museum
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted (subject to
the limitations in the disclaimer below) provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Computer History Museum nor the names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE
COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE. */
#ifdef SPEECH_ENABLED
#include "speechutil.h"
#define FILE_NUM 116
#define G4_PROBLEM 1
#pragma segment speech
pascal void TextDoneCallback (SpeechChannel chan, SpeakQueueHandle SpeakQueueHandle, const void **nextBuf, unsigned long *byteLen, long *controlFlags);
pascal void SpeechDoneCallback (SpeechChannel chan, SpeakQueueHandle SpeakQueueHandle);
pascal Boolean TalkingAlertFilter (DialogPtr dgPtr, EventRecord *event, short *item);
SpeakQueueHandle gSpeakQueue = nil; // Queue of buffers submitted to be spoken
StringPtr gTalkError = nil; // The error string we will speak
StringPtr gTalkExplain = nil; // The explanation string we will speak
StringPtr gTalkPhrase = nil; // The alert phrase we will speak
long gTalkingAlertTicks; // Ticks at which to speak alerts
short gNextPhrase = 1; // Index to the next alert phrase to be spoken
Boolean gTalkingAlertPresent = false;
extern UHandle GetMessText(MessHandle messH);
//
// SpeechAvailable
//
// Checks to see if the Speech Manager is avilable
Boolean SpeechAvailable (void)
{
#ifdef SPEECH_ENABLED
long result;
Boolean present;
if (!HasFeature (featureSpeak))
return (false);
if (present = (Gestalt (gestaltSpeechAttr, &result) == noErr))
present = (result & (1 << gestaltSpeechMgrPresent)) ? true : false;
#if TARGET_RT_MAC_CFM
if (present)
present = ((long) MakeVoiceSpec != kUnresolvedCFragSymbolAddress);
#endif
return (present);
#else
return (false);
#endif
}
//
// Speak
//
// This is a low level routine for speaking a buffer of text over an open speech channel.
// It converts the text into speech and plays the speech asynchronously.
//
// This routine accepts:
// ; a speech channel that describes how the text is to be spoken
// ; a pointer to text that is to be spoken
// ; the length of the text to be spoken
// And returns:
// ; noErr if the text is spoken successfully (or the Speech Manager is not present)
// ; Speech Manager errors
//
OSErr Speak (VoiceSpec *voice, Ptr textBuf, long textBytes)
{
SpeakQueueHandle element;
VoiceDescription info;
OSErr theError;
short oldResFile;
if (!SpeechAvailable ())
return (noErr);
theError = noErr;
// Create a queue element
if (element = NuHTempBetter (sizeof (SpeakQueueRec))) {
LDRef (element);
(*element)->validChannel = false;
(*element)->textDone = false;
(*element)->speechDone = false;
(*element)->next = nil;
oldResFile = CurResFile ();
if (voice)
(*element)->voice = *voice;
else
theError = GetVoiceDescription (voice, &info, sizeof (info));
UseResFile (oldResFile);
if (!theError) {
// Add it to our speaking queue
LL_Queue (gSpeakQueue, element, (SpeakQueueHandle));
// Make a copy of the text so we can manage our own buffer
(*element)->buffer = NuDHTempBetter (textBuf, textBytes);
theError = MemError ();
}
if (!theError) {
// Check to see if we or another app are already speaking (or making sound).
// If not, go ahead and start speaking. Otherwise, we'll have to wait for
// a pause in the conversation.
if (!SpeechBusySystemWide () && element == gSpeakQueue)
theError = SpeakOnNewChannel (gSpeakQueue);
}
// Clean up after any errors
if (theError)
ZapSpeakElement (element);
}
return (theError);
}
//
// SpeechIdle
//
// Always checks the head element in the queue, returning true if the Speech
// Manager is pending an event of some sort, false if we have nothing to do.
//
Boolean SpeechIdle (void)
{
SpeakQueueHandle element;
if (gSpeakQueue) {
if (!SpeechAvailable ())
return (false);
if ((*gSpeakQueue)->textDone && (*gSpeakQueue)->buffer)
ZapHandle ((*gSpeakQueue)->buffer);
if ((*gSpeakQueue)->speechDone && (*gSpeakQueue)->validChannel) {
DisposeSpeechChannel ((*gSpeakQueue)->channel);
element = gSpeakQueue;
LL_Remove (gSpeakQueue, element, (SpeakQueueHandle));
ZapHandle (element);
if (gSpeakQueue && !SpeechBusySystemWide ())
if (SpeakOnNewChannel (gSpeakQueue))
while (element = gSpeakQueue)
ZapSpeakElement (element);
}
}
return (gSpeakQueue ? true : false);
}
//
// CancelSpeech
//
// Should be called anytime the user presses cmd-period to cancel
// speaking.
//
// Returns true if we intercepted the cmd-period and canceled the
// speech, false if we were not speaking at the time -- indicating
// that the cmd-period was targeted at another operation.
//
Boolean CancelSpeech (void)
{
Boolean chatty;
chatty = false;
if (SpeechAvailable ())
if (SpeechBusy ()) {
SpeechShutup ();
chatty = gTalkingAlertPresent ? false : true;
}
return (chatty);
}
void SpeechShutup (void)
{
SpeakQueueHandle element;
if (!SpeechAvailable ())
return;
// Walk through the speech queue getting rid of everything
while (element = gSpeakQueue) {
if ((*element)->validChannel)
StopSpeech ((*element)->channel);
ZapSpeakElement (element);
}
}
OSErr SpeakOnNewChannel (SpeakQueueHandle element)
{
OSErr theError;
MightSwitch ();
// Grab a speech channel upon which we'll talk
if (theError = NewSpeechChannel (&(*element)->voice, &(*element)->channel))
theError = NewSpeechChannel (nil, &(*element)->channel);
if (!theError)
(void) InstallPronunciationDictionary ((*element)->channel, SPEAK_DICTIONARY);
// Setup callbacks for text done and speech done
if (!theError) {
(*element)->validChannel = true;
theError = SetupSpeechCallbacks ((*element)->channel, (long) element);
}
// Volume
{
Fixed volume = GetRLong(SPEECH_VOLUME)*6553;
SetSpeechInfo((*element)->channel, soVolume, (Ptr)&volume);
}
#ifdef NOT_YET_THIS_WILL_BE_SUPPORTED_AT_SOME_POINT_IN_THE_FUTURE
if (!theError)
theError = InstallPureVoiceOutputComponent ((*element)->channel);
#endif
// Check to see if we or another app are already speaking (or making sound).
// If not, go ahead and start speaking. Otherwise, we'll have to wait for
// a pause in the conversation.
if (!theError) {
MoveHHi ((*element)->buffer);
LDRef ((*element)->buffer);
theError = SpeakText ((*element)->channel, *(*element)->buffer, GetHandleSize ((*element)->buffer));
}
AfterSwitch ();
return (theError);
}
pascal void TextDoneCallback (SpeechChannel chan, SpeakQueueHandle SpeakQueueHandle, const void **nextBuf, unsigned long *byteLen, long *controlFlags)
{
(*SpeakQueueHandle)->textDone = true;
*nextBuf = nil;
*byteLen = 0;
}
pascal void SpeechDoneCallback (SpeechChannel chan, SpeakQueueHandle SpeakQueueHandle)
{
(*SpeakQueueHandle)->speechDone = true;
}
OSErr SetupSpeechCallbacks (SpeechChannel chan, long refcon)
{
OSErr theError;
DECLARE_UPP(TextDoneCallback,SpeechTextDone);
DECLARE_UPP(SpeechDoneCallback,SpeechDone);
INIT_UPP(TextDoneCallback,SpeechTextDone);
INIT_UPP(SpeechDoneCallback,SpeechDone);
theError = SetSpeechInfo (chan, soRefCon, (Ptr) refcon);
if (!theError)
theError = SetSpeechInfo (chan, soTextDoneCallBack, TextDoneCallbackUPP);
if (!theError)
theError = SetSpeechInfo (chan, soSpeechDoneCallBack, SpeechDoneCallbackUPP);
return (theError);
}
void ZapSpeakElement (SpeakQueueHandle element)
{
if (element) {
if ((*element)->validChannel)
DisposeSpeechChannel ((*element)->channel);
ZapHandle ((*element)->buffer);
LL_Remove (gSpeakQueue, element, (SpeakQueueHandle));
ZapHandle (element);
}
}
//
// GetVoiceDescriptionDefaultOkay
//
// Attempts to get the voice description of a particular voice, retrieving information
// about the default voice if the requested voice cannot be found.
//
// This routine accepts:
// ; a pointer to the voice we want a description of
// ; a pointer to a voice desription record into which we'll return information about
// the requested voice, or the default voice if we could not find the requested voice
// ; the length of the information we're requesting
// ; a pointer to a boolean variable into which we'll store 'true' if we were forced to
// return info about the default voice, or 'false' if the info is for the voice we
// requested
// And returns:
// ; noErr if the text is spoken successfully (or the Speech Manager is not present)
// ; userCanceledErr if the user halts the speech
// ; Speech Manager errors
//
OSErr GetVoiceDescriptionDefaultOkay (VoiceSpec *voice, VoiceDescription *info, long infoLength, Boolean *defaultVoice)
{
OSErr theError;
short resFile;
if (!SpeechAvailable ())
return (noErr);
resFile = CurResFile ();
theError = GetVoiceDescription (voice, info, infoLength);
if (!theError)
*defaultVoice = false;
else
*defaultVoice = (theError = GetVoiceDescription (nil, info, infoLength)) ? false : true;
UseResFile (resFile);
return (theError);
}
//
// BuildVoiceMenu
//
// Fills a menu handle with the names of the voices currently available
//
OSErr BuildVoiceMenu (MenuHandle theMenu)
{
VoiceDescription info;
VoiceSpec voice;
OSErr theError;
short numVoices,
resFile,
index;
if (!theMenu || !SpeechAvailable ())
return (noErr);
resFile = CurResFile ();
theError = CountVoices (&numVoices);
for (index = numVoices; !theError && index > 0; --index) {
theError = GetIndVoice (index, &voice);
if (!theError)
theError = GetVoiceDescription (&voice, &info, sizeof (VoiceDescription));
if (!theError)
MyAppendMenu (theMenu, info.name);
}
UseResFile (resFile);
return (theError);
}
//
// GetNewVoicePopupSmall
//
// Gets a new voice popup menu control listing all available voices and sets the popup
// selection to the current voice (or the default if the passed voice can't be found
//
// Returns nil if the Speech Manager is not present or the control could not be created
//
ControlHandle GetNewVoicePopupSmall (short id, WindowPtr win, VoiceSpec *currentVoice)
{
ControlHandle theControl;
MenuHandle theMenu;
VoiceDescription info;
OSErr theError;
short resFile,
item;
Boolean defaultVoice;
theControl = nil;
if (SpeechAvailable ()) {
resFile = CurResFile ();
if (theControl = GetNewControlSmall (id, win))
if (theMenu = GetControlPopupMenuHandle(theControl)) {
TrashMenu (theMenu, 1);
if (!BuildVoiceMenu (theMenu)) {
theError = GetVoiceDescriptionDefaultOkay (currentVoice, &info, sizeof (info), &defaultVoice);
if (!theError && defaultVoice)
*currentVoice = info.voice;
item = FindItemByName (theMenu, info.name);
SetControlMaximum (theControl, CountMenuItems (theMenu));
MySetCtlValue (theControl, item ? item : 1);
}
}
UseResFile (resFile);
}
return (theControl);
}
//
// GetVoiceName
//
// Retrieves the name of a given voice
//
OSErr GetVoiceName (VoiceSpec *voice, Str63 name)
{
VoiceDescription info;
OSErr theError;
short resFile;
resFile = CurResFile ();
theError = GetVoiceDescription (voice, &info, sizeof (VoiceDescription));
UseResFile (resFile);
if (!theError)
PSCopy (name, info.name);
else
name[0] = 0;
return (theError);
}
//
// FindVoiceFromName
//
// Iterate over all voices looking for a voice with a given name
// Returns the default voice if none can be found
//
OSErr FindVoiceFromName (Str63 name, VoiceSpec *foundVoice)
{
VoiceDescription info;
VoiceSpec voice;
OSErr theError;
short numVoices,
resFile,
index;
Boolean found;
if (!SpeechAvailable ())
return (noErr);
resFile = CurResFile ();
found = false;
theError = CountVoices (&numVoices);
for (index = 1; !theError && !found && index <= numVoices; ++index) {
theError = GetIndVoice (index, &voice);
if (!theError)
theError = GetVoiceDescription (&voice, &info, sizeof (VoiceDescription));
if (!theError)
if (found = StringSame (name, info.name))
*foundVoice = info.voice;
}
if (!theError && !found)
if (!(theError = GetVoiceDescription (nil, &info, sizeof (VoiceDescription))))
*foundVoice = info.voice;
UseResFile (resFile);
return (theError);
}
//
// SpeakMessage
//
// Speaks one or more "parts" of a message using a particular voice (or the default voice
// if a nil VoiceSpecPtr is passed). The message parts can be spoken individually (by
// passing a single selector) or collectively (by passing multiple selectors). Currently,
// the order that the parts will be spoken is hardcoded - which works well enough since we
// currently support only a couple of selectors.
//
// This routine accepts:
// ; a pointer to the voice to be used when speaking the message, or 'nil' if the
// text is to be spoken using the default voice
// ; a handle specifying the TOC containing the message summary
// ; an index into the specified TOC identifying the summary or the message to be spoken
// ; selectors identifying the parts of the message to be spoken. Currently, only
// a handful of selectors are defined:
// speakNothing : mute!
// speakSender : speaks the From line
// speakSubject : speaks the subject line
// speakBody : speaks the body of a message
// And returns:
// ; noErr if the text is spoken successfully (or the Speech Manager is not present)
// ; Speech Manager errors
//
OSErr SpeakMessage (VoiceSpec *voice, TOCHandle tocH, short sumNum, SpeakableParts speak, Boolean nextMessage)
{
QuoteTableHandle quoteTable;
Accumulator textAccumulator;
MyWindowPtr messWin;
WindowPtr messWinWP;
MessHandle messH;
Str255 scratch;
UHandle text;
OSErr theError;
uLong offset;
Boolean inUse;
if (!SpeechAvailable ())
return (noErr);
UseFeature (featureSpeak);
theError = noErr;
if (speak) {
inUse = false;
if (messH = (*tocH)->sums[sumNum].messH)
if ((*messH)->win)
inUse = (*messH)->win->inUse;
if(!(speak & speakSummary) || (speak & ~(speakSubject|speakSender|speakSummary)))
{
if (messWin = GetAMessage (tocH, sumNum, nil, nil, false))
messH = Win2MessH (messWin);
} else {
messWin = nil;
}
// First, let's build the text we wish to speak. We'll use an accumulator
theError = AccuInit (&textAccumulator);
// If we're speaking multiple messages, pause, and speak some sort of a "next message" phrase
if (nextMessage) {
if (!theError)
theError = AccuComposeR (&textAccumulator, SPEAK_SILENCE_COMMAND, SPEAK_MESSAGE_PAUSE_DURATION);
if (!theError && UseSpeakNewMessagePhrase) {
(void) AccuAddRes (&textAccumulator, SPEAK_NEXT_MESSAGE);
(void) AccuAddChar (&textAccumulator, ' ');
}
}
// Add the 'To:' line to the text to be spoken
if (speak & speakTo)
theError = AccuAddAddressHeader (&textAccumulator, messH, nil, TO_HEAD, SPEAK_TO_PREFIX);
// Add the 'From:' line to the text to be spoken
if (speak & speakSender) {
LDRef (tocH);
theError = AccuAddAddressHeader (&textAccumulator, messH, (*tocH)->sums[sumNum].from, FROM_HEAD, SPEAK_SENDER_PREFIX);
UL (tocH);
}
// Add the 'Subject:' line to the text to be spoken
if (!theError && (speak & speakSubject)) {
if (speak & speakSender)
theError = AccuAddStr (&textAccumulator, "\p ");
if (!theError) {
scratch[0] = 0;
if (messH)
SuckHeaderText (messH, scratch, sizeof (scratch), SUBJ_HEAD);
if (!scratch[0])
PSCopy (scratch, (*tocH)->sums[sumNum].subj);
TrimRe (scratch, true);
TrimInitialWhite (scratch);
TrimWhite (scratch);
theError = AccuAddSpeakableText (&textAccumulator, &scratch[1], scratch[0], nil, false);
}
}
// Add the body of the message
if (!theError && (speak & speakBody)) {
if (speak & (speakSender | speakSubject))
theError = AccuAddStr (&textAccumulator, "\p. ");
// Add the raw text to the speech buffer
if (!theError)
if (text = MessVisibleText (messH)) {
offset = BodyOffset (text);
if (UseSpeakEmailRules) {
quoteTable = BuildQuoteLevelTable (TheBody, offset);
theError = AccuAddSpeakableText (&textAccumulator, LDRef (text) + offset, GetHandleSize (text) - offset, quoteTable, true);
ZapHandle (quoteTable);
}
else
theError = AccuAddPtr (&textAccumulator, LDRef (text) + offset, GetHandleSize (text) - offset);
UL (text);
}
}
// Start flapping our electronic lips
if (!theError) {
AccuTrim (&textAccumulator);
MoveHHi (textAccumulator.data);
theError = Speak (voice, LDRef (textAccumulator.data), textAccumulator.size);
}
AccuZap (textAccumulator);
// Close the window, unless it is in the out mailbox
if (messWin && !inUse) {
messWinWP = GetMyWindowWindowPtr (messWin);
if (!IsWindowVisible(messWinWP))
CloseMyWindow (messWinWP);
}
}
return (theError);
}
OSErr SpeakSelectedText (VoiceSpec *voice, PETEHandle pte)
{
Accumulator textAccumulator;
Handle text;
OSErr theError = noErr;
long selStart,
selEnd;
if (!SpeechAvailable ())
return (noErr);
// First, let's build the text we wish to speak. We'll use an accumulator
theError = AccuInit (&textAccumulator);
if (!theError)
theError = PeteGetTextAndSelection (pte, &text, &selStart, &selEnd);
if (!theError)
if (UseSpeakEmailRules)
theError = AccuAddSpeakableText (&textAccumulator, LDRef (text) + selStart, selEnd - selStart, nil, false);
else
theError = AccuAddPtr (&textAccumulator, LDRef (text) + selStart, selEnd - selStart);
// Start flapping our electronic lips
if (!theError) {
AccuTrim (&textAccumulator);
MoveHHi (textAccumulator.data);
theError = Speak (voice, LDRef (textAccumulator.data), textAccumulator.size);
}
AccuZap (textAccumulator);
return theError;
}
pascal Boolean TalkingAlertFilter (DialogPtr dgPtr, EventRecord *event, short *item)
{
Boolean retVal = false;
#ifdef THREADING_ON
if (NEED_YIELD)
MyYieldToAnyThread ();
#endif
if (event->what == mouseDown || event->what == keyDown) {
gTalkingAlertTicks = 0;
}
#ifdef CTB
if (CnH)
CMIdle (CnH);
#endif
if (MiniMainLoop (event) || HasCommandPeriod ()) {
MyStdFilterProc (dgPtr,event,item);
return (true);
}
else if (event->what != nullEvent)
retVal = MyStdFilterProc (dgPtr, event, item);
else {
SpeechIdle ();
if (gTalkingAlertTicks && gTalkingAlertTicks < TickCount ()) {
(void) SpeakAlert (gTalkPhrase, gTalkError, gTalkExplain);
gTalkingAlertTicks = 0;
}
}
return retVal;
}
//
// TalkingAlert
//
// Called instead of StandardAlert to produce either a Talking Alert (an alert that
// speak's it's text) or a Spoken Warning (passive speech without a visible alert).
// If anything goes haywire and we had intended to speak a Talking Alert, we just
// back off and produce a StandardAlert.
//
// Note: This routine dips into the Speech Preference file to retrieve various
// talking alert preferences.
//
void TalkingAlert (Boolean spokenWarning, AlertType alertType, StringPtr error, StringPtr explanation, AlertStdAlertParamPtr alertParam, short *item)
{
TalkingAlertPrefHandle talkingAlertPrefs;
Str255 speakPhrase,
displayError,
speakError,
displayExplanation,
speakExplanation;
Handle phraseList;
Ptr p;
OSErr theError;
short oldResFile,
refnum,
delayTicks,
numPhrases,
index;
Boolean speechAvail;
DECLARE_UPP (TalkingAlertFilter,ModalFilter);
refnum = -1;
gTalkError = nil;
gTalkExplain = nil;
gTalkPhrase = nil;
gTalkingAlertTicks = 0;
speechAvail = SpeechAvailable ();
*displayError = 0;
*displayExplanation = 0;
*speakError = 0;
*speakExplanation = 0;
// Make sense of all these dang meta characters to build strings for both display and speech
ParseTalkingAlertString (error, displayError, speakError);
ParseTalkingAlertString (explanation, displayExplanation, speakExplanation);
// If we were not able to parse a speakable error (like, for memory errors), don't talk!
if (!*speakError)
speechAvail = false;
// Perform a last second memory check. How we doin'?
if (speechAvail)
if (MonitorGrow (false) > 0)
speechAvail = false;
if (speechAvail) {
// Open the Speech Preferences file
oldResFile = CurResFile ();
refnum = FSpOpenResFile (&SpeechPrefFileSpec, fsRdPerm);
theError = ResError();
if (!theError && refnum >= 0) {
// Grab the Talking Alert preference from Speech Preferences
if (talkingAlertPrefs = Get1Resource (rTalkingAlertResType, rTalkingAlertPrefResID)) {
// Save the amount of time we should wait before speaking the alert
delayTicks = (*talkingAlertPrefs)->delayTicks;
// Setup the error and explanation strings, handle meta characters to build a
// speakable string for each.
if ((*talkingAlertPrefs)->speakAlertText) {
gTalkError = speakError;
gTalkExplain = speakExplanation;
}
// Get the alert phrase if the user's into that kind of thing
if ((*talkingAlertPrefs)->speakPhrase)
if (phraseList = Get1Resource (rTalkingAlertResType, rTalkingAlertPhraseListResID)) {
BlockMoveData (*phraseList, &numPhrases, sizeof (short));
switch ((*talkingAlertPrefs)->phraseModifier) {
case kSpeakTheNextPhrase:
index = gNextPhrase;
if (++gNextPhrase > numPhrases)
gNextPhrase = 1;
break;
case kSpeakARandomPhrase:
index = (TickCount () % numPhrases) + 1;
break;
default:
index = (*talkingAlertPrefs)->phraseIndex;
break;
}
// Point to the first available phrase
p = *phraseList + sizeof (short);
// find the phrase we want
while (--index)
p = p + (*p + 1);
PCopy (speakPhrase, p);
gTalkPhrase = speakPhrase;
}
else
speechAvail = false;
}
else
speechAvail = false;
CloseResFile (refnum);
}
else
speechAvail = false;
UseResFile (oldResFile);
}
if (speechAvail && (gTalkPhrase ||gTalkError || gTalkExplain)) {
if (spokenWarning)
SpeakAlert (nil, gTalkError, gTalkExplain);
else {
if (gTalkingAlertTicks < TickCount ())
gTalkingAlertTicks = TickCount () + delayTicks;
INIT_UPP (TalkingAlertFilter,ModalFilter);
alertParam->filterProc = TalkingAlertFilterUPP;
gTalkingAlertPresent = true;
MyStandardAlert (alertType, displayError, displayExplanation, alertParam, item);
gTalkingAlertPresent = false;
SpeechShutup ();
}
}
else
if (!spokenWarning)
MyStandardAlert (alertType, displayError, displayExplanation, alertParam, item);
}
//
// ParseTalkingAlertString
//
// Parse an alert string for meta characters, returning the text to be displayed in
// the alert and the text to be spoken. An "alert string" is a portion of a larger
// formatted alert template; these being the "error" or "explanation" pieces. The
// format of an "alert string" is:
//
// <09>displayText<78>spokenText
//
// where both the '<27>' (option-l) and '<27>' (option-w) act as metacharacters to
// control speakable aspects of the string.
//
// '<27>' ...indicates that the text which follows is not to be read
// '<27>' ...indicates that the text which follows is to be read, but not displayed
//
// An example is knocking at the door... let's listen in!
//
// <09>Something really complex happened that I can't explain<69>Just hit OK, trust me
//
// Displays: Something really complex happened that I can't explain
//
// Speaks: Just hit OK, trust me
//
void ParseTalkingAlertString (StringPtr alertString, StringPtr displayError, StringPtr speakError)
{
Accumulator textAccumulator;
Str255 scratch;
UPtr text;
Byte meta[2];
meta[0] = 0xB7; // '<27>'
meta[1] = 0;
if (*alertString) {
text = alertString + (alertString[1] == 0xC2 ? 2 : 1); // '<27>'
PToken (alertString, displayError, &text, meta);
if (alertString[1] != 0xC2)
PCat (speakError, displayError);
PToken (alertString, scratch, &text, meta);
if (scratch[0]) {
speakError[++speakError[0]] = ' ';
PCat (speakError, scratch);
}
}
if (!AccuInit (&textAccumulator)) {
if (!AccuAddSpeakableText (&textAccumulator, &speakError[1], speakError[0], nil, false)) {
AccuTrim (&textAccumulator);
if (textAccumulator.size < sizeof (Str255)) {
speakError[0] = textAccumulator.size;
BlockMoveData (*(textAccumulator.data), &speakError[1], speakError[0]);
}
}
AccuZap (textAccumulator);
}
}
//
// SpeakAlert
//
// Speaks an alert text using the Speech control panel's Talking Alert setting.
// If the 'spokenWarning' flag is set this is an alert spoken without an
// actual alert. As such, we should disregard the talking delay and speak
// the alert immediately.
//
OSErr SpeakAlert (StringPtr phrase, StringPtr error, StringPtr explanation)
{
OSErr theError;
theError = noErr;
if (phrase)
if (*phrase)
theError = Speak (nil, &phrase[1], *phrase);
if (error)
if (*error)
theError = Speak (nil, &error[1], *error);
if (!theError && explanation)
if (*explanation)
theError = Speak (nil, &explanation[1], *explanation);
gTalkingAlertTicks = 0;
return (theError);
}
OSErr SpeakSelectedMessages (TOCHandle tocH)
{
SpeakableParts speak;
OSErr theError;
short sumNum;
Boolean nextMessage;
theError = noErr;
nextMessage = false;
if (tocH)
for (sumNum = 0; !theError && sumNum < (*tocH)->count; ++sumNum)
if ((*tocH)->sums[sumNum].selected) {
speak = speakSubject | speakBody;
speak |= (((*tocH)->which==OUT || (*tocH)->sums[sumNum].state == SENT || (*tocH)->sums[sumNum].state == UNSENT || (*tocH)->sums[sumNum].flags & FLAG_OUT) ? speakTo : speakSender);
theError = SpeakMessage (nil, tocH, sumNum, speak, nextMessage);
nextMessage = true;
}
return (theError);
}
#define spek true
// Map the alphanumerics, spaces and tabs to be immediately speakable
#define isspeakable(c) (speakableMap[c])
//
// AccuAddSpeakableText
//
// Attempt to make a speech buffer a bit more readable...
//
OSErr AccuAddSpeakableText (AccuPtr a, UPtr textPtr, long len, QuoteTableHandle quoteTable, Boolean lookForQuotes)
{
unsigned char speakableMap[256] = {
// -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F
0, 0, 0, 0, 0, 0, 0, 0, 0, spek, spek, 0, 0, spek, 0, 0, // 0-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1-
spek, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2-
spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, 0, 0, 0, 0, 0, 0, // 3-
0, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, // 4-
spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, 0, 0, 0, 0, 0, // 5-
0, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, // 6-
spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, 0, 0, 0, 0, 0, // 7-
spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, // 8-
spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, // 9-
0, 0, 0, 0, 0, 0, 0, spek, 0, 0, 0, 0, 0, 0, spek, spek, // A-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, spek, spek, // B-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, spek, spek, spek, spek, spek, spek, // C-
0, 0, 0, 0, 0, 0, 0, 0, spek, spek, 0, 0, 0, 0, spek, spek, // D-
0, 0, 0, 0, 0, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, spek, // E-
0, spek, spek, spek, spek, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // F-
};
Str31 prefix;
Str8 tla;
UPtr p,
#ifdef G4_PROBLEM
before_p,
#endif
after_p,
base,
oldBase,
endPtr,
digitPtr,
alphaPtr,
upperPtr,
lowerPtr,
beginingOfLine;
OSErr theError;
short patternCount,
quoteLevel,
quoteLevelThisLine,
numParagraphs,
paragraphIndex;
Boolean processingSpeakableText,
mightBeURL,
wasTranslated;
theError = noErr;
base = textPtr;
beginingOfLine = textPtr;
patternCount = 0;
digitPtr = nil;
alphaPtr = nil;
upperPtr = nil;
lowerPtr = nil;
processingSpeakableText = false;
mightBeURL = false;
quoteLevel = 0;
paragraphIndex = 0;
numParagraphs = HandleCount (quoteTable);
GetRString (prefix, QUOTE_PREFIX);
p = textPtr;
endPtr = textPtr + len;
while (!theError && p < endPtr) {
// If we're at the beginning of a line...
if (lookForQuotes)
if (p == beginingOfLine && (UseSpeakFindQuote || !UseSpeakQuotes)) {
// Calculate the quote level for this line: from the quote table, by looking for quote characters... or, both!
quoteLevelThisLine = GetQuoteLevel (p, endPtr, textPtr, quoteTable, quoteLevel, numParagraphs, prefix, &paragraphIndex, &p);
// Save off the text between the base and the begining of the line, and move the base
// to point to the first character beyond the conclusion of the quoting character.
theError = AccuAddPtr (a, base, beginingOfLine - base);
base = p;
// If we're not planning on speaking quotes, skip all of the text until the next return,
// setting the base to point to the character following the return. Note that we leave
// 'p' pointing to the return... This will be caught below during our character 'switch',
// with the new beginingOfLine set there.
if (!theError)
if (!UseSpeakQuotes) {
while (p < endPtr && *p != returnChar)
++p;
base = p + 1; // Note
}
else {
// We're increasing the quote level...
if (quoteLevelThisLine > quoteLevel) {
// Add the quote phrase
if (UseSpeakQuotePhrase)
(void) AccuAddQuoteStr (a, SPEAK_QUOTE);
// Use a slightly modified voice for the quoted text
if (UseSpeakModifyQuoteVoice)
(void) AccuAddRes (a, SPEAK_QUOTE_MODIFY_VOICE_COMMAND);
}
// We're decreasing the quote level...
if (quoteLevelThisLine < quoteLevel) {
// Reset the voice characteristics once the quote level returns to normal
if (UseSpeakModifyQuoteVoice)
(void) AccuAddRes (a, SPEAK_UNQUOTE_MODIFY_VOICE_COMMAND);
// Add the unquote phrase
if (UseSpeakQuotePhrase)
(void) AccuAddQuoteStr (a, SPEAK_UNQUOTE);
}
}
quoteLevel = quoteLevelThisLine;
}
// Get fancy only once we've moved past the beginning of a new run of text (the base)
if (!theError && p > base) {
// If we are currently proccessing speakable text...
if (processingSpeakableText) {
oldBase = base;
// When we are processing a string of digits...
if (digitPtr) {
// Check to see if we're making an numeric-to-alpha transition "123abc"
if (UseSpeakMixedCase && isalpha (*p) && !NumberShorthand (p))
theError = InsertAndMoveBase (a, p, " ", 1, &base);
}
else
// When we are processing a string of alphas...
if (alphaPtr) {
// Check to see if we're making an alpha-to-numeric transition "abc123"
if (isdigit (*p)) {
if (UseSpeakMixedCase)
theError = InsertAndMoveBase (a, p, " ", 1, &base);
}
else
// When we are processing a string of uppers...
if (upperPtr) {
// Check to see if we are transitioning from an uppercase string
// Possibilities include:
// 1. John - (lower follows) Simple transition to lowercase, ignore it
// 2. URLs - (lower follows) Plural! We shouldn't do anything.
// 3. ATTNet - (lower follows) Tricky! Should be translated to ATT Net
// 4. THXsound - (lower follows) Tricky! Should be translated to THX sound
// 5. AFAIK - (non-alpha, non-digit) might be an acronym that needs to be replaced
// 6. URL's - (apostrophe) should be translated to URLs -- but might be an acronym!
// and, probably... more!
if (!isupper (*p)) {
// Handle lowercase transitions (cases 1, 2, 3 and 4)
if (islower (*p)) {
// If the uppercase string is more than one character (cases 2, 3 and 4)
if (p - upperPtr > 1)
// If what follows is NOT just pluralization (cases 3 and 4)
if (!(*p == 's' && (p == endPtr - 1 || (p + 1 < endPtr && !isalnum (*(p + 1)))))) {
if (UseSpeakStrictUpperToLower)
// Case 4
theError = InsertAndMoveBase (a, p, " ", 1, &base);
else
if (UseSpeakMixedCase) {
// Case 3
theError = AccuAddPtr (a, base, p - base - 1);
if (!theError)
theError = AccuAddChar (a, ' ');
base = p - 1;
}
}
}
else
// Handle an apostrophe (case 6)
// Strip the apostrophe if it is a pluralized all caps word
if (*p == '\'' || *p == '<EFBFBD>') {
if (p + 1 < endPtr && (*(p + 1) == 's' || *(p + 1) == 'S') && (p + 1 == endPtr - 1 || (p + 2 < endPtr && !isalnum (*(p + 2))))) {
theError = AccuAddPtr (a, base, p - base);
base = p + 1;
}
}
else {
// Handle everything else (case 5)
// Flush everything up to the first character in the uppercase string
theError = AccuAddPtr (a, base, upperPtr - base);
oldBase = base = upperPtr;
// Check to see if we have an acronym and replace it
if (UseSpeakAcronyms && !theError) {
MakePStr (tla, upperPtr, p - upperPtr);
theError = AccuAddAcronym (a, tla, &wasTranslated);
if (wasTranslated)
base = p;
}
}
}
}
else
// When we are processing a string of lowers...
if (lowerPtr) {
// Check to see if we're making a lower-to-upper transition "johnPurlia"
if (UseSpeakMixedCase && isupper (*p))
theError = InsertAndMoveBase (a, p, " ", 1, &base);
}
}
// Flush our buffer if we've hit something unspeakable and we haven't already handled
// one of the above cases
if (!isspeakable (*p) && oldBase == base) {
theError = AccuAddPtr (a, base, p - base);
base = p;
}
}
// Looks like we've been processing a run of unspeakable text
// Replace runs of 3 or more non-speakable characters with a period and a space.
// Otherwise, keep what we found.
else
if (UseSpeakEliminateTheUnspeakable && isspeakable (*p)) {
if (p - base >= 3)
theError = AccuAddStr (a, "\p. ");
else
theError = AccuAddPtr (a, base, p - base);
base = p;
}
}
// Handle certain characters with a little care...
if (!theError)
switch (*p) {
case returnChar:
// If we've hit the end of a line that did not terminate with a period, see if
// we might want to insert a pause of our own.
if (UseSpeakShortLines)
if ((p - beginingOfLine < 48 || (p - beginingOfLine < 64 && (p + 1 < endPtr && isupper (*(p + 1)))))
&& p > textPtr && *(p - 1) != returnChar && !ispunct (*(p - 1)))
theError = InsertAndMoveBase (a, p, ". ", 2, &base);
// Mark the next character as the beginning of a new line
beginingOfLine = p + 1;
break;
case periodChar:
// If the period breaks between two words, insert the word "dot". (qualcomm.com)
// Anything that is ".edu" should be changed to ".EDU"
// Likewise, ".fourorlesschars." should be changed to ".uppercase."
// If the period breaks between a word and a number, insert the word "dot". (john316.com)
// If the period breaks between two numbers, things get interesting...
// We might have to deal with (john316.98degrees.com, 129.46.138.16, 21.2, $19.99, 8.5.1)
#ifdef G4_PROBLEM
if (UseSpeakDotty)
if (p > textPtr && p + 1 < endPtr)
if ((isalpha (*(p - 1)) && isalnum (*(p + 1))) ||
(isalnum (*(p - 1)) && isalpha (*(p + 1)))) {
mightBeURL = true;
theError = AccuAddRes (a, SPEAK_DOT);
base = p + 1;
if (p + 4 <= endPtr && !memcmp (p + 1, "edu", 3) && (p + 4 == endPtr || !isalnum (*(p + 4)))) {
theError = InsertAndMoveBase (a, p, " EDU", 4, &base);
base = p + 4;
p += 3;
}
else
// Weird logic to determine whether or not a segment in a dodted address _might_ be
// an acronym (for example, "www.ucsd.edu"). We want the Speech Manager to speak
// such things as letters -- "w w w dot U C S D dot E D U"
if (p + 5 < endPtr && (*(p + 4) == periodChar || *(p + 5) == periodChar))
if (memcmp (p + 1, "com", 3) && memcmp (p + 1, "org", 3) &&
memcmp (p + 1, "net", 3) && memcmp (p + 1, "mil", 3) &&
memcmp (p + 1, "gov", 3)) {
for (++p; !theError && *p != periodChar; ++p)
theError = AccuAddChar (a, islower (*p) ? toupper (*p) : *p);
base = p;
--p;
}
}
else
if (isdigit (*(p - 1)) && isdigit (*(p + 1))) {
// Scan backwards and forwards until we hit things that aren't digits
before_p = p - 1;
while (before_p >= textPtr && isdigit (*before_p))
--before_p;
after_p = p + 1;
while (after_p < endPtr && isdigit (*after_p))
++after_p;
// If we hit a letter or another period -- in either direction, let's just dot it!
if (isalpha (*before_p) || *before_p == periodChar || isalpha (*after_p) || *after_p == periodChar) {
theError = AccuAddRes (a, SPEAK_DOT);
base = p + 1;
}
}
#endif
break;
case '/':
// If we hit a slash in a URL, skip the rest of the URL
if (UseSpeakTruncUrl && mightBeURL) {
theError = AccuAddPtr (a, base, p - base);
if (!theError) {
while (p < endPtr && !isspace (*p))
++p;
base = p;
--p; // Because it will be incremented at the end of the loop
}
}
break;
case ':':
// If a colon is the last character on a line, replace it with a period (full stop)
if (p + 1 < endPtr && *(p + 1) == returnChar && !mightBeURL) {
theError = AccuAddPtr (a, base, p - base);
if (!theError)
theError = AccuAddChar (a, periodChar);
base = p + 1;
}
// If we might be working on a URL, save what we have and skip past any '/' characters
if (mightBeURL) {
theError = AccuAddPtr (a, base, p - base + 1);
if (!theError) {
++p;
while (p < endPtr && *p == '/')
++p;
base = p;
--p; // Because it will be incremented at the end of the loop
}
}
break;
case '<':
if (UseSpeakAttemptUrl) {
after_p = p + 1;
while (after_p < endPtr && (*after_p != ':' && !isspace (*after_p)))
++after_p;
if (mightBeURL = (*after_p == ':')) {
theError = AccuAddPtr (a, base, p - base);
base = p + 1;
}
}
break;
case '>':
if (UseSpeakAttemptUrl && mightBeURL) {
theError = AccuAddPtr (a, base, p - base);
base = p + 1;
}
break;
case 'E':
case 'e':
// How embarrassing... "email" is generally spoken incorrectly. Let's fix that.
if (UseSpeakJohnnyCantRead)
if (p >= textPtr && p + 4 < endPtr)
if (!memcmp (p + 1, "mail", 4))
theError = InsertAndMoveBase (a, p + 1, " ", 1, &base);
break;
case '*':
case '_':
// Look for creative use of ASCII emphasis... You are a _total_ moron. If we find something
// like this, let's tell the speech manager to emphasize the word. (We may also want to do
// this for words in all caps, but I suspect that would be problematic.)
if (UseSpeakEmphasis && p > textPtr && isspace (*(p - 1))) {
after_p = p + 1;
while (after_p < endPtr && isalpha (*after_p))
++after_p;
if (*after_p == *p) {
theError = AccuAddPtr (a, base, p - base);
if (!theError)
theError = AccuAddChar (a, ' ');
if (!theError)
theError = AccuAddStr (a, "\p[[emph +]]");
++p;
while (!theError && p < endPtr && isalpha (*p)) {
theError = AccuAddChar (a, isupper (*p) ? tolower (*p) : *p);
++p;
}
if (!theError)
theError = AccuAddChar (a, ' ');
base = p + 1;
}
}
break;
case 'w':
case 'W':
// Look for the shorthand use of 'w' to mean 'with'
if (!UseSpeakShorthand)
if (p > textPtr && isspace (*(p - 1)) && (p + 1 < endPtr && *(p + 1) == '/') && (p + 2 < endPtr && isalnum(*(p + 2)))) {
theError = AccuAddPtr (a, base, p - base);
if (!theError)
theError = AccuAddChar (a, ' ');
if (!theError)
theError = AccuAddRes (a, SPEAK_WITH);
if (!theError)
theError = AccuAddChar (a, ' ');
base = (p += 2);
}
break;
}
processingSpeakableText = isspeakable (*p);
digitPtr = !isdigit (*p) ? nil : (digitPtr ? digitPtr : p);
alphaPtr = !isalpha (*p) ? nil : (alphaPtr ? alphaPtr : p);
upperPtr = !isupper (*p) ? nil : (upperPtr ? upperPtr : p);
lowerPtr = !islower (*p) ? nil : (lowerPtr ? lowerPtr : p);
if (isspace (*p))
mightBeURL = false;
++p;
}
// Finish up by appending the remainder of the line if we were working on speakable text
if (!theError && processingSpeakableText) {
// Special case if we're doing uppercase processing at the end (might have an acronym)
if (UseSpeakAcronyms && upperPtr) {
theError = AccuAddPtr (a, base, upperPtr - base);
base = upperPtr;
// Check to see if we have an acronym and replace it
if (UseSpeakAcronyms && !theError) {
MakePStr (tla, upperPtr, p - upperPtr);
theError = AccuAddAcronym (a, tla, &wasTranslated);
if (wasTranslated)
base = p;
}
}
if (!theError)
theError = AccuAddPtr (a, base, p - base);
}
if (!theError && lookForQuotes && quoteLevel && UseSpeakQuotePhrase) {
theError = AccuAddStr (a, "\p[[rset]]");
if (!theError)
theError = AccuAddQuoteStr (a, SPEAK_UNQUOTE);
}
return (theError);
}
OSErr AccuAddQuoteStr (AccuPtr a, short resID)
{
OSErr theError;
theError = AccuComposeR (a, SPEAK_SILENCE_COMMAND, SPEAK_QUOTE_PAUSE_DURATION);
if (!theError)
theError = AccuAddRes (a, resID);
if (!theError)
theError = AccuAddStr (a, "\p. ");
return (theError);
}
//
// NumberShorthand
//
// Hard coded to only work for English... sorry.
//
Boolean NumberShorthand (UPtr p)
{
if (memcmp (p, "st", 2)) // 1st
if (memcmp (p, "nd", 2)) // 2nd
if (memcmp (p, "rd", 2)) // 3rd
if (memcmp (p, "th", 2)) // 4th
return (false);
return (!isalnum(*(p + 2)));
}
QuoteTableHandle BuildQuoteLevelTable (PETEHandle pte, uLong offset)
{
PETEParaInfo pinfo;
Accumulator table;
QuoteTableRec quoteInfo;
OSErr theError;
short paragraphIndex;
theError = AccuInit (&table);
for (paragraphIndex = 1; !theError; paragraphIndex++) {
Zero (pinfo);
if (PETEGetParaInfo (PETE, pte, paragraphIndex, &pinfo))
break;
if (pinfo.paraOffset >= offset) {
Zero (quoteInfo);
quoteInfo.quoteLevel = pinfo.quoteLevel;
quoteInfo.paraOffset = pinfo.paraOffset - offset;
theError = AccuAddPtr (&table, &quoteInfo, sizeof (quoteInfo));
}
}
if (theError)
AccuZap (table);
else
AccuTrim (&table);
return ((QuoteTableHandle) table.data);
}
//
// GetQuoteLevel
//
short GetQuoteLevel (UPtr p, UPtr endPtr, UPtr textPtr, QuoteTableHandle quoteTable, short quoteLevel, short numParagraphs, Str31 prefix, short *paragraphIndex, UPtr *speakAbleText)
{
QuoteTablePtr qtPtr;
UPtr q;
short quoteLevelThisLine;
// First, see if the current pointer matches that in the quote table, and use that -- otherwise, the level has not changed
if (quoteTable && numParagraphs) {
qtPtr = *quoteTable;
if (*paragraphIndex < numParagraphs && textPtr + qtPtr[*paragraphIndex].paraOffset == p)
quoteLevelThisLine = qtPtr[(*paragraphIndex)++].quoteLevel;
else
quoteLevelThisLine = *paragraphIndex < numParagraphs ? quoteLevel : 0;
}
else
quoteLevelThisLine = 0;
// Next, let's check to see if the message contains quote prefixes (which _could_ be separated by white space)
if (prefix[0]) {
q = p;
while (q < endPtr - prefix[0] && !memcmp (q, &prefix[1], prefix[0])) {
++quoteLevelThisLine;
p = (q += prefix[0]);
// Skip any spaces or tabs
while (q < endPtr && *q == ' ' || *q == tabChar)
++q;
}
*speakAbleText = p;
}
return (quoteLevelThisLine);
}
//
// InsertAndMoveBase
//
// Flushes those charcters from the base to the pointer, inserts text, then
// moves the base to the pointer location.
//
OSErr InsertAndMoveBase (AccuPtr a, UPtr p, UPtr text, long length, UPtr *base)
{
OSErr theError;
theError = AccuAddPtr (a, *base, p - *base);
if (!theError && text)
theError = AccuAddPtr (a, text, length);
*base = p;
return (theError);
}
//
// SmartAddressSpeaking
//
PStr SmartAddressSpeaking (PStr scratch)
{
Str63 dot;
UPtr atPtr;
short i;
atPtr = PIndex (scratch, '@');
for (i = 2; i < scratch[0]; ++i) {
if (((isdigit (scratch[i - 1]) && !isdigit (scratch[i])) ||
(!isdigit (scratch[i - 1]) && isdigit (scratch[i]))) && scratch[i] != periodChar && scratch[i] != '@')
PInsert (scratch, sizeof (scratch), "\p ", &scratch[i++]);
else
if (scratch[i] == periodChar)
if (&scratch[i] < atPtr)
scratch[i] = ' ';
}
PReplace (scratch, "\p.", GetRString (dot, SPEAK_DOT));
return (scratch);
}
//
// AccuAddAddressHeader
//
// Added so that we can speak either To or From fields.
//
OSErr AccuAddAddressHeader (AccuPtr a, MessHandle messH, PStr substitute, short headerIndex, short headerResID)
{
Str255 scratch;
OSErr theError;
theError = noErr;
// Grab the header text from the message, clean it up a bit and (maybe) substitute other text
scratch[0] = 0;
if (messH) {
SuckHeaderText (messH, scratch, sizeof (scratch), headerIndex);
BeautifyFrom (scratch);
}
if (!scratch[0] && substitute)
PSCopy (scratch, substitute);
if (scratch[0]) {
theError = AccuAddRes (a, headerResID);
// Be tricky and make addresses almost speakable!
// <09> Place a space around strings of numbers "john316" beccomes "john 316"
// <09> Replace periods with a spoken "dot" so that we get "devseed at apple dot com"
// However, if the period is before the @, put in a space instead. Thus,
// "john.purlia@qualcomm.com" becomes "john purlia at qualcomm dot com"
if (!theError) {
if (!PrefIsSet (PREF_SPEAK_NO_NICE_ADDRESSES))
SmartAddressSpeaking (scratch);
theError = AccuAddStr (a, scratch);
}
if (!theError)
theError = AccuAddChar (a, periodChar);
}
return (theError);
}
//
// AccuAddAcronym
//
// Lookup an acronym in the resource fork and add its spoken equivalent
//
OSErr AccuAddAcronym (AccuPtr a, PStr tla, Boolean *wasTranslated)
{
AcronymHandle acronym;
OSErr theError;
theError = noErr;
if (acronym = GetNamedResource (rAcronymResType, tla)) {
LDRef (acronym);
theError = AccuAddStr (a, (*acronym)->spokenAs);
ReleaseResource (acronym);
*wasTranslated = true;
}
else
*wasTranslated = false;
return (theError);
}
//
// InstallPronunciationDictionary
//
// Installs a user Pronunciation Dictionary on the speech channel.
// Pass in the string ID of the dictionary to be used (which must be
// installed in the Eudora Stuff folder).
//
OSErr InstallPronunciationDictionary (SpeechChannel channel, short resID)
{
Handle dictionary;
FSSpec dictionarySpec;
Str31 filename;
OSErr theError;
dictionary = nil;
theError = FindUserPronunciationDictionary (GetRString (filename, resID), &dictionarySpec);
if (!theError)
theError = CreateSpeechDictionary (&dictionarySpec, &dictionary);
if (!theError)
theError = UseDictionary (channel, dictionary);
if (dictionary)
DisposeHandle (dictionary);
return (theError);
}
OSErr FindUserPronunciationDictionary (Str31 filename, FSSpec *dictionarySpec)
{
Str31 folderName;
OSErr theError;
theError = GetFileByRef (AppResFile, dictionarySpec);
if (!theError)
theError = FSMakeFSSpec (dictionarySpec->vRefNum, dictionarySpec->parID, GetRString (folderName, STUFF_FOLDER), dictionarySpec);
if (!theError) {
IsAlias (dictionarySpec, dictionarySpec);
theError = FSMakeFSSpec (dictionarySpec->vRefNum, SpecDirId (dictionarySpec), filename, dictionarySpec);
}
return (theError);
}
OSErr CreateSpeechDictionary (FSSpec *dictionarySpec, Handle *dictionary)
{
DictHeaderRec dictHeader,
*dictHeaderPtr;
DictEntryHeaderRec dictEntryHeader;
Accumulator dictAccumulator;
LineIOD lid;
Str255 format,
word,
phoneme;
OSErr theError;
UInt32 numEntries;
long length;
Zero (dictAccumulator);
numEntries = 0;
// Open the pronunciation dictionary
theError = OpenLine (dictionarySpec->vRefNum, dictionarySpec->parID, dictionarySpec->name, fsRdWrPerm, &lid);
// Read the first line (which should contain the file format -- which I currently ignore)
if (!theError)
(void) GetLine (format + 1, sizeof (format) - 2, &length, &lid);
if (!theError)
theError = AccuInit (&dictAccumulator);
// Add a speech dictionary header
if (!theError) {
Zero (dictHeader);
dictHeader.atom = kDictionaryAtom;
dictHeader.version = kDictionaryAtomVersion;
dictHeader.script = smRoman;
dictHeader.language = langEnglish;
dictHeader.region = verUS;
GetDateTime (&dictHeader.modDate);
theError = AccuAddPtr (&dictAccumulator, &dictHeader, sizeof (dictHeader));
}
while (!theError) {
theError = GetSpeechDictionaryLine (&lid, word, phoneme);
// Add a dictionary entry header
if (!theError) {
++numEntries;
dictEntryHeader.length = sizeof (dictEntryHeader) + 2 * sizeof (DictEntryFieldHeaderRec) + word[0] + phoneme[0] + (word[0] % 2) + (phoneme[0] % 2);
dictEntryHeader.type = pronunciationEntry;
dictEntryHeader.numFields = 2;
theError = AccuAddPtr (&dictAccumulator, &dictEntryHeader, sizeof (dictEntryHeader));
}
// Add a the 'word' as a field entry
if (!theError)
theError = AccuAddDictionaryEntryField (&dictAccumulator, word, textEntryField);
// Add a the 'phoneme' as a field entry
if (!theError)
theError = AccuAddDictionaryEntryField (&dictAccumulator, phoneme, phonEntryField);
}
if (theError == eofErr)
theError = noErr;
// Done!! trim the accumulator and go back and fill in the 'DictHeaderRec'
// length and count fields
if (!theError) {
AccuTrim (&dictAccumulator);
dictHeaderPtr = *dictAccumulator.data;
dictHeaderPtr->length = dictAccumulator.size;
dictHeaderPtr->numEntries = numEntries;
}
CloseLine (&lid);
*dictionary = dictAccumulator.data;
return (theError);
}
OSErr AccuAddDictionaryEntryField (AccuPtr a, PStr string, SInt16 type)
{
DictEntryFieldHeaderRec header;
OSErr theError;
// Add a dictionary entry field header
header.length = sizeof (header) + string[0];
header.type = type;
theError = AccuAddPtr (a, &header, sizeof (header));
if (!theError)
theError = AccuAddPtr (a, &string[1], string[0]);
if (!theError && (string[0] % 2))
theError = AccuAddChar (a, 0);
return (theError);
}
OSErr GetSpeechDictionaryLine (LineIOP lip, PStr word, PStr phoneme)
{
Str255 line;
UPtr spot;
OSErr theError;
long length;
int code;
char delims[2];
theError = noErr;
do {
code = GetLine (line + 1, sizeof (line) - 2, &length, lip);
if (code < 0)
theError = code;
if (!code || !length)
theError = eofErr;
if (!theError)
if (line[line[0] = length] != returnChar)
theError = SPEAK_DICTIONARY_BAD;
} while (!theError && line[1] == '#');
if (!theError) {
delims[0] = ' ';
delims[1] = 0;
spot = line + 1;
PToken (line, word, &spot, delims);
delims[0] = returnChar;
PToken (line, phoneme, &spot, delims);
if (!word[0] || !phoneme[0])
theError = SPEAK_DICTIONARY_BAD;
}
return (theError);
}
#ifdef NOT_YET_THIS_WILL_BE_SUPPORTED_AT_SOME_POINT_IN_THE_FUTURE
OSErr InstallPureVoiceOutputComponent (SpeechChannel chan)
{
ComponentDescription outputDevice;
Component theComponent;
OSErr theError;
// First, find the output component in the Eudora Folder
theError = noErr;
outputDevice.componentType = kSoundOutputDeviceType;
outputDevice.componentSubType = 'XPVC';
outputDevice.componentManufacturer = 'QCOM';
outputDevice.componentFlags = 0;
outputDevice.componentFlagsMask = 0;
if (theComponent = FindNextComponent (0, &outputDevice))
theError = SetSpeechInfo (chan, soSoundOutput, &theComponent);
return (theError);
}
#endif
#endif