unit music;
Play music in the background.
This unit gives you music capabilities with a BASIC syntax. The music
will be played in the background, so your program can continue with
other things. The music can also be played in the foreground.
This unit is very easy to use. There are only three procedures and one
function interfaced outwards. All the rest is automatic (installing,
uninstalling, interpreting the music, etc.)!
PlayMusic(string);
Start playing a string of music in the background. The string
is a normal character string containing music 'commands' as
described below. If there is already music playing, then it is
first shut off. All settings are reset to their default.
PlayMusicForeground(string);
Start playing a string of music, and wait for it to finish. If
there is already music playing, then it is first shut off. All
settings are reset to their default.
This procedure simply calls the "PlayMusic" procedure, and then
loops until MusicBusy (described later) is true.
MusicOff;
Turn music off.
if MusicBusy then ...
Return TRUE if there is currently music playing.
See at the end of this unit for a small demonstration program.
The music-commands syntax is (BASIC compatible):
[>,<]A..G[#,+,-](n)[.]
Play note A..G in the current octave. There are 12 notes per
octave: C, C#, D, D#, E, F, F#, G, G#, A, A#, B.
If the note is prefixed by '>', then it is transposed one octave
upwards.
If the note is prefixed by '<', then it is transposed one octave
downward.
if the note is followed by '#' or '+', then the note is made
"sharp" (one note up, 'D' becomes 'D#', 'E' becomes 'F').
If the note is followed by '-', then the note is made "flat" (one
note down, 'D' becomes 'C#').
If the note is followed by a number, then the number specifies the
length of this note, overriding the default notelength set by 'L'.
Every period following the notenumber will increase the playtime
by 3/2.
Example: >B+3.
> : transposed
B : note B
+ : sharp
3 : length 3
. : 3/2 longer
N(n)[.] Play note "n", in which "n" is a number 0..84. There are 7 octaves,
12 notes per octave. Note 0 means: silence. The first note in the
first octave is 'N1'.
Every period following the notenumber will increase the playtime
by 3/2.
O(n) Sets the octave to "n", in which "n" is a number 0..7. Each octave
goes from note 'C' to 'B'. Octave 3 starts with middle 'C'. Default
octave is 4.
L(n) Set the default length of following notes to "n", in which "n" is
a number 1..64. L1 = whole notes, L2 = half notes, L4 = quarter
notes, etc. Default length is 4. In one minute fit 120 quarter
notes ('L4'), adjustable with the 'T' (tempo) command.
T(n) Set the tempo to "n", in which "n" is a number 32..255. The tempo
is the number of quarter notes ('L4') that are played per minute.
The higher the tempo, the faster the music. Default tempo is 120.
MN Music Normal. Every note plays seven-eights of the time set by
'L', and is followed by a pause of one-eight. Thus, every note is
followed by a small silence, making the music more natural.
ML Music Legato. Every note plays the full time set by 'L'. Thus, every
note is immediately followed by the next note, making the music a
bit synthetic.
MS Music Staccato. Every note plays three-quarters of the time
set by 'L', and is followed by a pause of one-quarter. Thus, every
note is followed by a clearly audible silence, making the music
very rithmic.
P(n)[.] Insert a pause with a length of "n", in which "n" is a number
1..64.
Every period following the number will increase the playtime
by 3/2.
Not supported (ignored):
MF Foreground: Cannot switch between foreground/background.
MB Background: Cannot switch between foreground/background.
Xs$; Include string: Cannot include substrings.
=n; Use variable "n": Cannot replace variable's names by their contents.
Spaces are allowed between commands, but not inside commands.
Upper/lowercase is not important.
THEORY.
This unit installs itself in the timertick interrupt $1C (procedure
"MusicNext"). With every timertick a buffer is checked. If there is any
music to be played in the buffer, then a single note from the buffer is
played.
This unit was inspired by a (buggy and incomplete) public domain unit
written by Michael Quinlan, 9/17/85.
J.C. Kessels
Philips de Goedelaan 7
5615 PN Eindhoven
Netherlands
*******************************************************************************)
Interface
procedure MusicOff;
procedure PlayMusic(s : string);
procedure PlayMusicForeground(s : string);
function MusicBusy : boolean;
Implementation
uses dos;
var
OldInt1C : pointer; { Pointer to old interrupt routine. }
ExitSave : pointer; { Pointer to previous exit procedure. }
MusicString : string; { The string to be played. }
MusicHere : word; { Pointer into MusicString, non-zero while playing. }
MusicDelay1 : word; { Clockticks countdown for current note. }
MusicDelay2 : word; { Clockticks countdown for current note. }
MusicNoteLength : word; { Current note length. }
MusicTempo : word; { Current tempo. }
MusicOctave : word; { Current octave. }
MusicKind : word; { 8 = Legato, 7 = Normal, 6 = Staccato. }
{ Array with coded frequencies: 12 notes per octave (C, C#, D, D#, E, F, F#,
G, G#, A, A#, B), 7 octaves. }
Frequency : array[0..83] of word;
function GetNumber(min, max, default : word) : word;
{ Get a number from the MusicString, starting at MusicHere. Increment MusicHere
past the end of the number. If the number is <min or >max then the default
number is returned. This routine will also skip the Basic syntax for a
variable: '=name;' }
var
n : word;
begin
{ Ignore Basic syntax for embedded variable instead of constant, and exit with
the default. }
if (MusicHere <= length(MusicString)) and (MusicString[MusicHere] = '=') then
begin
while (MusicHere <= length(MusicString)) and (MusicString[MusicHere] <> ';')
do inc(MusicHere);
if (MusicHere <= length(MusicString)) and (MusicString[MusicHere] = ';')
then inc(MusicHere);
GetNumber := default;
exit;
end;
{ Accept a number from the MusicString. The number is finished by anything that
is not a number '0'..'9'. }
n := 0;
while (MusicHere <= length(MusicString)) and
(MusicString[MusicHere] in ['0'..'9']) do
begin
n := n * 10 + (Ord(MusicString[MusicHere]) - Ord('0'));
inc(MusicHere);
end;
{ Test if the number is within range, otherwise return the default. }
if (n < min) or (n > max)
then GetNumber := default
else GetNumber := n;
end;
procedure SetupDelays;
{ Setup MusicDelay1 and MusicDelay2. The first determines the time that a note
is audible, the second determines a rest between two notes (Legato, Normal,
Staccato). To do this, accept a note-length number from the MusicString, or
use the default NoteLength. Also accept trailing dot's from the MusicString,
which lengthen the note-length by 1.5. }
var
r : real;
begin
r := GetNumber(1,64,MusicNoteLength); { Accept number. }
{ Note: the number is reciprocal. A high number means a short note. If the
number is 4, then it is a 'normal' note. Think of the number as: "the number
of quarter notes that the note will last". }
while (MusicHere <= length(MusicString)) and { Accept trailing dot's. }
(MusicString[MusicHere] = '.') do
begin
inc(MusicHere);
r := r * 0.75; { Every dot increases the note time by 1.5 times. }
end;
{ Translate into clocktick delays. The following formula is used:
There are 120 'standard' notes per minute.
ticks = ThisNoteLength * ThisTempo * TicksPerStandardNote
ThisNoteLength = 4 / NoteLength
ThisTempo = 120 / MusicTempo
TicksPerStandardNote = TicksPerMinute / 120
TicksPerMinute = TicksPerSecond * 60
TicksPerSecond = 18.2
ticks := 4 * 18.2 * 60 * / (NoteLength * MusicTempo)
}
MusicDelay1 := Round(4368.0 / (r * MusicTempo));
{ The clockticks are split two ways: every note is followed by a small amount
of silence (Legato, Normal, Staccato). MusicDelay1 determines the 'on' time,
MusicDelay2 determines the 'off' time. }
if MusicKind < 8
then MusicDelay2 := MusicDelay1 * (8 - MusicKind) div 8
else MusicDelay2 := 0;
dec(MusicDelay1,MusicDelay2);
end;
procedure MusicNext; interrupt;
{ Play the MusicString. This procedure is installed into the timer interrupt,
and therefore runs with every timer-tick. The routine takes music from the
MusicString, from position MusicHere. If MusicHere is zero, then the music is
disabled. The duration of a note is determined by MusicDelay1 and
MusicDelay2, both set by the SetupDelays procedure. }
var
note : word; { Temporary variables. }
ch : char;
begin
{ Call the old timer handler. The address of the old handler is saved by the
installation code at the end of the unit. }
Inline(
$9C/ {pushf}
$FF/$1E/>OLDINT1C); {call far [>OldInt1C]}
{ Decrement MusicDelay1. This determines the time that a note is 'on'. }
if MusicDelay1 > 0 then
begin
dec(MusicDelay1); { Decrement delay. }
if MusicDelay1 > 0 then exit; { Exit if delay not zero. }
end;
{ If there is a second delay, then move it to the main delay counter and exit.
The second delay time determines a silence after each note (Legato, Normal,
Staccato). }
if MusicDelay2 > 0 then
begin
MusicDelay1 := MusicDelay2; { Move second delay into first delay. }
MusicDelay2 := 0;
Port[$61] := Port[$61] and $F8; { Sound off. }
exit; { Exit. }
end;
{ If MusicString all done then sound off and exit. }
if MusicHere = 0 then exit;
if MusicHere > length(MusicString) then
begin
MusicHere := 0;
Port[$61] := Port[$61] and $F8; { Sound off. }
exit; { Exit. }
end;
{ Process commands from MusicString, until a note or a pause can be played. A
few Basic commands are not supported, these are ignored. }
while MusicHere <= length(MusicString) do
begin
ch := upcase(MusicString[MusicHere]); { Get character from MusicString. }
inc(MusicHere);
case ch of
'O' : MusicOctave := GetNumber(0,7,4); { Set octave. }
'L' : MusicNoteLength := GetNumber(1,64,4); { Set note length. }
'T' : MusicTempo := Getnumber(32,255,120); { Set tempo. }
'M' : if MusicHere <= length(MusicString) then { 'M' commands. }
begin
ch := upcase(MusicString[MusicHere]);
inc(MusicHere);
case ch of
'L' : MusicKind := 8; { Set legato. }
'N' : MusicKind := 7; { Set normal. }
'S' : MusicKind := 6; { Set staccato. }
end;
end;
'P' : begin { Pause. }
Port[$61] := Port[$61] and $F8;
SetupDelays;
exit;
end;
'A'..'G','>','<' : begin { Play a note. }
note := MusicOctave * 12;
if ch = '>' then
begin { Accept '>'. }
if MusicHere <= length(MusicString) then
ch := upcase(MusicString[MusicHere]);
inc(MusicHere);
if note <= 71 then inc(note,12);
end;
if ch = '<' then
begin { Accept '<'. }
if MusicHere <= length(MusicString) then
ch := upcase(MusicString[MusicHere]);
inc(MusicHere);
if note >= 12 then dec(note,12);
end;
case ch of { Determine frequency of note. }
'A' : inc(note,9);
'B' : inc(note,11);
'C' : inc(note,0);
'D' : inc(note,2);
'E' : inc(note,4);
'F' : inc(note,5);
'G' : inc(note,7);
end;
{ Accept '#' or '+' following the letter. }
if (MusicHere <= length(MusicString)) and
( (MusicString[MusicHere] = '#') or (MusicString[MusicHere] = '+') )
then
begin
inc(MusicHere);
if note < 83 then inc(note);
end;
{ Accept '-' following the letter. }
if (MusicHere <= length(MusicString)) and
(MusicString[MusicHere] = '-') then
begin
inc(MusicHere);
if note > 0 then dec(note);
end;
note := Frequency[note]; { Translate note into 'frequency'. }
Port[$61] := Port[$61] and $F8; { Sound off. }
Port[$43] := $B6; { Setup timer chip. }
Port[$42] := Lo(note); { Setup frequency. }
Port[$42] := Hi(note);
Port[$61] := Port[$61] or $03; { Sound on. }
SetupDelays; { Setup note length delays. }
exit;
end;
'N' : begin { Play a specific note. }
note := GetNumber(1,84,0); { Accept note number. }
Port[$61] := Port[$61] and $F8; { Sound off. }
if note > 0 then { Zero means silence. }
begin
note := Frequency[note-1]; { Translate note into 'frequency'. }
Port[$43] := $B6; { Setup timer chip. }
Port[$42] := Lo(note); { Setup frequency. }
Port[$42] := Hi(note);
Port[$61] := Port[$61] or $03; { Sound on. }
end;
SetupDelays; { Setup note length delays. }
exit;
end;
'X' : begin { Skip the Basic syntax for an embedded string. }
while (MusicHere <= length(MusicString)) and
(MusicString[MusicHere] <> ';') do inc(MusicHere);
if (MusicHere <= length(MusicString)) and
(MusicString[MusicHere] = ';') then inc(MusicHere);
end;
end;
end;
end;
procedure MusicOff;
{ Turn music off. }
begin
MusicHere := 0; { Index is zero. }
MusicDelay1 := 0; { Delay is zero. }
MusicDelay2 := 0; { Delay is zero. }
Port[$61] := Port[$61] and $F8; { Sound off. }
end;
procedure PlayMusic(s : string);
{ Start playing a string of music in the background. If there is already music
playing, then first shut it off. All settings revert to their default. }
begin
MusicOff; { Shutup current music. }
MusicString := s; { Save string into MusicString. }
MusicNoteLength := 4; { Setup defaults. }
MusicTempo := 120;
MusicOctave := 4;
MusicKind := 7;
MusicHere := 1; { Start music (at begin of string). }
end;
procedure PlayMusicForeground(s : string);
{ Start playing a string of music, and wait for it to finish. If there is
already music playing, then first shut it off. All settings revert to their
default. }
begin
PlayMusic(s);
while MusicHere > 0 do ;
end;
function MusicBusy : boolean;
{ If there is music playing then return TRUE. }
begin
if MusicHere > 0
then MusicBusy := true
else MusicBusy := false;
end;
{$F+} { Must be compiled FAR. }
procedure ShutDown;
{ Un-install the unit, and turn music off. It is absolutely necessary that the
MusicNext procedure is un-installed from the timertick interrupt, or the
system may crash. }
begin
MusicOff; { Music off. }
ExitProc := ExitSave; { Reinstall old exit procedure. }
SetIntVec($1C,OldInt1C); { Install old interrupt handler. }
end;
{$F-}
procedure Initialize;
var
i : word;
r1, r2 : real;
begin
{ Fill the frequency array with words that can be fed into the timer chip. The
array contains coded frequencies, one for every note (0..11) in every octave
(0..6). The first note of an octave is exactly 2 times as high as the first
note in the first-lower octave. This means that the distance between two
notes is exactly 12û2 = exp(ln(2)/12). Starting at a 'base' frequency for the
highest note in the highest octave, we can calculate all the notes in all the
octaves. The timer chip expects a reciprocal number (1193180 / frequency). }
r1 := 1193180.0 / 8000.0; { Highest note is 8000 Hz. }
r2 := exp(ln(2.0)/12.0); { Distance between 2 notes. }
for i := 83 downto 0 do { Fill frequency array. }
begin
Frequency[i] := round(r1);
r1 := r1 * r2;
end;
MusicOff; { Initialize variables. }
GetIntVec($1C,OldInt1C); { Save address of previous int-1C handler. }
SetIntVec($1C,@MusicNext); { Install our interrupt handler. }
ExitSave := ExitProc; { Save address of previous exit procedure. }
ExitProc := @ShutDown; { Install ShutDown procedure. }
end;
{ Initialization code. }
begin
Initialize;
end.
(***************************** Example program *********************************
program test;
uses music;
begin
{ Anthem }
PlayMusic('T100O3L8E-.L16CO2L4A-O3L4CE-L2A-O4L8C.O3L16B-L4A-CDL2E-L8E-E-O4L4C.'+
'O3L8B-L4A-L2GL8FGL4A-A-E-CO2L4A-O4L8CCL4CD-E-L2E-L8D-CO3L4B-O4L4CD-L2D-L8D-'+
'D-L4C.O3L8B-L4A-L2GL8FL16G.L4A-CDL2E-L8E-E-L4A-A-L8A-GL4FFFB-O4L8D-CO3L8B-'+
'A-L4A-L4G.P8L8E-E-O3L4A-.L8B-O4L8CD-L2E-O3L8A-B-O4L4C.L8D-O3L4B-L2A-..');
while MusicBusy do write('Playing the Anthem....');
{ Anvil }
PlayMusic('T200O3E2E4.E8E4.D8C4.O2A8G4.B8O3D4.F8E2C2E2E4.E8E4.D8C4.O2A8G4.B8'+
'O3D4.F8E4C4E2C4P4D4P4O2B4O3C4O2A4B4E4P4P8G+8A8B8O3C4C4P8O2B8O3C8D8E4E4P8D8'+
'E8F8G2.F8G16F16E4P4P2');
while MusicBusy do write('Playing Anvil....');
{ Bouree }
PlayMusic('MBMLL8T150O4DEF4EDC+4DEO3A4BO4C+DP10CO3B-A4GFE4FGAP16GF16E16D8P10'+
'O4DEF4EDA4FAO3A4BO4C+DP10CO3B-A4GFP32F16G16F16E16F16.P32F2');
while MusicBusy do write('Playing Bouree....');
writeln('Music is done.');
end.
*******************************************************************************)