DIRECTORY IO, Atom USING [ GetPropFromList, PutPropOnList ], Convert USING [ IntFromRope, RopeFromInt ], GList USING [ Nconc ], Lark USING [ StatusEvent ], LarkOpsRpcControl, LarkSynthesizer USING [ LarkSynthReport ], Rope USING [ Cat, Concat, FromChar, Length, ROPE, Translate, TranslatorType ], Synthesizer USING [ BreakText, SynthSpec, SynthSpecs ], SynthesizerServer, ThSmartsPrivate USING [ Deb, EnterLarkSt, Fail, FailInt, LarkCallBody, LarkInfo ], ThNet USING [ pd ], Thrush USING [ NB ], VoiceUtils USING [ Problem, ProblemFR ] ; LarkSynthesizerImpl: CEDAR MONITOR LOCKS info USING info: LarkInfo IMPORTS IO, Atom, Convert, GList, LarkOpsRpcControl, LarkSynthesizer, Rope, Synthesizer, ThNet, ThSmartsPrivate, VoiceUtils EXPORTS LarkSynthesizer = { OPEN IO; LarkInfo: TYPE = ThSmartsPrivate.LarkInfo; SynthSpecs: TYPE = Synthesizer.SynthSpecs; SynthCmd: TYPE = Rope.ROPE; SynthInfo: TYPE = REF SynthInfoBody; SynthInfoBody: TYPE = RECORD [ synthResponse: Rope.ROPE_NIL, -- holds incomplete Synthesizer responses flushJustFinished: BOOL_FALSE, -- can expect to have to flush synthQueue next time thru LarkSynthesizerImpl.HandleSynthOutput synthQueue: Synthesizer.SynthSpecs_NIL, -- holds queue of client markers and synthSpecs textToSpeak: Rope.ROPE_NIL, clientMarker: INT_maxClientMarker, controlMarker: INT_maxControlMarker, ctrlMarkerQueue: LIST OF REF INT_NIL, -- holds queue of control markers pktsOutstanding: INT_0, flushInProgress: BOOL_FALSE -- consider combining w flushJustFinished? ]; pReset: PUBLIC SynthCmd _ "cP;z:cp0\\P;z:pp0\\P;z:ra180\\P;z:np\\"; stopAndFlush: PUBLIC SynthCmd _ "P;10z\\"; commenceSpeech: SynthCmd = "P;z,\\"; -- phonemic comma indexMarkerStart: SynthCmd = "P;21;"; -- for constructing index commands indexMarkerLen: INT = 12; indexMarkerEnd: SynthCmd = "z\\P;z,\\"; indexMarkerEndChar: CHAR = 'z; pResetConfirmEnd: CHAR = 'n; stopAndFlushEnd: CHAR = 'n; -- for confirm, rename to flushConfirmEnd flushMarker: INT = 1000; synthFailure: INT = 1001; maxControlMarker: INT = 250; maxClientMarker: INT = 199; maxTextPktLen: INT _ 150; minClientMarker: INT = 1; minControlMarker: INT = 200; -- reserved for packet control maxPkts: INT = 250 / maxTextPktLen; -- Prose can hold `several hundred' chars numPkts: INT _ 2; speechDoneMarker: INT = 255; speechDone: SynthCmd = "P;21;255z\\P;z,\\"; plainSpeechDone: SynthCmd = " P;21;255z\\ "; NconcSpecs: PROC[l1, l2: SynthSpecs] RETURNS [SynthSpecs] = INLINE { RETURN[NARROW[GList.Nconc[l1, l2]]]; }; AddText: PUBLIC ENTRY PROC[ info: LarkInfo, synthSpec: Synthesizer.SynthSpec, filter: BOOL_TRUE, queueIt: BOOL_TRUE] = { ENABLE UNWIND => NULL; newText: Rope.ROPE; synthInfo: SynthInfo _ GetSynthInfo[info]; IF synthInfo=NIL THEN RETURN; -- error already reported IF ~queueIt THEN DoSynthFlush[info, stopAndFlush]; IF ThNet.pd.debug THEN ThSmartsPrivate.Deb[info, "Rq Prose", rope[synthSpec.textToSpeak], bool[queueIt], int[synthSpec.actionID]]; newText _ IF ~filter THEN synthSpec.textToSpeak ELSE Rope.Translate[base: synthSpec.textToSpeak, translator: FilterText]; synthSpec.synthMarker _ synthInfo.clientMarker _ IF synthInfo.clientMarker = maxClientMarker THEN minClientMarker ELSE synthInfo.clientMarker + 1; synthInfo.textToSpeak _ Rope.Cat[synthInfo.textToSpeak, newText, indexMarkerStart, Convert.RopeFromInt[synthInfo.clientMarker], indexMarkerEnd]; synthInfo.synthQueue _ NconcSpecs[synthInfo.synthQueue, LIST[synthSpec]]; SpeakText[info, synthInfo]; }; HandleAndReport: PROC [info: LarkInfo, sEvent: Lark.StatusEvent] ~ { nb: Thrush.NB; sS: Synthesizer.SynthSpec _ HandleSynthesizerOutput[info, sEvent]; IF sS=NIL THEN RETURN; IF (nb _ LarkSynthesizer.LarkSynthReport[sS, $finished]) = $success THEN RETURN; IF info#NIL AND ~info.failed THEN ThSmartsPrivate.Fail[info, IO.PutFR["Couldn't report text-synthesizer completion -- %g", atom[nb]], TRUE] ELSE VoiceUtils.ProblemFR["Couldn't report text-synthesizer completion, probably due to earlier failure -- %g", $System, NIL, atom[nb]]; }; HandleSynthesizerOutput: ENTRY PROC[info: LarkInfo, commandEvent: Lark.StatusEvent] RETURNS [sS: Synthesizer.SynthSpec _ NIL] = { ENABLE UNWIND => NULL; c: CHAR _ commandEvent.event; synthInfo: SynthInfo _ GetSynthInfo[info]; IF synthInfo = NIL THEN RETURN; -- error already reported SELECT c FROM IO.BEL => NULL; -- take some error action? IO.ESC, ';, '\\ => synthInfo.synthResponse _ ""; -- start of some response '[ => NULL; -- peel this character off stopAndFlushEnd => { SynthControlDone[info, synthInfo, flushMarker]; synthInfo.flushJustFinished _ TRUE; }; indexMarkerEndChar => { marker: INT; IF Rope.Length[synthInfo.synthResponse]=0 THEN RETURN; marker _ Convert.IntFromRope[synthInfo.synthResponse]; IF marker > maxClientMarker THEN SynthControlDone[info, synthInfo, marker] ELSE { sQ: Synthesizer.SynthSpecs _ synthInfo.synthQueue; IF sQ=NIL THEN ERROR; IF synthInfo.flushJustFinished THEN { FOR sSkip: Synthesizer.SynthSpecs _ sQ, sSkip.rest WHILE sSkip#NIL DO IF sSkip.first.synthMarker = marker THEN sQ _ sSkip; ENDLOOP; synthInfo.flushJustFinished _ FALSE; }; IF sQ.first.synthMarker = marker THEN { sS _ sQ.first; synthInfo.synthQueue _ sQ.rest; -- Flush any skipped items. } ELSE SynthControlDone[info, synthInfo, synthFailure]; -- fail!!! }; }; ENDCASE => synthInfo.synthResponse _ Rope.Concat[synthInfo.synthResponse, Rope.FromChar[c]]; }; SynthControlDone: PUBLIC INTERNAL PROC[ info: LarkInfo, synthInfo: SynthInfo, marker: INT] = { problem: Rope.ROPE_NIL; { marker1: INT; IF ~info.textToSpeech OR info.failed THEN RETURN; IF marker= synthFailure THEN { problem _ "Text-to-speech service failed"; GOTO Failed; }; IF marker = flushMarker THEN { -- Flushing complete synthInfo.flushInProgress _ FALSE; synthInfo.ctrlMarkerQueue _ NIL; synthInfo.pktsOutstanding _ 0; SpeakText[info, synthInfo]; RETURN; }; IF synthInfo.ctrlMarkerQueue = NIL THEN { problem _ "Text-to-speech service: empty control marker list"; GOTO Failed; }; marker1 _ NARROW[synthInfo.ctrlMarkerQueue.first, REF INT]^; IF marker # marker1 THEN { problem _ "Text-to-speech service: wrong marker received"; GOTO Failed; }; synthInfo.ctrlMarkerQueue _ synthInfo.ctrlMarkerQueue.rest; synthInfo.pktsOutstanding _ synthInfo.pktsOutstanding-1; SpeakText[info, synthInfo]; EXITS Failed => ThSmartsPrivate.FailInt[info, problem, TRUE]; }; }; SpeakText: INTERNAL PROC[info: LarkInfo, synthInfo: SynthInfo] = { textPkt: Rope.ROPE; controlMarker: INT; elt: LIST OF REF INT; IF synthInfo.flushInProgress THEN RETURN; WHILE synthInfo.pktsOutstanding < numPkts AND synthInfo.textToSpeak.Length[] > 0 DO synthText: Rope.ROPE _ synthInfo.textToSpeak; IF synthText.Length[] <= maxTextPktLen THEN { textPkt _ Rope.Concat[synthText, speechDone]; synthInfo.textToSpeak _ ""; controlMarker _ speechDoneMarker; } ELSE { [packet: textPkt, remainder: synthText] _ Synthesizer.BreakText[synthText, maxTextPktLen]; controlMarker _ synthInfo.controlMarker _ IF synthInfo.controlMarker>= maxControlMarker THEN minControlMarker ELSE synthInfo.controlMarker+1; textPkt _ Rope.Cat[indexMarkerStart, Convert.RopeFromInt[synthInfo.controlMarker], indexMarkerEnd, textPkt]; synthInfo.textToSpeak _ synthText; -- info is monitored record }; elt _ LIST[NEW[INT _ controlMarker]]; synthInfo.ctrlMarkerQueue _ NARROW[GList.Nconc[synthInfo.ctrlMarkerQueue, elt]]; ScheduleCommandString[info, textPkt]; synthInfo.pktsOutstanding _ synthInfo.pktsOutstanding+1; ENDLOOP; }; FilterText: Rope.TranslatorType = { -- PROC [old: CHAR] RETURNS [new: CHAR] SELECT old FROM IO.TAB, IO.LF, IO.CR, IO.ESC => new _ old; < IO.SP => new _ IO.SP; ENDCASE => new _ old; }; SynthFlush: PUBLIC ENTRY PROC[info: LarkInfo] = { ENABLE UNWIND => NULL; DoSynthFlush[info, stopAndFlush]; }; SynthReset: ENTRY PROC[info: LarkInfo] = { ENABLE UNWIND => NULL; DoSynthFlush[info, pReset] }; DoSynthFlush: INTERNAL PROC[info: LarkInfo, synthCmd: SynthCmd] = { synthInfo: SynthInfo _ GetSynthInfo[info]; IF synthInfo=NIL THEN RETURN; -- failure already reported IF synthInfo.flushInProgress THEN RETURN; synthInfo.flushInProgress _ TRUE; synthInfo.textToSpeak _ ""; IF ThNet.pd.debug THEN ThSmartsPrivate.Deb[info, "Rq Prose flush", rope[synthCmd], bool[FALSE], int[0]]; ScheduleCommandString[info, synthCmd]; }; GetSynthInfo: INTERNAL PROC[info: LarkInfo] RETURNS [synthInfo: SynthInfo_NIL] = { IF info=NIL THEN { VoiceUtils.Problem["No larkInfo at GetSynthInfo", $System]; RETURN; }; info.keyboardEventHandler _ HandleAndReport; info.keyboardResetHandler _ SynthReset; synthInfo _ NARROW[Atom.GetPropFromList[info.props, $synthInfo]]; IF synthInfo#NIL THEN RETURN; synthInfo _ NEW[SynthInfoBody_[]]; info.props _ Atom.PutPropOnList[info.props, $synthInfo, synthInfo]; }; ScheduleCommandString: PROC[info: LarkInfo, command: SynthCmd] = { ThSmartsPrivate.EnterLarkSt[info, info.larkState, LIST[NEW[ThSmartsPrivate.LarkCallBody _ [IssueCommandString, command]]]]; }; IssueCommandString: PROC[info: LarkInfo, clientData: REF] = { textPkt: Rope.ROPE = NARROW[clientData]; IF ThNet.pd.debug THEN ThSmartsPrivate.Deb[info, "Issue prose", rope[textPkt]]; info.interface.CommandString[shh: info.shh, device: keyboard, commands: textPkt]; }; }. LarkSynthesizerImpl.mesa Copyright c 1986 by Xerox Corporation. All rights reserved. Last modified by D. Swinehart, February 3, 1987 8:24:53 am PST Types and Values Per-synthesizer information, linked to LarkInfo Synthesizer control values definitions to interface to DECTalk text-to-speech synthesizer; more in ThSmartsPrivate  is ESC. Also explicitly resets voice, rate, etc, because machine reset doesn't. DSR brief () not strictly needed for reset or flush, but makes the DECTalk (which is normally quiet in this case) respond more like the Prose. The comma pause is to separate this index-reply command from any immediately following one, because of a DECtalk bug which will respond only to the second one when there are two in a row. In particular, there will always be two in a row at the end of a connection. Byte (char) count; RPCLupine.maxDataLength is word count No bigger than RPCLupine.maxDataLength - 2*indexMarkerLen Commence speech & report back when done. 38 chars counting the preceding index-reply and pause(!), but it is safe, even if the client has explicitly turned mode square on. The index-reply tells us that we are all done. The final comma pause flushes the preceding index-reply, which would otherwise sit in the machine indefinitely. Contains Ctrl Ks as flush chars, which turn phoneme mode off. Only safe if client is neither in explicit phoneme mode (which I detect) nor in mode square on mode & inside square brackets (which I don't). New request from service to Lark: Do some reasonable-state checking? At present, am assuming service did it at the conversation level, and that LarkOut will keep us out of trouble. Check for really info.textToSpeech? Probably higher up, unless this is higher up. Lark-to-Server keyboard events Only pass on index marker notifications. Swallow or handle all others. Assume Prose XON / XOFF disabled. Assume only get the following responses: On pReset: pResetConfirmOK if everything's okay cmdLeader <0-16> pResetConfirmEnd otherwise Index marker response: Undetermined problem: BEL Avoid deadlock - shouldn't call Smarts-level entry procs while holding the LarkIn/Out lock. That's why this isn't an entry procedure. For now, we'll assume that this was a successful dictionary entry response. It's okay to skip ahead in the queue. Except for 1st item in a conversation, which follows an initial RESET, should be to some entry with queueIt=FALSE. Don't report skipped entries as finished; FinchSmarts will take care of skipping ahead in its queue. (May want to add a synthSpec.type = flushed?) pResetConfirmEnd => { -- this case is now the same as stopAndFlushEnd IF Convert.IntFromRope[synthInfo.synthResponse] # 0 THEN SynthControlDone[info, synthInfo, ThSmartsPrivate.synthFailure] -- fail!!!; ELSE { SynthControlDone[info, synthInfo, ThSmartsPrivate.flushMarker]; synthInfo.flushJustFinished _ TRUE; }; }; Meter text to Lark speechDone (or some control marker) is needed to keep the Prose going if new text comes in after the last bit of the previous request was sent, but before the Prose finishes speaking it. Triggers a call from LarkInImpl.HandleProseOutput. Now add the control marker on the end of the text packet. Utilities Remove chars that the Prose considers illegal. Don't allow user to send reset (ctrl R) for Prose. Don't allow ctrl K or ctrl Z for DECtalk (terminate clause & phoneme mode) Arrange to flush outstanding Arrange to flush outstanding Synthesizer-specific information is stored on the LarkInfo property list. If that's not true coming in, it's true going out. Also registers the input-handing procedure for keyboard events. Queues up "IssueCommandString[info, command]" for serial execution by the Lark Swinehart, October 23, 1986 6:50:48 am PDT Loosely derived from Thrush 6 Synthesizer code. Ê X˜šœ™Icodešœ Ïmœ1™™>J˜—šÏk ˜ Jšžœ˜Jšœžœ$˜.Jšœžœ˜+Jšœžœ ˜Jšœžœ˜J˜Jšœžœ˜+Jšœžœ"žœ˜NJšœ žœ&˜7J˜Jšœžœ=˜RJšœžœ˜Jšœžœžœ˜Jšœ žœ˜'J˜J˜—šœž œžœžœ˜BJšžœžœr˜|Jšžœ˜Jšžœžœ˜J˜—™J™Jšœ žœ˜*Jšœ žœ˜*Jšœ žœžœ˜J˜™/Jšœ žœžœ˜$šœžœžœ˜J™JšœžœžœÏc)˜HJšœžœžœŸ^˜~Jšœ#žœŸ/˜XJšœžœžœ˜Jšœžœ˜"Jšœžœ˜$Jš œžœžœžœžœŸ!˜HJšœžœ˜JšœžœžœŸ*˜GJ˜J™——JšœW™WšœžœB˜PJšœR™R—šœžœž˜0Jšœ’™’—Jšœ(Ÿ˜9Jšœ(Ÿ"˜JJšœžœ˜šœ*˜*Jšœ‰™‰—Jšœžœ˜Jšœžœ˜JšœžœŸ)˜FJšœ žœ˜Jšœžœ˜J˜Jšœžœ˜Jšœžœ˜šœžœ˜Jšœ8™8Jšœ9™9—Jšœžœ˜Jšœžœ Ÿ˜R