LarkSynthesizerImpl.mesa
Copyright © 1986 by Xerox Corporation. All rights reserved.
Last modified by D. Swinehart, February 3, 1987 8:24:53 am PST
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;
Types and Values
LarkInfo: TYPE = ThSmartsPrivate.LarkInfo;
SynthSpecs: TYPE = Synthesizer.SynthSpecs;
SynthCmd: TYPE = Rope.ROPE;
Per-synthesizer information, linked to LarkInfo
SynthInfo: TYPE = REF SynthInfoBody;
SynthInfoBody: TYPE = RECORD [
Synthesizer control values
synthResponse: Rope.ROPENIL, -- holds incomplete Synthesizer responses
flushJustFinished: BOOLFALSE, -- 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.ROPENIL,
clientMarker: INT←maxClientMarker,
controlMarker: INT←maxControlMarker,
ctrlMarkerQueue: LIST OF REF INTNIL, -- holds queue of control markers
pktsOutstanding: INT𡤀,
flushInProgress: BOOLFALSE -- consider combining w flushJustFinished?
];
definitions to interface to DECTalk text-to-speech synthesizer; more in ThSmartsPrivate
pReset: PUBLIC SynthCmd ← "cP;z:cp0\\P;z:pp0\\P;z:ra180\\P;z:np\\";
 is ESC. Also explicitly resets voice, rate, etc, because machine reset doesn't.
stopAndFlush: PUBLIC SynthCmd ← "P;10z\\";
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.
commenceSpeech: SynthCmd = "P;z,\\"; -- phonemic comma
indexMarkerStart: SynthCmd = "P;21;"; -- for constructing index commands
indexMarkerLen: INT = 12;
indexMarkerEnd: SynthCmd = "z\\P;z,\\";
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.
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;
Byte (char) count; RPCLupine.maxDataLength is word count
No bigger than RPCLupine.maxDataLength - 2*indexMarkerLen
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,\\";
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.
plainSpeechDone: SynthCmd = " P;21;255z\\ ";
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).
NconcSpecs: PROC[l1, l2: SynthSpecs] RETURNS [SynthSpecs] = INLINE { RETURN[NARROW[GList.Nconc[l1, l2]]]; };
New request from service to Lark:
AddText: PUBLIC ENTRY PROC[
info: LarkInfo, synthSpec: Synthesizer.SynthSpec, filter: BOOLTRUE, queueIt: BOOLTRUE] = {
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.
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];
};
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
HandleAndReport: PROC [info: LarkInfo, sEvent: Lark.StatusEvent] ~ {
Avoid deadlock - shouldn't call Smarts-level entry procs while holding the LarkIn/Out lock. That's why this isn't an entry procedure.
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;
For now, we'll assume that this was a successful dictionary entry response.
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 {
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?)
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!!!
};
};
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;
};
};
ENDCASE => synthInfo.synthResponse ←
Rope.Concat[synthInfo.synthResponse, Rope.FromChar[c]];
};
SynthControlDone: PUBLIC INTERNAL PROC[
info: LarkInfo, synthInfo: SynthInfo, marker: INT] = {
problem: Rope.ROPENIL; {
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];
};
};
Meter text to Lark
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];
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.
synthInfo.textToSpeak ← "";
controlMarker ← speechDoneMarker;
}
ELSE {
[packet: textPkt, remainder: synthText] ←
Synthesizer.BreakText[synthText, maxTextPktLen];
Now add the control marker on the end of the text packet.
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;
};
Utilities
FilterText: Rope.TranslatorType = { -- PROC [old: CHAR] RETURNS [new: CHAR]
Remove chars that the Prose considers illegal.
SELECT old FROM
IO.TAB, IO.LF, IO.CR, IO.ESC => new ← old;
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)
< 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] = {
Arrange to flush outstanding
ENABLE UNWIND => NULL;
DoSynthFlush[info, pReset]
};
DoSynthFlush: INTERNAL PROC[info: LarkInfo, synthCmd: SynthCmd] = {
Arrange to flush outstanding
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] = {
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.
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] = {
Queues up "IssueCommandString[info, command]" for serial execution by the Lark
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];
};
}.
Swinehart, October 23, 1986 6:50:48 am PDT
Loosely derived from Thrush 6 Synthesizer code.