VoicePlayBackImpl.mesa
routines replay sounds, plus all the guff to move a cue along a slab display
Ades, April 29, 1986 4:49:06 pm PDT
DIRECTORY
BasicTime USING [GetClockPulses, PulsesToMicroseconds, MicrosecondsToPulses, Pulses],
Jukebox USING [bytesPerMS],
Process USING [Pause, MsecToTicks, Priority, priorityRealTime, SetPriority, Detach],
Rope USING [ROPE, Length],
Menus USING [MenuProc],
MessageWindow USING [Append, Blink],
TiogaOps USING [GetRope, SaveSelA, RestoreSelA, SetSelection, AddLooks, SubtractLooks, ViewerDoc, FirstChild, SelectDocument, SetStyle, SetLooks, GetSelection, SelectPoint, ClearLooks, CancelSelection],
TiogaOpsDefs USING [Ref, Location, SelectionGrain],
VoiceAging USING [AgeAllViewers, ReColorViewer],
VoiceViewers USING [VoiceViewerInfo, SetVoiceViewerEditStatus, soundRopeCharLength, soundRopeCharsPerSecond],
VoiceInText USING [thrushHandle, PlaySelection],
ViewerClasses USING [Viewer],
ViewerOps USING [FetchProp],
ViewerTools USING [TiogaContents, TiogaContentsRec, SetTiogaContents],
VoiceMarkers USING [RedrawTextMarkers],
VoiceRecord USING [RecordingInProgress],
VoiceRope USING [VoiceRope, VoiceRopeInterval, Play, Length],
VoicePlayBack;
VoicePlayBackImpl: CEDAR MONITOR IMPORTS BasicTime, MessageWindow, Process, Rope, TiogaOps, VoiceAging, VoiceViewers, VoiceInText, ViewerOps, ViewerTools, VoiceMarkers, VoiceRecord, VoiceRope EXPORTS VoicePlayBack = BEGIN
see the head of VoicePlayBack.mesa for the raison d'etre of this module
playBackState: RECORD
[ running: BOOLEANFALSE, -- is there currently a forked instance of PlayBackProcess ?
queue: LIST OF PlayBackRequest ← 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
[ display: BOOLEAN, -- if not set then there is no displaying to be done
viewer: ViewerClasses.Viewer,
node: TiogaOpsDefs.Ref, -- the text node is assumed to contain of rope of characters in the
start: INT, -- range [start..end]: this is not checked by PlayBackProcess
end: INT, -- except that if display=FALSE then viewer, node are unused and these
currentPos: INT, -- integers are used only for counting
timeRemnant: INT -- (end-start+1) represents the duration of the rope section in 'whole characters' rounded down: this is the rounding error in voice samples
];
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
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 -- so as to cope properly with wrap-around
THEN Process.Pause[Process.MsecToTicks[BasicTime.PulsesToMicroseconds[ nextWakeUp-now]/1000]];
[moreToDo, nextWakeUp] ← PlayBackNext[]
ENDLOOP
};
PlayBackNext: ENTRY PROC RETURNS [moreToDo: BOOLEANTRUE, nextWakeUp: BasicTime.Pulses] = {
currRequest: PlayBackRequest ← playBackState.queue.first;
IF playBackState.abort THEN
{ IF currRequest.display AND currRequest.currentPos>=currRequest.start THEN
{ TiogaOps.SaveSelA[];
TiogaOps.SetSelection[currRequest.viewer, [currRequest.node, MAX[currRequest.start, currRequest.currentPos-1]], [currRequest.node, MIN[currRequest.end, currRequest.currentPos]]];
TiogaOps.AddLooks["v"];
TiogaOps.SubtractLooks["w"];
TiogaOps.RestoreSelA[]
}; -- all that restored any W looks to V looks
playBackState.queue ← NIL;
playBackState.running ← FALSE;
playBackState.abort ← FALSE;
VoiceViewers.SetVoiceViewerEditStatus[currRequest.viewer];
BROADCAST abortCleared;
RETURN [moreToDo: FALSE, nextWakeUp: 0] -- the latter only to satisfy the compiler
};
currRequest.currentPos ← currRequest.currentPos + 1;
IF currRequest.display THEN
{ TiogaOps.SaveSelA[];
IF currRequest.currentPos-2 >= currRequest.start THEN
{ TiogaOps.SetSelection[currRequest.viewer, [currRequest.node, currRequest.currentPos-2], [currRequest.node, currRequest.currentPos-2]];
TiogaOps.AddLooks["v"];
TiogaOps.SubtractLooks["w"]
};
IF currRequest.currentPos <= currRequest.end THEN
{ TiogaOps.SetSelection[currRequest.viewer, [currRequest.node, currRequest.currentPos], [currRequest.node, currRequest.currentPos]];
TiogaOps.AddLooks["w"];
TiogaOps.SubtractLooks["v"]
};
TiogaOps.RestoreSelA[]
};
IF currRequest.currentPos < currRequest.end + 2
THEN
{ playBackState.nextTime ← playBackState.nextTime + BasicTime.MicrosecondsToPulses[(IF currRequest.currentPos = currRequest.end + 1 THEN currRequest.timeRemnant*(1000/Jukebox.bytesPerMS) ELSE 1000000/VoiceViewers.soundRopeCharsPerSecond)];
RETURN[nextWakeUp: playBackState.nextTime]
};
getting down here means the current display is finished: if there is another we will start it, if not kill the process. first make sure the 'edited' status of the viewer is set correctly
VoiceViewers.SetVoiceViewerEditStatus[currRequest.viewer];
playBackState.queue ← playBackState.queue.rest;
IF playBackState.queue = NIL THEN
{ playBackState.running ← FALSE;
RETURN[moreToDo: FALSE, nextWakeUp: 0]
}
ELSE RETURN [nextWakeUp: playBackState.nextTime] -- will cause PlayBackNext to be called again immediately to colour the first cell of the next sound section
};

QueuePlayBackCue: ENTRY PROC [request: PlayBackRequest] = {
WHILE playBackState.running AND playBackState.abort DO WAIT abortCleared ENDLOOP;
IF playBackState.running
THEN AppendRequest[request]
ELSE
{ p: PROCESS;
playBackState.queue ← CONS[request, NIL];
playBackState.nextTime ← BasicTime.GetClockPulses[];
playBackState.running ← TRUE;
p ← FORK PlayBackProcess[];
TRUSTED {Process.Detach[p]};
}
};
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: ENTRY PROC = { IF playBackState.running THEN playBackState.abort ← TRUE };



-------------------------
the public interface
PlayBackMenuProc: PUBLIC Menus.MenuProc = {
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
IF VoiceRecord.RecordingInProgress[] THEN
{ MessageWindow.Append["Stop recording before trying to play back", TRUE];
MessageWindow.Blink[];
RETURN
};
IF mouseButton = blue -- i.e. right
THEN PlayWholeSlab[NARROW[parent, ViewerClasses.Viewer]]
ELSE
{ viewer: ViewerClasses.Viewer;
start, end: TiogaOpsDefs.Location;
[viewer: viewer, start: start, end: end] ← TiogaOps.GetSelection[];
IF viewer = NIL -- no selection
THEN PlayWholeSlab[NARROW[parent, ViewerClasses.Viewer]]
ELSE
{ IF ViewerOps.FetchProp[viewer, $voiceViewerInfo] # NIL
THEN
{ IF start.node # end.node THEN ERROR; -- voice viewers only have one display node !!!!
TiogaOps.SelectPoint[viewer, start]; -- leave caret at left of playback selection
PlaySlabSection[viewer, start.node, start.where, end.where]
}
ELSE VoiceInText.PlaySelection[]
}
}
};
PlaySlabSection: PUBLIC PROC [viewer: ViewerClasses.Viewer, node: TiogaOpsDefs.Ref, from, to: INT] = {
viewerInfo: VoiceViewers.VoiceViewerInfo ← NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.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 ← [display: TRUE, viewer: viewer, node: node, start: from, end: to, currentPos: from-1, timeRemnant: 0]];
VoiceRope.Play[handle: VoiceInText.thrushHandle, voiceRope: NEW [VoiceRope.VoiceRopeInterval ← [viewerInfo.ropeInterval.ropeID, from*VoiceViewers.soundRopeCharLength, (to-from)*VoiceViewers.soundRopeCharLength]]];
QueuePlayBackCue[newRequest]
};
PlayWholeSlab: PUBLIC PROC [viewer: ViewerClasses.Viewer] = {
viewerInfo: VoiceViewers.VoiceViewerInfo ← NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo];
IF viewerInfo = NIL THEN MessageWindow.Append["Not a voice window or no selection", TRUE]
ELSE
{ node: TiogaOpsDefs.Ref ← TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]];
nodeLength: INT ← TiogaOps.GetRope[node].Length;
newRequest: PlayBackRequest ← NEW[PlayBackRequestRec ← [display: TRUE, viewer: viewer, node: node, start: 0, end: nodeLength-1, currentPos: -1, timeRemnant: viewerInfo.remnant]];
VoiceRope.Play[handle: VoiceInText.thrushHandle, voiceRope: NEW [VoiceRope.VoiceRopeInterval ← viewerInfo.ropeInterval]];
QueuePlayBackCue[newRequest]
}
};
PlayRopeWithoutCue: PUBLIC PROC [voiceID: Rope.ROPE] = {
fullRope: VoiceRope.VoiceRope ← NEW [VoiceRope.VoiceRopeInterval ← [voiceID, 0, 0]];
newRequest: PlayBackRequest;
**** ought to check that voice rope actually exists
fullRope.length ← VoiceRope.Length[handle: VoiceInText.thrushHandle, vr: fullRope];
IF fullRope.length = 0 THEN RETURN;
newRequest ← NEW [PlayBackRequestRec ← [display: FALSE, start: 0, end: fullRope.length/VoiceViewers.soundRopeCharLength, currentPos: -1, timeRemnant: fullRope.length MOD VoiceViewers.soundRopeCharLength]];
VoiceRope.Play[handle: VoiceInText.thrushHandle, voiceRope: fullRope];
QueuePlayBackCue[newRequest]
};
PlayBackInProgress: PUBLIC PROC RETURNS [BOOLEAN] = { RETURN [playBackState.running AND ~playBackState.abort] }; -- only a hint: a new playback request may be queued up and wating for the abort to clear before becoming the new playBackState.queue
CancelPlayBack: PUBLIC PROC = { AbortCues[] };
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: VoicePlayBack.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 VoiceAging 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;
viewercontents: ViewerTools.TiogaContents ← NEW[ViewerTools.TiogaContentsRec ← []];
[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];
viewercontents.contents ← newContents;
ViewerTools.SetTiogaContents[viewer, viewercontents];
TiogaOps.SaveSelA[];
TiogaOps.SelectDocument[viewer];
TiogaOps.SetStyle["voiceProfile", root];
TiogaOps.ClearLooks[];
TiogaOps.SetLooks["v"];
TiogaOps.RestoreSelA[];
newNode ← TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]];
VoiceMarkers.RedrawTextMarkers[viewer, newNode];
for all I know newNode may actually be the same as the old one, but I don't have any evidence that it is defined to be so. Also possibly the SetStyle and/or SetLooks may be redundant - ditto
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 VoiceAging.AgeAllViewers[viewer] ELSE VoiceAging.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 ← [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 ← [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 ← [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 occured
IF playBackState.queue # NIL THEN
{ currRequest: PlayBackRequest ← playBackState.queue.first;
currentPosFound: BOOLEANFALSE;
IF currRequest.display AND currRequest.viewer = viewer THEN
{ oldCurrentPos: INT ← currRequest.currentPos;
newRequests: LIST OF PlayBackRequest ← playBackState.queue.rest;
currRequest.node ← newNode;
IF currRequest.end >= unchangedHead -- if not, unaffected by the edit [but still will need to put back the playback looks]
THEN
{ -- one advantage of going backwards 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 ← [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 ← [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 ← [display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: 0]], newRequests]
};
link the new structure into the queue in place of the old head
playBackState.queue ← newRequests
};
now redraw the playback cue: beware - the head of the queue might now be a ~display entry
IF newRequests.first.display AND newRequests.first.currentPos>=currRequest.start THEN
{ TiogaOps.SaveSelA[];
TiogaOps.SetSelection[newRequests.first.viewer, [newRequests.first.node, MAX[newRequests.first.start, newRequests.first.currentPos-1]], [newRequests.first.node, MIN[newRequests.first.end, newRequests.first.currentPos]]];
TiogaOps.AddLooks["w"];
TiogaOps.SubtractLooks["v"];
TiogaOps.RestoreSelA[]
}
}
}
};
END.