FILE: TuneAccessImpl.mesa
Ades, February 27, 1986 6:17:12 pm PST
DIRECTORY
Jukebox USING [Handle, WindowOrigin, Error, Tune, Info, FindClientSpace, magicTuneHeader, RunArrayRange, RunComponent, bytesPerChirp, pagesPerChirp, FindChirp, singlePktLength, MissingChirp, LengthRange, EnergyRange, RunData, EOF, RunArray, bytesPerMS],
VM USING [Allocate, Interval, AddressForPageNumber, Free],
File USING [wordsPerPage],
FS USING [Read, Write],
PrincOps USING [ByteBltBlock],
PrincOpsUtils USING [ByteBlt],
TuneAccess;
TuneAccessImpl: CEDAR PROGRAM IMPORTS Jukebox, VM, FS, PrincOpsUtils EXPORTS TuneAccess SHARES Jukebox =
BEGIN OPEN TuneAccess;
NextTuneNumber: PUBLIC PROC [jukebox: Jukebox.Handle, currentTuneID: INT ← -1] RETURNS [tuneID: INT] = TRUSTED {
This really ought not to be fully implemented in here: it ought to call a procedure FindHeaderSpace analogous to FindClientSpace in Jukebox. But FindClientSpace is not an entry procedure, so why bother?
headerWindow: Jukebox.WindowOrigin;
space: VM.Interval ← VM.Allocate[count: 1];
tuneFirstDiskHeaderPage: Jukebox.Tune ← LOOPHOLE[VM.AddressForPageNumber[page: space.page]];-- name is a reminder only to use those parts which reside on disk and no more than the first page thereof
tuneFound: BOOLEANFALSE;
tuneHWM: INT;
IF currentTuneID < -1 THEN currentTuneID ← -1;
IF jukebox.hdr = NIL THEN ERROR Jukebox.Error[reason: NoJukebox, rope: "Jukebox not open"];
[tuneHWM: tuneHWM] ← Jukebox.Info[jukebox];
DO
currentTuneID ← currentTuneID + 1;
IF currentTuneID > tuneHWM THEN EXIT;
headerWindow.file ← jukebox.window.file;
headerWindow.base ← jukebox.firstDesPageNumber + 2*currentTuneID;
FS.Read[file: headerWindow.file, from: headerWindow.base, nPages: space.count, to: VM.AddressForPageNumber[space.page]];
tuneFound ← tuneFirstDiskHeaderPage.state = inUse;
IF tuneFound THEN EXIT
ENDLOOP;
VM.Free[space];
RETURN [IF tuneFound THEN currentTuneID ELSE -1]
};
ReadTuneHeader: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, nBytes: CARDINAL ← userHeaderLength, block: ByteBlock ← NIL] RETURNS [ByteBlock] = TRUSTED {
headerFileWindow: Jukebox.WindowOrigin;
headerVMWindow: VM.Interval;
byteBltBlockOfVMWindow: PrincOps.ByteBltBlock;
pointerToSequence: LONG POINTER;
byteBltBlockOfSequence: PrincOps.ByteBltBlock;
bytesBlted: INT;
IF nBytes>userHeaderLength THEN nBytes ← userHeaderLength;
IF jukebox.hdr = NIL THEN ERROR Jukebox.Error[reason: NoJukebox, rope: "Jukebox not open"];
IF tune.magic # Jukebox.magicTuneHeader THEN ERROR Jukebox.Error[BadTunePointer, "Corrupt tune or tune pointer"];
after all the statutory checks, do we need to allocate space for the return block?
IF block = NIL OR block.max<nBytes THEN block ← NEW[ByteSequence[nBytes]];
block.length ← 0; -- until otherwise shown
headerFileWindow ← Jukebox.FindClientSpace[jukebox, tune.tuneId];
headerVMWindow ← VM.Allocate[count: 1];
byteBltBlockOfVMWindow ← [LOOPHOLE[VM.AddressForPageNumber[headerVMWindow.page]], 0, nBytes];
pointerToSequence ← LOOPHOLE[block]; -- this rather grotty code points us beyond the
pointerToSequence ← pointerToSequence + SIZE[ByteSequence[0]]; -- sequence header, to where
byteBltBlockOfSequence ← [pointerToSequence, 0, nBytes]; -- the elements should be
FS.Read[file: headerFileWindow.file, from: headerFileWindow.base, nPages: 1, to: VM.AddressForPageNumber[headerVMWindow.page]];
bytesBlted ← PrincOpsUtils.ByteBlt[from: byteBltBlockOfVMWindow, to: byteBltBlockOfSequence];
VM.Free[headerVMWindow];
IF bytesBlted # nBytes THEN ERROR ELSE block.length ← nBytes;
RETURN [block]
};

WriteTuneHeader: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, block: ByteBlock, nBytes: CARDINAL ← userHeaderLength] = TRUSTED {
headerFileWindow: Jukebox.WindowOrigin;
headerVMWindow: VM.Interval;
byteBltBlockOfVMWindow: PrincOps.ByteBltBlock;
pointerToSequence: LONG POINTER;
byteBltBlockOfSequence: PrincOps.ByteBltBlock;
bytesBlted: INT;
nBytes ← MIN[nBytes, userHeaderLength, block.length];
IF jukebox.hdr = NIL THEN ERROR Jukebox.Error[reason: NoJukebox, rope: "Jukebox not open"];
IF tune.magic # Jukebox.magicTuneHeader THEN ERROR Jukebox.Error[BadTunePointer, "Corrupt tune or tune pointer"];
headerFileWindow ← Jukebox.FindClientSpace[jukebox, tune.tuneId];
headerVMWindow ← VM.Allocate[count: 1];
byteBltBlockOfVMWindow ← [LOOPHOLE[VM.AddressForPageNumber[headerVMWindow.page]], 0, nBytes];
pointerToSequence ← LOOPHOLE[block]; -- this rather grotty code points us beyond the
pointerToSequence ← pointerToSequence + SIZE[ByteSequence[0]]; -- sequence header, to where
byteBltBlockOfSequence ← [pointerToSequence, 0, nBytes]; -- the elements should be
FS.Read[file: headerFileWindow.file, from: headerFileWindow.base, nPages: 1, to: VM.AddressForPageNumber[headerVMWindow.page]];
bytesBlted ← PrincOpsUtils.ByteBlt[from: byteBltBlockOfSequence, to: byteBltBlockOfVMWindow];
IF bytesBlted # nBytes
THEN {VM.Free[headerVMWindow]; ERROR}
ELSE
{  
FS.Write[file: headerFileWindow.file, to: headerFileWindow.base, nPages: 1, from:
VM.AddressForPageNumber[headerVMWindow.page]];
VM.Free[headerVMWindow]
}
};
ReadRunArray: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, chirpNumber: INT, runArray: REF Jukebox.RunArray ← NIL] RETURNS [REF Jukebox.RunArray] = TRUSTED {
runArrayFileWindow: Jukebox.WindowOrigin;
runArrayVMWindow: VM.Interval;
byteBltBlockOfVMWindow: PrincOps.ByteBltBlock;
byteBltBlockOfResult: PrincOps.ByteBltBlock;
bytesBlted: INT;
these constants make it clearer whence to pick up the run length data
pageOffsetInChirp: INT = Jukebox.bytesPerChirp/(File.wordsPerPage*2);
byteOffsetInPage: INT = Jukebox.bytesPerChirp MOD (File.wordsPerPage*2);
runDataPages: INT = Jukebox.pagesPerChirp - pageOffsetInChirp;
runArrayBytes: INT = (LAST[Jukebox.RunArrayRange] - FIRST[Jukebox.RunArrayRange] + 1)*2;
each runArray element is machine dependent and two bytes long
IF runArray = NIL THEN runArray ← NEW[ARRAY Jukebox.RunArrayRange OF Jukebox.RunComponent];
no need to check if jukebox and tune specifications are okay, since FindChirp must do so
runArrayFileWindow ← Jukebox.FindChirp[self: jukebox, tune: tune, chirp: chirpNumber, signalMissingChirp: TRUE, signalEOF: TRUE];
runArrayFileWindow.base ← runArrayFileWindow.base + pageOffsetInChirp;
runArrayVMWindow ← VM.Allocate[count: runDataPages];
byteBltBlockOfVMWindow ← [LOOPHOLE[VM.AddressForPageNumber[runArrayVMWindow.page]], byteOffsetInPage, byteOffsetInPage + runArrayBytes];
byteBltBlockOfResult ← [LOOPHOLE[runArray], 0, runArrayBytes];
FS.Read[file: runArrayFileWindow.file, from: runArrayFileWindow.base, nPages: runDataPages, to: VM.AddressForPageNumber[runArrayVMWindow.page]];
bytesBlted ← PrincOpsUtils.ByteBlt[from: byteBltBlockOfVMWindow, to: byteBltBlockOfResult];
VM.Free[runArrayVMWindow];
IF bytesBlted # runArrayBytes THEN ERROR;
RETURN [runArray];
};
InterpretRunArrayElement: PUBLIC PROC [runArray: REF Jukebox.RunArray, currArrayIndex: Jukebox.RunArrayRange] RETURNS [length: Jukebox.LengthRange, energy: Jukebox.EnergyRange, skipNextArrayElement: BOOLEANFALSE, silence: BOOLEANFALSE] = {
WITH currEl: runArray[currArrayIndex] SELECT FROM
silence =>
{ length ← currEl.length;
energy ← 0;
silence ← TRUE
};
singlePkt =>
{ length ← Jukebox.singlePktLength;
energy ← currEl.energy
};
soundEnergy =>
{ energy ← currEl.energy;
length ← NARROW[runArray[currArrayIndex+1], Jukebox.RunComponent[soundLength]].length;
skipNextArrayElement ← TRUE
};
ENDCASE => ERROR;
};
GetEnergyProfile: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, chirpNumber: INT, energyBlock: EnergyBlock ← NIL, divisions: INT ← Jukebox.bytesPerChirp/Jukebox.singlePktLength, signalMissingChirp: BOOLEANFALSE] RETURNS [EnergyBlock] = {
ENABLE Jukebox.MissingChirp =>
{ IF signalMissingChirp THEN REJECT ELSE
{ FOR i: NAT IN [0..energyBlock.length) DO energyBlock[i] ← 0 ENDLOOP;
GOTO ReturnEnergyBlock
}
};
runArray: REF ARRAY Jukebox.RunArrayRange OF Jukebox.RunComponent;
endOfLastDivision: Jukebox.LengthRange;
divisionSize: INT;
currDivision: INT ← 0;
endOfCurrDivision: Jukebox.LengthRange;
endOfCurrRunElement: Jukebox.LengthRange ← 0;
energyOfCurrRunElement: Jukebox.EnergyRange;
currRunArrayIndex: Jukebox.RunArrayRange ← 0;
samplesAccountedFor: Jukebox.LengthRange ← 0;
samplesBeingAccounted: Jukebox.LengthRange;
energyAccumulator: INT ← 0;
NextRunElement: PROC RETURNS [energyOfCurrRunElement: Jukebox.EnergyRange] = INLINE {
length: Jukebox.LengthRange;
dualElementRepresentation: BOOLEAN;
[length: length, energy: energyOfCurrRunElement, skipNextArrayElement: dualElementRepresentation] ← InterpretRunArrayElement[runArray, currRunArrayIndex];
currRunArrayIndex ← currRunArrayIndex + (IF dualElementRepresentation THEN 2 ELSE 1);
endOfCurrRunElement ← endOfCurrRunElement + length
};
IF divisions < 1 OR divisions>Jukebox.bytesPerChirp THEN ERROR;
divisionSize ← Jukebox.bytesPerChirp/divisions;
endOfLastDivision ← divisionSize*divisions;
endOfCurrDivision ← divisionSize;
IF energyBlock = NIL OR energyBlock.max<divisions THEN energyBlock ← NEW[EnergySequence[divisions]];
energyBlock.length ← divisions;
runArray ← ReadRunArray[jukebox: jukebox, tune: tune, chirpNumber: chirpNumber, runArray: NIL];
WHILE endOfCurrDivision <= endOfLastDivision DO
WHILE samplesAccountedFor < endOfCurrDivision DO
IF samplesAccountedFor >= endOfCurrRunElement
THEN energyOfCurrRunElement ← NextRunElement[];
samplesBeingAccounted ← MIN[endOfCurrRunElement, endOfCurrDivision] - samplesAccountedFor;
energyAccumulator ← energyAccumulator + INT[samplesBeingAccounted]*INT[energyOfCurrRunElement];
samplesAccountedFor ← samplesAccountedFor + samplesBeingAccounted
ENDLOOP;
energyBlock[currDivision] ← energyAccumulator/INT[divisionSize];
energyAccumulator ← 0;
currDivision ← currDivision + 1;
IF endOfCurrDivision = Jukebox.bytesPerChirp THEN EXIT ELSE endOfCurrDivision ← endOfCurrDivision + divisionSize
ENDLOOP;
RETURN[energyBlock];
EXITS -- for handling MissingChirp error
ReturnEnergyBlock => RETURN[energyBlock]
};
ReadChirpSamples: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, chirpNumber: INT, nBytes: CARDINAL ← Jukebox.bytesPerChirp, block: ByteBlock ← NIL] RETURNS [ByteBlock] = TRUSTED {
sampleFileWindow: Jukebox.WindowOrigin;
sampleVMWindow: VM.Interval;
byteBltBlockOfVMWindow: PrincOps.ByteBltBlock;
pointerToSequence: LONG POINTER;
byteBltBlockOfSequence: PrincOps.ByteBltBlock;
bytesBlted: INT;
filePagesToRead: INT;
work out how many pages are needed to get all the data from disk -- it starts on the first byte of the chirp's first page
IF nBytes>Jukebox.bytesPerChirp THEN nBytes ← Jukebox.bytesPerChirp;
filePagesToRead ← (nBytes+(File.wordsPerPage*2)-1)/(File.wordsPerPage*2);
IF block = NIL OR block.max<nBytes THEN block ← NEW[ByteSequence[nBytes]];
block.length ← 0; -- until otherwise shown
no need to check if jukebox and tune specifications are okay, since FindChirp must do so
sampleFileWindow ← Jukebox.FindChirp[self: jukebox, tune: tune, chirp: chirpNumber, signalMissingChirp: TRUE, signalEOF: TRUE];
sampleVMWindow ← VM.Allocate[count: filePagesToRead];
byteBltBlockOfVMWindow ← [LOOPHOLE[VM.AddressForPageNumber[sampleVMWindow.page]], 0, nBytes];
pointerToSequence ← LOOPHOLE[block]; -- this rather grotty code points us beyond the
pointerToSequence ← pointerToSequence + SIZE[ByteSequence[0]]; -- sequence header, to where
byteBltBlockOfSequence ← [pointerToSequence, 0, nBytes]; -- the elements should be
FS.Read[file: sampleFileWindow.file, from: sampleFileWindow.base, nPages: filePagesToRead, to: VM.AddressForPageNumber[sampleVMWindow.page]];
bytesBlted ← PrincOpsUtils.ByteBlt[from: byteBltBlockOfVMWindow, to: byteBltBlockOfSequence];
VM.Free[sampleVMWindow];
IF bytesBlted # nBytes THEN ERROR ELSE block.length ← nBytes;
RETURN [block]
};
internal working record used by ReadFormattedSamples for restructuring the samples in a chirp
ChirpFormat: TYPE = REF ChirpFormatRecord;
ChirpFormatRecord: TYPE = RECORD
[ elementsPlus1: Jukebox.RunArrayRange,
count of how many of the following are actually in use + 1
array: ARRAY Jukebox.RunArrayRange OF RECORD
[ length: Jukebox.LengthRange, -- length of this lump
storedBase: Jukebox.LengthRange, -- where in the chirp it is stored
trueBase: Jukebox.LengthRange, -- the correct position in time that it represents
silence: BOOLEAN
]
];
ReadFormattedSamples: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, chirpNumber: INT, block: ByteBlock ← NIL, encryptedSilence: EncryptedSilence ← NIL, signalMissingChirp: BOOLEANFALSE] RETURNS [ByteBlock] = {
ENABLE Jukebox.MissingChirp =>
{ IF signalMissingChirp THEN REJECT ELSE
{ sampleNumber, silenceNumber: INT ← 0;
WHILE sampleNumber < Jukebox.bytesPerChirp DO
block[sampleNumber] ← encryptedSilence[silenceNumber];
sampleNumber ← sampleNumber + 1;
silenceNumber ← (silenceNumber + 1) MOD Jukebox.bytesPerMS
ENDLOOP;
GOTO Quit
}
};
runArray: REF Jukebox.RunArray;
chirpFormat: ChirpFormat ← NEW[ChirpFormatRecord];
samplesAccounted: Jukebox.LengthRange ← 0;
newLength: Jukebox.LengthRange;
skipNextArrayElement, currentlySilence: BOOLEAN;
currArrayIndex: Jukebox.RunArrayRange ← 0;
validSamples: Jukebox.LengthRange;
for efficiency this routine works out how the chirp divides into sound and silence, reads the samples straight from the jukebox into the return array and then ByteBlts maximally sized blocks, ingoring any lumps already in correct place. This is efficient in that the chirps usually have very few silence/sound transitions and many are 100% sound. Alas this needs a reverse byteblt operation, which is not implemented on Dorados although part of PrincOps. Since most chirps are entirely sound, writing a fake reverse byte blt isn't as bad a solution as it seems!
first allocate any blocks required, since ReadRunArray may well produce a signal of MissingChirp
IF block = NIL OR block.max<Jukebox.bytesPerChirp THEN block ← NEW[ByteSequence[Jukebox.bytesPerChirp]];
IF encryptedSilence = NIL THEN encryptedSilence ← NEW[EncryptedSilenceArray ← ALL[0]];
now read the run array and from it build the ChirpFormat record
runArray ← ReadRunArray[jukebox, tune, chirpNumber, NIL];
chirpFormat.elementsPlus1 ← 0;
chirpFormat.array[0].length ← 0; -- set up the first element as sound, length 0
chirpFormat.array[0].storedBase ← 0;
chirpFormat.array[0].trueBase ← 0;
chirpFormat.array[0].silence ← FALSE;
WHILE samplesAccounted < Jukebox.bytesPerChirp DO
[length: newLength, skipNextArrayElement: skipNextArrayElement, silence: currentlySilence] ← InterpretRunArrayElement[runArray, currArrayIndex];
samplesAccounted ← samplesAccounted + newLength;
currArrayIndex ← currArrayIndex + (IF skipNextArrayElement THEN 2 ELSE 1);
IF chirpFormat.array[chirpFormat.elementsPlus1].silence # currentlySilence THEN
{ chirpFormat.elementsPlus1 ← chirpFormat.elementsPlus1 + 1;
chirpFormat.array[chirpFormat.elementsPlus1].length ← 0;
chirpFormat.array[chirpFormat.elementsPlus1].storedBase ← chirpFormat.array[chirpFormat.elementsPlus1-1].storedBase + (IF currentlySilence THEN chirpFormat.array[chirpFormat.elementsPlus1-1].length ELSE 0); -- think about it!!
chirpFormat.array[chirpFormat.elementsPlus1].trueBase ← chirpFormat.array[chirpFormat.elementsPlus1-1].trueBase + chirpFormat.array[chirpFormat.elementsPlus1-1].length;
chirpFormat.array[chirpFormat.elementsPlus1].silence ← currentlySilence
};
chirpFormat.array[chirpFormat.elementsPlus1].length ← chirpFormat.array[chirpFormat.elementsPlus1].length + newLength
ENDLOOP;
we now know amongst other things how many samples are stored on disk: read them and then use the chirpFormat to reposition them: it goes without saying that we need to work backwards through the chirpFormat list to avoid overwriting things incorrectly!
validSamples ← chirpFormat.array[chirpFormat.elementsPlus1].storedBase + (IF ~chirpFormat.array[chirpFormat.elementsPlus1].silence THEN chirpFormat.array[chirpFormat.elementsPlus1].length ELSE 0);
block ← ReadChirpSamples[jukebox, tune, chirpNumber, validSamples, block];
FOR el: Jukebox.RunArrayRange DECREASING IN [0..chirpFormat.elementsPlus1] DO
IF chirpFormat.array[el].silence
THEN -- okay to do this loop forwards as it doesn't involve any copies
{ currSample: Jukebox.LengthRange ← chirpFormat.array[el].trueBase;
lastSamplePlus1: Jukebox.LengthRange ← currSample + chirpFormat.array[el].length;
silenceNumber: INT ← 0;
WHILE currSample < lastSamplePlus1 DO
block[currSample] ← encryptedSilence[silenceNumber];
currSample ← currSample + 1;
silenceNumber ← (silenceNumber + 1) MOD Jukebox.bytesPerMS
ENDLOOP;
IF silenceNumber # 0 THEN ERROR; -- silence/sound runs must be multiples of 8!!!!
}
ELSE
{ IF (chirpFormat.array[el].length MOD 8) # 0 THEN ERROR;
IF ~(chirpFormat.array[el].trueBase = chirpFormat.array[el].storedBase OR chirpFormat.array[el].length = 0)
THEN WimpFakeReverseByteBlt[block: block, from: chirpFormat.array[el].storedBase, to: chirpFormat.array[el].trueBase, nBytes: chirpFormat.array[el].length]
}
ENDLOOP;
block.length ← Jukebox.bytesPerChirp;
RETURN[block]
EXITS
Quit =>
{ block.length ← Jukebox.bytesPerChirp;
RETURN[block]
}
};
WimpFakeReverseByteBlt: PROC [block: ByteBlock, from: Jukebox.LengthRange, to: Jukebox.LengthRange, nBytes: Jukebox.LengthRange] = INLINE {
destCounter: INT ← to + nBytes - 1;
FOR sourceCounter: INT DECREASING IN [from..from+nBytes) DO
block[destCounter] ← block[sourceCounter];
destCounter ← destCounter - 1
ENDLOOP
};
ReadAmbientLevel: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, chirpNumber: INT] RETURNS [ambientLevel: Jukebox.EnergyRange] = TRUSTED {
runDataFileWindow: Jukebox.WindowOrigin;
runDataVMWindow: VM.Interval;
runData: Jukebox.RunData;
these constants make it clearer whence to pick up the RunDataObject - which ends in the ambient level (a single 16 bit field)
the code below picks up the entire RunDataObject, even if it means reading more than one page to do so. This is so that the code wouldn't stop working if the run data were to stretch across more than one page, but does not impede efficiency since this is only a very remote possibility [a major implementation change]
pageOffsetInChirp: INT = Jukebox.bytesPerChirp/(File.wordsPerPage*2);
byteOffsetInPage: INT = Jukebox.bytesPerChirp MOD (File.wordsPerPage*2);
runDataPages: INT = Jukebox.pagesPerChirp - pageOffsetInChirp;
no need to check if jukebox and tune specifications are okay, since FindChirp must do so
runDataFileWindow ← Jukebox.FindChirp[self: jukebox, tune: tune, chirp: chirpNumber, signalMissingChirp: TRUE, signalEOF: TRUE];
runDataFileWindow.base ← runDataFileWindow.base + pageOffsetInChirp;
runDataVMWindow ← VM.Allocate[count: 1];
runData ← VM.AddressForPageNumber[runDataVMWindow.page] + (byteOffsetInPage/2);
the above line seems to blow up if byteOffsetInpage is odd: however I'm not going to test for it because (i) the entire jukebox blows up (ii) it would do so in the same way and this line would effectively still be correct (iii) no clown is going to make bytesPerChirp odd anyhow
FS.Read[file: runDataFileWindow.file, from: runDataFileWindow.base, nPages: runDataPages, to: VM.AddressForPageNumber[runDataVMWindow.page]];
ambientLevel ← runData.ambientLevel;
VM.Free[runDataVMWindow]
};
WriteAmbientLevel: PUBLIC PROC [jukebox: Jukebox.Handle, tune: Jukebox.Tune, ambientLevel: Jukebox.EnergyRange, fromChirp: INT, toChirp: INT] = TRUSTED {
all the comments from ReadAmbientlevel apply here too
runDataFileWindow: Jukebox.WindowOrigin;
runDataVMWindow: VM.Interval;
runData: Jukebox.RunData;
pageOffsetInChirp: INT = Jukebox.bytesPerChirp/(File.wordsPerPage*2);
byteOffsetInPage: INT = Jukebox.bytesPerChirp MOD (File.wordsPerPage*2);
runDataPages: INT = Jukebox.pagesPerChirp - pageOffsetInChirp;
IF fromChirp < 0 THEN ERROR;
IF toChirp = -1 THEN toChirp ← tune.size - 1;
IF toChirp < fromChirp THEN toChirp ← fromChirp;
IF toChirp >= tune.size THEN ERROR Jukebox.EOF[];
error signalled here to avoid half the chirps becoming updated before it is signalled by Jukebox.FindChirp
runDataVMWindow ← VM.Allocate[count: 1];
runData ← VM.AddressForPageNumber[runDataVMWindow.page] + (byteOffsetInPage/2);
FOR chirp: INT IN [fromChirp..toChirp] DO
ENABLE
Jukebox.MissingChirp => LOOP;
runDataFileWindow ← Jukebox.FindChirp[self: jukebox, tune: tune, chirp: chirp, signalMissingChirp: TRUE, signalEOF: TRUE];
runDataFileWindow.base ← runDataFileWindow.base + pageOffsetInChirp;
FS.Read[file: runDataFileWindow.file, from: runDataFileWindow.base, nPages: runDataPages, to: VM.AddressForPageNumber[runDataVMWindow.page]];
runData.ambientLevel ← ambientLevel;
FS.Write[file: runDataFileWindow.file, to: runDataFileWindow.base, nPages: runDataPages, from: VM.AddressForPageNumber[runDataVMWindow.page]]
ENDLOOP;
VM.Free[runDataVMWindow]
};
END.