DIRECTORY BasicTime USING [earliestGMT, Now, Period], BluejayUtils USING [DescribeTune, DescribeInterval], BluejayUtilsRpcControl USING [ImportInterface, UnimportInterface], Convert USING [IntFromRope, RopeFromInt], FinchSmarts USING [CurrentFinchState, FinchState, GetProcs, Procs, RecordReason], IO USING [card, GetCard, GetInt, int, PutFR, RIS, rope, STREAM], LoganBerry USING [AttributeType, AttributeValue, Entry, Error, ErrorCode, Open, OpenDB, ReadEntry, WriteEntry], LoganBerryRpcControl USING [ImportInterface, UnimportInterface], LupineRuntime USING [BindingError], Rope USING [Cat, Concat, ROPE], Thrush USING [EncryptionKey, IntervalSpecs, Tune, VoiceInterval], UserProfile USING [Token], UserCredentials USING [CredentialsChangeProc, Get, RegisterForChange], VoiceRope, VoiceUtils USING [Problem, RnameToRspec] ; VoiceRopeImpl: CEDAR PROGRAM -- Should this be a monitor? IMPORTS BasicTime, BluejayUtils, BluejayUtilsRpcControl, Convert, FinchSmarts, IO, LoganBerry, LoganBerryRpcControl, LupineRuntime, Rope, UserCredentials, UserProfile, VoiceUtils EXPORTS VoiceRope ~ BEGIN OPEN VoiceRope; ROPE: TYPE ~ Rope.ROPE; STREAM: TYPE ~ IO.STREAM; defaultHandle: Handle_NIL; TuneList: TYPE = LoganBerry.Entry; Open: PUBLIC PROC[voiceRopeDBName: Rope.ROPE _ NIL, voiceRopeDBInstance: Rope.ROPE _ NIL, localName: Rope.ROPE _ NIL, Complain: PROC[complaint: Rope.ROPE]_NIL] RETURNS [handle: Handle] = { server: Rope.ROPE _ UserProfile.Token["ThrushClientServerInstance", "Strowger.Lark"]; vdbHandle: VoiceDBHandle; ec: LoganBerry.ErrorCode; expl: ROPE; IF Complain = NIL THEN Complain _ FinchProblem; IF voiceRopeDBInstance = NIL THEN voiceRopeDBInstance _ server; IF voiceRopeDBName = NIL THEN voiceRopeDBName _ Rope.Cat["///", VoiceUtils.RnameToRspec[server].simpleName, "/VoiceRopeDB"]; [vdbHandle, ec, expl] _ OpenDB[voiceRopeDBName, voiceRopeDBInstance, "Morley.Lark"]; IF ec#NIL THEN Complain[expl]; -- Trouble opening; success of future calls in doubt handle _ NEW[HandleRec _ [vdbHandle: vdbHandle, Complain: Complain]]; }; FinchProblem: PROC[complaint: Rope.ROPE] = { VoiceUtils.Problem[complaint, $Finch]; }; StartFinch: PROC[handle: Handle, complain: BOOL_TRUE] RETURNS [state: FinchSmarts.FinchState] = { RW: PROC[c: Rope.ROPE] = { IF ~complain THEN RETURN; handle.Complain[complaint: c]; }; SELECT (state _ FinchSmarts.CurrentFinchState[]) FROM unknown => RW["Sorry, Finch needs to be loaded and started.\n"]; stopped => RW["Sorry, Finch needs to be connected to telephone server.\nUse \"Finch\" command.\n"]; running => NULL; ENDCASE => ERROR; handle.procs _ IF state=unknown THEN NIL ELSE FinchSmarts.GetProcs[]; }; ValidateHandle: PROC[oldHandle: Handle] RETURNS [handle: Handle] = { handle _ oldHandle; IF handle=NIL THEN { IF defaultHandle=NIL THEN defaultHandle _ Open[]; handle _ defaultHandle; }; }; Record: PUBLIC PROC[handle: Handle _ NIL] RETURNS [voiceRope: VoiceRope] = { ENABLE LoganBerry.Error => { handle.Complain[explanation]; CONTINUE; }; interval: Thrush.VoiceInterval; key: Thrush.EncryptionKey; reason: FinchSmarts.RecordReason; tune: Thrush.Tune; handle _ ValidateHandle[handle]; IF StartFinch[handle]#running THEN RETURN; [reason, tune, interval, key] _ handle.procs.recordTune[queueIt: TRUE]; IF reason#ok THEN RETURN; IF interval.length = -1 THEN interval.length _ BluejayUtils.DescribeTune[tune].size * 8000; voiceRope _ WriteVoiceRope[handle: handle.vdbHandle, struct: SimpleTuneList[tune, interval, key]]; }; Play: PUBLIC PROC[handle: Handle_NIL, voiceRope: VoiceRope, queueIt: BOOL_TRUE, failOK: BOOL_FALSE, wait: BOOL_FALSE] = { ENABLE LoganBerry.Error => { handle.Complain[explanation]; CONTINUE; }; tune: Thrush.Tune; interval: Thrush.VoiceInterval; key: Thrush.EncryptionKey; struct: TuneList; handle _ ValidateHandle[handle]; IF StartFinch[handle]#running THEN RETURN; struct _ ReadVoiceRope[handle.vdbHandle, voiceRope].struct; IF struct=NIL THEN { handle.Complain["No such voice rope to play."]; RETURN;}; UNTIL struct=NIL DO [tune, interval, key, struct] _ NextTuneOnList[struct]; []_handle.procs.playbackTune[tune: tune, interval: interval, key: key, queueIt: queueIt, failOK: failOK, wait: wait]; ENDLOOP; }; Stop: PUBLIC PROC[handle: Handle _ NIL] = { ENABLE UNWIND => NULL; handle _ ValidateHandle[handle]; IF StartFinch[handle, FALSE]#running THEN RETURN; handle.procs.stopTune[]; }; Retain: PUBLIC PROC [ handle: Handle _ NIL, voiceRope: VoiceRope, refID: Rope.ROPE, refIDType: Rope.ROPE ] ~ { handle _ ValidateHandle[handle]; }; Forget: PUBLIC PROC [ handle: Handle _ NIL, refID: Rope.ROPE, refIDType: Rope.ROPE ] ~ { handle _ ValidateHandle[handle]; }; Cat: PUBLIC PROC [handle: Handle _ NIL, vr1, vr2, vr3, vr4, vr5: VoiceRope _ NIL] RETURNS [new: VoiceRope] ~ { struct: TuneList; IF vr1 = NIL THEN RETURN[NIL]; handle _ ValidateHandle[handle]; struct _ ReadVoiceRope[handle.vdbHandle, vr1].struct; IF vr2 # NIL THEN struct _ CatEntries[struct, ReadVoiceRope[handle.vdbHandle, vr2].struct]; IF vr3 # NIL THEN struct _ CatEntries[struct, ReadVoiceRope[handle.vdbHandle, vr3].struct]; IF vr4 # NIL THEN struct _ CatEntries[struct, ReadVoiceRope[handle.vdbHandle, vr4].struct]; IF vr5 # NIL THEN struct _ CatEntries[struct, ReadVoiceRope[handle.vdbHandle, vr5].struct]; new _ WriteVoiceRope[handle.vdbHandle, struct]; }; Substr: PUBLIC PROC [handle: Handle _ NIL, vr: VoiceRope, start: INT _ 0, len: INT _ LAST[INT]] RETURNS [new: VoiceRope] ~ { struct: TuneList; handle _ ValidateHandle[handle]; struct _ ReadVoiceRope[handle.vdbHandle, vr].struct; IF struct=NIL THEN { handle.Complain["No such voice rope."]; RETURN[NIL];}; struct _ TuneListInterval[struct, start, len]; new _ WriteVoiceRope[handle.vdbHandle, struct]; }; Replace: PUBLIC PROC [handle: Handle _ NIL, vr: VoiceRope, start: INT _ 0, len: INT _ LAST[INT], with: VoiceRope _ NIL] RETURNS [new: VoiceRope] ~ { base, struct: TuneList; handle _ ValidateHandle[handle]; base _ ReadVoiceRope[handle.vdbHandle, vr].struct; IF base=NIL THEN { handle.Complain["No such voice rope."]; RETURN[NIL];}; struct _ TuneListInterval[CopyEntry[base], 0, start]; struct _ CatEntries[struct, ReadVoiceRope[handle.vdbHandle, with].struct]; struct _ CatEntries[struct, TuneListInterval[base, start+len]]; new _ WriteVoiceRope[handle.vdbHandle, struct]; }; Length: PUBLIC PROC [handle: Handle _ NIL, vr: VoiceRope] RETURNS [len: INT] ~ { header: LoganBerry.Entry; value: LoganBerry.AttributeValue; handle _ ValidateHandle[handle]; header _ ReadVoiceRope[handle.vdbHandle, vr].header; IF header = NIL THEN { handle.Complain["No such voice rope."]; RETURN[0];}; value _ GetAttributeValue[entry: header, type: $Length]; len _ Convert.IntFromRope[value]; }; DescribeRope: PUBLIC PROC [handle: Handle _ NIL, vr: VoiceRope, minSilence: INT _ -1] RETURNS [noise: IntervalSpecs] ~ { struct: TuneList; tuneSpec: Thrush.IntervalSpecs; tune: Thrush.Tune; interval: Thrush.VoiceInterval; noiseEnd: IntervalSpecs _ NIL; -- last item on current list of noise intervals samples: INT _ 0; handle _ ValidateHandle[handle]; struct _ ReadVoiceRope[handle.vdbHandle, vr].struct; UNTIL struct=NIL DO [tune: tune, interval: interval, rest: struct] _ NextTuneOnList[struct]; tuneSpec _ BluejayUtils.DescribeInterval[targetTune: tune, targetInterval: interval, minSilence: minSilence].intervals; UNTIL tuneSpec = NIL DO IF noiseEnd = NIL THEN { noise _ LIST[[start: samples + tuneSpec.first.interval.start - interval.start, length: tuneSpec.first.interval.length]]; noiseEnd _ noise; } ELSE { noiseEnd.rest _ LIST[[start: samples + tuneSpec.first.interval.start - interval.start, length: tuneSpec.first.interval.length]]; noiseEnd _ noiseEnd.rest; }; tuneSpec _ tuneSpec.rest; ENDLOOP; samples _ samples + interval.length; ENDLOOP; }; OpenDB: PROC[dbName, instance, localName: Rope.ROPE] RETURNS [handle: VoiceDBHandle _ NIL, openEc: LoganBerry.ErrorCode _ NIL, expl: Rope.ROPE] = { ENABLE LoganBerry.Error => { openEc _ ec; expl _ explanation; CONTINUE; }; handle _ NEW[VoiceDBHandleRec _ [voiceRopeDBName: dbName.Concat[".df"], voiceInterestDBName: dbName.Concat["Refs.df"]]]; handle.instance _ instance; ImportLoganberryAndBluejay[instance]; handle.voiceRopeDB _ LoganBerry.Open[dbName: handle.voiceRopeDBName]; }; ImportLoganberryAndBluejay: PROC[instance: ROPE] = { TRUSTED { LoganBerryRpcControl.UnimportInterface[!LupineRuntime.BindingError => CONTINUE]; BluejayUtilsRpcControl.UnimportInterface[!LupineRuntime.BindingError => CONTINUE]; }; LoganBerryRpcControl.ImportInterface[["Loganberry.Lark", instance]]; BluejayUtilsRpcControl.ImportInterface[["BluejayUtils.Lark", instance]]; }; ReadVoiceRope: PROC [handle: VoiceDBHandle, voiceRope: VoiceRope] RETURNS [header: LoganBerry.Entry, struct: TuneList] ~ { actualLength: INT; IF voiceRope = NIL THEN RETURN[NIL, NIL]; header _ LoganBerry.ReadEntry[db: handle.voiceRopeDB, key: $VRID, value: voiceRope.ropeID].entry; struct _ EntryToTuneList[header]; actualLength _ Convert.IntFromRope[GetAttributeValue[header, $Length]]; IF voiceRope.length # actualLength OR voiceRope.start # 0 THEN struct _ TuneListInterval[list: struct, start: voiceRope.start, len: voiceRope.length]; }; WriteVoiceRope: PROC [handle: VoiceDBHandle, struct: TuneList, len: INT _ 0] RETURNS [voiceRope: VoiceRope] ~ { id: ROPE; entry: LoganBerry.Entry; interval: Thrush.VoiceInterval; IF len = 0 THEN { -- must compute length of voice rope list: TuneList _ struct; UNTIL list = NIL DO [interval: interval, rest: list] _ NextTuneOnList[list]; len _ len + interval.length; ENDLOOP; }; id _ GenerateUniqueID[]; entry _ struct; entry _ CONS[[$Length, Convert.RopeFromInt[len]], entry]; entry _ CONS[[$Creator, UserCredentials.Get[].name], entry]; entry _ CONS[[$VRID, id], entry]; LoganBerry.WriteEntry[db: handle.voiceRopeDB, entry: entry ! LoganBerry.Error => IF ec = $ValueNotUnique THEN {id _ BadUniqueID[]; entry.first.value _ id; RETRY}]; voiceRope _ NEW[VoiceRopeInterval _ [ropeID: id, start: 0, length: len]]; }; CatEntries: PROC [e1, e2: LoganBerry.Entry] RETURNS [LoganBerry.Entry] ~ { ptr: LoganBerry.Entry _ e1; IF ptr = NIL THEN RETURN[e2]; UNTIL ptr.rest = NIL DO ptr _ ptr.rest; ENDLOOP; ptr.rest _ e2; RETURN[e1]; }; CopyEntry: PROC [entry: LoganBerry.Entry] RETURNS [LoganBerry.Entry] ~ { new, end: LoganBerry.Entry; IF entry = NIL THEN RETURN[NIL]; new _ LIST[entry.first]; end _ new; FOR e: LoganBerry.Entry _ entry.rest, e.rest WHILE e # NIL DO end.rest _ LIST[e.first]; end _ end.rest; ENDLOOP; RETURN[new]; }; GetAttributeValue: PROC [entry: LoganBerry.Entry, type: LoganBerry.AttributeType] RETURNS [LoganBerry.AttributeValue] ~ { FOR e: LoganBerry.Entry _ entry, e.rest WHILE e # NIL DO IF e.first.type = type THEN RETURN[e.first.value]; ENDLOOP; RETURN[NIL]; }; SimpleTuneList: PROC [tune: Thrush.Tune, interval: Thrush.VoiceInterval, key: Thrush.EncryptionKey] RETURNS [list: TuneList] ~ { list _ LIST[[$TID, Convert.RopeFromInt[tune]], [$Key, MarshalKey[key]], [$SL, MarshalInterval[interval]]]; }; NextTuneOnList: PROC [list: TuneList] RETURNS [tune: Thrush.Tune, interval: Thrush.VoiceInterval, key: Thrush.EncryptionKey, rest: TuneList] ~ { tune _ Convert.IntFromRope[list.first.value]; key _ UnmarshalKey[list.rest.first.value]; interval _ UnmarshalInterval[list.rest.rest.first.value]; rest _ list.rest.rest.rest; }; EntryToTuneList: PROC [entry: LoganBerry.Entry] RETURNS [TuneList] ~ { IF entry = NIL THEN RETURN[NIL]; UNTIL entry.first.type = $TID DO entry _ entry.rest ENDLOOP; RETURN[entry]; }; TuneListInterval: PROC [list: TuneList, start: INT, len: INT _ LAST[INT]] RETURNS [new: TuneList] ~ { interval: Thrush.VoiceInterval; -- the current tune's interval prevSamples: INT; -- number of samples in entry excluding the current tune interval samples: INT; -- number of samples in entry including the current tune interval intervalMustChange: BOOLEAN _ FALSE; prevSamples _ 0; interval _ UnmarshalInterval[list.rest.rest.first.value]; samples _ interval.length; UNTIL samples > start DO prevSamples _ samples; list _ list.rest.rest.rest; IF list = NIL THEN RETURN[NIL]; interval _ UnmarshalInterval[list.rest.rest.first.value]; samples _ samples + interval.length; ENDLOOP; new _ list; IF prevSamples # start THEN { -- don't want first part of existing interval interval.start _ interval.start + start - prevSamples; interval.length _ interval.length - (start - prevSamples); intervalMustChange _ TRUE; }; IF (len # LAST[INT]) AND (samples >= start + len) THEN { -- interval contained within a single tune interval interval.length _ len; list.rest.rest.rest _ NIL; -- truncate list since we have has much as we need intervalMustChange _ TRUE; }; IF intervalMustChange THEN new.rest.rest.first.value _ MarshalInterval[interval]; IF (len = LAST[INT]) OR (samples >= start + len) THEN -- no need to go on RETURN[new]; UNTIL samples >= start + len DO prevSamples _ samples; list _ list.rest.rest.rest; IF list = NIL THEN RETURN[new]; interval _ UnmarshalInterval[list.rest.rest.first.value]; samples _ samples + interval.length; ENDLOOP; IF samples # start + len THEN { -- want only first part of tune interval list.rest.rest.first.value _ MarshalInterval[[start: interval.start, length: start + len - prevSamples]]; }; list.rest.rest.rest _ NIL; -- truncate list since we have has much as we need RETURN[new]; }; MarshalKey: PROC [key: Thrush.EncryptionKey] RETURNS [r: ROPE] ~ TRUSTED { cardKey: LONG POINTER TO ARRAY[0..2) OF LONG CARDINAL=LOOPHOLE [LONG[@key]]; r _ IO.PutFR["%bB %bB", IO.card[cardKey[0]], IO.card[cardKey[1]]]; }; UnmarshalKey: PROC [r: ROPE] RETURNS [key: Thrush.EncryptionKey] ~ TRUSTED { cardKey: LONG POINTER TO ARRAY[0..2) OF LONG CARDINAL=LOOPHOLE[LONG[@key]]; keyStream: IO.STREAM _ IO.RIS[r]; cardKey[0] _ IO.GetCard[keyStream]; cardKey[1] _ IO.GetCard[keyStream]; }; MarshalInterval: PROC [interval: Thrush.VoiceInterval] RETURNS [r: ROPE] ~ INLINE { r _ IO.PutFR["%g %g", IO.int[interval.start], IO.int[interval.length]]; }; UnmarshalInterval: PROC [r: ROPE] RETURNS [interval: Thrush.VoiceInterval] ~ INLINE { s: IO.STREAM _ IO.RIS[r]; interval.start _ IO.GetInt[s]; interval.length _ IO.GetInt[s]; }; userName: ROPE; counter: INT; kicks: INT; -- for instrumentation purposes GenerateUniqueID: PROC [] RETURNS [id: ROPE] ~ { timestamp: INT _ MAX[counter, BasicTime.Period[BasicTime.earliestGMT, BasicTime.Now[]]]; id _ IO.PutFR["%g#%g", IO.rope[userName], IO.int[timestamp]]; counter _ counter + 1; }; BadUniqueID: PROC [] RETURNS [id: ROPE] ~ { counter _ BasicTime.Period[BasicTime.earliestGMT, BasicTime.Now[]]+1; -- kick the counter kicks _ kicks + 1; id _ GenerateUniqueID[]; }; NewUser: UserCredentials.CredentialsChangeProc ~ { userName _ UserCredentials.Get[].name; }; InitUniqueID: PROC [] RETURNS [] ~ { counter _ BasicTime.Period[BasicTime.earliestGMT, BasicTime.Now[]]; kicks _ 0; userName _ UserCredentials.Get[].name; UserCredentials.RegisterForChange[NewUser]; }; InitUniqueID[]; END. ΌVoiceRopeImpl.mesa Copyright c 1986 by Xerox Corporation. All rights reserved. Doug Terry, April 25, 1986 3:57:41 pm PST Swinehart, May 19, 1986 11:16:58 am PDT Operations for manipulating recorded voice. This should probably be separated into different modules: PlayRecordImpl, VoiceRopeImpl, VoiceInterestImpl. It would also be nice to have a LoganBerryClient package that incorporates some of the general mechanisms in the current VoiceDBImpl. These will get done when I have time to think about the right structure. For the moment I just want to get something on which a voice editing tool can be built. ... Doug It is unresolved where a tune's encryption key should be stored. For now, it is kept with the tune interval; though this is the wrong place. It should probably be kept in the tune's header. Can't do that since FinchSmarts wants to be passed the key. RefID USING [nullID], RPC USING [CallFailed, CallFailure, ImportFailed], Handle: TYPE = REF HandleRec; HandleRec: PUBLIC TYPE = RECORD [ -- Concrete implementation of VoiceRope.Handle procs: FinchSmarts.Procs_NIL, vdbHandle: VoiceDBHandle, Complain: PROC[complaint: Rope.ROPE] ]; VoiceDBHandle: TYPE = REF VoiceDBHandleRec; VoiceDBHandleRec: TYPE = RECORD [ voiceRopeDB: LoganBerry.OpenDB_RefID.nullID, voiceInterestDB: LoganBerry.OpenDB_RefID.nullID, voiceRopeDBName: Rope.ROPE, voiceInterestDBName: Rope.ROPE, instance: Rope.ROPE, imported: BOOL_FALSE ]; Intervoice operations (record and play) The following routines were adapted from RecordPlayImpl.mesa If voiceRopeDBName, voiceRopeDBInstance, or localName is omitted, a default based on the user profile choice of Thrush Server is invented. If Complain is omitted, VoiceUtils.Problem[...$Finch] is used. Should arrange to load Finch if not loaded, start it if not started. At present, does neither, tells caller what's what, and complains if asked to. Records a voice rope, registers it , and returns its ID. A NIL return value indicates that something went wrong. Nothing much can be done about it; just tell the user. Should eventually log for system administrator's benefit. -- no BeepTune until we store SysNoises in the new voice database Play[handle: handle, refID: "BeepTune", refIDType: "SysNoises", failOK: TRUE]; May need to call Bluejay to get actual length; note the size returned is in chirps so must multiply by 8000 to get samples Currently, the Play routine can only play a voice rope given its ID; the refID and refIDType fields are missing. How to manage system noises, like Beeps and intolerably cute rollback tunes, in the context of voice ropes needs to be resolved. Play a specified voice rope. The boolean arguments are interpreted as follows: queueIt => play after all other record/playback requests are satisfied. failOK => playing is optional; leave connection open if tune doesn't exist. wait => wait until things appear to be started properly, or have failed. Sender and Walnut Message viewer STOP buttons Voice Interests Voice interests and the garbage collection of voice ropes is not yet implemented. Existence of refID retains interest in voiceFileID until corresponding Forget. Creator is assumed to be logged-in user. Remove refID from database, eliminating its interest in any voiceFileID's Creator is assumed to be logged-in user. Editing operations Concatenates together the non-NIL voice ropes to produce a new voice rope. Creates a new voice rope that is a substring of an existing voice rope. Creates a new voice rope in which the given interval of the voice rope "vr" is replaced by the voice rope "with". Returns the actual length of the voice rope. This operation ignores the start and length values specified in the voice rope. Thus, vr.start _ 0; vr.length _ Length[handle, vr] will restore a voice rope to its full contents. Information about voice ropes Fetch: PUBLIC PROC [handle: Handle _ NIL, vr: VoiceRope, index: INT] RETURNS [VoiceSample]; Database operations handle.voiceInterestDB _ LoganBerry.Open[dbName: handle.voiceInterestDBName]; ENABLE { RPC.ImportFailed => NULL; }; Returns the structure of the given voice rope. TuneListInterval can be slow, so don't want to call it unless necessary Writes the voice rope information into the LoganBerry database. will have to change how we get creator across RPC connection. Concatenates the two lists together (destructive to the first). Copies one list to another. Voice rope structure A voice rope is represented in a LoganBerry database as an ordered list of $TID (the tune's ID), $Key (the tune's encryption key), and $SL (the start and length of a tune interval) attributes. The tune's encryption key should probably be kept in the tune's header, but that can't be done if FinchSmarts is used to record and play tunes. Builds a tune list with a single tune's information. Returns information about the next tune on the list; assumes that list really points to a properly structured TuneList so doesn't bother to check attribute types. The rest of the list is returned so this routine can be repetitively called to get all the tunes on the list. Returns the tune list representing the structure of the voice rope. Returns the tune list representing the structure of the voice rope interval. Warning: this operation modifies the original list! at this point: prevSamples <= start < samples Note: there's a possibility that start + len could cause an integer overflow; do I want to pay the cost to check for this? at this point: prevSamples <= start + len <= samples Conversions (marshalling) Writes a start and length field into a rope. Parses the input rope into a start and length field. Generating unique identifiers A unique identifier is generated by concatenating a user's Rname with a timestamp. This assumes that the same user is not simultaneously creating voice ropes from two different workstations. This problem would not arise if machine names were used instead of user names. The technique used for obtaining the user's name will have to change when this code runs on the voice server instead of on client machines. The timestamp is taken to be the maximum of the current time (converted to an integer) and a simple counter. The current time alone is not sufficient since it has a granularity of seconds. Several voice ropes may be created within a second, but the long-term creation rate should be much less than one per second. The counter is initialized to the current time, which could cause some problems if this module is rerun before the current time has a chance to catch up to the old counter (or if a machine's clock is reset to an earlier time). This problem is detected by $ValueNotUnique errors from LoganBerry when one attempts to write a new voice rope. Note that the counter, as maintain by these routines, is sufficient as a unique ID if this code is run on the voice server. However, having a user's name and current timestamp as part of the permanent voice rope ID provides information that might be useful. This routine should be called if some generated ID does not turn out to be unique. It trys once again to generate a unique ID after advancing the counter. Initializations Doug Terry, March 18, 1986 10:16:33 am PST created. Swinehart, May 19, 1986 11:16:53 am PDT Cedar 6.1 changes to: DIRECTORY , IMPORTS , Open , FinchProblem  Κ4˜codešœ™Kšœ Οmœ1™˜>—Jšœb˜bJšœ˜—J˜Jšœς™ςJ˜š‘œžœžœžœ!žœžœ žœžœžœžœ˜yšœO™OJšœ  <™GJšœ  A™KJšœ ?œ™H—Jšžœ5žœ˜GJ˜J˜J˜J˜J˜ Jšžœžœžœ˜*Jšœ;˜;Jš žœžœžœžœ.žœ˜Nšžœžœž˜Kšœ7˜7Jšœu˜uKšžœ˜—J˜J˜—š‘œžœžœžœ˜+Jšœ!Οbœ™-Jšžœžœžœ˜J˜ Jšžœžœ žœžœ˜1Jšœ˜Jšœ˜J˜——™K™QK˜š‘œžœžœ˜K™NK™(Kšœžœ˜Jšœ˜Jšœ žœ˜Jšœž˜Jšœ˜J˜ Jšœ˜J˜—š‘œžœžœ˜K™IK™(Kšœžœ˜Jšœ žœ˜Jšœž˜Jšœ˜J˜ Jšœ˜——™š ‘œžœžœžœ'žœžœ˜nK™JKšœ˜Kš žœžœžœžœžœ˜J˜ Kšœ5˜5šžœžœž˜KšœI˜I—šžœžœž˜KšœI˜I—šžœžœž˜KšœI˜I—šžœžœž˜KšœI˜I—Kšœ/˜/K˜K™—š‘œžœžœžœžœ žœžœžœžœ˜|K™GKšœ˜J˜ Kšœ4˜4Jš žœžœžœ+žœžœ˜KKšœ.˜.Kšœ/˜/K˜K™—š‘œžœžœžœžœ žœžœžœžœžœ˜”K™qJšœ˜J˜ Kšœ2˜2Jš žœžœžœ+žœžœ˜IKšœ5˜5KšœJ˜JKšœ?˜?Kšœ/˜/K˜K™—š ‘œžœžœžœžœžœ˜PK™αKšœ˜Jšœ!˜!J˜ Kšœ4˜4Kšžœ žœžœ,žœ˜LKšœ8˜8Kšœ!˜!K˜K™——™š ‘ œž œžœžœžœ˜xJ˜Jšœ˜J˜J˜Jšœžœ /˜OJšœ žœ˜J˜ Jšœ4˜4šžœžœž˜KšœH˜HJšœw˜wšžœ žœž˜šžœ žœžœ˜Jšœžœl˜xJšœ˜J˜—šžœ˜Jšœžœl˜€Jšœ˜J˜—Jšœ˜Jšžœ˜—K˜$Kšžœ˜—K˜—K™Kš‘œž œ.žœžœ™[J˜—™š‘œžœ#žœžœžœ!žœ žœ˜“Jšžœ8žœ˜JJšœ žœl˜xJ˜Jšœ%˜%JšœE˜EJšœM™MJ˜—K˜š‘œžœ žœ˜4šžœž™Jšžœžœ™J™—šžœ˜ JšœFžœ˜PJšœHžœ˜RJšœ˜—JšœD˜DJšœH˜HJ˜J˜—š‘ œžœ/žœ1˜zK™.Kšœžœ˜Kš žœ žœžœžœžœžœ˜)Kšœa˜aKšœ!˜!KšœG™GKšœG˜Gšžœ!žœžœ˜?JšœW˜W—K˜K™—š‘œžœ0žœžœ˜oK™?Kšœžœ˜ K˜Kšœ˜šžœ žœ $˜7Kšœ˜šžœžœž˜Kšœ8˜8Kšœ˜Kšžœ˜—K˜—Kšœ˜Kšœ˜Kšœžœ-˜9Kšœ=™=Kšœžœ0˜—K™—…—:Ji: