VoicePlaybackImpl.mesa
Copyright Ó 1987 by Xerox Corporation. All rights reserved.
Ades, September 24, 1986 5:11:21 pm PDT
Swinehart, April 10, 1987 9:38:59 am PDT
Polle Zellweger (PTZ) September 13, 1988 1:21:46 pm PDT
DIRECTORY
BasicTime USING [GetClockPulses, MicrosecondsToPulses, Pulses, PulsesToMicroseconds ],
MessageWindow USING [Append, Blink ],
Process USING [Detach, MsecToTicks, Priority, priorityRealTime, SetPriority, SetTimeout, Ticks ],
Rope USING [Length, ROPE ],
TiogaOps USING [CancelSelection, CommandProc, FirstChild, GetRope, GetSelection, SelectionGrain, SelectPoint, SetSelection, ViewerDoc ],
TiogaOpsDefs USING [Location, Ref, SelectionGrain ],
TiogaVoicePrivate USING [ AgeAllViewers, BadRope, ButtonParams, PlayBackRequestState, PlaySelection, ReColorViewer, RecordingInProgress, SelectionsAfterRedraw, SetTiogaViewerParams, SetVoiceLooks, SetVoiceViewerEditStatus, SoundList, soundRopeCharLength, soundRopeCharsPerSecond, thrushHandle, VoiceViewerInfo ],
ViewerClasses USING [MouseButton, Viewer ],
ViewerOps USING [FetchProp ],
VoiceRope USING [Length, NB, NewRequestID, nullRequestID, PauseNB, PlayNB, RequestID, ResumeNB, VoiceRope, VoiceRopeInterval ]
;
VoicePlaybackImpl: CEDAR MONITOR
IMPORTS BasicTime, MessageWindow, Process, Rope, TiogaOps, TiogaVoicePrivate, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate = {
Routines replay sounds, plus all the guff to move a cue along a slab display
see TiogaVoicePrivate.mesa for the raison d'etre of this module
PlayBackRequestList: TYPE = LIST OF PlayBackRequest;
playBackState: RECORD [
running: BOOLEANFALSE, -- is there currently a forked instance of PlayBackProcess ?
queue: PlayBackRequestList ← NIL,
nextTime: BasicTime.Pulses, -- time to wake up playBackProcess next
abort: BOOLEANFALSE -- set when a 'stop voice playback' button is hit - playBackProcess will destroy itself next time it wakes up
];
abortCleared: CONDITION; -- signalled by PlayBackProcess when it has acted on an abort
PlayBackRequest: TYPE = REF PlayBackRequestRec;
PlayBackRequestRec: TYPE = RECORD[
requestID: VoiceRope.RequestID ← VoiceRope.nullRequestID,
state: TiogaVoicePrivate.PlayBackRequestState ← waiting,
expectFinishedReport: BOOLEAN ← TRUE, -- if FALSE, then this entry was edited after it was queued, so go on to the next entry automatically. See RedrawViewer.
display: BOOLEANTRUE, -- if not set then there is no displaying to be done
viewer: ViewerClasses.Viewer ← NIL,
node: TiogaOpsDefs.Ref, -- the text node is assumed to contain of rope of characters in the
start: INT ← 0, -- range [start..end]: this is not checked by PlayBackProcess
pauseStart: BasicTime.Pulses ← 0,
end: INT ← 0, -- except that if display=FALSE then viewer, node are unused and these
currentPos: INT ← -1, -- integers are used only for counting
startTime: BasicTime.Pulses ← 0, -- when playback was started
timeRemnant: INT ← 0 -- (end-start+1) represents the duration of the rope section in 'whole characters' rounded down: this is the rounding error in voice samples (OBSOLETE)
];
when placed in the playback queue, a request should have currentPos=start-1
requests with end<start should not be queued!!!
when a PlayBackRequest is on the head of playBackState.queue and display=TRUE then the characters currentPos and currentPos-1 in the node currentlyhave looks w instead of v, provided in each case that start<=characterPosition<=end
cuePriority: Process.Priority = Process.priorityRealTime;
aVeryLongTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[300000000];
the BasicTime ClockPulses wrap around every hour [roughly]. Since we use the clock to time intervals of 250ms, five minutes is indeed aVeryLongTime for the purposes of spotting wrap-around
oneSoundCharTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[1000000/TiogaVoicePrivate.soundRopeCharsPerSecond];
waitForReportTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[100000000]; -- 100 sec
PlayBackProcess: PROC = {
moreToDo: BOOLEAN;
nextWakeUp: BasicTime.Pulses;
Process.SetPriority[cuePriority];
try to keep this process up to time: however there is compensation for delay in each wake-up by looking at a real time clock; see below
[moreToDo, nextWakeUp] ← PlayBackNext[];
WHILE moreToDo DO
now: BasicTime.Pulses ← BasicTime.GetClockPulses[];
IF nextWakeUp - now < aVeryLongTime THEN { -- so as to cope properly with wrap-around
timeout: Process.Ticks ← Process.MsecToTicks[BasicTime.PulsesToMicroseconds[ nextWakeUp-now]/1000];
TRUSTED { Process.SetTimeout[@playBackCue, timeout]; };
Wait[];
};
[moreToDo, nextWakeUp] ← PlayBackNext[];
ENDLOOP;
};
playBackCue: CONDITION;
Wait: ENTRY PROC [] RETURNS [] ~ {
WAIT playBackCue;
};
PlayBackNext: ENTRY PROC RETURNS [moreToDo: BOOLEANTRUE, nextWakeUp: BasicTime.Pulses] = {
The playBackQueue contains two different kinds of requests:
normal requests are queued by some kind of playback initiation; they have an associated VoiceRope.Play and therefore will have $started and $finished reports. expectFinishedReport=TRUE.
edited requests are created by RedrawViewer in response to user edits in front of the playback cue after a playback request; they must be started and finished more manually. expectFinishedReport=FALSE.
currRequest: PlayBackRequest ← playBackState.queue.first;
IF playBackState.abort THEN {
RemoveCue[currRequest];
playBackState.queue ← NIL;
playBackState.running ← FALSE;
playBackState.abort ← FALSE;
TiogaVoicePrivate.SetVoiceViewerEditStatus[currRequest.viewer];
BROADCAST abortCleared;
RETURN [moreToDo: FALSE, nextWakeUp: 0] -- the latter only to satisfy the compiler
};
WHILE currRequest.state = done OR
(NOT
currRequest.expectFinishedReport AND currRequest.currentPos>currRequest.end) DO
prevRequest: PlayBackRequest;
RemoveCue[currRequest];
TiogaVoicePrivate.SetVoiceViewerEditStatus[currRequest.viewer];
playBackState.queue ← playBackState.queue.rest;
IF playBackState.queue = NIL THEN {
playBackState.running ← FALSE;
RETURN[moreToDo: FALSE, nextWakeUp: 0];
};
prevRequest ← currRequest;
currRequest ← playBackState.queue.first;
IF NOT prevRequest.expectFinishedReport THEN
request arose from editing after playback queued - need to start the next piece
IF currRequest.start > currRequest.end THEN currRequest.state ← done
ELSE {currRequest.startTime ← BasicTime.GetClockPulses[]; currRequest.state ← busy};
ENDLOOP;
IF currRequest.state = busy AND (currRequest.display OR NOT currRequest.expectFinishedReport) THEN {
currRequest.currentPos ← currRequest.currentPos + 1;
IF currRequest.display THEN {
IF currRequest.currentPos-2 >= currRequest.start THEN
TiogaVoicePrivate.SetVoiceLooks[currRequest.node, currRequest.currentPos-2, 1, ""];
IF currRequest.currentPos <= currRequest.end THEN
TiogaVoicePrivate.SetVoiceLooks[currRequest.node, currRequest.currentPos, 1, "w"];
};
playBackState.nextTime ← currRequest.startTime +
(currRequest.currentPos-currRequest.start+1)*oneSoundCharTime;
endcase: currRequest.timeRemnant*(1000/Jukebox.bytesPerMS) handled by $finished report
}
ELSE playBackState.nextTime ← BasicTime.GetClockPulses[] + waitForReportTime;
either not started, not displaying, or done displaying: wait for a report to get going again
RETURN[moreToDo: TRUE, nextWakeUp: playBackState.nextTime]
};
RemoveCue: INTERNAL PROC [request: PlayBackRequest] ~ {
restores any w looks to v looks, using the invariant "currentPos-1 & currentPos are the only chars that can have w looks (although they may not)"
IF NOT request.display THEN RETURN;
IF request.start <= request.currentPos AND request.currentPos-1 <= request.end THEN
TiogaVoicePrivate.SetVoiceLooks[request.node, request.currentPos-1, 2, ""];
no need to worry about node boundaries; TextEdit.ChangeLooks prunes as needed
};
QueuePlayBackCue: ENTRY PROC [request: PlayBackRequest] = {
WHILE playBackState.running AND playBackState.abort DO WAIT abortCleared ENDLOOP;
IF playBackState.running
THEN AppendRequest[request]
ELSE {
playBackState.queue ← CONS[request, NIL];
playBackState.nextTime ← BasicTime.GetClockPulses[];
playBackState.running ← TRUE;
TRUSTED {Process.Detach[FORK PlayBackProcess[]]};
};
};
AppendRequest: INTERNAL PROC [request: PlayBackRequest] = {
hangOffPoint: LIST OF PlayBackRequest ← playBackState.queue;
oneElementList: LIST OF PlayBackRequest ← CONS[request, NIL];
WHILE hangOffPoint.rest # NIL DO hangOffPoint ← hangOffPoint.rest ENDLOOP;
hangOffPoint.rest ← oneElementList
};
AbortCues: INTERNAL PROC = {
IF playBackState.running THEN {playBackState.abort ← TRUE; BROADCAST playBackCue}
};
The public procedures
PlayBackMenuProc: PUBLIC TiogaOps.CommandProc = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOLTRUE, quit: BOOLFALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
General playback proc for all viewers: on a right click or if there is no primary selection, if the viewer is a voice viewer then play the whole of it otherwise do nothing; in other cases play all the voice represented in/by [for text/voice viewers] the current selection.
buttonParams: TiogaVoicePrivate.ButtonParams ← NARROW[ViewerOps.FetchProp[viewer, $ButtonParams]];
mouseButton: ViewerClasses.MouseButton ← IF buttonParams#NIL THEN buttonParams.mouseButton ELSE $red;
IF TiogaVoicePrivate.RecordingInProgress[] THEN {
MessageWindow.Append["Stop recording before trying to play back", TRUE];
MessageWindow.Blink[];
RETURN
};
IF mouseButton = blue THEN -- i.e. right
PlayWholeSlab[NARROW[viewer, ViewerClasses.Viewer]]
ELSE {
v: ViewerClasses.Viewer;
start, end: TiogaOpsDefs.Location;
[viewer: v, start: start, end: end] ← TiogaOps.GetSelection[];
IF v = NIL THEN -- no selection
PlayWholeSlab[NARROW[viewer, ViewerClasses.Viewer]]
ELSE IF ViewerOps.FetchProp[v, $voiceViewerInfo] # NIL THEN {
IF start.node # end.node THEN ERROR; -- voice viewers only have one display node !!!!
TiogaOps.SelectPoint[v, start]; -- leave caret at left of playback selection
PlaySlabSection[v, start.node, start.where, end.where];
}
ELSE TiogaVoicePrivate.PlaySelection[];
};
};
PlaySlabSection: PUBLIC PROC [viewer: ViewerClasses.Viewer, node: TiogaOpsDefs.Ref, from, to: INT] = {
nb: VoiceRope.NB;
viewerInfo: TiogaVoicePrivate.VoiceViewerInfo ← NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo];
newRequest: PlayBackRequest;
IF viewerInfo = NIL THEN RETURN; -- somebody's buggering around with the selection
IF node # TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]] THEN ERROR;
newRequest ← NEW[PlayBackRequestRec ← [requestID: VoiceRope.NewRequestID[], viewer: viewer, node: node, start: from, end: to, currentPos: from-1]];
QueuePlayBackCue[newRequest];
[nb: nb] ← VoiceRope.PlayNB[handle: TiogaVoicePrivate.thrushHandle,
voiceRope: NEW [VoiceRope.VoiceRopeInterval ← [viewerInfo.ropeInterval.ropeID, from*TiogaVoicePrivate.soundRopeCharLength,
(to-from)*TiogaVoicePrivate.soundRopeCharLength]],
requestID: newRequest.requestID, clientData: $TiogaVoice];
IF nb # $success THEN SetPlayBackState[newRequest.requestID, done];
};
PlayWholeSlab: PUBLIC PROC [viewer: ViewerClasses.Viewer] = {
viewerInfo: TiogaVoicePrivate.VoiceViewerInfo ← NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo];
IF viewerInfo = NIL THEN
MessageWindow.Append["Not a voice window or no selection", TRUE]
ELSE {
nb: VoiceRope.NB;
node: TiogaOpsDefs.Ref ← TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]];
nodeLength: INT ← TiogaOps.GetRope[node].Length;
newRequest: PlayBackRequest ← NEW[PlayBackRequestRec ← [requestID: VoiceRope.NewRequestID[], viewer: viewer, node: node, end: nodeLength-1, timeRemnant: viewerInfo.remnant]];
QueuePlayBackCue[newRequest];
[nb: nb] ← VoiceRope.PlayNB[handle: TiogaVoicePrivate.thrushHandle,
voiceRope: NEW [VoiceRope.VoiceRopeInterval ← viewerInfo.ropeInterval],
requestID: newRequest.requestID, clientData: $TiogaVoice];
IF nb # $success THEN SetPlayBackState[newRequest.requestID, done];
}
};
PlayRopeWithoutCue: PUBLIC PROC [voiceID: Rope.ROPE] RETURNS [badRope: TiogaVoicePrivate.BadRope]= {
nb: VoiceRope.NB;
fullRope: VoiceRope.VoiceRope ← NEW [VoiceRope.VoiceRopeInterval ← [voiceID, 0, 0]];
newRequest: PlayBackRequest;
fullRope.length ← VoiceRope.Length[handle: TiogaVoicePrivate.thrushHandle, vr: fullRope];
badRope ← SELECT fullRope.length FROM
= -1 => doesNotExist,
= 0 => zeroLength,
ENDCASE => okay;
IF badRope < okay THEN RETURN;
newRequest ← NEW [PlayBackRequestRec ← [requestID: VoiceRope.NewRequestID[], display: FALSE, end: fullRope.length/TiogaVoicePrivate.soundRopeCharLength, timeRemnant: fullRope.length MOD TiogaVoicePrivate.soundRopeCharLength]];
QueuePlayBackCue[newRequest];
[nb: nb] ← VoiceRope.PlayNB[handle: TiogaVoicePrivate.thrushHandle, voiceRope: fullRope,
requestID: newRequest.requestID, clientData: $TiogaVoice];
IF nb # $success THEN SetPlayBackState[newRequest.requestID, done];
};
PlayBackInProgress: PUBLIC PROC RETURNS [BOOLEAN] = { RETURN [playBackState.running AND ~playBackState.abort] }; -- only a hint: a new playback request may be queued up and waiting for the abort to clear before becoming the new playBackState.queue
PauseProc: PUBLIC TiogaOps.CommandProc = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOLTRUE, quit: BOOLFALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
buttonParams: TiogaVoicePrivate.ButtonParams ← NARROW[ViewerOps.FetchProp[viewer, $ButtonParams]];
mouseButton: ViewerClasses.MouseButton ← IF buttonParams#NIL THEN buttonParams.mouseButton ELSE $red;
SELECT mouseButton FROM
$red => [] ← VoiceRope.PauseNB[TiogaVoicePrivate.thrushHandle];
$yellow => [] ← VoiceRope.ResumeNB[TiogaVoicePrivate.thrushHandle];
ENDCASE;
};
CancelPlayBack: PUBLIC ENTRY PROC = { AbortCues[] };
GetCurrentPlayBackPos: PUBLIC PROC RETURNS [display: BOOLEANFALSE, viewer: ViewerClasses.Viewer←NIL, currentPos: INT←-1] = {
IF playBackState.queue#NIL THEN {
currRequest: PlayBackRequest ← playBackState.queue.first;
IF currRequest.state=busy THEN
RETURN[currRequest.display, currRequest.viewer, currRequest.currentPos];
};
};
SetPlayBackState: PUBLIC ENTRY PROC [reqID: VoiceRope.RequestID, newState: TiogaVoicePrivate.PlayBackRequestState] = {
Respond to playback progress reports from Finch/Thrush.
FOR playQ: PlayBackRequestList ← playBackState.queue, playQ.rest WHILE playQ#NIL DO
IF playQ.first.requestID = reqID THEN {
playQ.first.state ← newState; -- worry about busyi after donei ?
IF newState=busy THEN playQ.first.startTime ← BasicTime.GetClockPulses[];
IF newState = paused THEN playQ.first.pauseStart ← BasicTime.GetClockPulses[];
IF newState = resumed THEN {
playQ.first.startTime ← playQ.first.startTime + BasicTime.GetClockPulses[] -
 playQ.first.pauseStart;
playQ.first.state ← busy;
newState ← busy;
};
IF playQ#playBackState.queue THEN
FOR pQ: PlayBackRequestList ← playBackState.queue, pQ.rest WHILE pQ#playQ DO
pQ.first.state ← done; -- must have missed the report
ENDLOOP;
BROADCAST playBackCue;
RETURN;
};
ENDLOOP;
};
RemoveViewerReferences: PUBLIC ENTRY PROC [viewer: ViewerClasses.Viewer] RETURNS [okay: BOOLEANTRUE] = {
IF playBackState.queue = NIL THEN RETURN;
IF playBackState.queue.first.display AND playBackState.queue.first.viewer = viewer THEN RETURN [FALSE];
FOR l: LIST OF PlayBackRequest ← playBackState.queue.rest, l.rest WHILE l # NIL DO
IF l.first.viewer = viewer THEN l.first.display ← FALSE
ENDLOOP
};
RedrawViewer: PUBLIC ENTRY PROC [viewer: ViewerClasses.Viewer, newContents: Rope.ROPE, unchangedHead, deleteChars, insertChars: INT, timeRemnant: INT, age: BOOLEAN, selectionsAfterRedraw: TiogaVoicePrivate.SelectionsAfterRedraw] RETURNS [newNode: TiogaOpsDefs.Ref] = {
called in order to redraw an edited voice viewer, which may or may not be in the queue of viewers in which cues are appearing or are about to appear: newContents is the result of the edit as is timeRemnant, and the three INT arguments refer to the display prior to the edit
all voice viewers will be 'aged' by this call [see TiogaVoicePrivate for details] unless age=FALSE OR insertChars = 0
because the contents of the viewer are to be completely reset, any selections therein will be upset: how selections are to be affected must always be specified explicitly
these preserve the selection info for reasoning after the redraw
pViewer, sViewer: ViewerClasses.Viewer;
pStart, pEnd, sStart, sEnd: TiogaOpsDefs.Location;
pLevel, sLevel: TiogaOpsDefs.SelectionGrain;
pCaretBefore, sCaretBefore, pPendingDelete, sPendingDelete: BOOLEAN;
[viewer: pViewer, start: pStart, end: pEnd, level: pLevel, caretBefore: pCaretBefore, pendingDelete: pPendingDelete] ← TiogaOps.GetSelection[primary];
[viewer: sViewer, start: sStart, end: sEnd, level: sLevel, caretBefore: sCaretBefore, pendingDelete: sPendingDelete] ← TiogaOps.GetSelection[secondary];
newNode ← TiogaVoicePrivate.SetTiogaViewerParams[viewer, newContents];
now attend to resetting selections:
SELECT selectionsAfterRedraw FROM
unAltered => -- if the selected character have been edited then we'll try to keep the selection on the same bits of voice
{ lastCharInNode: INT ← (TiogaOps.GetRope[newNode]).Length - 1;
IF pViewer = viewer THEN
{ IF pStart.node # pEnd.node THEN TiogaOps.CancelSelection[primary]
ELSE
{ pStart.node ← pEnd.node ← newNode;
IF pStart.where >= unchangedHead THEN pStart.where ← pStart.where - deleteChars + insertChars;
IF pEnd.where >= unchangedHead THEN pEnd.where ← pEnd.where - deleteChars + insertChars;
IF pStart.where > lastCharInNode THEN pStart.where ← lastCharInNode;
IF pEnd.where > lastCharInNode THEN pEnd.where ← lastCharInNode;
TiogaOps.SetSelection[viewer: pViewer, start: pStart, end: pEnd, level: pLevel, caretBefore: pCaretBefore, pendingDelete: pPendingDelete, which: primary]
}
};
IF sViewer = viewer THEN
{ IF sStart.node # sEnd.node THEN TiogaOps.CancelSelection[secondary]
ELSE
{ sStart.node ← sEnd.node ← newNode;
IF sStart.where >= unchangedHead THEN sStart.where ← sStart.where - deleteChars + insertChars;
IF sEnd.where >= unchangedHead THEN sEnd.where ← sEnd.where - deleteChars + insertChars;
IF sStart.where > lastCharInNode THEN sStart.where ← lastCharInNode;
IF sEnd.where > lastCharInNode THEN sEnd.where ← lastCharInNode;
TiogaOps.SetSelection[viewer: sViewer, start: sStart, end: sEnd, level: sLevel, caretBefore: sCaretBefore, pendingDelete: sPendingDelete, which: secondary]
}
}
};
deSelected =>
{ IF pViewer = viewer THEN TiogaOps.CancelSelection[primary];
IF sViewer = viewer THEN TiogaOps.CancelSelection[secondary]
};
primaryOnInsert =>
{ nodeLength: INT ← (TiogaOps.GetRope[newNode]).Length;
IF sViewer = viewer THEN TiogaOps.CancelSelection[secondary];
IF nodeLength = 0 THEN TiogaOps.CancelSelection[primary] -- in case the viewer is empty
ELSE
{ pViewer ← viewer;
pStart.node ← pEnd.node ← newNode;
IF insertChars = 0
THEN -- leave a point selection
{ pStart.where ← pEnd.where ← (IF unchangedHead > nodeLength THEN nodeLength ELSE unchangedHead);
pLevel ← point
}
ELSE
{ pStart.where ← unchangedHead;
pEnd.where ← unchangedHead + insertChars -1;
pLevel ← char
};
pCaretBefore ← pPendingDelete ← FALSE;
TiogaOps.SetSelection[viewer: pViewer, start: pStart, end: pEnd, level: pLevel, caretBefore: pCaretBefore, pendingDelete: pPendingDelete, which: primary]
}
};
ENDCASE => ERROR;
IF age AND insertChars # 0 THEN TiogaVoicePrivate.AgeAllViewers[viewer] ELSE TiogaVoicePrivate.ReColorViewer[viewer];
process any instances of this viewer on the queue [not including the one at the head]
IF playBackState.queue # NIL THEN FOR p: LIST OF PlayBackRequest ← playBackState.queue, p.rest WHILE p.rest # NIL DO
currRequest: PlayBackRequest ← p.rest.first;
IF currRequest.display AND currRequest.viewer = viewer THEN
{ currRequest.node ← newNode; -- I've just changed it under your feet!! [okay - monitored]
IF currRequest.end >= unchangedHead -- if not, unaffected by the edit
THEN
{ newRequests: LIST OF PlayBackRequest ← p.rest.rest;
if this PlayBackRequest is part of something that has been edited, we need to split it into up to three sections: the first section is the unedited head of the slab, to be displayed. The second is the spliced out section, which is not to be displayed [but appears to keep the timing correct]. The third is the tail of the slab which is to be displayed: its position in the slab has in general moved. We will assemble these components in reverse order, simply because that's what CONS allows. newRequests was initialised to point to the list of entries beyond the element we are about to replace
**** this and the following section need changing so that the remnant goes in the last of the split parts, as opposed to the third part if it exists
IF currRequest.end >= unchangedHead + deleteChars THEN -- non-empty third part
{ start: INTMAX[ unchangedHead + deleteChars, currRequest.start ]
+ (insertChars - deleteChars);
end: INT ← currRequest.end + (insertChars - deleteChars);
newRequests ← CONS[ NEW[ PlayBackRequestRec ← [requestID: currRequest.requestID, state: waiting, expectFinishedReport: TRUE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: start-1, timeRemnant: timeRemnant]], newRequests]
};
IF currRequest.start < unchangedHead + deleteChars THEN -- non-empty second part [since request cannot lie entirely in first part - see two IFs back]
{ start: INTMAX[unchangedHead, currRequest.start];
end: INTMIN [unchangedHead + deleteChars - 1, currRequest.end];
newRequests ← CONS[ NEW[ PlayBackRequestRec ← [requestID: currRequest.requestID, state: waiting, expectFinishedReport: FALSE, display: FALSE, start: start, end: end, currentPos: start-1, timeRemnant: 0]], newRequests]
};
IF currRequest.start < unchangedHead THEN -- non-empty first part
{ start: INT ← currRequest.start;
end: INTMIN [unchangedHead - 1, currRequest.end];
newRequests ← CONS[ NEW[ PlayBackRequestRec ← [requestID: currRequest.requestID, state: waiting, expectFinishedReport: FALSE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: start-1, timeRemnant: 0]], newRequests]
};
link the new structure into the queue in place of the old one
p.rest ← newRequests
}
}
ENDLOOP;
now we have to go through all that guff again for the head of the queue, except that there is an additional complication: the head of the queue, if being played back, must have the playback looks put back into it. Also the head of the queue must be whichever of the three sections discussed above is currently being played back. Alas we must also remove any character looks corresponding to the cue before this redraw occurred
IF playBackState.queue # NIL THEN {
currRequest: PlayBackRequest ← playBackState.queue.first;
currentPosFound: BOOLEANFALSE;
IF currRequest.display AND currRequest.viewer = viewer THEN {
newRequests: LIST OF PlayBackRequest ← playBackState.queue.rest;
currRequest.node ← newNode;
IF currRequest.end >= unchangedHead THEN {
If not, unaffected by the edit [but still will need to put back the playback looks].
One advantage of going backward through these is that it's easy to stop when we find the one currently being played back.
IF currRequest.end >= unchangedHead + deleteChars THEN {
start: INTMAX[ unchangedHead + deleteChars, currRequest.start ]
+ (insertChars - deleteChars);
end: INT ← currRequest.end + (insertChars - deleteChars);
currentPos: INTMAX [ start-1, currRequest.currentPos + (insertChars - deleteChars) ];
newRequests ← CONS[ NEW[ PlayBackRequestRec ← [requestID: currRequest.requestID, state: waiting, expectFinishedReport: TRUE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: timeRemnant]], newRequests];
IF currentPos # start-1 THEN
currentPosFound ← TRUE;
};
IF (~currentPosFound) AND currRequest.start < unchangedHead + deleteChars THEN {
start: INTMAX[unchangedHead, currRequest.start];
end: INTMIN [unchangedHead + deleteChars - 1, currRequest.end];
currentPos: INTMAX [ start-1, currRequest.currentPos]; -- no need to put in a MIN clause since otherwise currentPosFound=TRUE and we'd never have got here
newRequests ← CONS[ NEW[ PlayBackRequestRec ← [requestID: currRequest.requestID, state: waiting, expectFinishedReport: FALSE, display: FALSE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: 0]], newRequests];
IF currentPos # start-1 THEN
currentPosFound ← TRUE;
};
IF (~currentPosFound) AND currRequest.start < unchangedHead THEN {
start: INT ← currRequest.start;
end: INTMIN [unchangedHead - 1, currRequest.end];
currentPos: INT ← currRequest.currentPos;
newRequests ← CONS[ NEW[ PlayBackRequestRec ← [requestID: currRequest.requestID, state: currRequest.state, expectFinishedReport: FALSE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: 0]], newRequests]
};
newRequests.first.state ← currRequest.state;
newRequests.first.startTime ← currRequest.startTime;
link the new structure into the queue in place of the old head
playBackState.queue ← newRequests;
currRequest ← playBackState.queue.first;
};
now redraw the playback cue: beware - the head of the queue might now be a ~display entry
IF currRequest.display AND currRequest.currentPos>=currRequest.start THEN {
start: INTMAX[currRequest.start, currRequest.currentPos-1];
len: INTMIN[currRequest.end, currRequest.currentPos] - start;
TiogaVoicePrivate.SetVoiceLooks[node: currRequest.node, start: start, len: len, looks: "w"];
}
}
}
};
}.
Swinehart, April 10, 1987 9:38:29 am PDT
Merged VoicePlayImpl and VoiceRecordImpl, from Ades. Fixed some nested ML problems resulting from merging two MONITORs.
changes to: AddVoiceProc, DictationMachine, AbortCues, CancelPlayBack
Polle Zellweger (PTZ) May 14, 1987 5:03:48 pm PDT
Trying to avoid intermittent viewer lockups.
changes to: DIRECTORY, ChangeVoiceInputFocus, PlayBackNext, PlayBackMenuProc, RedrawViewer, IncrementRecordTimer, DoIt (local of SetPlayLooks), PlayBackNext, SetPlayLooks (new procedure)
Polle Zellweger (PTZ) May 15, 1987 7:04:36 pm PDT
Avoid using the Tioga primary selection to change looks when animating the gray square for voice play feedback; also when adding indication of new voice. Intended as a general improvement and trying to avoid intermittent viewer lockups.
changes to: DIRECTORY, VoiceRecordPlayImpl, IncrementRecordTimer, NewRecordingMarker, DoIt (local of NewRecordingMarker), StopRecording, SetPlayLooks, DoIt (local of SetPlayLooks), PlayBackNext, RedrawViewer
Polle Zellweger (PTZ) May 22, 1987 4:51:43 pm PDT
Split back into VoicePlaybackImpl and VoiceRecordImpl: too many monitor lock difficulties to fix quickly.
Polle Zellweger (PTZ) May 27, 1987 11:24:52 am PDT
changes to: DIRECTORY, RedrawViewer(SetStyle was necessary after all - now done w/o selections)
Polle Zellweger (PTZ) May 28, 1987 4:32:22 pm PDT
New procedure to allow Marking the current playback spot directly.
changes to: GetCurrentPlayBackPos
Polle Zellweger (PTZ) June 8, 1987 6:45:18 pm PDT
Moved SetPlayLooks to VoiceViewersImpl.SetVoiceLooks; use new VoiceViewersImpl.SetTiogaViewerParams.
changes to: PlayBackNext, RedrawViewer
Polle Zellweger (PTZ) June 19, 1987 12:11:12 pm PDT
parameter change for SetVoiceLooks
changes to: PlayBackNext, RedrawViewer
Polle Zellweger (PTZ) July 20, 1987 5:12:15 pm PDT
Add Finch/Thrush error handling (disappearing conversations, etc) + voicerope monitoring. Extensive changes to playback cue stuff; now done by following reports from Finch/Thrush rather than open-loop.
changes to: DIRECTORY, PlayBackRequestList, playBackState, abortCleared, PlayBackRequestRec, QueuePlayBackCue, AppendRequest, PlaySlabSection, PlayWholeSlab, PlayRopeWithoutCue, PlayBackProcess, PlayBackInProgress, GetCurrentPlayBackPos, SetPlayBackState, RemoveViewerReferences, RedrawViewer
Polle Zellweger (PTZ) July 27, 1987 1:43:58 pm PDT
changes to: PlaySlabSection, PlayWholeSlab, PlayRopeWithoutCue
Polle Zellweger (PTZ) January 22, 1988 4:51:14 pm PST
user message improvements
changes to: DIRECTORY, PlayRopeWithoutCue
Polle Zellweger (PTZ) September 8, 1988 11:11:40 pm PDT
Keep nextTime from growing when a succession of requests are queued without voice viewers & cues; signal playBackCue immediately when an abort is received.
changes to: PlayBackNext, AbortCues
Polle Zellweger (PTZ) September 13, 1988 1:21:56 pm PDT
Changes to allow clients to alter button behaviors via Tioga registry (for WalnutTiogaVoice).
changes to: DIRECTORY, PlayBackMenuProc, PauseProc