PlayUtils:
PROGRAM
IMPORTS Basics, FS, Process, Rope, RopeFile, BasicTime
EXPORTS PlayOps = {
--------------------------------------------------------------
TYPEs and Constants
--------------------------------------------------------------
Note:
TYPE =
MACHINE
DEPENDENT
RECORD [
SELECT
OVERLAID *
FROM
initial => [
twelfths, octave, duration: CARDINAL, notify: BOOL←FALSE, pause: BOOL←FALSE, x:X𡤀],
intermediate => [
cps: CARDINAL, usecs: Microseconds, ni: BOOL←FALSE, pi: BOOL←FALSE, y:X𡤀],
final => [
freq, pulses: CARDINAL, ticks: Process.Ticks, nf: BOOL←FALSE, pf: BOOL←FALSE, z:X𡤀],
ENDCASE ];
X: TYPE = [0..37777B];
NoteArray: TYPE = REF NoteArrayObject;
NoteArrayObject: TYPE = RECORD [SEQUENCE size: CARDINAL OF Note];
Microseconds: TYPE = LONG CARDINAL;
--------------------------------------------------------------
Utilities
--------------------------------------------------------------
initFreq: CARDINAL = LAST[CARDINAL];
lastFreq: CARDINAL ← initFreq;
notifyNext: BOOL←FALSE;
pauseNext: BOOL←FALSE;
PlayBlock is the main procedure. It interprets its block as follows:
A letter from "A" through "G" specifies a note. If the letter is followed by "#" then the corresponding sharp-note is played (meaningful only for C, D, F, G, and A). All notes are eighth-notes, but upper-case letters cause tones that are "held" the full time while lower-case notes last for 3/64 seconds less and followed by a 3/64 rest.
C is the bottom of the octave; B is above C. When ">" is encountered, all subsequent notes are an octave higher; a "<" lowers all subsequent notes by an octave. Going up more than 3 octaves is not permitted (additional ">"s are ignored), and notes near the top of the highest octave may not be struck accurately.
A "/" in the string halves the note durations, down to a minimum of 64th-notes; a "*" doubles the durations up to a maximum of full-notes. A lower-case 1/16th-note would actually be a 64th-note followed by a 3/64 rest, which may or may not be what you want; a lower-case 32nd-note vanishes completely! To halve and double the amount of implicit rest "stolen" from lower-case notes, use "←" and "^", respectively.
Use "%" to get an explicit rest (as distinct from the implicit ones after lower-case notes). The rest is the same length as a note, i.e., initially an eighth-rest, and changed via "/" and "*".
A "+" causes the preceding note to be held for an extra 50%. Thus a quarter-note followed by a "+" becomes a 3/8-note, etc. A "-" causes the preceding note's duration to be halved. This is effectively a shorthand for bracketing the note with "/" and "*".
A left parenthesis causes subsequent notes and explicit rests to be at 2/3 normal duration, until a right parenthesis is reached. Three notes enclosed in parentheses yield a "triplet".
If you think you know exactly what tempo you want, use "@<n>" to give the note duration and/or ",<d>" for the lower-case implicit rest, where <n> and <d> are strings of digits representing milliseconds. The values will be constrained to the usual limits (e.g., <n> will be forced between 16 and 1024 ms). Subsequent "*", "←", etc., have their usual effects.
Two or more notes and/or rests enclosed in braces, as "{C%%%G#}", yield a "slide" from the first note to the last. The duration of the slide equals the total duration of the notes and rests; observe that upper- and lower-case notes have the same effect. The slide consists of 64th-notes at equally-spaced (logarithmic) frequencies. Warning: This can eat up a lot of array space!
If the argument "random" is TRUE, it's assumed the input string is random text, and the limits on octaves and note durations are compressed in order to keep the sounds reasonable. The "@", ",", and "." commands have no effect when in "random" mode.
The argument "chunkSize" is the number of tones (including rests, explicit and implicit) that are played as a unit; if you exceed this limit, the first chunk of music gets played and there is a slight pause while the next "chunkSize" notes get parsed. If you know where you want this pause to occur, you can force it by putting a semi-colon at that point in the string.
A period (".") in the string causes the buffer to be shipped out, as for a semi-colon, and resets the octave, note duration, and implicit lower-case rest to their initial values. This is for when you're playing an entire file that contains several separate pieces.
PlayString:
PUBLIC PlayOps.PlayTuneProc = {
ENABLE
ABORTED =>
CONTINUE;
rT: REF TEXT;
IF music #
NIL
THEN {
IF file THEN music ← RopeFile.Create[name: music, raw: FALSE ! FS.Error => CONTINUE];
rT ← Rope.ToRefText[base: music];
PlayBlock[musicBlock: [LOOPHOLE[@rT.text+SIZE[INTEGER]], 0, rT.length], random: random, beepProc: beepProc, chunkSize: chunkSize];
};
};
PlayBlock:
PUBLIC
PROCEDURE [
musicBlock: Basics.UnsafeBlock, random: BOOLEAN ← FALSE,
beepProc: PlayOps.BeepProc,
chunkSize: CARDINAL ← 75, quietFinish: BOOLEAN ← TRUE
--wh: Window.Handle ← NIL--] = {
initialOctave: CARDINAL = 8;
initialDuration: CARDINAL = 128; -- approximately an eighth-note, as default
initialBreak: CARDINAL = 48; -- lower-case note gap, 3/64 by default
music: LONG POINTER TO PACKED ARRAY [0..0) OF CHARACTER ← musicBlock.base;
note: CARDINAL ← 0; -- number of tones so far
noteArray: NoteArray ← NEW[NoteArrayObject[chunkSize]];
scale: TYPE = CHARACTER ['A..'G];
twelfths: ARRAY scale OF CARDINAL = [12, 14, 3, 5, 7, 8, 10];
numberPtr: LONG POINTER TO CARDINAL ← NIL; -- where digits go, if anywhere
number, numberMin, numberMax, card: CARDINAL ← 0;
octave: CARDINAL ← initialOctave;
duration: CARDINAL ← initialDuration;
break: CARDINAL ← initialBreak;
triplet: BOOLEAN ← FALSE; -- gets set to TRUE between parentheses
freqA: LONG CARDINAL = 1760; -- highest octave we'll bother with
slideStart: CARDINAL ← 0; -- index of first note of slide, if one is in progress
slide: CARDINAL ← 0; -- length of current slide, if any, in 64th-notes
root: LONG CARDINAL = 10595; -- 12th root of 2, times 10000
root96:
ARRAY [0..8)
OF
LONG
CARDINAL =
-- 2^(n/96), times 10000, for slides
[10000, 10072, 10145, 10219, 10293, 10368, 10443, 10518];
limits:
ARRAY {min,max}
OF
ARRAY
BOOLEAN
OF
RECORD
[duration, break, octave:
CARDINAL] = [
min
[[duration: 8, break: 2, octave: 1],
-- normal
[duration: 64, break: 24, octave: 2]], -- random
max
[[duration: 1024, break: 384, octave: 256],
-- normal
[duration: 256, break: 96, octave: 16]] -- random
];
WARNING! Due to a Mesa bug, we have to be careful NEVER have a LONG
CARDINAL as large as 2^31; dividing into such a number doesn't work right!
Frequency:
PROCEDURE [key: Note, tweak:
CARDINAL ← 0
-- 96ths -- ]
RETURNS [CARDINAL] = {
freq: LONG CARDINAL;
twelfths: CARDINAL ← key.twelfths;
octave: CARDINAL ← key.octave*32;
IF octave = 0 OR twelfths = 0 THEN RETURN[twelfths];
if octave is 0, believe whatever's already there; might be precomputed slide
freq ← freqA*LONG[32];
IF tweak >= 8 THEN twelfths ← twelfths + (tweak/8);
THROUGH [0..twelfths/12) DO octave ← octave/2 ENDLOOP;
THROUGH [0..twelfths
MOD 12)
DO
freq ← (freq*root+LONG[5000])/LONG[10000] ENDLOOP;
IF tweak # 0
THEN
freq ← (freq*root96[tweak MOD 8]+LONG[5000])/LONG[10000];
RETURN[Basics.LongDiv[freq + LONG[octave/2], octave]];
};
ShipOneChunk:
PROCEDURE [last:
CARDINAL]
RETURNS [
CARDINAL] = {
c: CARDINAL;
maxNoteDuration: Microseconds = 1024000;
FOR c
IN [1..last]
DO
noteArray[c].cps ← Frequency[noteArray[c]];
noteArray[c].usecs ← Basics.LongMult[noteArray[c].duration, 1000];
IF c > 1
AND noteArray[c].cps = noteArray[c - 1].cps
THEN {
combine notes for smoother play (reduce calculation time in PlayNotes)
don't let duration sum to over 1 sec, so pulses fits in one word
IF (noteArray[c].usecs ← noteArray[c].usecs + noteArray[c-1].usecs) >
maxNoteDuration
THEN {
noteArray[c].usecs ← noteArray[c].usecs - maxNoteDuration;
noteArray[c - 1].usecs ← maxNoteDuration;
}
ELSE noteArray[c - 1].usecs ← 0;
};
ENDLOOP;
PlayNotes [last, noteArray--, wh--];
RETURN[0];
};
DonePlaying: PROCEDURE = {beepProc[beepFreq: 0, beepTime: 0]; lastFreq ← initFreq};
Beep:
PROCEDURE [note: Note] = {
IF note.freq # lastFreq
THEN
beepProc[beepFreq: (lastFreq ← note.freq), beepTime: Process.TicksToMsec[note.ticks]+BasicTime.PulsesToMicroseconds[note.pulses]/1000,
notify: note.notify, pause: note.pause];
};
PlayNotes:
PROCEDURE [n:
CARDINAL, notes: NoteArray
--, wh: Window.Handle--] =
{
c: CARDINAL;
tix: Process.Ticks;
FOR c
IN [1..n]
DO
-- convert usecs to ticks and pulses
tix ← Process.MsecToTicks[Basics.LongDiv[notes[c].usecs, 1000]];
WHILE Basics.LongMult[Process.TicksToMsec[tix], 1000] > notes[c].usecs
DO
tix ← tix - 1;
ENDLOOP;
notes[c].pulses ← Basics.LowHalf [BasicTime.MicrosecondsToPulses [notes[c].usecs]];
notes[c].ticks ← tix;
ENDLOOP;
FOR c
IN [1..n]
DO
-- play this batch of notes
IF notes[c].pulses # 0
THEN {
IF UserInput.UserAbort [wh] THEN {DonePlaying []; ERROR ABORTED};
Beep [notes[c]];
};
ENDLOOP;
};
NextNote:
PROCEDURE [need:
CARDINAL] =
INLINE {
IF slideStart # 0 AND note > slideStart THEN RETURN; -- looking for end of slide
IF note >= chunkSize - need THEN note ← ShipOneChunk[note];
note ← note + 1;
};
AddToSlide:
PROCEDURE [incr:
CARDINAL] = {
slide ← MIN[chunkSize, slide + incr/limits[min][FALSE].duration];
IF slideStart - 1 + slide > chunkSize
THEN {
-- slide won't fit, empty the buffer
[] ← ShipOneChunk[slideStart - 1];
noteArray[1] ← noteArray[slideStart];
noteArray[2] ← noteArray[slideStart + 1];
note ← note - (slideStart - 1);
slideStart ← 1;
};
};
SetNotify:
PROC = {
IF notifyNext THEN noteArray[note].notify ← TRUE;
IF pauseNext THEN noteArray[note].pause ← TRUE;
notifyNext ← FALSE;
pauseNext ← FALSE;
};
{ ENABLE UNWIND => CONTINUE;
IF music #
NIL
THEN
FOR card IN [musicBlock.startIndex..musicBlock.stopIndexPlusOne) DO
FOR card ← musicBlock.startIndex, cardrd+1
UNTIL card=musicBlock.count
DO
IF numberPtr #
NIL
AND music[card]
NOT
IN ['0..'9]
THEN {
numberPtr^ ← MIN[MAX[number, numberMin], numberMax]; numberPtr ← NIL};
SELECT music[card] FROM
IN ['A..'G] => {
NextNote[1];
IF slideStart # 0 THEN AddToSlide[noteArray[note].duration];
noteArray[note] ← [
initial[
twelfths[music[card]], octave,
IF triplet THEN (duration*2 + 1)/3 ELSE duration]];
SetNotify[];
};
IN ['a..'g] => {
-- this is where the "slop" mentioned for brackets comes in
NextNote[2];
IF slideStart # 0 THEN AddToSlide[noteArray[note].duration];
noteArray[note] ← [
initial[
twelfths[music[card] + ('A - 'a)], octave,
MAX[(
IF triplet
THEN (duration*2 + 1)/3
ELSE duration), break] -
break]];
SetNotify[];
IF slideStart = 0
THEN
noteArray[note ← note + 1] ← [initial[0, 0, break]]
ELSE
noteArray[note].duration ←
(IF triplet THEN (duration*2 + 1)/3 ELSE duration);
};
'# =>
IF note > 0
THEN {
-- sharps are one twelfth-octave higher
IF noteArray[note].octave = 0
THEN
-- octave 0 is really the "break" between notes
noteArray[note - 1].twelfths ← noteArray[note - 1].twelfths + 1
ELSE noteArray[note].twelfths ← noteArray[note].twelfths + 1;
};
'+ =>
IF note > 0
THEN {
-- warning: you can exceed 2147 msecs using ***C++
IF noteArray[note].octave = 0
THEN
-- octave 0 is the "break" between notes
noteArray[note - 1].duration ←
noteArray[note - 1].duration*3/2 + noteArray[note].duration/2
ELSE noteArray[note].duration ← noteArray[note].duration*3/2;
};
'- =>
IF note > 0
THEN {
IF noteArray[note].octave # 0
THEN
-- octave 0 is the "break" between notes
noteArray[note].duration ← noteArray[note].duration/2
ELSE
IF noteArray[note - 1].duration > noteArray[note].duration
THEN
noteArray[note - 1].duration ←
noteArray[note - 1].duration/2 - noteArray[note].duration/2
ELSE noteArray[note - 1].duration ← 0;
};
'< => octave ← MIN[octave*2, limits[max][random].octave];
'> => octave ← MAX[octave/2, limits[min][random].octave];
'/ => duration ← MAX[duration/2, limits[min][random].duration];
'* => duration ← MIN[duration*2, limits[max][random].duration];
'← => break ← MAX[break/2, limits[min][random].break];
'^ => break ← MIN[break*2, limits[max][random].break];
'(, ') => triplet ← (music[card] = '();
'% => {
NextNote[1];
IF slideStart # 0 THEN AddToSlide[noteArray[note].duration];
noteArray[note] ← [
initial[
0, octave, IF triplet THEN (duration*2 + 1)/3 ELSE duration]];
SetNotify[];
};
'{ =>
IF slideStart = 0
THEN {
NextNote[3]; -- need room for starting and ending notes, plus slop (see above)
slideStart ← note;
slide ← noteArray[note].duration ← noteArray[note + 1].duration ← 0;
note ← note - 1; -- haven't actually found starting note yet
};
'} =>
IF slideStart # 0
THEN {
IF note # slideStart + 1
OR noteArray[note].twelfths = 0
OR noteArray[note - 1].twelfths = 0
don't have two notes to work with --
OR slide + noteArray[note - 1].duration + noteArray[note].duration
< 2 THEN
can't slide this fast!
note ← slideStart - 1 -- ignore slide completely
ELSE {
diff: INTEGER; -- number of 96ths of an octave between the two ends
temp, delta: CARDINAL;
low: Note;
AddToSlide[
noteArray[note - 1].duration + noteArray[note].duration];
diff ← (INTEGER[noteArray[note].twelfths] - INTEGER[noteArray[note - 1].twelfths])*8;
temp ← noteArray[note].octave;
DO
SELECT noteArray[note - 1].octave
FROM
< temp => {temp ← temp/2; diff ← diff - 96};
> temp => {temp ← temp*2; diff ← diff + 96};
ENDCASE => EXIT;
ENDLOOP;
low ← noteArray[IF diff < 0 THEN note ELSE note - 1];
FOR temp
IN [0..slide)
DO
delta ← Basics.LongDiv[
Basics.LongMult[
IF diff < 0 THEN (slide - 1 - temp) ELSE temp, ABS[diff]] +
LONG[(slide - 1)/2], slide - 1];
noteArray[slideStart + temp] ← [
initial[Frequency[low, delta], 0, limits[min][FALSE].duration]];
SetNotify[];
ENDLOOP;
note ← slideStart + slide - 1;
};
slide ← slideStart ← 0;
};
'; => { note ← ShipOneChunk[note]; slide ← slideStart ← 0; };
'. =>
IF
NOT random
THEN {
note ← ShipOneChunk[note];
octave ← initialOctave;
duration ← initialDuration;
break ← initialBreak;
triplet ← FALSE;
slide ← slideStart ← 0;
NextNote[1];
noteArray[note] ← [initial[0, octave, 1000]];
SetNotify[];
note ← ShipOneChunk[note];
};
'[ => notifyNext ← TRUE;
'] => pauseNext ← TRUE;
IN ['0..'9] => number ← number*10 + music[card] - '0;
'@ =>
IF
NOT random
THEN {
numberPtr ← @duration;
numberMin ← limits[min][FALSE].duration;
numberMax ← limits[max][FALSE].duration;
number ← 0;
};
', =>
IF
NOT random
THEN {
numberPtr ← @break;
numberMin ← limits[min][FALSE].break;
numberMax ← limits[max][FALSE].break;
number ← 0;
};
ENDCASE;
ENDLOOP;
IF note > 0 THEN [] ← ShipOneChunk[note];
IF quietFinish THEN DonePlaying [];
noteArray ← NIL;
};
};
}.
Edited on May 25, 1984 2:26:24 pm PDT, by Pier
changes to: NextNote to fix >= bug , PlayUtils, PlayUtils
Edited on July 31, 1984 9:32:14 am PDT, by Swinehart
changes to: PlayUtils, Beep, Beep, AddToSlide, SetNotify