My estimation on how long would it take me to complete my current project “Alien Intruder” was a bit off, and it is because I subestimated –or more like I forgot– that I had to write my own sound driver.
In Gold Mine Run! I used the excellent libmikmod, that gives you everything you need, supporting mixing of music and sound effects. Unfortunately for DOS it only supports DJGPP, which is 32-bit
, and I’m targeting 16-bit
with the IA-16 GCC port.
So I decided I would write my own driver, how hard could it be?
The plan was supporting:
- FM for music, via AdLib compatibility.
- 1 channel
8-bit
PCM sound via Sound Blaster.
And I have all that now, but it took me longer than expected because the docs I found were missing small details, and my own incompetence, of course.
Playing an IMF
file, like the ones used in “Wolfenstein 3D” for example, is very easy. They are just an array of triplets (register, value and time) that you send to the sound card at an specific frequency, which was my first problem: I didn’t change the Programmable Interval Timer (PIT) chip from the default 18.2065Hz
in Gold Mine Run!, and that was fine for MikMod, but not good enough for the FM playback.
So I had to do that, and it took me a bit to get it right because I had something else getting in the way and I though I was reprogramming the PIT wrong.
At the end it was simple:
/* disable interrupts */
disable();
outportb(0x43, 0x36);
outportb(0x40, TIMER_SPEED % 256);
outportb(0x40, TIMER_SPEED / 256);
/* enable interrupts back */
enable();
With TIMER_SPEED
calculated based on the desired timer rate dividing 1193182
, which is the clock frequency of the PIT.
Then, to restore the PIT, we do:
/* disable interrupts */
disable();
outportb(0x43, 0x36);
outportb(0x40, 0xff);
outportb(0x40, 0xff);
/* enable interrupts back */
enable();
Which goes back to the default (1193182 / 18.2065
gives us a TIMER_SPEED
of 65535
).
With that out of the way, we use the interrupt handler to send data to the sound card (those “register and value” from the triplets I mentioned before). And that’s all, other that I need to learn to use a tracker that can export to this format. It will be OK, I’m sure of that.
Programming the Sound Blaster for PCM sound is not too complicated, but it has two parts: setup the DMA
(the hard bits), and writing to the DSP
(easy and well documented, for example Programming the Sound Blaster DSP).
To keep things simple I’m focusing on “plain” Sound Blaster, without using the new features of later revisions (e.g. the Sound Blaster 16 has other ways of setting up the DMA
), and it should work fine with most configurations, but I kind of expect things to be a bit standard via the BLASTER
environment variable. For most people playing the game with DosBox it will be fine, and I don’t need to support all the weird configurations out there.
There were two gotchas in this part:
- the memory used by the
DMA
can’t cross a64k
page physical boundary. - for whatever reason, when I used a large buffer for the
DMA
(say48K
), I got pops and weird noise in some samples.
My first big mistake, that took me some time to fix, was that I’m using DOS’ int 21h
service 48h
to allocate memory, and that returns a segment. So if you ask for 64k
, that won’t cross a page boundary, right? Well, that is not correct, the page you get is virtual.
The way to calculate and find the page boundary is using a 20-bit
memory address. If the end of our buffer is on a different segment on that 20-bit
address, we are crossing a boundary.
A simple way of dealing with this is allocating double the memory you need (for example 32K
for a 16K
buffer), and if the end of the first 16K
are on a different segment, just use the second 16K
that are guaranteed then to fit on a 64k
page.
/* falloc is my own "far alloc" as I implemented far pointers */
dma_buffer = falloc(MAX_SAMPLE_LEN * 2);
if (!dma_buffer)
return 0;
dma_start = dma_buffer;
/* 20-bit address */
dma_linear = (dma_buffer >> 16) << 4;
/* avoid crossing a page boundary */
if ((dma_linear >> 16) != ((dma_linear + MAX_SAMPLE_LEN) >> 16))
{
dma_linear += MAX_SAMPLE_LEN;
dma_start += MAX_SAMPLE_LEN;
}
/* these won't change */
dma_page = (dma_linear >> 16) & 0xff;
dma_offs = dma_linear & 0xffff;
I keep dma_buffer
unaltered because I need it to free the memory with DOS later on, but my working buffer is really pointed by dma_start
. Also the DMA
works with the 20-bit
address (also called linear), so I pre-calculate the values here.
After this is just matter of copying the sample I want to play (in my case MAX_SAMPLE_LEN
is 16K
), setup the DMA
to use the buffer and the right size (depending on the sample), and ask the DSP
so play it.
I implemented a simple priority based playback, like I do in my 8-bit
games, so a sample plays only if there is nothing playing already, or the sample you want to play has higher priority than what is currently playing.
I track what I’m playing by setting a variable when I start playing a sample, and clearing it when the IRQ
interrupt is triggered by the sound card when it has finished playing a sample.
With this priority table, the action sounds fine with only one channel and saving myself from doing mixing of multiple channels:
enum {
FXJUMP = 0,
FXLASER,
FXIMPACT,
FXPICKUP,
FXTIME,
FXWARP,
FXBLAST,
FXBOMB,
FXEXPLO,
FXCREW,
FXDEAD,
FXBLIP,
};
Initially I thought I could load all my samples in a 64K
page –being careful with the size of the samples–, and play them from there; but for whatever reason, even if the memory is not crossing a page boundary, the playback isn’t right and there are unexpected pops or noises in real hardware (emultted) but not in DosBox (that is more permissive). I don’t know what is this, and I can only test with the excellent 86Box, so I decided to use a smaller buffer and just copy there the sample I want to play. After that, all plays and sounds as expected.
I’m assuming this has a performance hit, but so far the game plays nice on a 286
–as long as the VGA
card is decent–, so I’m not concerned.
Anyway, I didn’t want to make this a tutorial, and is not; but I explained a bit what I have been doing. The game is progressing nicely, but I’m not going to suggest any release date this time!