VoiceRopeDBImpl.mesa
Copyright Ó 1986, 1987 by Xerox Corporation. All rights reserved.
Doug Terry, June 30, 1986 2:38:05 pm PDT
Swinehart, March 23, 1987 2:13:37 pm PST
Routines for storing voice ropes in a database and manipulating their structure.
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 [GMT, earliestGMT, Now, Period, Update],
Convert USING [IntFromRope, RopeFromInt],
FS USING [ComponentPositions, ExpandName],
IO USING [card, GetCard, GetInt, int, PutFR, RIS, rope, STREAM],
LoganBerry USING [AttributeType, AttributeValue, Cursor, DeleteEntry, EndGenerate, Entry, Error, GenerateEntries, NextEntry, Open, ReadEntry, WriteEntry],
Rope USING [Concat, Equal, Find, Replace, ROPE, Substr],
Thrush USING [EncryptionKey, Tune, VoiceInterval],
UserCredentials USING [CredentialsChangeProc, Get, RegisterForChange],
VoiceRopeDB;
VoiceRopeDBImpl: CEDAR PROGRAM -- Should this be a monitor?
IMPORTS BasicTime, Convert, FS, IO, LoganBerry, Rope, UserCredentials
EXPORTS VoiceRopeDB
~ BEGIN
OPEN VoiceRopeDB;
ROPE: TYPE ~ Rope.ROPE;
STREAM: TYPE ~ IO.STREAM;
VoiceRope database operations
Voice ropes are stored in a LoganBerry database as a header followed by a list of tune intervals (a TuneList). Interests in voice ropes are also maintained in a LoganBerry database.
Error: PUBLIC ERROR [ec: ATOM, explanation: Rope.ROPENIL] = CODE;
Open: PUBLIC PROC[dbName: Rope.ROPE] RETURNS [handle: Handle ← NIL] = {
Prepares the given database for service. The name of the interest database is derived from the voice rope database name by adding "Refs" after the base name.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
vrDB, interestDB: ROPE;
cp: FS.ComponentPositions;
IF dbName = NIL THEN RETURN[NIL];
[vrDB, cp] ← FS.ExpandName[dbName];
IF cp.ext.length = 0 THEN
vrDB ← Rope.Concat[vrDB, ".df"];
interestDB ← Rope.Replace[base: vrDB, start: cp.base.start+cp.base.length, len: 0, with: "Refs"];
handle ← NEW[HandleRec ← [voiceRopeDBName: vrDB, voiceInterestDBName: interestDB]];
handle.voiceRopeDB ← LoganBerry.Open[dbName: handle.voiceRopeDBName];
handle.voiceInterestDB ← LoganBerry.Open[dbName: handle.voiceInterestDBName];
};
ReadVoiceRope: PUBLIC PROC [handle: Handle, ropeID: ID] RETURNS [header: Header, struct: TuneList] ~ {
Returns the structure of the given voice rope.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
header ← LoganBerry.ReadEntry[db: handle.voiceRopeDB, key: $VRID, value: ropeID].entry;
struct ← EntryToTuneList[header];
};
WriteVoiceRope: PUBLIC PROC [handle: Handle, struct: TuneList] RETURNS [info: VoiceRopeInfo] ~ {
Writes the voice rope information into the LoganBerry database.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
entry: LoganBerry.Entry;
interval: Thrush.VoiceInterval;
list: TuneList;
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
compute length of voice rope
list ← struct;
info.length ← 0;
UNTIL list = NIL DO
[interval: interval, rest: list] ← NextTuneOnList[list];
info.length ← info.length + interval.length;
ENDLOOP;
info.vrID ← GenerateUniqueID[];
will have to change how we get creator across RPC connection.
info.creator ← UserCredentials.Get[].name;
info.struct ← struct;
entry ← PackHeader[info];
LoganBerry.WriteEntry[db: handle.voiceRopeDB, entry: entry ! LoganBerry.Error => IF ec = $ValueNotUnique THEN {info.vrID ← ReplaceID[entry]; RETRY}];
};
DeleteVoiceRope: PUBLIC PROC [handle: Handle, ropeID: ID] ~ {
Deletes the given voice rope (regardless of what interests may exist).
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
LoganBerry.DeleteEntry[db: handle.voiceRopeDB, key: $VRID, value: ropeID];
};
VoiceRopeContainingTune: PUBLIC PROC [handle: Handle, tune: INT] RETURNS [header: Header] ~ {
Returns a voice rope containing the given tune, if one exists.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
header ← LoganBerry.ReadEntry[db: handle.voiceRopeDB, key: $TID, value: Convert.RopeFromInt[tune]].entry;
};
EnumerateVoiceRopes: PUBLIC PROC [handle: Handle, start: IDNIL, proc: EnumProc] ~ {
Calls the enumeration procedure for every voice rope with id greater than start; start=NIL represents the first element of the database. Stops when proc returns FALSE or the end of the database is encountered.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
continue: BOOLEANTRUE;
entry: LoganBerry.Entry;
info: VoiceRopeInfo;
cursor: LoganBerry.Cursor;
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
cursor ← LoganBerry.GenerateEntries[db: handle.voiceRopeDB, key: $VRID, start: start];
entry ← LoganBerry.NextEntry[cursor: cursor];
UNTIL entry = NIL OR NOT continue DO
info ← UnpackHeader[entry];
continue ← proc[info];
entry ← LoganBerry.NextEntry[cursor: cursor];
ENDLOOP;
LoganBerry.EndGenerate[cursor: cursor];
};
Voice rope structure
WARNING: For efficiency reasons, much of this code relies on the exact structure of entries stored in a LoganBerry database. For instance, it assumes that the first attribute of an entry is a voice rope (or interest) ID and does not check the attribute type to verify this. Do NOT reorder attributes unless care is taken to change the code dependencies.
A tune list, the structure of 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: PUBLIC 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: PUBLIC 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.
IF list = NIL THEN Error[$BadTuneList, "NIL tune 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 [struct: 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;
IF entry = NIL THEN Error[$BadTuneList, "No TID in entry"];
ENDLOOP;
struct ← entry;
};
TuneListInterval: PUBLIC PROC [list: TuneList, interval: Thrush.VoiceInterval] RETURNS [new: TuneList] ~ {
Returns the tune list representing the structure of the voice rope interval. Warning: this operation modifies the original list!
tuneInterval: 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;
IF list = NIL THEN RETURN[NIL];
IF interval.length = -1 THEN interval.length ← LAST[INT];
prevSamples ← 0;
tuneInterval ← UnmarshalInterval[list.rest.rest.first.value];
samples ← tuneInterval.length;
UNTIL samples > interval.start DO
prevSamples ← samples;
list ← list.rest.rest.rest;
IF list = NIL THEN RETURN[NIL];
tuneInterval ← UnmarshalInterval[list.rest.rest.first.value];
samples ← samples + tuneInterval.length;
ENDLOOP;
at this point: prevSamples <= interval.start < samples
new ← list;
IF prevSamples # interval.start THEN { -- don't want first part of existing interval
tuneInterval.start ← tuneInterval.start + interval.start - prevSamples;
tuneInterval.length ← tuneInterval.length - (interval.start - prevSamples);
intervalMustChange ← TRUE;
};
Note: there's a possibility that interval.start + interval.length could cause an integer overflow; do I want to pay the cost to check for this?
IF (interval.length # LAST[INT]) AND (samples >= interval.start + interval.length) THEN { -- interval contained within a single tune interval
tuneInterval.length ← interval.length;
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[tuneInterval];
IF (interval.length = LAST[INT]) OR (samples >= interval.start + interval.length) THEN -- no need to go on
RETURN[new];
UNTIL samples >= interval.start + interval.length DO
prevSamples ← samples;
list ← list.rest.rest.rest;
IF list = NIL THEN RETURN[new];
tuneInterval ← UnmarshalInterval[list.rest.rest.first.value];
samples ← samples + tuneInterval.length;
ENDLOOP;
at this point: prevSamples <= interval.start + interval.length <= samples
IF samples # interval.start + interval.length THEN { -- want only first part of tune interval
list.rest.rest.first.value ← MarshalInterval[[start: tuneInterval.start, length: interval.start + interval.length - prevSamples]];
};
list.rest.rest.rest ← NIL; -- truncate list since we have has much as we need
RETURN[new];
};
PackHeader: PROC [info: VoiceRopeInfo] RETURNS [header: Header] ~ {
entry: LoganBerry.Entry;
entry ← info.struct;
entry ← CONS[[$Length, Convert.RopeFromInt[info.length]], entry];
the creator field is not really needed, but is included for compatibility with existing databases
entry ← CONS[[$Creator, info.creator], entry];
entry ← CONS[[$VRID, info.vrID], entry];
RETURN[entry];
};
ReplaceID: PROC [header: Header] RETURNS [newID: ID] ~ {
newID ← BadUniqueID[];
header.first.value ← newID;
};
UnpackHeader: PUBLIC PROC [header: Header] RETURNS [info: VoiceRopeInfo] ~ {
Returns the information associated with a voice rope header.
list: LoganBerry.Entry ← header;
IF header = NIL THEN RETURN;
info.vrID ← list.first.value;
list ← list.rest;
ignore creator field
list ← list.rest;
info.length ← Convert.IntFromRope[list.first.value];
[info.creator, info.timestamp] ← ParseUniqueID[info.vrID];
info.struct ← list.rest;
};
Interests database
AddInterest: PUBLIC PROC [handle: Handle, interest: InterestInfo] ~ {
Adds an entry to the interest database if a similar entry does not already exist.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
entry: LoganBerry.Entry ← NIL;
oldEntry: LoganBerry.Entry;
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
oldEntry ← ReadInterest[handle, interest.vrID, interest.class, interest.refID];
interest.interestID ← IF oldEntry = NIL THEN GenerateUniqueID[] ELSE oldEntry.first.value;
entry ← PackInterest[interest];
LoganBerry.WriteEntry[db: handle.voiceInterestDB, entry: entry, replace: oldEntry # NIL ! LoganBerry.Error => IF ec = $ValueNotUnique AND oldEntry = NIL THEN {interest.interestID ← ReplaceInterestID[entry]; RETRY}];
};
DropInterest: PUBLIC PROC [handle: Handle, interest: InterestInfo] ~ {
Removes an entry from the interest database.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
entry: LoganBerry.Entry;
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
entry ← ReadInterest[handle, interest.vrID, interest.class, interest.refID];
IF entry = NIL THEN RETURN;
LoganBerry.DeleteEntry[db: handle.voiceInterestDB, key: $IID, value: entry.first.value];
};
ReadInterest: PROC [handle: Handle, ropeID: ID ← NIL, class: Rope.ROPE ← NIL, refID: Rope.ROPE] RETURNS [entry: LoganBerry.Entry] ~ {
Searches for an entry with matching ropeID, class, and refID.
info: InterestInfo;
cursor: LoganBerry.Cursor ← LoganBerry.GenerateEntries[db: handle.voiceInterestDB, key: $RefID, start: refID, end: refID];
entry ← LoganBerry.NextEntry[cursor: cursor];
UNTIL entry = NIL DO
info ← UnpackInterest[entry];
IF (ropeID = NIL OR Rope.Equal[info.refID, refID]) AND (class = NIL OR Rope.Equal[info.class, class]) THEN EXIT;
entry ← LoganBerry.NextEntry[cursor: cursor];
ENDLOOP;
LoganBerry.EndGenerate[cursor: cursor];
};
InterestForRef: PUBLIC PROC [handle: Handle, class: Rope.ROPE, refID: Rope.ROPE] RETURNS [interest: Interest] ~ {
Returns an interest with the given class and refID, if one exists.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
interest ← ReadInterest[handle: handle, class: class, refID: refID];
};
InterestInVoiceRope: PUBLIC PROC [handle: Handle, ropeID: ID] RETURNS [interest: Interest] ~ {
Returns an interest for the voice rope, if one exists.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
interest ← LoganBerry.ReadEntry[db: handle.voiceInterestDB, key: $VRID, value: ropeID].entry;
};
EnumerateInterestClass: PUBLIC PROC [handle: Handle, class: Rope.ROPE, proc: InterestProc] ~ {
Calls the enumeration procedure for every interest of the given class. Stops when proc returns FALSE or the end of the database is encountered.
ENABLE LoganBerry.Error => ERROR Error[ec, explanation];
continue: BOOLEANTRUE;
entry: LoganBerry.Entry;
info: InterestInfo;
cursor: LoganBerry.Cursor;
IF handle = NIL THEN Error[$BadHandle, "NIL handle"];
IF proc = NIL THEN Error[$NoProc, "No procedure passed to EnumerateInterestClass"];
cursor ← LoganBerry.GenerateEntries[db: handle.voiceInterestDB, key: $Class, start: class, end: class];
entry ← LoganBerry.NextEntry[cursor: cursor];
UNTIL entry = NIL OR NOT continue DO
info ← UnpackInterest[entry];
continue ← proc[info];
entry ← LoganBerry.NextEntry[cursor: cursor];
ENDLOOP;
LoganBerry.EndGenerate[cursor: cursor];
};
PackInterest: PUBLIC PROC [info: InterestInfo] RETURNS [interest: Interest] ~ {
interest ← NIL;
IF info.data # NIL THEN
interest ← CONS[[$Data, info.data], interest];
interest ← CONS[[$RefID, info.refID], interest];
interest ← CONS[[$Class, info.class], interest];
interest ← CONS[[$VRID, info.vrID], interest];
interest ← CONS[[$IID, info.interestID], interest];
};
ReplaceInterestID: PROC [interest: Interest] RETURNS [newID: ID] ~ {
newID ← BadUniqueID[];
interest.first.value ← newID;
};
UnpackInterest: PUBLIC PROC [interest: Interest] RETURNS [info: InterestInfo] ~ {
list: LoganBerry.Entry ← interest;
IF interest = NIL THEN RETURN;
info.interestID ← list.first.value;
list ← list.rest;
info.vrID ← list.first.value;
list ← list.rest;
info.class ← list.first.value;
list ← list.rest;
info.refID ← list.first.value;
list ← list.rest;
IF list # NIL THEN
info.data ← list.first.value;
[info.creator, info.timestamp] ← ParseUniqueID[info.interestID];
};
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[];
};
ParseUniqueID: PROC [id: ROPE] RETURNS [creator: ROPE, timestamp: BasicTime.GMT] ~ {
seconds: INT;
i: INT ← Rope.Find[s1: id, s2: "#"];
creator ← Rope.Substr[base: id, start: 0, len: i];
seconds ← Convert.IntFromRope[Rope.Substr[base: id, start: i+1]];
timestamp ← BasicTime.Update[BasicTime.earliestGMT, seconds];
};
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, June 3, 1986 4:19:14 pm PDT
Extracted database operations from VoiceRopeImpl.mesa.
changes to: VoiceRopeDBImpl , EXPORTS , ~ , WriteVoiceRope 
Doug Terry, June 3, 1986 5:23:23 pm PDT
changes to: VoiceRopeDBImpl, EXPORTS, ~, Open, WriteVoiceRope, END, DIRECTORY, IMPORTS, Read, SimpleTuneList, NextTuneOnList, TuneListInterval, EntryToTuneList
Doug Terry, June 5, 1986 11:47:40 am PDT
changes to: Open, Read, Write, EntryToTuneList, Error, DIRECTORY
Doug Terry, June 5, 1986 3:07:30 pm PDT
changes to: EntryToTuneList, TuneListInterval
Doug Terry, June 10, 1986 5:55:22 pm PDT
changes to: AddInterest, DropInterest, LookupInterest, Write, Delete, ~, LookupVoiceRope, LookupTune
Doug Terry, June 11, 1986 3:40:47 pm PDT
changes to: LookupTune, Enumerate, LookupVoiceRope, EnumerateInterests, ParseInterestEntry, AddInterest, DropInterest, LookupInterest, CRQuery, RCRQuery
Doug Terry, June 12, 1986 4:45:50 pm PDT
changes to: Open, ReadVoiceRope, EntryToTuneList, ~, WriteVoiceRope, PackHeader, ReplaceID, UnpackHeader, PackInterest, UnpackInterest, DeleteVoiceRope, VoiceRopeContainingTune, EnumerateVoiceRopes, AddInterest, DropInterest, ReplaceInterestID, ReadInterest, InterestForRef, InterestInVoiceRope, EnumerateInterestClass, BadUniqueID, ParseUniqueID, DIRECTORY
Doug Terry, June 17, 1986 12:02:00 pm PDT
changes to: UnpackHeader
Doug Terry, June 26, 1986 2:08:18 pm PDT
changes to: TuneListInterval
Doug Terry, June 26, 1986 5:03:38 pm PDT
changes to: Open, DIRECTORY, IMPORTS
Doug Terry, June 30, 1986 1:11:17 pm PDT
changes to: PackInterest
Doug Terry, June 30, 1986 2:36:30 pm PDT
changes to: UnpackHeader, Open, ReadVoiceRope, WriteVoiceRope, DeleteVoiceRope, VoiceRopeContainingTune, EnumerateVoiceRopes, AddInterest, DropInterest, InterestForRef, InterestInVoiceRope, EnumerateInterestClass, NextTuneOnList, EntryToTuneList, UnpackInterest