VoiceRopeServerImpl.mesa
Copyright © 1986 by Xerox Corporation. All rights reserved.
Doug Terry, November 18, 1986 4:57:45 pm PST
Swinehart, October 19, 1986 5:32:04 pm PDT
Operations for manipulating recorded voice.
DIRECTORY
BluejaySmarts USING [ConvDesc, GetConversation, DistributeKey, IntervalReq, IntervalReqBody, SetInterval],
BluejayUtils USING [DescribeTune, DescribeInterval],
IV,
Jukebox USING [CloseTune, CreateTune, EOF, Error, IntervalSpec, IntervalSpecBody, IntervalSpecs, MissingChirp, Tune, TuneID],
RecordingServiceRegister USING [database, jukebox],
RefID USING [ID],
Rope USING [Cat, ROPE],
RPC USING [GetCaller],
Thrush USING [ConversationID, Credentials, EncryptionKey, NB, none, SHHH],
VoiceStream USING [AddPiece, FlushPieces],
VoiceUtils USING [Problem],
VoiceRopeDB USING [AddInterest, DropInterest, Error, Handle, Header, Interest, InterestInfo, InterestForRef, NextTuneOnList, ReadVoiceRope, SimpleTuneList, TuneID, TuneList, TuneListInterval, UnpackHeader, UnpackInterest, VoiceRopeInfo, WriteVoiceRope],
VoiceRopeServer;
VoiceRopeServerImpl: CEDAR PROGRAM -- Should this be a monitor?
IMPORTS BluejaySmarts, BluejayUtils, Jukebox, RecordingServiceRegister, Rope, RPC, VoiceStream, VoiceUtils, VoiceRopeDB
EXPORTS VoiceRopeServer
~ BEGIN
OPEN VoiceRopeServer;
ROPE: TYPE ~ Rope.ROPE;
Creating and playing voice ropes
beep: VoiceRope ← NIL; -- beep to prompt recording
playBeep: BOOLEANFALSE;
Record: PUBLIC PROC[shhh: Thrush.SHHH ← Thrush.none, credentials: Thrush.Credentials, serviceID: RefID.ID, intID: CARD ← 0, queueIt: BOOLTRUE] RETURNS [nb: Thrush.NB ← $success, voiceRope: VoiceRope] = {
Records a voice rope, registers it, and returns its ID. The following action reports are generated if intID#0:
{ $recording, $started, intID } when recording begins.
{ $recording, $finished, intID } when the recording of this interval has finished.
{ $recording, $abandoned, intID } when all recording and playback actions ending with this interval has been flushed.
ENABLE {
VoiceRopeDB.Error => {
VoiceUtils.Problem[explanation, $VoiceRope];
nb ← ec; GOTO Fail; };
Jukebox.Error, Jukebox.MissingChirp, Jukebox.EOF => {
VoiceUtils.Problem["Error detected in Bluejay", $Bluejay];
nb ← $voiceStreamFailure; GOTO Fail; };
};
key: Thrush.EncryptionKey;
tune: Jukebox.Tune;
tuneID: Jukebox.TuneID;
intervalSpec: Jukebox.IntervalSpec ← NEW[Jukebox.IntervalSpecBody];
cDesc: BluejaySmarts.ConvDesc;
ir: BluejaySmarts.IntervalReq;
vrinfo: VoiceRopeDB.VoiceRopeInfo;
Check that conversation exists, etc.
[nb, cDesc] ← BluejaySmarts.GetConversation[smartsID: serviceID, conv: credentials.convID];
IF nb # $success THEN RETURN;
Lookup beep tune
IF beep = NIL THEN {
[voiceRope: beep] ← GetByInterest[shhh: shhh, class: "SysNoises", refID: "BeepTune"];
IF beep = NIL THEN
VoiceUtils.Problem["Can't find beep tune to prompt recording", $VoiceRope];
};
Get tune identifier
TRUSTED {
tune ← Jukebox.CreateTune[RecordingServiceRegister.jukebox, -1];
tuneID ← intervalSpec.tuneID ← tune.tuneId;
Jukebox.CloseTune[RecordingServiceRegister.jukebox, tune];
};
Have Bluejay record tune
note: playing the beep will flush if necessary
IF playBeep AND beep#NIL THEN
[] ← Play[shhh: shhh, voiceRope: beep, credentials: credentials, serviceID: serviceID, queueIt: queueIt]
ELSE IF NOT queueIt THEN
TRUSTED {
VoiceStream.FlushPieces[cDesc.info.stream]; -- Will report, synchronously
};
ir ← NEW[BluejaySmarts.IntervalReqBody ← [
iSpec: intervalSpec^, direction: $record, cDesc: cDesc, requestingParty: credentials.partyID,
intID: intID, firstInterval: TRUE, lastInterval: TRUE]];
[nb] ← BluejaySmarts.SetInterval[ir];
IF nb # $success THEN RETURN;
intervalSpec.length ← ir.iSpec.length;
IF cDesc.keyTable # NIL THEN
key ← cDesc.keyTable[1];
TRUSTED {
VoiceStream.AddPiece[cDesc.info.stream, intervalSpec, $record, ir];
};
Write voice rope entry in database
note: the length of the voice rope is unknown at this point since it has yet to be recorded
vrinfo.struct ← VoiceRopeDB.SimpleTuneList[tune: tuneID, start: 0, length: -1, key: key];
vrinfo.creator ← RPC.GetCaller[shhh];
vrinfo ← VoiceRopeDB.WriteVoiceRope[handle: RecordingServiceRegister.database, vr: vrinfo];
voiceRope ← NEW[VoiceRopeInterval ← [ropeID: vrinfo.vrID, start: 0, length: vrinfo.length]];
EXITS Fail => RETURN[nb, NIL];
};
Play: PUBLIC PROC[shhh: Thrush.SHHH ← Thrush.none, voiceRope: VoiceRope, credentials: Thrush.Credentials, serviceID: RefID.ID, intID: CARD ← 0, queueIt: BOOL TRUE] RETURNS [nb: Thrush.NB ← $success] = {
Play a specified voice rope. If queueIt is FALSE, flush existing playback and recording requests, first. The following action reports are generated if intID#0:
{ $recording/$playback, $started, intID } as the interval begins playing/recording.
{ $recording/$playback, $scheduled, intID } as the interval is accepted for recording/playback, if there is already a list of pieces in the queue ahead of this request.
{ $recording/$playback, $finished, intID } when the interval has finished.
{ $recording/$playback, $abandoned, intID } when all recording and playback actions ending with this interval has been flushed.
ENABLE {
VoiceRopeDB.Error => {
VoiceUtils.Problem[explanation, $VoiceRope];
nb ← ec; GOTO Fail; };
Jukebox.Error, Jukebox.MissingChirp, Jukebox.EOF => {
VoiceUtils.Problem["Error detected in Bluejay", $Bluejay];
nb ← $voiceStreamFailure; GOTO Fail; };
};
key: Thrush.EncryptionKey;
cDesc: BluejaySmarts.ConvDesc;
ir: BluejaySmarts.IntervalReq;
struct: VoiceRopeDB.TuneList;
intervalSpec: Jukebox.IntervalSpec;
playQueue, last: Jukebox.IntervalSpecs;
[nb, cDesc] ← BluejaySmarts.GetConversation[smartsID: serviceID, conv: credentials.convID];
IF nb # $success THEN RETURN;
IF NOT queueIt THEN
TRUSTED {
VoiceStream.FlushPieces[cDesc.info.stream]; -- Will report, synchronously
};
struct ← ReadVoiceRope[voiceRope].info.struct;
IF struct=NIL THEN { nb ← $noSuchVoiceRope; RETURN;};
distribute encryption keys for tune segments (and build play queue)
UNTIL struct=NIL DO
intervalSpec ← NEW[Jukebox.IntervalSpecBody];
[intervalSpec.tuneID, intervalSpec.start, intervalSpec.length, key, struct] ← VoiceRopeDB.NextTuneOnList[struct];
IF last = NIL THEN
last ← playQueue ← LIST[intervalSpec]
ELSE {
last.rest ← LIST[intervalSpec];
last ← last.rest;
};
[nb, intervalSpec.keyIndex] ← BluejaySmarts.DistributeKey[cDesc: cDesc, key: key, wait: struct=NIL];
SELECT nb FROM $success, $newKeys => NULL; ENDCASE => GOTO Fail;
ENDLOOP;
schedule the various tune segments for playback
FOR q: Jukebox.IntervalSpecs ← playQueue, q.rest WHILE q#NIL DO
ir ← NEW[BluejaySmarts.IntervalReqBody ← [
iSpec: q.first^, direction: $play, cDesc: cDesc, requestingParty: credentials.partyID,
intID: intID, firstInterval: q=playQueue, lastInterval: q.rest=NIL]];
[nb] ← BluejaySmarts.SetInterval[ir];
IF nb # $success THEN RETURN;
intervalSpec.length ← ir.iSpec.length;
TRUSTED {
VoiceStream.AddPiece[cDesc.info.stream, q.first, $play, ir];
};
ENDLOOP;
EXITS
Fail => RETURN[nb];
};
Stop: PUBLIC PROC[shhh: Thrush.SHHH ← Thrush.none, credentials: Thrush.Credentials, serviceID: RefID.ID] RETURNS [nb: Thrush.NB ← $success] = {
Stop playing or recording some or all of the scheduled intervals.
Action reports are generated if intID#0 was specified in original request:
{ $recording, $abandoned, intID } as indicated above.
ENABLE {
Jukebox.Error, Jukebox.MissingChirp, Jukebox.EOF => {
VoiceUtils.Problem["Error detected in Bluejay", $Bluejay];
nb ← $voiceStreamFailure; GOTO Fail; };
};
cDesc: BluejaySmarts.ConvDesc;
[nb, cDesc] ← BluejaySmarts.GetConversation[smartsID: serviceID, conv: credentials.convID];
IF nb # $success THEN RETURN;
TRUSTED {
VoiceStream.FlushPieces[cDesc.info.stream]; -- Will report, synchronously
};
EXITS Fail => RETURN[nb];
};
Voice Interests
The VoiceRope interface simply allows clients to register (and unregister) interests in voice ropes. Garbage collection is actually done through VoiceCleanup.mesa.
Retain: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr: VoiceRope, class: InterestClass, refID: Rope.ROPE, other: Rope.ROPENIL] RETURNS [nb: Thrush.NB ← $success] ~ {
Registers a new interest in the voice rope. The voice rope will be retained until either a corresponding Forget is done or the class' garbage collection process determines that the voice rope is no longer referenced, e.g. refID no longer exists. Taken together, the vr, class, and refID must be unique. Repeated calls of Retain with the same parameters (ignoring other) will only register a single interest.
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
info: VoiceRopeDB.InterestInfo;
info.vrID ← vr.ropeID;
info.class ← class;
info. refID ← refID;
info.data ← other;
[] ← VoiceRopeDB.AddInterest[RecordingServiceRegister.database, info];
};
Forget: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr: VoiceRope, class: InterestClass, refID: Rope.ROPE] RETURNS [nb: Thrush.NB ← $success] ~ {
The specified refID of the specified class drops its interest in the voice rope. The voice rope is not necessarily deleted, however, since other interests in the same voice rope may exist.
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
info: VoiceRopeDB.InterestInfo;
info.vrID ← vr.ropeID;
info.class ← class;
info. refID ← refID;
VoiceRopeDB.DropInterest[RecordingServiceRegister.database, info];
};
GetByInterest: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, class: InterestClass, refID: Rope.ROPE] RETURNS [nb: Thrush.NB ← $success, voiceRope: VoiceRope] ~ {
Returns any voice rope that is of interest to the given class and refID; returns NIL if no such voice rope exists.
N.B.: System noises, like Beeps and intolerably cute rollback tunes are indexed in the interest database as refID's like "beep", "rollback", and so on, under the class "SysNoises".
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
interest: VoiceRopeDB.Interest;
info: VoiceRopeDB.InterestInfo;
interest ← VoiceRopeDB.InterestForRef[RecordingServiceRegister.database, class, refID];
IF interest = NIL THEN RETURN[nb: $noSuchVoiceRope, voiceRope: NIL];
info ← VoiceRopeDB.UnpackInterest[interest];
voiceRope ← NEW[VoiceRopeInterval ← [ropeID: info.vrID, start: 0, length: -1]];
};
Editing operations
Cat: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr1, vr2, vr3, vr4, vr5: VoiceRope ← NIL] RETURNS [nb: Thrush.NB ← $success, new: VoiceRope] ~ {
Concatenates together the non-NIL voice ropes to produce a new voice rope.
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
piece, info: VoiceRopeDB.VoiceRopeInfo;
info.struct ← NIL;
info.length ← 0;
IF vr1 # NIL THEN {
piece ← ReadVoiceRope[vr: vr1, unpackHeader: TRUE];
IF piece.struct=NIL THEN {nb ← $noSuchVoiceRope; RETURN[new: NIL]; };
info.struct ← piece.struct;
info.length ← piece.length;
};
IF vr2 # NIL THEN {
piece ← ReadVoiceRope[vr: vr2, unpackHeader: TRUE];
IF piece.struct=NIL THEN {nb ← $noSuchVoiceRope; RETURN[new: NIL]; };
info.struct ← CatEntries[info.struct, piece.struct];
info.length ← info.length + piece.length;
};
IF vr3 # NIL THEN {
piece ← ReadVoiceRope[vr: vr3, unpackHeader: TRUE];
IF piece.struct=NIL THEN {nb ← $noSuchVoiceRope; RETURN[new: NIL]; };
info.struct ← CatEntries[info.struct, piece.struct];
info.length ← info.length + piece.length;
};
IF vr4 # NIL THEN {
piece ← ReadVoiceRope[vr: vr4, unpackHeader: TRUE];
IF piece.struct=NIL THEN {nb ← $noSuchVoiceRope; RETURN[new: NIL]; };
info.struct ← CatEntries[info.struct, piece.struct];
info.length ← info.length + piece.length;
};
IF vr5 # NIL THEN {
piece ← ReadVoiceRope[vr: vr5, unpackHeader: TRUE];
IF piece.struct=NIL THEN {nb ← $noSuchVoiceRope; RETURN[new: NIL]; };
info.struct ← CatEntries[info.struct, piece.struct];
info.length ← info.length + piece.length;
};
IF info.struct = NIL THEN RETURN[new: NIL];
info.creator ← RPC.GetCaller[shhh];
info ← VoiceRopeDB.WriteVoiceRope[RecordingServiceRegister.database, info];
new ← NEW[VoiceRopeInterval ← [ropeID: info.vrID, start: 0, length: info.length]];
};
Substr: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr: VoiceRope, start: INT ← 0, len: INTLAST[INT]] RETURNS [nb: Thrush.NB ← $success, new: VoiceRope] ~ {
Creates a new voice rope that is a substring of an existing voice rope.
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
info: VoiceRopeDB.VoiceRopeInfo;
info ← ReadVoiceRope[vr: vr, unpackHeader: TRUE];
IF info.struct=NIL THEN {
nb ← $noSuchVoiceRope;
RETURN[new: NIL];
};
subintervals must be on 8-byte boundaries since tunes are block-mode encrypted
start ← start - (start MOD 8); -- round down to multiple of 8
len ← IF len<0 OR len=LAST[INT] THEN -1 ELSE len - (len MOD 8);
info.struct ← VoiceRopeDB.TuneListInterval[info.struct, start, len];
IF len # -1 AND len < info.length - start THEN
info.length ← len
ELSE
info.length ← info.length - start;
info.creator ← RPC.GetCaller[shhh];
info ← VoiceRopeDB.WriteVoiceRope[RecordingServiceRegister.database, info];
new ← NEW[VoiceRopeInterval ← [ropeID: info.vrID, start: 0, length: info.length]];
};
Replace: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr: VoiceRope, start: INT ← 0, len: INTLAST[INT], with: VoiceRope ← NIL] RETURNS [nb: Thrush.NB ← $success, new: VoiceRope] ~ {
Creates a new voice rope in which the given interval of the voice rope "vr" is replaced by the voice rope "with".
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
baseInfo, withInfo, info: VoiceRopeDB.VoiceRopeInfo;
baseInfo ← ReadVoiceRope[vr: vr, unpackHeader: TRUE];
withInfo ← ReadVoiceRope[vr: with, unpackHeader: TRUE];
IF baseInfo.struct=NIL OR withInfo.struct=NIL THEN {
nb ← $noSuchVoiceRope;
RETURN[new: NIL];
};
info.struct ← NIL;
info.length ← 0;
IF start # 0 THEN {
subintervals must be on 8-byte boundaries since tunes are block-mode encrypted
start ← start - (start MOD 8); -- round down to multiple of 8
info.struct ← VoiceRopeDB.TuneListInterval[CopyEntry[baseInfo.struct], 0, start];
info.length ← start;
};
info.struct ← CatEntries[info.struct, withInfo.struct];
info.length ← info.length + withInfo.length;
IF len # -1 AND len < baseInfo.length-start THEN {
subintervals must be on 8-byte boundaries since tunes are block-mode encrypted
len ← len - (len MOD 8); -- round down to multiple of 8
info.struct ← CatEntries[info.struct, VoiceRopeDB.TuneListInterval[baseInfo.struct, start+len, LAST[INT]]];
info.length ← info.length + baseInfo.length - (start+len);
};
info.creator ← RPC.GetCaller[shhh];
info ← VoiceRopeDB.WriteVoiceRope[RecordingServiceRegister.database, info];
new ← NEW[VoiceRopeInterval ← [ropeID: info.vrID, start: 0, length: info.length]];
};
Length: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr: VoiceRope] RETURNS [nb: Thrush.NB ← $success, 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[vr] will restore a voice rope to its full contents.
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
info: VoiceRopeDB.VoiceRopeInfo;
vr.start ← 0;
vr.length ← -1;
info ← ReadVoiceRope[vr: vr, unpackHeader: TRUE];
IF info.struct = NIL THEN
nb ← $noSuchVoiceRope;
RETURN[len: info.length];
};
Information about voice ropes
DescribeRope: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr: VoiceRope, minSilence: INT ← -1] RETURNS [nb: Thrush.NB ← $success, length: INT, noise: IntervalSpecs] ~ {
ENABLE VoiceRopeDB.Error => { VoiceUtils.Problem[explanation, $VoiceRope]; nb ← ec; CONTINUE; };
struct: VoiceRopeDB.TuneList;
tuneSpec: Jukebox.IntervalSpec ← NEW[Jukebox.IntervalSpecBody];
tuneNoises: Jukebox.IntervalSpecs;
tuneExists: BOOLEAN;
noiseEnd: IntervalSpecs ← NIL; -- last item on current list of noise intervals
samples: INT ← 0;
struct ← ReadVoiceRope[vr].info.struct;
IF struct = NIL THEN
nb ← $noSuchVoiceRope;
UNTIL struct=NIL DO -- find noises in each tune segment
[tune: tuneSpec.tuneID, start: tuneSpec.start, length: tuneSpec.length, rest: struct] ← VoiceRopeDB.NextTuneOnList[struct];
[tuneExists, tuneNoises] ← BluejayUtils.DescribeInterval[intervalSpec: tuneSpec, minSilence: minSilence];
IF NOT tuneExists THEN {
VoiceUtils.Problem[Rope.Cat["Nonexistent tune in voice rope: ", vr.ropeID], $VoiceRope];
RETURN[nb: $badVoiceRope, length: -1, noise: NIL];
};
UNTIL tuneNoises = NIL DO
IF noiseEnd = NIL THEN {
noise ← LIST[[start: samples + tuneNoises.first.start - tuneSpec.start, length: tuneNoises.first.length]];
noiseEnd ← noise;
}
ELSE {
noiseEnd.rest ← LIST[[start: samples + tuneNoises.first.start - tuneSpec.start, length: tuneNoises.first.length]];
noiseEnd ← noiseEnd.rest;
};
tuneNoises ← tuneNoises.rest;
ENDLOOP;
samples ← samples + tuneSpec.length;
ENDLOOP;
};
Fetch: PUBLIC PROC [shhh: Thrush.SHHH ← Thrush.none, vr: VoiceRope, index: INT] RETURNS [nb: Thrush.NB ← $success, data: VoiceSample];
Miscellaneous
ReadVoiceRope: PROC [vr: VoiceRope, unpackHeader: BOOLEANFALSE] RETURNS [info: VoiceRopeDB.VoiceRopeInfo] ~ {
header: VoiceRopeDB.Header;
tune: VoiceRopeDB.TuneID;
start, length: INT;
key: Thrush.EncryptionKey;
rest: VoiceRopeDB.TuneList;
tuneExists: BOOLEAN;
wantSubinterval: BOOLEAN;
IF vr = NIL THEN RETURN;
read voice rope info recorded in database
[header, info.struct] ← VoiceRopeDB.ReadVoiceRope[RecordingServiceRegister.database, vr.ropeID];
IF header = NIL THEN RETURN;
wantSubinterval ← NOT (vr.start = 0 AND vr.length < 0);
IF unpackHeader OR wantSubinterval THEN {
info ← VoiceRopeDB.UnpackHeader[header];
IF info.length < 0 THEN { -- must determine the actual tune length
[tune, start, length, key, rest] ← VoiceRopeDB.NextTuneOnList[info.struct];
only freshly-recorded (simple) voice ropes should have length=-1
IF NOT (rest=NIL AND length<0) THEN {
VoiceUtils.Problem[Rope.Cat["Negative length field for voice rope: ", vr.ropeID], $VoiceRope];
info.struct ← NIL;
RETURN;
};
[tuneExists, info.length] ← BluejayUtils.DescribeTune[tune];
IF NOT tuneExists THEN {
VoiceUtils.Problem[Rope.Cat["Nonexistent tune in voice rope: ", vr.ropeID], $VoiceRope];
info.struct ← NIL;
RETURN;
};
note: must multiply the size in chirps by 8000 to get samples
info.length ← info.length * 8000 - start;
info.struct ← VoiceRopeDB.SimpleTuneList[tune, start, info.length, key];
};
wantSubinterval ← wantSubinterval AND NOT (vr.start = 0 AND vr.length >= info.length);
};
now get desired voice rope interval
IF wantSubinterval THEN {
subintervals must be on 8-byte boundaries since tunes are block-mode encrypted
vr.start ← vr.start - (vr.start MOD 8); -- round down to multiple of 8
vr.length ← IF vr.length<0 THEN -1 ELSE vr.length - (vr.length MOD 8);
info.struct ← VoiceRopeDB.TuneListInterval[info.struct, vr.start, vr.length];
info.length ← info.length - vr.start;
IF vr.length > 0 THEN
info.length ← MIN[info.length, vr.length];
};
};
CatEntries: PROC [e1, e2: VoiceRopeDB.TuneList] RETURNS [VoiceRopeDB.TuneList] ~ {
Concatenates the two lists together (destructive to the first).
ptr: VoiceRopeDB.TuneList ← e1;
IF ptr = NIL THEN RETURN[e2];
UNTIL ptr.rest = NIL DO
ptr ← ptr.rest;
ENDLOOP;
ptr.rest ← e2;
RETURN[e1];
};
CopyEntry: PROC [entry: VoiceRopeDB.TuneList] RETURNS [VoiceRopeDB.TuneList] ~ {
Copies one list to another.
new, end: VoiceRopeDB.TuneList;
IF entry = NIL THEN RETURN[NIL];
new ← LIST[entry.first];
end ← new;
FOR e: VoiceRopeDB.TuneList ← entry.rest, e.rest WHILE e # NIL DO
end.rest ← LIST[e.first];
end ← end.rest;
ENDLOOP;
RETURN[new];
};
END.
Doug Terry, March 18, 1986 10:16:33 am PST
created.
Doug Terry, August 25, 1986 10:15:57 am PDT
Modifications to handle voice ropes in database with unknown length.
changes to: ReadVoiceRope, CatEntries
Doug Terry, August 25, 1986 2:23:25 pm PDT
changes to: ReadVoiceRope, CatEntries, Replace, Length, DescribeRope, Substr, Cat, Play, DIRECTORY, IMPORTS, Record, Stop, GetByInterest
Doug Terry, October 8, 1986 11:57:02 am PDT
changes to: Record, Open
Doug Terry, October 8, 1986 4:39:12 pm PDT
changes to: Open
Doug Terry, October 10, 1986 1:44:32 pm PDT
changes to: ReadVoiceRope, DescribeRope, GetByInterest
Doug Terry, October 10, 1986 5:01:30 pm PDT
changes to: beep, playBeep, Record
Doug Terry, October 28, 1986 4:24:55 pm PST
changes to: Play
Doug Terry, November 3, 1986 4:38:50 pm PST
Restricted editing to 8-sample boundaries.
changes to: Cat 
Doug Terry, November 18, 1986 4:57:45 pm PST
changes to: Record, DIRECTORY, IMPORTS, ~, Play, Retain, Forget, GetByInterest, Cat, Substr, Replace, Length, DescribeRope, ReadVoiceRope