Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

Developing a PC-speaker MIDI Player for the IBM PC XT

2.60/5 (2 votes)
12 May 2023CPOL4 min read 9.5K  
How to develop PC-speaker MIDI player for IBM PC XT
In this post, you will learn to develop a PC-speaker MIDI player for the IBM PC XT.

In my experiment with a PC XT running on an NEC V20 at 12MHz, I attempted to play a MIDI file through the PC speaker using the MIDIPLAY utility written by James Allwright. I have used the player previously and was able to play various MIDI files nicely through the PC speaker on my 286, 386 and 486. However, on my XT, after loading the file for a few seconds, it simply played a short beep and quit. After having a quick look at the code, I suspected that the problem lies within the assembly code in NOTE.A that is responsible for playing sound. This code is called from MIDIPLAY.C in a while loop to play the MIDI note by note:

C++
while (place != NULL) {
note(place->pitch, place->start);
place = place->next;
};

This is the actual assembly code to play a note:

ASM
dosound:
in al, 61h
push ax
or al, 03h
out 61h, al ; turn speaker on
xor si, si

mov ax, [bp+4] ; timer interval
push ax
mov al, 0B6h
out 43h, al
pop ax
out 42h, al
mov al, ah
out 42h, al ; set up interval

call delay

pop ax
out 61h, al ; turn speaker off

endtune:
mov sp, bp ; restore stack pointer
pop bp
ret

delay:
mov cx, word [bp+8] ; high word
mov dx, word [bp+6] ; low word
mov ah, 86h
int 15h
ret

The above assembly code relies on AH=86h/INT 15h call to generate a suitable delay after writing the note frequency to the PC speaker at port 61h. This feature was first implemented on the IBM PC/AT, aka 5170, running on a 80286 CPU. On the PC XT, this interrupt is not implemented, causing the MIDI player to fail since there is no delay after each note.

I managed to compile the code using Personal C Compiler that is originally used by the author. To avoid having to specify the include paths, I simply copied all source files into the sample folder as the compiler, and used the following batch file to compile and run the MIDI player:

BAT
del midiplay.exe
del midifile.o
del midiplay.o
del note.o
pcc midiplay.c
pcc midifile.c
pcca note.a
pccl midiplay.o midifile.o note.o
midiplay.exe test.mid 

The batch file removes all previous build output, uses PCC to compile the C files, PCCA to compile the NOTE.A assembly code, and PCCL to link the compilation output to produce the final .EXE file. Despite some linker warnings, the compilation is successful and the generated MIDIPLAY.EXE can be used to play MIDI files.

Fixing the issue to make the code work on an XT is however non-trivial. As modifying the code to use INT 21H to get the system date/time or to read the 8253 Programmable Interval Timer counter and manually calculate the delay would be complicated, I chose the easier way of porting the code to Turbo C and use the sound() and nosound() functions in conio.h. To do this, I studied the original code and merged MIDIFILE.H and MIDIFILE.C into a single MIDIPLAY.C file while at the same time removing unnecessary function headers. During the process, I encountered archaic C syntax in the original MIDIPLAY code which requires method variable types to be separately declared:

C
void
write16bit(data)
int data;
{
}

This is the modern equivalent:

C
void write16bit(int data)
{
}

In PCC, to use forward-declared methods in another method, one would have to declare the method again, without any parameters, similar to how variables are declared. The following code shows how method WriteVarLen is declared again before use:

C++
int
mf_write_midi_event(delta_time, type, chan, data, size)
long delta_time;
int chan,type;
int size;
char *data;
{
    void WriteVarLen();
    WriteVarLen(delta_time);
...
}

The method WriteVarLen is written as below:

C++
void
WriteVarLen(value)
long value;
{
....
}

Fortunately, Turbo C++ 3.00 has no issues with this type of syntax and compiles my modified code just fine. There were references to unimplemented methods such as clearkey() and flushbuffer() which originally caused linker errors, but the original code still worked fine after these references were removed.

Using Turbo C++’s sound functions, I changed the note playback code to the following:

C++
while (place != NULL) {
	if (place->pitch == -1)
	{
		nosound();
	}
	else {
		sound(1193180 / place->pitch);
		delay(place->start / 1000);
	}
        place = place->next;
   	if (kbhit())
		break;
};
nosound();

The original code accepts delays in microseconds (place->start parameter) and needs to be converted to milliseconds for use with delay(). For the note frequency, we will also need to convert place->pitch, which is originally the input value for the timer chip running at 1.19318 MHz, back to the original frequency value for the note. Also, the author used -1 to indicate that no sound will be played and this can be changed to Turbo C’s nosound() function. Finally, kbhit() function is used to terminate playback as soon as a key is pressed.

With this change, MIDIPLAY is ready to be tested from the command line. Various command line parameters such as -t and -c to specify which track or channel to be played, which can be useful when dealing with large MIDI files, are also available.

After extensive tests, the modified MIDI player works well on the PC XT, 286, 386 and 486 or even newer machines so long as the PC speaker is present. It even works on my Core i5 machine booting DOS in 16-bit mode – there was no Runtime Error 200 since we are using Turbo C, not Turbo Pascal. Among my MIDI collection, most MIDI files can be read and played (or more precisely, beeped) through this player, except for a few very complicated files. The only drawback is that the codes read the entire file into memory before playing and will have issues with very big MIDI files, e.g., larger than available DOS conventional memory. Still, I believe the results are good enough for a PC-speaker MIDI player.

Downloads

See Also

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)