VoiceRopeImpl.mesa
Copyright © 1986 by Xerox Corporation. All rights reserved.
Doug Terry, June 2, 1986 4:39:37 pm PDT
Operations for manipulating recorded voice.
This should probably be separated into different modules: PlayRecordImpl, VoiceRopeImpl, VoiceInterestImpl. 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.
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],
FS USING [ComponentPositions, ExpandName],
IO USING [card, GetCard, GetInt, int, PutFR, RIS, rope, STREAM],
LoganBerryStub USING [AttributeType, AttributeValue, Entry, Error, ErrorCode, Open, OpenDB, ReadEntry, WriteEntry],
LupineRuntime USING [BindingError],
Rope USING [Cat, Concat, ROPE, Substr],
RPC USING [CallFailed, CallFailure, ImportFailed],
Thrush USING [EncryptionKey, IntervalSpecs, Tune, VoiceInterval],
UserProfile USING [Token],
UserCredentials USING [CredentialsChangeProc, Get, RegisterForChange],
VoiceUtils USING [Problem, RnameToRspec],
VoiceRope;
VoiceRopeImpl: CEDAR PROGRAM -- Should this be a monitor?
IMPORTS BasicTime, BluejayUtils, BluejayUtilsRpcControl, Convert, FinchSmarts, FS, IO, LupineRuntime, Rope, UserCredentials, UserProfile, VoiceUtils, LoganBerry: LoganBerryStub
EXPORTS VoiceRope
~ BEGIN
OPEN VoiceRope;
ROPE: TYPE ~ Rope.ROPE;
STREAM: TYPE ~ IO.STREAM;
defaultHandle: Handle←NIL;
TuneList: TYPE = LoganBerry.Entry;
Intervoice operations (record and play)
The following routines were adapted from RecordPlayImpl.mesa
Open: PUBLIC PROC[voiceRopeDBName: Rope.ROPENIL, voiceRopeDBInstance: Rope.ROPENIL, localName: Rope.ROPENIL, Complain: PROC[complaint: Rope.ROPE]←NIL] RETURNS [handle: Handle] = {
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.
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: BOOLTRUE]
RETURNS [state: FinchSmarts.FinchState] = {
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.
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] = {
Records a voice rope, registers it , and returns its ID. A NIL return value indicates that something went wrong.
ENABLE LoganBerry.Error => { handle.Complain[explanation]; CONTINUE; };
Nothing much can be done about it; just tell the user. Should eventually log for system administrator's benefit.
interval: Thrush.VoiceInterval;
key: Thrush.EncryptionKey;
reason: FinchSmarts.RecordReason;
tune: Thrush.Tune;
handle ← ValidateHandle[handle];
IF StartFinch[handle]#running THEN RETURN;
-- no BeepTune until we store SysNoises in the new voice database
Play[handle: handle, refID: "BeepTune", refIDType: "SysNoises", failOK: TRUE];
[reason, tune, interval, key] ← handle.procs.recordTune[queueIt: TRUE];
IF reason#ok THEN RETURN;
May need to call Bluejay to get actual length; note the size returned is in chirps so must multiply by 8000 to get samples
IF interval.length = -1 THEN
interval.length ← BluejayUtils.DescribeTune[tune].size * 8000;
voiceRope ← WriteVoiceRope[handle: handle.vdbHandle, struct: SimpleTuneList[tune, interval, key]];
};
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: PUBLIC PROC[handle: Handle←NIL, voiceRope: VoiceRope, queueIt: BOOLTRUE, failOK: BOOLFALSE, wait: BOOLFALSE] = {
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.
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] = {
Sender and Walnut Message viewer STOP buttons
ENABLE UNWIND => NULL;
handle ← ValidateHandle[handle];
IF StartFinch[handle, FALSE]#running THEN RETURN;
handle.procs.stopTune[];
};
Voice Interests
Voice interests and the garbage collection of voice ropes is not yet implemented.
Retain: PUBLIC PROC [
Existence of refID retains interest in voiceFileID until corresponding Forget.
Creator is assumed to be logged-in user.
handle: Handle ← NIL,
voiceRope: VoiceRope,
refID: Rope.ROPE,
refIDType: Rope.ROPE
] ~ {
handle ← ValidateHandle[handle];
};
Forget: PUBLIC PROC [
Remove refID from database, eliminating its interest in any voiceFileID's
Creator is assumed to be logged-in user.
handle: Handle ← NIL,
refID: Rope.ROPE,
refIDType: Rope.ROPE
] ~ {
handle ← ValidateHandle[handle];
};
Editing operations
Cat: PUBLIC PROC [handle: Handle ← NIL, vr1, vr2, vr3, vr4, vr5: VoiceRope ← NIL] RETURNS [new: VoiceRope] ~ {
Concatenates together the non-NIL voice ropes to produce a new voice rope.
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: INTLAST[INT]] RETURNS [new: VoiceRope] ~ {
Creates a new voice rope that is a substring of an existing voice rope.
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: INTLAST[INT], with: VoiceRope ← NIL] RETURNS [new: VoiceRope] ~ {
Creates a new voice rope in which the given interval of the voice rope "vr" is replaced by the voice rope "with".
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] ~ {
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.
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];
};
Information about voice ropes
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;
};
Fetch: PUBLIC PROC [handle: Handle ← NIL, vr: VoiceRope, index: INT] RETURNS [VoiceSample];
Database operations
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; };
convert dbName and instance to form desired by LoganBerryStub
cp: FS.ComponentPositions;
remoteDBName: ROPE;
[remoteDBName, cp] ← FS.ExpandName[dbName];
remoteDBName ← Rope.Cat["[", instance, "]<", Rope.Substr[base: remoteDBName, start: cp.dir.start]];
build handle and import Bluejay
handle ← NEW[VoiceDBHandleRec ← [voiceRopeDBName: remoteDBName.Concat[".df"], voiceInterestDBName: remoteDBName.Concat["Refs.df"]]];
handle.instance ← instance;
ImportBluejay[instance];
handle.voiceRopeDB ← LoganBerry.Open[dbName: handle.voiceRopeDBName];
handle.voiceInterestDB ← LoganBerry.Open[dbName: handle.voiceInterestDBName];
};
ImportBluejay: PROC[instance: ROPE] = {
ENABLE {
RPC.ImportFailed => NULL;
};
TRUSTED {
BluejayUtilsRpcControl.UnimportInterface[!LupineRuntime.BindingError => CONTINUE];
};
BluejayUtilsRpcControl.ImportInterface[["BluejayUtils.Lark", instance]];
};
ReadVoiceRope: PROC [handle: VoiceDBHandle, voiceRope: VoiceRope] RETURNS [header: LoganBerry.Entry, struct: TuneList] ~ {
Returns the structure of the given voice rope.
actualLength: INT;
IF voiceRope = NIL THEN RETURN[NIL, NIL];
header ← LoganBerry.ReadEntry[db: handle.voiceRopeDB, key: $VRID, value: voiceRope.ropeID].entry;
struct ← EntryToTuneList[header];
TuneListInterval can be slow, so don't want to call it unless necessary
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] ~ {
Writes the voice rope information into the LoganBerry database.
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];
will have to change how we get creator across RPC connection.
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] ~ {
Concatenates the two lists together (destructive to the first).
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] ~ {
Copies one list to another.
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];
};
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.
SimpleTuneList: PROC [tune: Thrush.Tune, interval: Thrush.VoiceInterval, key: Thrush.EncryptionKey] RETURNS [list: TuneList] ~ {
Builds a tune list with a single tune's information.
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] ~ {
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.
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] ~ {
Returns the tune list representing the structure of the voice rope.
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: INTLAST[INT]] RETURNS [new: TuneList] ~ {
Returns the tune list representing the structure of the voice rope interval. Warning: this operation modifies the original list!
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: BOOLEANFALSE;
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;
at this point: prevSamples <= start < samples
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;
};
Note: there's a possibility that start + len could cause an integer overflow; do I want to pay the cost to check for this?
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;
at this point: prevSamples <= start + len <= samples
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];
};
Conversions (marshalling)
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.STREAMIO.RIS[r];
cardKey[0] ← IO.GetCard[keyStream];
cardKey[1] ← IO.GetCard[keyStream];
};
MarshalInterval: PROC [interval: Thrush.VoiceInterval] RETURNS [r: ROPE] ~ INLINE {
Writes a start and length field into a rope.
r ← IO.PutFR["%g %g", IO.int[interval.start], IO.int[interval.length]];
};
UnmarshalInterval: PROC [r: ROPE] RETURNS [interval: Thrush.VoiceInterval] ~ INLINE {
Parses the input rope into a start and length field.
s: IO.STREAM ← IO.RIS[r];
interval.start ← IO.GetInt[s];
interval.length ← IO.GetInt[s];
};
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.
userName: ROPE;
counter: INT;
kicks: INT; -- for instrumentation purposes
GenerateUniqueID: PROC [] RETURNS [id: ROPE] ~ {
timestamp: INTMAX[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] ~ {
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.
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];
};
Initializations
InitUniqueID[];
END.
Doug Terry, March 18, 1986 10:16:33 am PST
created.