DIRECTORY
BasicTime USING [GetClockPulses, MicrosecondsToPulses, Pulses, PulsesToMicroseconds ],
Convert USING [RopeFromInt],
Jukebox USING [bytesPerMS],
Menus USING [MenuProc ],
MessageWindow USING [Append, Blink ],
Process USING [Detach, MsecToTicks, Pause, Priority, priorityRealTime, SetPriority ],
Rope USING [Concat, Length, ROPE ],
TextEdit USING [GetCharProp, PutCharProp ],
TextNode USING [Ref],
TiogaButtons USING [TextNodeRef],
TiogaExtraOps USING [GetTextKey, PutTextKey, RemoveTextKey ],
TiogaOps USING [AddLooks, CallWithLocks, CancelSelection, ClearLooks, FirstChild, GetRope, GetSelection, InsertChar, NoSelection, RestoreSelA, Root, SaveSelA, SelectDocument, SelectionGrain, SelectPoint, SetLooks, SetSelection, SetStyle, SubtractLooks, ViewerDoc ],
TiogaOpsDefs USING [Location, Ref, SelectionGrain ],
TiogaVoicePrivate USING [ AgeAllViewers, ApplyToLockedChars, BuildVoiceViewer, DeleteVoiceFromChar, DescribeSelection, LockedAddCharMark, PlaySelection, ReColorViewer, RedrawTextMarkers, RedrawViewer, ReplaceSelectionWithSavedInterval, Selection, SelectionRec, SelectionsAfterRedraw, SetParentViewer, SetViewerContents, SetVoiceViewerEditStatus, SoundChars, SoundInterval, SoundIntervalRec, SoundList, SoundListFromIntervalSpecs, soundRopeCharLength, soundRopeCharsPerSecond, thrushHandle, ToggleDictationMenu, VoiceViewerInfo, VoiceWindowRec ],
ViewerClasses USING [Viewer ],
ViewerOps USING [DestroyViewer, FetchProp, PaintViewer ],
ViewerTools USING [SetTiogaContents, TiogaContents, TiogaContentsRec ],
VoiceRope USING [DescribeRope, Length, Play, Record, VoiceRope, VoiceRopeInterval ]
;
Recording
basic code to add voice to windows of both text and voice, also the DictationMachine button which transfers input from a current window to a newly created dictation window
recordingInProgress: BOOLEAN ← FALSE;
whilst a recording is being made, the following are its state variables
addingIntoTextViewer:
BOOLEAN;
the selection where the voice is to be added, as a TiogaOps node: a text key gives the position within the node but that is implicitly represented by the atom $recordingMark
targetNode: TiogaOpsDefs.Ref;
the viewer where the voice is to be added, if a text viewer
targetTextViewer: ViewerClasses.Viewer;
the selection where the voice is to be added, as a TiogaVoicePrivate selection
targetSoundSelection: TiogaVoicePrivate.Selection;
cueInsertPosition:
INT;
-- where the little arrows will go
how long has the recording been going on, in terms of sound characters generated
recordingTimeInSoundChars:
INT;
clock for timing recording
nextWakeUp: BasicTime.Pulses;
timerState: {off, abort, on};
the rope which is created as the result of this recording process
recordedRope: VoiceRope.VoiceRope;
timerOff, recordingDone:
CONDITION;
RecordingInProgress:
PUBLIC
PROC
RETURNS [
BOOLEAN] = {
RETURN [recordingInProgress] };
AddVoiceProc:
PUBLIC
ENTRY Menus.MenuProc = {
the procedure behind all AddVoice buttons
p: PROCESS;
IF recordingInProgress THEN
{ MessageWindow.Append["Already recording",
TRUE];
MessageWindow.Blink[];
RETURN
};
IF PlayBackInProgress[] THEN
{ MessageWindow.Append["Cancel playback before trying to record",
TRUE];
MessageWindow.Blink[];
RETURN
}; -- PlayBackInProgress is only a hint: to be sure we'll do a cancel to avoid race conditions
AbortCues[];
IF ~PrepareSelection[] THEN RETURN;
recordingInProgress ← TRUE;
timerState ← on;
recordedRope ← NIL;
recordingTimeInSoundChars ← 0;
fork processes (a) to call VoiceRope.Record and (b) to time the recording. (a) is detached but will return when a VoiceRope.Stop occurs. It will then place the ID of the Rope in the appropriate state variable, where the routine called as a result of hitting the stop key can get it
p ← FORK RecordNewRope[];
TRUSTED {Process.Detach[p]};
p ← FORK RecordingTimer[];
TRUSTED {Process.Detach[p]}
};
PrepareSelection:
INTERNAL
PROC
RETURNS [succeeded:
BOOLEAN ←
FALSE] = {
this procedure locks the selection and fills in the TiogaOps target info above. If the selection is a voice selection then targetSoundSelection is set up. If the selection is text then a text key is placed in the text. If the selection is pending delete then the delete is done. If the selection is a voice selection then the viewer is locked [against other voice edits]
targetViewer: ViewerClasses.Viewer;
targetStart, targetEnd: TiogaOpsDefs.Location;
targetCaretBefore, targetPendingDelete: BOOLEAN;
alreadyBeingEdited: BOOLEAN ← FALSE;
alreadyVoiceThere: BOOLEAN ← FALSE;
suitableViewer: BOOLEAN;
DoIt:
INTERNAL
PROC [root: TiogaOpsDefs.Ref] = {
level: TiogaOps.SelectionGrain;
[viewer: targetViewer, start: targetStart, end: targetEnd, caretBefore: targetCaretBefore, pendingDelete: targetPendingDelete, level: level] ← TiogaOps.GetSelection[];
suitableViewer ← targetViewer.class.flavor = $Text;
IF suitableViewer THEN
{ addingIntoTextViewer ← ViewerOps.FetchProp[targetViewer, $voiceViewerInfo] = NIL;
IF addingIntoTextViewer THEN
{
IF targetPendingDelete
THEN
{ TiogaVoicePrivate.ApplyToLockedChars[TiogaVoicePrivate.DeleteVoiceFromChar];
TiogaOps.SetSelection[viewer: targetViewer, start: targetStart, end: targetEnd,
level: level, caretBefore: targetCaretBefore,
pendingDelete: FALSE, which: primary] -- simply makes not pending delete
};
{
targetChar: TiogaOpsDefs.Location ← IF targetCaretBefore THEN targetStart ELSE targetEnd;
node: TextNode.Ref ← TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor
alreadyVoiceThere ← TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL;
targetNode ← targetChar.node;
targetTextViewer ← targetViewer;
IF ~alreadyVoiceThere THEN TiogaExtraOps.PutTextKey[targetChar.node, targetChar.where, $recordingMark]
}
}
ELSE
{
[selection: targetSoundSelection, failed: alreadyBeingEdited] ← TiogaVoicePrivate.DescribeSelection[which: primary, forceDelete: FALSE, returnSoundInterval: FALSE];
IF alreadyBeingEdited THEN RETURN;
IF targetSoundSelection.ropeInterval.length # 0 -- i.e. pending delete
THEN
IF TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[targetSoundSelection,
NIL, FALSE].viewerDeleted
THEN NewDictationWindow[]; -- this means "if applicable, do the pending delete. If that delete should reduce the window contents to zero it will disappear, so create a new one"
cueInsertPosition ← MIN [targetSoundSelection.ropeInterval.start/TiogaVoicePrivate.soundRopeCharLength, TextEdit.Size[TiogaButtons.TextNodeRef[TiogaOps.FirstChild[TiogaOps.ViewerDoc[targetSoundSelection.viewer]]]]-1] -- because you can't position the cursor as a point after the last character
cueInsertPosition ← targetSoundSelection.ropeInterval.start/ TiogaVoicePrivate.soundRopeCharLength
}
}
};
TiogaOps.CallWithLocks[DoIt ! TiogaOps.NoSelection => {suitableViewer ← FALSE; CONTINUE}];
test for failure conditions and report them to the user after releasing the viewer lock
IF NOT suitableViewer THEN
{ MessageWindow.Append["Make a selection in a tioga or text viewer first",
TRUE];
MessageWindow.Blink[]
}
ELSE
{
IF alreadyBeingEdited
THEN
{ MessageWindow.Append["Previous voice editing operation has yet to complete",
TRUE];
MessageWindow.Blink[]
}
ELSE
{
IF alreadyVoiceThere
THEN
{ MessageWindow.Append["Cannot add sound on top of another sound",
TRUE];
MessageWindow.Blink[]
}
ELSE succeeded ← TRUE
}
}
};
RecordNewRope:
PROC = {
BroadcastRecordingDone: ENTRY PROC = { BROADCAST recordingDone }; -- keeps the compiler happy!
recordedRope ← VoiceRope.Record[TiogaVoicePrivate.thrushHandle];
BroadcastRecordingDone[]
};
RecordingTimer:
PROC = {
aVeryLongTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[300000000];
see comments in VoicePlayBackImpl on this constant
cuePriority: Process.Priority = Process.priorityRealTime;
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
nextWakeUp ← BasicTime.GetClockPulses[];
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]];
IF ~ IncrementRecordTimer[] THEN RETURN
ENDLOOP
};
IncrementRecordTimer:
ENTRY
PROC
RETURNS [stillRunning:
BOOLEAN ←
TRUE] = {
IF timerState # on THEN
{ timerState ← off;
BROADCAST timerOff;
RETURN [FALSE]
};
nextWakeUp ← nextWakeUp + BasicTime.MicrosecondsToPulses[ 1000000/TiogaVoicePrivate.soundRopeCharsPerSecond];
recordingTimeInSoundChars ← recordingTimeInSoundChars + 1;
IF ~addingIntoTextViewer THEN
{ voiceViewer: ViewerClasses.Viewer ← targetSoundSelection.viewer;
caretLocation: TiogaOpsDefs.Location ← [TiogaOps.FirstChild[TiogaOps.ViewerDoc [voiceViewer]], cueInsertPosition + recordingTimeInSoundChars - 1];
this code adds another marker into a voice viewer to indicate how much has been recorded
TiogaOps.SaveSelA[];
TiogaOps.SetSelection[viewer: voiceViewer, start: caretLocation, end: caretLocation, level: point, caretBefore: TRUE, pendingDelete: FALSE, which: primary];
TiogaOps.InsertChar['>]; -- used as an 'inserting voice' indicator
TiogaOps.SetSelection[viewer: voiceViewer, start: caretLocation, end: caretLocation, level: char, caretBefore: TRUE, pendingDelete: FALSE, which: primary];
TiogaOps.SetLooks["v"];
TiogaOps.RestoreSelA[]
}
};
StopRecording:
PUBLIC
ENTRY
PROC = {
this gets called every time a STOP button is clicked, after VoiceRope.Stop has been called
IF ~recordingInProgress THEN RETURN;
IF timerState = on THEN timerState ← abort;
WHILE recordedRope = NIL DO WAIT recordingDone ENDLOOP;
WHILE timerState # off DO WAIT timerOff ENDLOOP;
recordingInProgress ← FALSE;
IF addingIntoTextViewer
THEN
{ IF recordedRope.length = 0
THEN
{ MessageWindow.Append["Zero length voice annotation - discarding",
TRUE];
MessageWindow.Blink[];
}
ELSE
{
AddVoice:
PROC [root: TiogaOpsDefs.Ref] = {
TextEdit.PutCharProp[node, targetChar.where, $voice, recordedRope.ropeID];
next line places a 'talks bubble' on the selected character - see TalksBubbleImpl
TextEdit.PutCharProp[node, targetChar.where, $Artwork, NARROW["TalksBubble", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created
};
targetChar: TiogaOpsDefs.Location ← TiogaExtraOps.GetTextKey[targetNode, $recordingMark];
node: TextNode.Ref ← TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor
IF targetNode # targetChar.node THEN ERROR; -- **** because you've caught all those !!!
TiogaExtraOps.RemoveTextKey[targetNode, $recordingMark];
IF TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL THEN RETURN; -- can happen - a store might have been performed in the intervening time
TiogaOps.CallWithLocks[AddVoice, TiogaOps.Root[targetChar.node]] -- called locked because otherwise tioga doesn't immediately repaint the artwork
}
}
ELSE
{
IF targetSoundSelection.voiceViewerInfo.ropeInterval.ropeID =
NIL
THEN -- this is a new dictation window, so just set its contents to the new rope
{ IF recordedRope.length = 0
THEN
{ MessageWindow.Append["Zero length voice dictation - destroying window",
TRUE];
MessageWindow.Blink[];
ViewerOps.DestroyViewer[targetSoundSelection.viewer]
this will cause the event proc which clears down the voiceViewerInfo etc. to be called
}
ELSE
{ targetSoundSelection.voiceViewerInfo.edited ←
TRUE;
TiogaVoicePrivate.SetViewerContents[targetSoundSelection.viewer, targetSoundSelection.voiceViewerInfo, recordedRope.ropeID, NIL, TRUE]
}
}
ELSE
{ IF recordedRope.length = 0
THEN
{ -- if the new rope to be added is of zero length then just redraw the viewer
trueContents: Rope.ROPE ← TiogaVoicePrivate.SoundChars[targetSoundSelection.voiceViewerInfo].soundRope;
MessageWindow.Append["Zero length voice addition - viewer contents unchanged", TRUE];
[] ← TiogaVoicePrivate.RedrawViewer[targetSoundSelection.viewer, trueContents, 0, 0, 0, targetSoundSelection.voiceViewerInfo.remnant, FALSE, deSelected];
TiogaVoicePrivate.SetVoiceViewerEditStatus[targetSoundSelection.viewer];
targetSoundSelection.voiceViewerInfo.editInProgress ← FALSE
}
ELSE
{ newSoundList: TiogaVoicePrivate.SoundList ← TiogaVoicePrivate.SoundListFromIntervalSpecs [VoiceRope.DescribeRope[TiogaVoicePrivate.thrushHandle, recordedRope], recordedRope.length];
newSound: TiogaVoicePrivate.SoundInterval ← NEW [TiogaVoicePrivate.SoundIntervalRec ← [ropeInterval: recordedRope^, soundList: newSoundList]];
targetSoundSelection.ropeInterval.length ← 0;
this may already be the case, but if not we already did the delete before starting to record, so set it zero. If we did the delete then targetSoundSelection.ropeInterval will not bear the correct ropeID but targetSoundSelection.voiceViewerInfo will and that is the ropeID which is used
targetSoundSelection.voiceViewerInfo.edited ← TRUE;
[] ← TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[targetSoundSelection, newSound, TRUE];
targetSoundSelection.voiceViewerInfo.editInProgress ← FALSE
}
}
}
};
NewDictationWindow:
INTERNAL
PROC = {
creates a new dictation window and sets targetSoundSelection up correctly to refer to it, also sets addingIntoTextViewer false
addingIntoTextViewer ← FALSE;
targetSoundSelection ← NEW [TiogaVoicePrivate.SelectionRec];
[viewerInfo: targetSoundSelection.voiceViewerInfo, viewer: targetSoundSelection.viewer] ← TiogaVoicePrivate.BuildVoiceViewer[NIL, NIL, TRUE];
BuildVoiceViewer[NIL] will set the voiceRope represented by the rope to NIL, the test used for a currently 'empty' viewer in StopRecording above
cueInsertPosition ← 1
};
Dictation machine creation stuff [the previous procedure is a general one for creating window with no voice in it]:
DictationMachine:
PUBLIC
ENTRY Menus.MenuProc = {
the procedure behind all DictationMachine buttons: if a recording is in progress then transfer it into another [newly created] window, otherwise make a new window and start recording
p: PROCESS;
IF recordingInProgress THEN
{ ChangeVoiceInputFocus[];
-- the non-trivial case - recording in progress
RETURN
};
the rest of this procedure implements the case of DictationMachine bugged when no recording is in progress: create a new viewer and start recording into it
IF PlayBackInProgress[] THEN
{ MessageWindow.Append["Cancel playback before trying to record",
TRUE];
MessageWindow.Blink[];
RETURN
}; -- PlayBackInProgress is only a hint: to be sure we'll do a cancel to avoid race conditions
AbortCues[];
NewDictationWindow[];
TiogaVoicePrivate.ToggleDictationMenu[targetSoundSelection.viewer];
recordingInProgress ← TRUE;
timerState ← on;
recordedRope ← NIL;
recordingTimeInSoundChars ← 0;
fork processes (a) to call VoiceRope.Record and (b) to time the recording. (a) is detached but will return when a VoiceRope.Stop occurs. It will then place the ID of the Rope in the appropriate state variable, where the routine called as a result of hitting the stop key can get it
p ← FORK RecordNewRope[];
TRUSTED {Process.Detach[p]};
p ← FORK RecordingTimer[];
TRUSTED {Process.Detach[p]}
};
ChangeVoiceInputFocus:
INTERNAL
PROC = {
DictationMachine was bugged when a recording was in progress: create a new voice viewer and alter the recording focus to it. In the case where recording into a voice viewer, leave a marker where the focus was. In the case where recording into a text viewer, place a VoiceWindow marker where the focus was.
IF ~addingIntoTextViewer
THEN
{
IF targetSoundSelection.voiceViewerInfo.ropeInterval.ropeID =
NIL
THEN
{ MessageWindow.Append["you're already using the dictation machine!",
TRUE];
MessageWindow.Blink[];
RETURN
};
TiogaVoicePrivate.LockedAddCharMark[targetSoundSelection.viewer, MAX [targetSoundSelection.ropeInterval.start/TiogaVoicePrivate.soundRopeCharLength-1, 0]];
this procedure not only sets the mark but also redraws the viewer, removing the voice input markers
MessageWindow.Append["marker set where you were inserting voice", TRUE];
targetSoundSelection.voiceViewerInfo.editInProgress ← FALSE
};
{ currentContents: Rope.
ROPE ← " ";
wasIntoText: BOOLEAN ← addingIntoTextViewer; -- gets altered by NewDictationWindow
NewDictationWindow[]; -- that's an empty one: set its contents to reflect the sound already recorded
IF wasIntoText THEN
{
AddVoiceWindowMarker:
PROC [root: TiogaOpsDefs.Ref] = {
**** this is all a bit dubious - the pointer enables a SAVE of the voice to work, but if the text is SAVED before the voice is either saved or destroyed, the bubble will hang around without voice underneath it
alreadyEdited: BOOLEAN ← targetTextViewer.newVersion;
this line places a 'talks bubble' on the selected character - see TalksBubbleImpl
TextEdit.PutCharProp[node, targetChar.where, $Artwork, NARROW["TalksBubble", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created
TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, NEW[TiogaVoicePrivate.VoiceWindowRec ← [label: Rope.Concat["Sound Viewer #", Convert.RopeFromInt[targetSoundSelection.voiceViewerInfo.viewerNumber]]]]];
TiogaVoicePrivate.SetParentViewer[targetSoundSelection.voiceViewerInfo, targetTextViewer, targetChar];
because we are under a tioga lock, it is safe to muck with the 'edited' status of the viewer
philosophically, putting a source marker in a voice viewer does not constitute editing it, so keep the 'edited' status of the viewer constant through this operation
IF ~alreadyEdited THEN
{ targetTextViewer.newVersion ←
FALSE;
ViewerOps.PaintViewer[targetTextViewer, caption, FALSE]
}
};
targetChar: TiogaOpsDefs.Location ← TiogaExtraOps.GetTextKey[targetNode, $recordingMark];
node: TextNode.Ref ← TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor
IF targetNode # targetChar.node THEN ERROR; -- **** because you've caught all those !!!
TiogaExtraOps.RemoveTextKey[targetNode, $recordingMark];
IF TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL THEN RETURN; -- can happen - a store might have been performed in the intervening time
IF TextEdit.GetCharProp[node, targetChar.where, $voiceWindow] # NIL THEN RETURN;
TiogaOps.CallWithLocks[AddVoiceWindowMarker, TiogaOps.Root[targetChar.node]] -- called locked because otherwise tioga doesn't immediately repaint the artwork, also so we can muck with the 'edited' status of the viewer
};
TiogaVoicePrivate.ToggleDictationMenu[targetSoundSelection.viewer];
FOR i: INT IN [1..recordingTimeInSoundChars] DO currentContents ← currentContents.Concat[">"] ENDLOOP;
currentContents ← currentContents.Concat[" "];
[] ← TiogaVoicePrivate.RedrawViewer[targetSoundSelection.viewer, currentContents, 0, 0, 0, 0, FALSE, deSelected]
}
};
RecordInPlaceOfSelection:
PUBLIC
ENTRY
PROC [selection: TiogaVoicePrivate.Selection] = {
this gets called by TiogaVoicePrivate when a selection [possibly zero length, in which case don't delete it] is to be replaced by new voice input: the selection is assumed to be locked with GetVoiceLock and there is assumed to be no playback or recording in progress at the time of the call
p: PROCESS;
targetSoundSelection ← selection;
addingIntoTextViewer ← FALSE;
IF targetSoundSelection.ropeInterval.length # 0 -- i.e. pending delete
THEN
IF TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[targetSoundSelection,
NIL, FALSE].viewerDeleted
THEN NewDictationWindow[]; -- this means "if applicable, do the pending delete. If that delete should reduce the window contents to zero it will disappear, so create a new one"
cueInsertPosition ← targetSoundSelection.ropeInterval.start/TiogaVoicePrivate.soundRopeCharLength;
recordingInProgress ← TRUE;
timerState ← on;
recordedRope ← NIL;
recordingTimeInSoundChars ← 0;
fork processes (a) to call VoiceRope.Record and (b) to time the recording. (a) is detached but will return when a VoiceRope.Stop occurs. It will then place the ID of the Rope in the appropriate state variable, where the routine called as a result of hitting the stop key can get it
p ← FORK RecordNewRope[];
TRUSTED {Process.Detach[p]};
p ← FORK RecordingTimer[];
TRUSTED {Process.Detach[p]}
};
Playback
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
playBackState:
RECORD
[ running:
BOOLEAN ←
FALSE,
-- is there currently a forked instance of PlayBackProcess ?
queue: LIST OF PlayBackRequest ← NIL,
nextTime: BasicTime.Pulses, -- time to wake up playBackProcess next
abort: BOOLEAN ← FALSE -- 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:
BOOLEAN ←
TRUE, 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;
TiogaVoicePrivate.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/TiogaVoicePrivate.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
TiogaVoicePrivate.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: INTERNAL PROC = { IF playBackState.running THEN playBackState.abort ← TRUE };
The public procedures
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 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 TiogaVoicePrivate.PlaySelection[]
}
}
};
PlaySlabSection:
PUBLIC
PROC [viewer: ViewerClasses.Viewer, node: TiogaOpsDefs.Ref, from, to:
INT] = {
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 ← [display: TRUE, viewer: viewer, node: node, start: from, end: to, currentPos: from-1, timeRemnant: 0]];
VoiceRope.Play[handle: TiogaVoicePrivate.thrushHandle, voiceRope: NEW [VoiceRope.VoiceRopeInterval ← [viewerInfo.ropeInterval.ropeID, from*TiogaVoicePrivate.soundRopeCharLength, (to-from)*TiogaVoicePrivate.soundRopeCharLength]]];
QueuePlayBackCue[newRequest]
};
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
{ 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: TiogaVoicePrivate.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;
fullRope.length ← VoiceRope.Length[handle: TiogaVoicePrivate.thrushHandle, vr: fullRope];
IF fullRope.length = 0 THEN RETURN;
IF fullRope.length = -1 THEN
{ MessageWindow.Append["non-existant voice utterance(s) found in selection",
TRUE];
RETURN
};
newRequest ← NEW [PlayBackRequestRec ← [display: FALSE, start: 0, end: fullRope.length/TiogaVoicePrivate.soundRopeCharLength, currentPos: -1, timeRemnant: fullRope.length MOD TiogaVoicePrivate.soundRopeCharLength]];
VoiceRope.Play[handle: TiogaVoicePrivate.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
ENTRY PROC = { AbortCues[] };
RemoveViewerReferences:
PUBLIC
ENTRY
PROC [viewer: ViewerClasses.Viewer]
RETURNS [okay:
BOOLEAN ←
TRUE] = {
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;
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]];
TiogaVoicePrivate.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 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:
INT ←
MAX[ 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:
INT ←
MAX[unchangedHead, currRequest.start];
end: INT ← MIN [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: INT ← MIN [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: BOOLEAN ← FALSE;
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:
INT ←
MAX[ unchangedHead + deleteChars, currRequest.start ]
+ (insertChars - deleteChars);
end: INT ← currRequest.end + (insertChars - deleteChars);
currentPos: INT ← MAX [ 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:
INT ←
MAX[unchangedHead, currRequest.start];
end: INT ← MIN [unchangedHead + deleteChars - 1, currRequest.end];
currentPos: INT ← MAX [ 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: INT ← MIN [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[]
}
}
}
};
}.