DIRECTORY BasicTime USING [Pulses, MicrosecondsToPulses, PulsesToMicroseconds, GetClockPulses], 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], 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], VoiceEditOps USING [DescribeSelection, ReplaceSelectionWithSavedInterval], VoiceInText USING [ApplyToLockedChars, DeleteVoiceFromChar, thrushHandle], 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], VoiceRecord; VoiceRecordImpl: CEDAR MONITOR IMPORTS BasicTime, 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; 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; 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 { 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 { targetSoundSelection.voiceViewerInfo.edited _ TRUE; VoiceViewers.SetViewerContents[targetSoundSelection.viewer, targetSoundSelection.voiceViewerInfo, recordedRope.ropeID, NIL, TRUE] } 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 }; 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 { TiogaExtraOps.RemoveTextKey[targetNode, $recordingMark] } ELSE { 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 _ " "; NewDictationWindow[]; -- that's an empty one: set its contents to reflect the sound already recorded 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. ŽVoiceRecordImpl.mesa: 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 Ades, April 29, 1986 4:41:03 pm PDT whilst a recording is being made, the following are its state variables 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 the selection where the voice is to be added, as a VoiceViewers selection how long has the recording been going on, in terms of sound characters generated clock for timing recording the rope which is created as the result of this recording process the procedure behind all AddVoice buttons 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 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] cueInsertPosition _ MIN [targetSoundSelection.ropeInterval.start/VoiceViewers.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 test for failure conditions and report them to the user after releasing the viewer lock see comments in VoicePlayBackImpl on this constant 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 this code adds another marker into a voice viewer to indicate how much has been recorded this gets called every time a STOP button is clicked, after VoiceRope.Stop has been called next line places a 'talks bubble' on the selected character - see TalksBubbleImpl 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 and that is the ropeID which is used creates a new dictation window and sets targetSoundSelection up correctly to refer to it, also sets addingIntoTextViewer false BuildVoiceViewer[NIL] will set the voiceRope represented by the rope to NIL, the test used for a currently 'empty' viewer in StopRecording above ---- the dictation machine creation stuff [the previous procedure is a general one for creating window with no voice in it]: 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 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 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 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 going into a voice viewer, leave a marker where the focus was. **** put a source marker into the text viewer? this procedure not only sets the mark but also redraws the viewer, removing the voice input markers this gets called by DictationOps 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 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 Κ ‚˜šœ™J™¬J™$J™—šΟk ˜ Jšœ œF˜UJšœ œ˜)Jšœœ ˜Jšœœ˜$JšœœG˜TJšœœœ ˜Jšœ œ˜-Jšœ œ˜*Jšœ œ˜Jšœ œ˜!Jšœœ)˜˜>J˜Jšœœ˜J˜Jšœœ˜J˜J™J™™Jšœœ˜Jšœ˜Jšœœ˜Jšœ˜—J˜K˜Kšžœœœ˜(šžœΘ™ΨKšœ˜Kš˜šœ:˜:K™.—K˜Kš˜šœœ<œ˜IšœGœ˜MJ˜Jš˜—Jšœ˜K˜Kšœ<œR˜‘K™cKšœBœ˜HKšœ6˜;—K˜K˜šœœ˜$KšœŸN˜dKšœ>˜>Kš œœœ œ/œ˜fKšœ.˜.KšœZœ ˜l—K˜—K˜K˜šžœœœœ(˜SK™Kšœœ˜ Jšœ!˜!Jšœœ˜J˜Jšœ.Ÿ˜FšœœFœ˜gJšœœŸ•˜΄—J˜šœ]˜]J™—Jšœœ˜J˜Jšœœ˜J˜J™J™™Jšœœ˜Jšœ˜Jšœœ˜Jšœ˜—J˜K˜Kšœ˜—…—/vL†