<> <> <> <> <> <<>> DIRECTORY BasicTime USING [GetClockPulses, MicrosecondsToPulses, Pulses, PulsesToMicroseconds ], Convert USING [RopeFromInt], FinchSmarts USING [RegisterForReports, ReportConversationStateProc, ReportRequestStateProc], Menus USING [MenuProc ], MessageWindow USING [Append, Blink ], Process USING [Detach, MsecToTicks, Pause, Priority, priorityRealTime, SetPriority ], Rope USING [Concat, ROPE ], TextEdit USING [GetCharProp, InsertChar, PutCharProp, Size ], TextLooks USING [RopeToLooks ], TextNode USING [Ref], TiogaButtons USING [TextNodeRef], TiogaExtraOps USING [GetTextKey, PutTextKey, RemoveTextKey, TextKeyNotFound ], TiogaOps USING [CallWithLocks, FirstChild, GetSelection, NoSelection, Root, SelectionGrain, SetSelection, ViewerDoc ], TiogaOpsDefs USING [Location, Ref ], TiogaVoicePrivate USING [ ApplyToLockedChars, BuildVoiceViewer, CancelPlayBack, DeleteVoiceFromChar, DescribeSelection, LockedAddCharMark, MakeVoiceEdited, PlayBackInProgress, RedrawViewer, ReplaceSelectionWithSavedInterval, Selection, SelectionRec, SelectionsAfterRedraw, SetParentViewer, SetPlayBackState, SetViewerContents, SetVoiceViewerEditStatus, SoundChars, SoundInterval, SoundIntervalRec, SoundList, SoundListFromIntervalSpecs, soundRopeCharLength, soundRopeCharsPerSecond, thrushHandle, ToggleDictationMenu, VoiceViewerInfo, VoiceWindowRec ], ViewerClasses USING [Viewer ], ViewerOps USING [DestroyViewer, FetchProp, PaintViewer ], VoiceRope USING [DescribeRope, GetClientData, NB, NewRequestID, RecordNB, RequestID, StopNB, VoiceRope, VoiceRopeInterval ] ; VoiceRecordImpl: CEDAR MONITOR IMPORTS BasicTime, Convert, FinchSmarts, MessageWindow, Process, Rope, TextEdit, TextLooks, TiogaButtons, TiogaExtraOps, TiogaOps, TiogaVoicePrivate, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate = { <> <<>> recordingState: RECORD [ recordingInProgress: BOOLEAN _ FALSE, addingIntoTextViewer: BOOLEAN, <> node: TiogaOpsDefs.Ref, <> textViewer: ViewerClasses.Viewer, <> soundSelection: TiogaVoicePrivate.Selection, <> cueInsertPosition: INT, -- where the little arrows will go timeInSoundChars: INT, <> requestID: VoiceRope.RequestID, nb: VoiceRope.NB, nextWakeUp: BasicTime.Pulses, -- clock for timing recording timerState: {off, quit, on}, newRope: VoiceRope.VoiceRope <> ]; timerOff, recordingDone: CONDITION; RecordingInProgress: PUBLIC PROC RETURNS [BOOLEAN] = { RETURN [recordingState.recordingInProgress] }; <<>> CancelProc: PUBLIC Menus.MenuProc = { IF VoiceRope.StopNB[TiogaVoicePrivate.thrushHandle] # $success THEN { StopRecording[]; TiogaVoicePrivate.CancelPlayBack[]; <> }; }; AddVoiceProc: PUBLIC ENTRY Menus.MenuProc = { <> IF recordingState.recordingInProgress THEN { MessageWindow.Append["Already recording", TRUE]; MessageWindow.Blink[]; RETURN }; IF TiogaVoicePrivate.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 TiogaVoicePrivate.CancelPlayBack[]; IF ~PrepareSelection[] THEN RETURN; StartRecording[]; }; <<>> PrepareSelection: INTERNAL PROC RETURNS [succeeded: BOOLEAN _ FALSE] = { <> 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 { recordingState.addingIntoTextViewer _ ViewerOps.FetchProp[targetViewer, $voiceViewerInfo] = NIL; IF recordingState.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; recordingState.node _ targetChar.node; recordingState.textViewer _ targetViewer; IF ~alreadyVoiceThere THEN TiogaExtraOps.PutTextKey[targetChar.node, targetChar.where, $recordingMark] } } ELSE { [selection: recordingState.soundSelection, failed: alreadyBeingEdited] _ TiogaVoicePrivate.DescribeSelection[which: primary, forceDelete: FALSE, returnSoundInterval: FALSE]; IF alreadyBeingEdited THEN RETURN; IF recordingState.soundSelection.ropeInterval.length # 0 -- pending delete AND TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[ recordingState.soundSelection, 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" <> recordingState.cueInsertPosition _ recordingState.soundSelection.ropeInterval.start/ TiogaVoicePrivate.soundRopeCharLength } } }; TiogaOps.CallWithLocks[DoIt ! TiogaOps.NoSelection => {suitableViewer _ FALSE; CONTINUE}]; <> 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 } } }; StartRecording: INTERNAL PROC = { recordingState.recordingInProgress _ TRUE; recordingState.timerState _ off; recordingState.newRope _ NIL; recordingState.timeInSoundChars _ 0; recordingState.requestID _ VoiceRope.NewRequestID[]; recordingState.nb _ $success; <<>> <> <> <<>> TRUSTED {Process.Detach[FORK RecordNewRope[]]}; }; RecordNewRope: PROC = { BroadcastRecordingDone: ENTRY PROC = { BROADCAST recordingDone }; -- keeps the compiler happy! [nb: recordingState.nb, voiceRope: recordingState.newRope] _ VoiceRope.RecordNB[handle: TiogaVoicePrivate.thrushHandle, requestID: recordingState.requestID, clientData: $TiogaVoice]; SELECT recordingState.nb FROM $success, $noLength => BroadcastRecordingDone[]; ENDCASE => StopRecording[]; <> }; RecordingTimer: PROC = { aVeryLongTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[300000000]; <> cuePriority: Process.Priority = Process.priorityRealTime; Process.SetPriority[cuePriority]; <> recordingState.nextWakeUp _ BasicTime.GetClockPulses[]; recordingState.timerState _ on; DO now: BasicTime.Pulses _ BasicTime.GetClockPulses[]; IF recordingState.nextWakeUp - now < aVeryLongTime -- so as to cope properly with wrap-around THEN Process.Pause[Process.MsecToTicks[BasicTime.PulsesToMicroseconds[ recordingState.nextWakeUp-now]/1000]]; IF ~ IncrementRecordTimer[] THEN RETURN; ENDLOOP; }; IncrementRecordTimer: ENTRY PROC RETURNS [stillRunning: BOOLEAN _ TRUE] = { IF recordingState.timerState # on THEN { recordingState.timerState _ off; BROADCAST timerOff; RETURN [FALSE] }; recordingState.nextWakeUp _ recordingState.nextWakeUp + BasicTime.MicrosecondsToPulses[ 1000000/TiogaVoicePrivate.soundRopeCharsPerSecond]; recordingState.timeInSoundChars _ recordingState.timeInSoundChars + 1; IF ~recordingState.addingIntoTextViewer THEN NewRecordingMarker[recordingState.soundSelection.viewer, recordingState.cueInsertPosition + recordingState.timeInSoundChars - 1]; }; NewRecordingMarker: INTERNAL PROC [viewer: ViewerClasses.Viewer, loc: INT] ~ { <> DoIt: PROC [root: TiogaOpsDefs.Ref] ~ { <> IF NOT viewer.newVersion THEN TiogaVoicePrivate.MakeVoiceEdited[viewer]; [] _ TextEdit.InsertChar[root: TiogaButtons.TextNodeRef[root], dest: TiogaButtons.TextNodeRef[TiogaOps.FirstChild[root]], char: '>, destLoc: loc, inherit: FALSE, looks: TextLooks.RopeToLooks["v"]]; <<'> is used as an 'inserting voice' indicator>> }; TiogaOps.CallWithLocks[DoIt, TiogaOps.ViewerDoc[viewer]]; }; StopRecording: PUBLIC ENTRY PROC = { <> IF ~recordingState.recordingInProgress THEN RETURN; IF recordingState.timerState = on THEN recordingState.timerState _ quit; WHILE recordingState.newRope = NIL AND recordingState.nb = $success DO WAIT recordingDone ENDLOOP; WHILE recordingState.timerState # off DO WAIT timerOff ENDLOOP; recordingState.recordingInProgress _ FALSE; IF recordingState.addingIntoTextViewer THEN { badTarget: BOOLEAN _ FALSE; IF recordingState.newRope = NIL OR recordingState.newRope.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, recordingState.newRope.ropeID]; <> TextEdit.PutCharProp[node, targetChar.where, $Artwork, NARROW["TalksBubble", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created }; targetChar: TiogaOpsDefs.Location; node: TextNode.Ref; targetChar _ TiogaExtraOps.GetTextKey[recordingState.node, $recordingMark ! TiogaExtraOps.TextKeyNotFound => {badTarget _ TRUE; CONTINUE}]; node _ TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor IF TextEdit.Size[node] <= targetChar.where OR badTarget OR TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL THEN { <> voiceViewer: ViewerClasses.Viewer; [viewer: voiceViewer] _ TiogaVoicePrivate.BuildVoiceViewer[recordingState.newRope.ropeID, NIL, TRUE]; TiogaVoicePrivate.MakeVoiceEdited[voiceViewer]; MessageWindow.Append["No character to add voice to; new voice viewer contains voice just recorded", TRUE]; MessageWindow.Blink[]; } ELSE { TiogaExtraOps.RemoveTextKey[recordingState.node, $recordingMark]; TiogaOps.CallWithLocks[AddVoice, TiogaOps.Root[targetChar.node]]; <> } } } ELSE IF recordingState.soundSelection.voiceViewerInfo.ropeInterval.ropeID = NIL THEN { <> IF recordingState.newRope = NIL OR recordingState.newRope.length = 0 THEN { MessageWindow.Append["Zero length voice dictation - destroying window", TRUE]; MessageWindow.Blink[]; ViewerOps.DestroyViewer[recordingState.soundSelection.viewer] <> } ELSE { TiogaVoicePrivate.MakeVoiceEdited[recordingState.soundSelection.viewer]; <> TiogaVoicePrivate.SetViewerContents[recordingState.soundSelection.viewer, recordingState.soundSelection.voiceViewerInfo, recordingState.newRope.ropeID, NIL, TRUE] } } ELSE IF recordingState.newRope = NIL OR recordingState.newRope.length = 0 THEN { <> trueContents: Rope.ROPE _ TiogaVoicePrivate.SoundChars[recordingState.soundSelection.voiceViewerInfo].soundRope; MessageWindow.Append["Zero length voice addition - viewer contents unchanged", TRUE]; [] _ TiogaVoicePrivate.RedrawViewer[recordingState.soundSelection.viewer, trueContents, 0, 0, 0, recordingState.soundSelection.voiceViewerInfo.remnant, FALSE, deSelected]; TiogaVoicePrivate.SetVoiceViewerEditStatus[recordingState.soundSelection.viewer]; <<This will always say edited now, because we make the voice viewer edited in NewRecordingMarker when we put the first '> character in>> recordingState.soundSelection.voiceViewerInfo.editInProgress _ FALSE } ELSE { newSoundList: TiogaVoicePrivate.SoundList _ TiogaVoicePrivate.SoundListFromIntervalSpecs [VoiceRope.DescribeRope[TiogaVoicePrivate.thrushHandle, recordingState.newRope], recordingState.newRope.length]; newSound: TiogaVoicePrivate.SoundInterval _ NEW [TiogaVoicePrivate.SoundIntervalRec _ [ropeInterval: recordingState.newRope^, soundList: newSoundList]]; recordingState.soundSelection.ropeInterval.length _ 0; <> TiogaVoicePrivate.MakeVoiceEdited[recordingState.soundSelection.viewer]; <> [] _ TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[recordingState.soundSelection, newSound, TRUE]; recordingState.soundSelection.voiceViewerInfo.editInProgress _ FALSE } }; <<>> NewDictationWindow: INTERNAL PROC = { <> recordingState.addingIntoTextViewer _ FALSE; recordingState.soundSelection _ NEW [TiogaVoicePrivate.SelectionRec]; [viewerInfo: recordingState.soundSelection.voiceViewerInfo, viewer: recordingState.soundSelection.viewer] _ TiogaVoicePrivate.BuildVoiceViewer[NIL, NIL, TRUE]; <> recordingState.cueInsertPosition _ 1 }; <> DictationMachine: PUBLIC ENTRY Menus.MenuProc = { <> IF recordingState.recordingInProgress THEN { ChangeVoiceInputFocus[]; -- the non-trivial case - recording in progress RETURN }; <> IF TiogaVoicePrivate.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 TiogaVoicePrivate.CancelPlayBack[]; NewDictationWindow[]; TiogaVoicePrivate.ToggleDictationMenu[recordingState.soundSelection.viewer]; StartRecording[]; }; ChangeVoiceInputFocus: INTERNAL PROC = { <> IF ~recordingState.addingIntoTextViewer THEN { IF recordingState.soundSelection.voiceViewerInfo.ropeInterval.ropeID = NIL THEN { MessageWindow.Append["you're already using the dictation machine!", TRUE]; MessageWindow.Blink[]; RETURN }; TiogaVoicePrivate.LockedAddCharMark[recordingState.soundSelection.viewer, MAX [recordingState.soundSelection.ropeInterval.start/TiogaVoicePrivate.soundRopeCharLength-1, 0]]; <> MessageWindow.Append["marker set where you were inserting voice", TRUE]; recordingState.soundSelection.voiceViewerInfo.editInProgress _ FALSE }; { currentContents: Rope.ROPE _ " "; wasIntoText: BOOLEAN _ recordingState.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 _ recordingState.textViewer.newVersion; <> 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[recordingState.soundSelection.voiceViewerInfo.viewerNumber]]]]]; TiogaVoicePrivate.SetParentViewer[recordingState.soundSelection.voiceViewerInfo, recordingState.textViewer, targetChar]; <> <> IF ~alreadyEdited THEN { recordingState.textViewer.newVersion _ FALSE; ViewerOps.PaintViewer[recordingState.textViewer, caption, FALSE] } }; targetChar: TiogaOpsDefs.Location _ TiogaExtraOps.GetTextKey[recordingState.node, $recordingMark]; node: TextNode.Ref _ TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor IF recordingState.node # targetChar.node THEN ERROR; -- **** because you've caught all those !!! TiogaExtraOps.RemoveTextKey[recordingState.node, $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[recordingState.soundSelection.viewer]; FOR i: INT IN [1..recordingState.timeInSoundChars] DO currentContents _ currentContents.Concat[">"] ENDLOOP; currentContents _ currentContents.Concat[" "]; [] _ TiogaVoicePrivate.RedrawViewer[recordingState.soundSelection.viewer, currentContents, 0, 0, 0, 0, FALSE, deSelected] } }; RecordInPlaceOfSelection: PUBLIC ENTRY PROC [selection: TiogaVoicePrivate.Selection] = { <> recordingState.soundSelection _ selection; recordingState.addingIntoTextViewer _ FALSE; IF recordingState.soundSelection.ropeInterval.length # 0 -- i.e. pending delete THEN IF TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[recordingState.soundSelection, 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" recordingState.cueInsertPosition _ recordingState.soundSelection.ropeInterval.start/TiogaVoicePrivate.soundRopeCharLength; <<>> StartRecording[]; }; ForkStopRecording: PROC = {TRUSTED {Process.Detach[FORK StopRecording[]]} }; <> <<>> ConversationReport: FinchSmarts.ReportConversationStateProc ~ { <<[ nb: NB, cDesc: ConvDesc, remark: ROPE_NIL ]>> IF cDesc = NIL THEN RETURN; IF NARROW[VoiceRope.GetClientData[handle: TiogaVoicePrivate.thrushHandle, cDesc: cDesc], ATOM] = $TiogaVoice THEN <> SELECT cDesc.situation.self.state FROM $active => NULL; $idle, $neverWas, $failed => { <> TiogaVoicePrivate.CancelPlayBack[]; ForkStopRecording[]; }; ENDCASE; }; RequestReport: FinchSmarts.ReportRequestStateProc ~ { <<[ cDesc: ConvDesc, actionReport: Thrush.ActionReport, actionRequest: REF ] RETURNS [betterActionRequest: REF]>> IF cDesc = NIL THEN RETURN; IF NARROW[VoiceRope.GetClientData[handle: TiogaVoicePrivate.thrushHandle, cDesc: cDesc], ATOM] = $TiogaVoice THEN <> SELECT actionReport.actionClass FROM $recording => { IF actionReport.actionID = recordingState.requestID THEN SELECT actionReport.actionType FROM $scheduled => NULL; $started => TRUSTED {Process.Detach[FORK RecordingTimer[]]}; $finished, $flushed => ForkStopRecording[]; ENDCASE; }; $playback => { SELECT actionReport.actionType FROM $scheduled => RETURN[NIL]; $started => TiogaVoicePrivate.SetPlayBackState[actionReport.actionID, busy]; $finished, $flushed => TiogaVoicePrivate.SetPlayBackState[actionReport.actionID, done]; ENDCASE; }; ENDCASE=> betterActionRequest _ NIL; -- a place to stand during debugging RETURN[NIL]; -- leave actionRequest alone }; <> FinchSmarts.RegisterForReports[c: ConversationReport, r: RequestReport, before: FALSE]; }. <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <<>>