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; beep: VoiceRope _ NIL; -- beep to prompt recording playBeep: BOOLEAN _ FALSE; Record: PUBLIC PROC[shhh: Thrush.SHHH _ Thrush.none, credentials: Thrush.Credentials, serviceID: RefID.ID, intID: CARD _ 0, queueIt: BOOL _ TRUE] RETURNS [nb: Thrush.NB _ $success, voiceRope: VoiceRope] = { 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; [nb, cDesc] _ BluejaySmarts.GetConversation[smartsID: serviceID, conv: credentials.convID]; IF nb # $success THEN RETURN; 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]; }; TRUSTED { tune _ Jukebox.CreateTune[RecordingServiceRegister.jukebox, -1]; tuneID _ intervalSpec.tuneID _ tune.tuneId; Jukebox.CloseTune[RecordingServiceRegister.jukebox, tune]; }; 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]; }; 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] = { 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;}; 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; 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] = { 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]; }; Retain: PUBLIC PROC [shhh: Thrush.SHHH _ Thrush.none, vr: VoiceRope, class: InterestClass, refID: Rope.ROPE, other: Rope.ROPE _ NIL] RETURNS [nb: Thrush.NB _ $success] ~ { 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] ~ { 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] ~ { 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]]; }; Cat: PUBLIC PROC [shhh: Thrush.SHHH _ Thrush.none, vr1, vr2, vr3, vr4, vr5: VoiceRope _ NIL] RETURNS [nb: Thrush.NB _ $success, new: VoiceRope] ~ { 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: INT _ LAST[INT]] RETURNS [nb: Thrush.NB _ $success, new: VoiceRope] ~ { 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]; }; 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: INT _ LAST[INT], with: VoiceRope _ NIL] RETURNS [nb: Thrush.NB _ $success, new: VoiceRope] ~ { 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 { 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 { 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] ~ { 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]; }; 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; }; ReadVoiceRope: PROC [vr: VoiceRope, unpackHeader: BOOLEAN _ FALSE] 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; [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]; 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; }; 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); }; IF wantSubinterval THEN { 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] ~ { 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] ~ { 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. ~VoiceRopeServerImpl.mesa Copyright c 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. Creating and playing voice ropes 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. Check that conversation exists, etc. Lookup beep tune Get tune identifier Have Bluejay record tune note: playing the beep will flush if necessary 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 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. distribute encryption keys for tune segments (and build play queue) schedule the various tune segments for playback 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. Voice Interests The VoiceRope interface simply allows clients to register (and unregister) interests in voice ropes. Garbage collection is actually done through VoiceCleanup.mesa. 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. 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. 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". 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. subintervals must be on 8-byte boundaries since tunes are block-mode encrypted Creates a new voice rope in which the given interval of the voice rope "vr" is replaced by the voice rope "with". subintervals must be on 8-byte boundaries since tunes are block-mode encrypted subintervals must be on 8-byte boundaries since tunes are block-mode encrypted 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. Information about voice ropes Fetch: PUBLIC PROC [shhh: Thrush.SHHH _ Thrush.none, vr: VoiceRope, index: INT] RETURNS [nb: Thrush.NB _ $success, data: VoiceSample]; Miscellaneous read voice rope info recorded in database only freshly-recorded (simple) voice ropes should have length=-1 note: must multiply the size in chirps by 8000 to get samples now get desired voice rope interval subintervals must be on 8-byte boundaries since tunes are block-mode encrypted Concatenates the two lists together (destructive to the first). Copies one list to another. 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 Κ:˜codešœ™Kšœ Οmœ1™Kšœžœžœžœžœžœžœ žœ˜?KšœD˜Dšžœ žœž˜.Kšœ˜—šž˜Kšœ"˜"—Jšœ#˜#JšœK˜KKšœžœI˜RK˜K™—š‘œžœžœžœ&žœ žœžœžœžœžœ žœ ˜ΉK™qJšžœNžœ˜`Jšœ4˜4Kšœ/žœ˜5Kšœ1žœ˜7š žœžœžœžœžœ˜5Jšœ˜Jšžœžœ˜Jšœ˜—Jšœž˜J˜šžœ žœ˜KšœN™NKšœžœ ˜>JšœQ˜QJ˜J˜—Kšœ7˜7Kšœ,˜,šžœ žœžœ˜2KšœN™NKšœžœ ˜8Kšœ_žœžœ˜kKšœ;˜;K˜—Jšœ#˜#JšœK˜KKšœžœI˜RK˜K™—š‘œžœžœžœžœ žœžœ˜uK™ΩJšžœNžœ˜`Jšœ ˜ J˜ Jšœ˜Kšœ+žœ˜1šžœžœžœ˜Jšœ˜—Kšžœ˜K˜K™——™š‘ œž œžœ+žœžœ žœžœ˜ͺJšžœNžœ˜`Jšœ˜Jšœ!žœ˜?Jšœ"˜"Kšœ žœ˜Jšœžœ /˜OJšœ žœ˜Jšœ'˜'šžœ žœžœ˜Jšœ˜—šžœžœžœ #˜8Kšœ{˜{Jšœi˜išžœžœ žœ˜KšœX˜XKšžœ'žœ˜2K˜—šžœžœž˜šžœ žœžœ˜Jšœžœ^˜jJšœ˜J˜—šžœ˜Jšœžœ^˜rJšœ˜J˜—Jšœ˜Jšžœ˜—Kšœ$˜$Kšžœ˜—K˜—K™Kš ‘œž œžœ&žœžœ žœ ™†—™ š ‘ œžœžœžœžœ&˜pKšœ˜Kšœ˜Kšœžœ˜Kšœ˜Kšœ˜Kšœ žœ˜Kšœžœ˜Kšžœžœžœžœ˜K™)Kšœ`˜`Kšžœ žœžœžœ˜Kšœžœžœ˜7šžœžœžœ˜)Kšœ(˜(šžœžœ (˜CJšœK˜KK™@š žœžœžœžœ žœ˜%Kšœ^˜^Kšœžœ˜Kšžœ˜K˜—Kšœ<˜<šžœžœ žœ˜KšœX˜XKšœžœ˜Kšžœ˜K˜—Kšœ=™=Jšœ)˜)JšœH˜HJ˜—Kšœ"žœžœžœ˜VK˜—K™#šžœžœ˜KšœN™NKšœ žœ ˜GKš œ žœ žœžœžœ˜FKšœM˜MKšœ%˜%šžœž˜Kšœžœ˜*—K˜—K˜—š‘ œžœ žœ˜RK™?Kšœ˜Kšžœžœžœžœ˜šžœ žœž˜Kšœ˜Kšžœ˜—Kšœ˜Kšžœ˜ K˜K˜—š‘ œžœžœ˜PK™Kšœ˜Kš žœ žœžœžœžœ˜ Kšœžœ˜K˜ šžœ.žœžœž˜AKšœ žœ ˜K˜Kšžœ˜—Kšžœ˜ K˜K˜——Kšžœ˜™*KšΟr™—™+K™DKšœ £™%—™*Kšœ £|™ˆ—™+Kšœ £ ™—™*Kšœ £™—™+Kšœ £*™6—™+Kšœ £™"—™+Kšœ £™—™+K™*Kšœ £œ£™—™,Kšœ £}™‰—K™K™K™—…—<ˆb@