VoiceInTextImpl.mesa:
basic code to add voice to textual tioga documents and to replay that voice
 Ades, April 28, 1986 10:57:39 am PDT
DIRECTORY
Convert USING [RopeFromInt, IntFromRope],
Menus USING [MenuProc, MenuEntry, FindEntry, CreateEntry, AppendMenuEntry, ReplaceMenuEntry, Menu, MenuLine, GetNumberOfLines, GetLine, SetLine, InsertMenuEntry],
MBQueue USING [Queue, Create, CreateMenuEntry],
Rope USING [ROPE, Concat, Equal, Substr, Find],
ViewerBLT USING [ChangeNumberOfLines],
ViewerOps USING [FetchProp, PaintViewer],
MessageWindow USING [Append, Blink],
ViewerClasses USING [Viewer],
SourceMarkerTracking USING [RemoveParentPointersTo, RegisterViewer],
TiogaAccess USING [Reader, TiogaChar, FromSelection, EndOf, Get, FromViewer],
TiogaOps USING [CallWithLocks, Ref, CommandProc, RegisterCommand, GetSelection, NoSelection, ViewerDoc, StepForward, SelectionGrain, SetSelection, Root, SaveSelA, SelectDocument, RestoreSelA],
TiogaOpsDefs USING [Location],
TiogaButtons USING [TextNodeRef],
TextNode USING [Ref],
TextEdit USING [PutCharProp, GetCharProp, PutProp, Size],
IO USING [STREAM, PutF, PutFR, int],
Atom USING [PropList],
VoiceMarkers USING [RopeFromTextList],
VoiceRecord USING [AddVoiceProc, StopRecording, DictationMachine],
VoiceRope USING [Open, Handle, Stop],
Commander USING [CommandProc, Register],
TiogaMenuOps USING [tiogaMenu],
VoiceViewers USING [BuildVoiceViewer, VoiceViewerInfo, SetParentViewer, RemoveParentViewer],
VoicePlayBack USING [CancelPlayBack, PlayRopeWithoutCue, PlayBackMenuProc],
VoiceInText;
VoiceInTextImpl: CEDAR PROGRAM IMPORTS Convert, Menus, MBQueue, Rope, ViewerBLT, ViewerOps, MessageWindow, TiogaAccess, SourceMarkerTracking, TiogaOps, TiogaButtons, TextEdit, IO, Commander, TiogaMenuOps, VoiceViewers, VoicePlayBack, VoiceMarkers, VoiceRecord, VoiceRope EXPORTS VoiceInText SHARES Menus = BEGIN

VoiceMenu: Menus.MenuProc = { ChangeMenu[NARROW[parent], voiceMenu] };
this just toggles the voice submenu on and off the screen
ChangeMenu: PUBLIC PROC [viewer: ViewerClasses.Viewer, subMenu: Menus.MenuEntry] = {
see comments in interface about this
menu: Menus.Menu ← viewer.menu;
found: BOOLFALSE;
numLines: Menus.MenuLine = Menus.GetNumberOfLines[menu];
newLines: Menus.MenuLine ← numLines;
FOR i: Menus.MenuLine IN [1..numLines) DO
see if already showing the submenu
IF Rope.Equal[Menus.GetLine[menu,i].name, subMenu.name] THEN { -- yes, so remove it
FOR j: Menus.MenuLine IN (i..numLines) DO
Menus.SetLine[menu, j-1, Menus.GetLine[menu, j]];
ENDLOOP;
newLines ← newLines-1;
found ← TRUE; EXIT
};
ENDLOOP;
IF ~found THEN {
add it. do insertion sort to get it in the right place
GoesBefore: PROC [m1, m2: Menus.MenuEntry] RETURNS [BOOL] = {
Priority: PROC [m: Menus.MenuEntry] RETURNS [INTEGER] = {
higher priority means goes above in series of submenus
looks at rope for first item to identify the subMenu.
RETURN [SELECT TRUE FROM
Rope.Equal[m.name, "Find"] => 1,
Rope.Equal[m.name, "FirstLevelOnly"] => 0,
ENDCASE => -1 -- unknown menu goes at bottom -- ]
};
RETURN [Priority[m1] > Priority[m2]]
};
newLast: Menus.MenuLine = MIN[numLines, LAST[Menus.MenuLine]];
newLines ← newLines+1;
FOR i: Menus.MenuLine IN [1..numLines) DO
IF GoesBefore[subMenu, Menus.GetLine[menu, i]] THEN {
put it here
FOR j: Menus.MenuLine DECREASING IN (i..newLast] DO
Menus.SetLine[menu, j, Menus.GetLine[menu, j-1]]; ENDLOOP;
Menus.SetLine[menu, i, subMenu];
found ← TRUE; EXIT
};
ENDLOOP;
IF ~found THEN Menus.SetLine[menu, newLast, subMenu];
};
ViewerBLT.ChangeNumberOfLines[viewer, newLines];
};
ApplyToCharsInPrimarySelection: PUBLIC PROC [ActionProc: PROC [TiogaOpsDefs.Location]] = {
a convenience: looks after all the tree walking and calls the given ActionProc once for each character in the primary selection
ScanLocked: PROC [root: TiogaOps.Ref] = {
current, end: TiogaOpsDefs.Location;
[start: current, end: end] ← TiogaOps.GetSelection[];
IF current.node = end.node
THEN FOR i: INT IN [current.where..end.where] DO ActionProc[[current.node, i]] ENDLOOP
ELSE
{ FOR i: INT IN [current.where..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO ActionProc[[current.node, i]] ENDLOOP;
DO
current.node ← TiogaOps.StepForward[current.node];
IF current.node = end.node THEN
{ FOR i: INT IN [0..end.where) DO ActionProc[[current.node, i]] ENDLOOP;
RETURN
}
ELSE FOR i: INT IN [0..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO ActionProc[[current.node, i]] ENDLOOP
ENDLOOP
}
};
TiogaOps.CallWithLocks[ScanLocked]
};
ApplyToLockedChars: PUBLIC PROC [ActionProc: PROC [TiogaOpsDefs.Location]] = {
this is the same as above, except that it should be called when the primary selection is already locked
current, end: TiogaOpsDefs.Location;
[start: current, end: end] ← TiogaOps.GetSelection[];
IF current.node = end.node
THEN FOR i: INT IN [current.where..end.where] DO ActionProc[[current.node, i]] ENDLOOP
ELSE
{ FOR i: INT IN [current.where..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO ActionProc[[current.node, i]] ENDLOOP;
DO
current.node ← TiogaOps.StepForward[current.node];
IF current.node = end.node THEN
{ FOR i: INT IN [0..end.where) DO ActionProc[[current.node, i]] ENDLOOP;
RETURN
}
ELSE FOR i: INT IN [0..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO ActionProc[[current.node, i]] ENDLOOP
ENDLOOP
}
};
StoreVoiceAtSelection: PUBLIC PROC [voiceViewerInfo: VoiceViewers.VoiceViewerInfo] RETURNS [succeeded: BOOLEANFALSE] = {
procedure to put voice from a voice viewer into a text viewer
selectedViewer: ViewerClasses.Viewer;
suitableViewer: BOOLEAN;
alreadyVoiceThere: BOOLEAN;
SuitableViewer: PROC RETURNS [BOOLEAN] = {
RETURN [selectedViewer.class.flavor = $Text AND ViewerOps.FetchProp[selectedViewer, $voiceViewerInfo] = NIL] };
AddVoiceMarkerAtPrimarySelection: PROC [root: TiogaOps.Ref] = {
startChar, endChar, targetChar: TiogaOpsDefs.Location;
node: TextNode.Ref;
caretBefore: BOOLEAN;
pendingDelete: BOOLEAN;
level: TiogaOps.SelectionGrain;
[viewer: selectedViewer, start: startChar, end: endChar, caretBefore: caretBefore, pendingDelete: pendingDelete, level: level] ← TiogaOps.GetSelection[];
suitableViewer ← SuitableViewer[];
IF pendingDelete AND suitableViewer THEN
{ ApplyToLockedChars[DeleteVoiceFromChar];
TiogaOps.SetSelection[viewer: selectedViewer, start: startChar, end: endChar,
level: level, caretBefore: caretBefore,
pendingDelete: FALSE, which: primary] -- simply makes not pending delete
};
IF suitableViewer THEN
{ targetChar ← IF caretBefore THEN startChar ELSE endChar;
node ← TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor
alreadyVoiceThere ← TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL;
IF alreadyVoiceThere THEN RETURN;
TextEdit.PutCharProp[node, targetChar.where, $voice, voiceViewerInfo.ropeInterval.ropeID];
TextEdit.PutCharProp[node, targetChar.where, $textInVoice, VoiceMarkers.RopeFromTextList[voiceViewerInfo.textMarkList]];
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
TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerInfo.viewerNumber]]];
voiceViewerInfo.parentViewer ← selectedViewer;
voiceViewerInfo.positionInParent ← targetChar;
SourceMarkerTracking.RegisterViewer[selectedViewer]
}
};
IF voiceViewerInfo.ropeInterval.ropeID = NIL THEN RETURN;
TiogaOps.CallWithLocks[AddVoiceMarkerAtPrimarySelection ! 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 viewer first", TRUE];
MessageWindow.Blink[]
}
ELSE
{ IF alreadyVoiceThere THEN
{ MessageWindow.Append["Cannot add sound on top of another sound", TRUE];
MessageWindow.Blink[]
}
ELSE succeeded ← TRUE
}
};
DeleteVoiceProc: Menus.MenuProc = {
ApplyToCharsInPrimarySelection[DeleteVoiceFromChar] };
DeleteVoiceFromChar: PUBLIC PROC [position: TiogaOpsDefs.Location] = {
node: TextNode.Ref ← TiogaButtons.TextNodeRef[position.node];
offset: INT ← position.where;
voiceWindowRope: Rope.ROPE;
TextEdit.PutCharProp[node, offset, $Artwork, NIL];
TextEdit.PutCharProp[node, offset, $voice, NIL];
TextEdit.PutCharProp[node, offset, $textInVoice, NIL];
voiceWindowRope ← NARROW[TextEdit.GetCharProp[node, offset, $voiceWindow], Rope.ROPE];
IF voiceWindowRope # NIL THEN
{ TextEdit.PutCharProp[node, offset, $voiceWindow, NIL];
VoiceViewers.RemoveParentViewer[Convert.IntFromRope[ voiceWindowRope.Substr[voiceWindowRope.Find["#"]+1]]]
}
};
PlaySelection: PUBLIC PROC = {
selectStream: TiogaAccess.Reader ← TiogaAccess.FromSelection[];
charsInSelection: INT ← 0;
soundsInSelection: INT ← 0;
heavyChar: TiogaAccess.TiogaChar;
props: Atom.PropList;
IF NOT TiogaAccess.EndOf[selectStream] THEN DO
heavyChar ← TiogaAccess.Get[selectStream];
IF TiogaAccess.EndOf[selectStream] THEN EXIT;
charsInSelection ← charsInSelection + 1;
FOR props ← heavyChar.propList, props.rest WHILE props # NIL DO
IF props.first.key = $voice THEN
{ VoicePlayBack.PlayRopeWithoutCue[NARROW[props.first.val, Rope.ROPE]];
soundsInSelection ← soundsInSelection + 1
};
ENDLOOP
ENDLOOP;
IF soundsInSelection = 0 THEN MessageWindow.Append["No sounds in selection", TRUE];
DebugRope[IO.PutFR["%d characters in selection\n", IO.int[charsInSelection]]]
};
CancelProc: PUBLIC Menus.MenuProc = {
VoiceRope.Stop[thrushHandle];
VoicePlayBack.CancelPlayBack[];
VoiceRecord.StopRecording[]
};
EditVoiceProc: Menus.MenuProc = {
this procedure is in three parts: first look in the primary selection for characters with the property $voice but not $voiceWindow; next open up a window for each: then add $voiceWindow to the relevant characters. If the expected characters don't occur in the expected order then voice window(s) may be created without association to the parent viewer.
voiceList: LIST OF Rope.ROPENIL;
textInVoiceList: LIST OF Rope.ROPENIL;
voiceViewerInfoList: LIST OF VoiceViewers.VoiceViewerInfo ← NIL;
voiceViewerNumberList: LIST OF INTNIL;
these routines are simply append procedures for the three list types above [alas]
AppendRope: PROC [list: LIST OF Rope.ROPE, entry: Rope.ROPE] RETURNS [LIST OF Rope.ROPE] = {
oneElementList: LIST OF Rope.ROPECONS[entry, NIL];
hangOffPoint: LIST OF Rope.ROPE ← list;
IF list = NIL THEN RETURN [oneElementList];
WHILE hangOffPoint.rest # NIL DO hangOffPoint ← hangOffPoint.rest ENDLOOP;
hangOffPoint.rest ← oneElementList;
RETURN [list]
};
AppendViewerInfo: PROC [list: LIST OF VoiceViewers.VoiceViewerInfo, entry: VoiceViewers.VoiceViewerInfo] RETURNS [LIST OF VoiceViewers.VoiceViewerInfo] = {
oneElementList: LIST OF VoiceViewers.VoiceViewerInfo ← CONS[entry, NIL];
hangOffPoint: LIST OF VoiceViewers.VoiceViewerInfo ← list;
IF list = NIL THEN RETURN [oneElementList];
WHILE hangOffPoint.rest # NIL DO hangOffPoint ← hangOffPoint.rest ENDLOOP;
hangOffPoint.rest ← oneElementList;
RETURN [list]
};
AppendInt: PROC [list: LIST OF INT, entry: INT] RETURNS [LIST OF INT] = {
oneElementList: LIST OF INTCONS[entry, NIL];
hangOffPoint: LIST OF INT ← list;
IF list = NIL THEN RETURN [oneElementList];
WHILE hangOffPoint.rest # NIL DO hangOffPoint ← hangOffPoint.rest ENDLOOP;
hangOffPoint.rest ← oneElementList;
RETURN [list]
};
SearchForVoice: PROC [targetChar: TiogaOpsDefs.Location] = {
node: TextNode.Ref ← TiogaButtons.TextNodeRef[targetChar.node];
voiceRopeID: Rope.ROPENARROW[TextEdit.GetCharProp[node, targetChar.where, $voice], Rope.ROPE];
IF voiceRopeID # NIL
THEN
{ IF TextEdit.GetCharProp[node, targetChar.where, $voiceWindow] = NIL
THEN
{ voiceList ← AppendRope[voiceList, voiceRopeID];
textInVoiceList ← AppendRope[textInVoiceList, NARROW[TextEdit.GetCharProp[node, targetChar.where, $textInVoice], Rope.ROPE]]
}
ELSE DebugRope["Voice already has an associated viewer\n"]
}
};
AddVoiceWindowProps: PROC [targetChar: TiogaOpsDefs.Location] = {
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
node: TextNode.Ref ← TiogaButtons.TextNodeRef[targetChar.node];
voiceRopeID: Rope.ROPENARROW[TextEdit.GetCharProp[node, targetChar.where, $voice], Rope.ROPE];
IF voiceList = NIL THEN RETURN;
IF voiceRopeID = NIL THEN RETURN;
IF TextEdit.GetCharProp[node, targetChar.where, $voiceWindow] # NIL THEN RETURN;
IF voiceRopeID.Equal[voiceList.first] THEN
{ parentViewer: ViewerClasses.Viewer ← TiogaOps.GetSelection[].viewer;
alreadyEdited: BOOLEAN ← parentViewer.newVersion;
TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumberList.first]]];
VoiceViewers.SetParentViewer[voiceViewerInfoList.first, parentViewer, targetChar];
IF ~alreadyEdited THEN
{ parentViewer.newVersion ← FALSE;
ViewerOps.PaintViewer[parentViewer, caption, FALSE]
}
};
if the ropes didn't match then you've changed the selection: I'll try feebly to match some of the entries on voiceList
voiceList ← voiceList.rest;
voiceViewerInfoList ← voiceViewerInfoList.rest;
voiceViewerNumberList ← voiceViewerNumberList.rest
};
ApplyToCharsInPrimarySelection[SearchForVoice ! TiogaOps.NoSelection => GOTO Quit];
IF voiceList = NIL THEN
{ MessageWindow.Append["No undisplayed sounds in selection", TRUE];
RETURN
};
FOR l: LIST OF Rope.ROPE ← voiceList, l.rest WHILE l # NIL DO
newInfo: VoiceViewers.VoiceViewerInfo;
newNumber: INT;
[viewerInfo: newInfo, viewerNumber: newNumber] ← VoiceViewers.BuildVoiceViewer[voiceID: l.first, textInVoice: textInVoiceList.first, youngVoice: FALSE];
voiceViewerInfoList ← AppendViewerInfo[voiceViewerInfoList, newInfo];
voiceViewerNumberList ← AppendInt[voiceViewerNumberList, newNumber];
textInVoiceList ← textInVoiceList.rest
ENDLOOP;
ApplyToCharsInPrimarySelection[AddVoiceWindowProps ! TiogaOps.NoSelection => CONTINUE]
EXITS
Quit => NULL
};
DeleteSourceMarker: PUBLIC PROC [viewer: ViewerClasses.Viewer, positionInParent: TiogaOpsDefs.Location, voiceViewerNumber: INT] = {
DoIt: PROC [root: TiogaOps.Ref] = {
node: TextNode.Ref ← TiogaButtons.TextNodeRef[positionInParent.node];
voiceWindowRope: Rope.ROPE ← Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumber]];
IF TiogaOps.ViewerDoc[viewer] # TiogaOps.Root[positionInParent.node]
THEN
{ DebugRope["source marker's viewer and node do not match!!\n"];
RETURN
};
IF voiceWindowRope.Equal[NARROW[TextEdit.GetCharProp[node, positionInParent.where, $voiceWindow], Rope.ROPE]]
THEN
{ -- see comments about 'edited' status at head of procedure AddVoiceWindowProps
alreadyEdited: BOOLEAN ← viewer.newVersion;
TextEdit.PutCharProp[node, positionInParent.where, $voiceWindow, NIL];
IF ~alreadyEdited THEN
{ viewer.newVersion ← FALSE;
ViewerOps.PaintViewer[viewer, caption, FALSE]
}
}
ELSE DebugRope["source marker not found at expected position!!\n"]
};
TiogaOps.CallWithLocks[DoIt, TiogaOps.ViewerDoc[viewer]];
};
SaveRopeAtSourceMarker: PUBLIC PROC [viewer: ViewerClasses.Viewer, positionInParent: TiogaOpsDefs.Location, voiceViewerNumber: INT, voiceRopeID: Rope.ROPE, textInVoice: Rope.ROPE] RETURNS [succeeded: BOOLEANFALSE] = {
DoIt: PROC [root: TiogaOps.Ref] = {
node: TextNode.Ref ← TiogaButtons.TextNodeRef[positionInParent.node];
voiceWindowRope: Rope.ROPE ← Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumber]];
IF TiogaOps.ViewerDoc[viewer] # TiogaOps.Root[positionInParent.node]
THEN
{ DebugRope["source marker's viewer and node do not match!!\n"];
RETURN
};
IF voiceWindowRope.Equal[NARROW[TextEdit.GetCharProp[node, positionInParent.where, $voiceWindow], Rope.ROPE]]
THEN
{ TextEdit.PutCharProp[node, positionInParent.where, $voice, voiceRopeID];
TextEdit.PutCharProp[node, positionInParent.where, $textInVoice, textInVoice];
succeeded ← TRUE
}
ELSE DebugRope["source marker not found at expected position!!\n"]
};
TiogaOps.CallWithLocks[DoIt, TiogaOps.ViewerDoc[viewer]];
};
ScanForVoice: TiogaOps.CommandProc = {
IF viewer.newFile OR viewer.newVersion THEN RecordVoiceInstancesAtRoot[viewer] ELSE MessageWindow.Append["File is not altered", TRUE]
};
DeleteLinks: Menus.MenuProc = {
this procedure severs any parent pointers that voice viewers might have back to this viewer and removes the source marker artworks. [It can also be used to get rid of source marker artworks embarassingly left in a file by bad tracking etc.]
viewer: ViewerClasses.Viewer ← NARROW[parent];
RemoveAnySourceMarker: PROC [position: TiogaOpsDefs.Location] = {
node: TextNode.Ref ← TiogaButtons.TextNodeRef[position.node];
IF TextEdit.GetCharProp[node, position.where, $voiceWindow] # NIL THEN
{ -- see comments about 'edited' status at head of procedure AddVoiceWindowProps
alreadyEdited: BOOLEAN ← viewer.newVersion;
TextEdit.PutCharProp[node, position.where, $voiceWindow, NIL];
IF ~alreadyEdited THEN
{ viewer.newVersion ← FALSE;
ViewerOps.PaintViewer[viewer, caption, FALSE]
}
}
};
TiogaOps.SaveSelA[];
TiogaOps.SelectDocument[viewer: viewer, level: branch];
ApplyToCharsInPrimarySelection[RemoveAnySourceMarker];
TiogaOps.RestoreSelA[];
SourceMarkerTracking.RemoveParentPointersTo[viewer];
};
RecordVoiceInstancesAtRoot: PROC [viewer: ViewerClasses.Viewer] = {
wholeFile: TiogaAccess.Reader ← TiogaAccess.FromViewer[viewer];
heavyChar: TiogaAccess.TiogaChar;
props: Atom.PropList;
soundsInDocument: INT ← 0;
rootNode: TextNode.Ref;
voiceList: Rope.ROPENIL;
DebugRope["Voice messages in document:"];
IF NOT TiogaAccess.EndOf[wholeFile] THEN DO
heavyChar ← TiogaAccess.Get[wholeFile];
IF TiogaAccess.EndOf[wholeFile] THEN EXIT;
FOR props ← heavyChar.propList, props.rest WHILE props # NIL DO
IF props.first.key = $voice THEN
{ DebugRope["\n"];
DebugRope[NARROW[props.first.val, Rope.ROPE]];
soundsInDocument ← soundsInDocument + 1;
voiceList ← voiceList.Concat["&"]; -- just used here as a separator: a character not found in the IDs
voiceList ← voiceList.Concat[NARROW[props.first.val, Rope.ROPE]]
}
ENDLOOP
ENDLOOP;
rootNode ← TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]]; -- TextNodeRef is just a type convertor - replace with a more cautious implementation?? [several references throughout TiogaVoice]
TextEdit.PutProp[rootNode, $voicelist, voiceList];
IF soundsInDocument = 0 THEN DebugRope[" none\n"] ELSE DebugRope["\n"]
};
InstallMenuButton: PROC [name: Rope.ROPE, proc: Menus.MenuProc] = {
old: Menus.MenuEntry =
Menus.FindEntry[menu: TiogaMenuOps.tiogaMenu, entryName: name];
new: Menus.MenuEntry =
Menus.CreateEntry[name: name, proc: proc];
IF old = NIL
THEN Menus.AppendMenuEntry[TiogaMenuOps.tiogaMenu, new]
ELSE Menus.ReplaceMenuEntry[TiogaMenuOps.tiogaMenu, old, new];
};
thrushHandle: PUBLIC VoiceRope.Handle ← VoiceRope.Open[];
voiceMenu: Menus.MenuEntry;
voiceButtonQueue: PUBLIC MBQueue.Queue ← MBQueue.Create[];
all buttons registered using MBQueue.CreateMenuEntry are serialised on this one queue: all the buttons in this voice system, or having to do with voice [e.g. Finch buttons], ought to be put on this queue, except for those which simply toggle menus on and off the display
debugStream: IO.STREAMNIL;
DebugRope: PUBLIC PROC [rope: Rope.ROPE] = {
IF debugStream # NIL THEN debugStream.PutF[rope]
};
DebugStreamInit: Commander.CommandProc = { debugStream ← cmd.out };
Commander.Register[key: "VoiceInfo", proc: DebugStreamInit, doc: "VoiceInfo: registers an output stream for debugging messages"];
**** voiceButtonQueue should be deleted, or used for ALL button activities
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DeleteLinks", DeleteLinks], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DictationMachine", VoiceRecord.DictationMachine], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DeleteVoice", DeleteVoiceProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "EditVoice", EditVoiceProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "STOP", CancelProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "PlayVoice", VoicePlayBack.PlayBackMenuProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "AddVoice", VoiceRecord.AddVoiceProc], 1];
voiceMenu ← Menus.GetLine[TiogaMenuOps.tiogaMenu, 1]; 
InstallMenuButton["Voice", VoiceMenu];
Menus.SetLine[TiogaMenuOps.tiogaMenu, 1, NIL];
TiogaOps.RegisterCommand[name: $RedSave, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $YellowSave, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $BlueSave, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $RedStore, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $YellowStore, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $BlueStore, proc: ScanForVoice];
END.