<> <> << Ades, September 24, 1986 5:11:21 pm PDT>> <<>> DIRECTORY BasicTime USING [Pulses, MicrosecondsToPulses, PulsesToMicroseconds, GetClockPulses], Convert USING [RopeFromInt], DictationOps USING [ToggleDictationMenu], Menus USING [MenuProc], MessageWindow USING [Append, Blink], Process USING [Detach, Priority, priorityRealTime, SetPriority, Pause, MsecToTicks], Rope USING [ROPE, Concat], SoundList USING [SoundListFromIntervalSpecs, SoundChars], TextEdit USING [GetCharProp, PutCharProp], TextNode USING [Ref], TiogaButtons USING [TextNodeRef], TiogaExtraOps USING [RemoveTextKey, PutTextKey, GetTextKey], TiogaOps USING [SelectionGrain, GetSelection, SetSelection, CallWithLocks, NoSelection, SaveSelA, FirstChild, ViewerDoc, InsertChar, RestoreSelA, Root, SetLooks], TiogaOpsDefs USING [Location, Ref], ViewerClasses USING [Viewer], ViewerOps USING [FetchProp, DestroyViewer, PaintViewer], VoiceEditOps USING [DescribeSelection, ReplaceSelectionWithSavedInterval], VoiceInText USING [ApplyToLockedChars, DeleteVoiceFromChar, thrushHandle, VoiceWindowRec], VoiceMarkers USING [LockedAddCharMark], VoicePlayBack USING [PlayBackInProgress, CancelPlayBack, RedrawViewer], VoiceRope USING [VoiceRope, Record, DescribeRope], VoiceViewers USING [Selection, SelectionRec, VoiceViewerInfo, soundRopeCharsPerSecond, soundRopeCharLength, SoundList, SoundInterval, SoundIntervalRec, SetViewerContents, BuildVoiceViewer, SetVoiceViewerEditStatus, SetParentViewer], VoiceRecord; <<>> VoiceRecordImpl: CEDAR MONITOR IMPORTS BasicTime, Convert, DictationOps, MessageWindow, Process, Rope, SoundList, TextEdit, TiogaButtons, TiogaExtraOps, TiogaOps, ViewerOps, VoiceEditOps, VoiceInText, VoiceMarkers, VoicePlayBack, VoiceRope, VoiceViewers EXPORTS VoiceRecord = BEGIN recordingInProgress: BOOLEAN _ FALSE; <> addingIntoTextViewer: BOOLEAN; <> targetNode: TiogaOpsDefs.Ref; <> targetTextViewer: ViewerClasses.Viewer; <> targetSoundSelection: VoiceViewers.Selection; cueInsertPosition: INT; -- where the little arrows will go <> recordingTimeInSoundChars: INT; <> nextWakeUp: BasicTime.Pulses; timerState: {off, abort, on}; <> recordedRope: VoiceRope.VoiceRope; timerOff, recordingDone: CONDITION; RecordingInProgress: PUBLIC PROC RETURNS [BOOLEAN] = { RETURN [recordingInProgress] }; <<>> AddVoiceProc: PUBLIC ENTRY Menus.MenuProc = { <> p: PROCESS; IF recordingInProgress THEN { MessageWindow.Append["Already recording", TRUE]; MessageWindow.Blink[]; RETURN }; IF VoicePlayBack.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 VoicePlayBack.CancelPlayBack[]; IF ~PrepareSelection[] THEN RETURN; recordingInProgress _ TRUE; timerState _ on; recordedRope _ NIL; recordingTimeInSoundChars _ 0; <<>> <> p _ FORK RecordNewRope[]; TRUSTED {Process.Detach[p]}; p _ FORK RecordingTimer[]; TRUSTED {Process.Detach[p]} }; <<>> 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 { addingIntoTextViewer _ ViewerOps.FetchProp[targetViewer, $voiceViewerInfo] = NIL; IF addingIntoTextViewer THEN { IF targetPendingDelete THEN { VoiceInText.ApplyToLockedChars[VoiceInText.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] _ VoiceEditOps.DescribeSelection[which: primary, forceDelete: FALSE, returnSoundInterval: FALSE]; IF alreadyBeingEdited THEN RETURN; IF targetSoundSelection.ropeInterval.length # 0 -- i.e. pending delete THEN IF VoiceEditOps.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/ VoiceViewers.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 } } }; RecordNewRope: PROC = { BroadcastRecordingDone: ENTRY PROC = { BROADCAST recordingDone }; -- keeps the compiler happy! recordedRope _ VoiceRope.Record[VoiceInText.thrushHandle]; BroadcastRecordingDone[] }; RecordingTimer: PROC = { aVeryLongTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[300000000]; <> cuePriority: Process.Priority = Process.priorityRealTime; Process.SetPriority[cuePriority]; <> 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/VoiceViewers.soundRopeCharsPerSecond]; recordingTimeInSoundChars _ recordingTimeInSoundChars + 1; IF ~addingIntoTextViewer THEN { voiceViewer: ViewerClasses.Viewer _ targetSoundSelection.viewer; caretLocation: TiogaOpsDefs.Location _ [TiogaOps.FirstChild[TiogaOps.ViewerDoc [voiceViewer]], cueInsertPosition + recordingTimeInSoundChars - 1]; <> 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 = { <> 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]; <> 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] <> } ELSE { targetSoundSelection.voiceViewerInfo.edited _ TRUE; VoiceViewers.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 _ SoundList.SoundChars[targetSoundSelection.voiceViewerInfo].soundRope; MessageWindow.Append["Zero length voice addition - viewer contents unchanged", TRUE]; [] _ VoicePlayBack.RedrawViewer[targetSoundSelection.viewer, trueContents, 0, 0, 0, targetSoundSelection.voiceViewerInfo.remnant, FALSE, deSelected]; VoiceViewers.SetVoiceViewerEditStatus[targetSoundSelection.viewer]; targetSoundSelection.voiceViewerInfo.editInProgress _ FALSE } ELSE { newSoundList: VoiceViewers.SoundList _ SoundList.SoundListFromIntervalSpecs [VoiceRope.DescribeRope[VoiceInText.thrushHandle, recordedRope], recordedRope.length]; newSound: VoiceViewers.SoundInterval _ NEW [VoiceViewers.SoundIntervalRec _ [ropeInterval: recordedRope^, soundList: newSoundList]]; targetSoundSelection.ropeInterval.length _ 0; <> targetSoundSelection.voiceViewerInfo.edited _ TRUE; [] _ VoiceEditOps.ReplaceSelectionWithSavedInterval[targetSoundSelection, newSound, TRUE]; targetSoundSelection.voiceViewerInfo.editInProgress _ FALSE } } } }; <<>> NewDictationWindow: INTERNAL PROC = { <> addingIntoTextViewer _ FALSE; targetSoundSelection _ NEW [VoiceViewers.SelectionRec]; [viewerInfo: targetSoundSelection.voiceViewerInfo, viewer: targetSoundSelection.viewer] _ VoiceViewers.BuildVoiceViewer[NIL, NIL, TRUE]; <> cueInsertPosition _ 1 }; <<---- the dictation machine creation stuff [the previous procedure is a general one for creating window with no voice in it]:>> DictationMachine: PUBLIC ENTRY Menus.MenuProc = { <> p: PROCESS; IF recordingInProgress THEN { ChangeVoiceInputFocus[]; -- the non-trivial case - recording in progress RETURN }; <> IF VoicePlayBack.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 VoicePlayBack.CancelPlayBack[]; NewDictationWindow[]; DictationOps.ToggleDictationMenu[targetSoundSelection.viewer]; recordingInProgress _ TRUE; timerState _ on; recordedRope _ NIL; recordingTimeInSoundChars _ 0; <<>> <> p _ FORK RecordNewRope[]; TRUSTED {Process.Detach[p]}; p _ FORK RecordingTimer[]; TRUSTED {Process.Detach[p]} }; ChangeVoiceInputFocus: INTERNAL PROC = { <> IF ~addingIntoTextViewer THEN { IF targetSoundSelection.voiceViewerInfo.ropeInterval.ropeID = NIL THEN { MessageWindow.Append["you're already using the dictation machine!", TRUE]; MessageWindow.Blink[]; RETURN }; VoiceMarkers.LockedAddCharMark[targetSoundSelection.viewer, MAX [targetSoundSelection.ropeInterval.start/VoiceViewers.soundRopeCharLength-1, 0]]; <> 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; <> 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[VoiceInText.VoiceWindowRec _ [label: Rope.Concat["Sound Viewer #", Convert.RopeFromInt[targetSoundSelection.voiceViewerInfo.viewerNumber]]]]]; VoiceViewers.SetParentViewer[targetSoundSelection.voiceViewerInfo, targetTextViewer, targetChar]; <> <> 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 }; DictationOps.ToggleDictationMenu[targetSoundSelection.viewer]; FOR i: INT IN [1..recordingTimeInSoundChars] DO currentContents _ currentContents.Concat[">"] ENDLOOP; currentContents _ currentContents.Concat[" "]; [] _ VoicePlayBack.RedrawViewer[targetSoundSelection.viewer, currentContents, 0, 0, 0, 0, FALSE, deSelected] } }; RecordInPlaceOfSelection: PUBLIC ENTRY PROC [selection: VoiceViewers.Selection] = { <> p: PROCESS; targetSoundSelection _ selection; addingIntoTextViewer _ FALSE; IF targetSoundSelection.ropeInterval.length # 0 -- i.e. pending delete THEN IF VoiceEditOps.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/VoiceViewers.soundRopeCharLength; <<>> recordingInProgress _ TRUE; timerState _ on; recordedRope _ NIL; recordingTimeInSoundChars _ 0; <<>> <> p _ FORK RecordNewRope[]; TRUSTED {Process.Detach[p]}; p _ FORK RecordingTimer[]; TRUSTED {Process.Detach[p]} }; END.