Ok, so I decided to port an existing program that uses a different means to play the audio over the SoundBlaster 16.
Just looking through that code, I'm not sure why they are trying to convert 8-bit audio to 16-bit, instead of just writing it to the soundblaster as 8-bit data. Maybe I'm misreading it. They may be trying to send 16-bit data over the 8-bit DMA channel, which also doesn't make much sense. Maybe they are trying to maintain compatibility with the older SoundBlaster models...
In any case, I'm not surprised that this code doesn't work in VirtualBox, since it seems to be trying to misuse the DMA channel.
I'm going to test out Windows XP Media Player again and see if I can get the audio to start failing after playing for an extended period of time. If not, then I think that will prove that the VirtualBox emulation is stable in at least in one situation.
EDIT: Other than the audio looping that I mentioned above when clicking Stop in Media Player, everything seems to work fine in XP. I tried playing audio from an audio CD for 5 minutes, including stopping and starting repeatedly to try to get it to "hiccup", playing 2 different WAV files for 5 minutes with no issues, and streaming live internet radio for 20 minutes with no glitches at all.
So I'm going to go ahead and say that, obviously, there is a way to get the SoundBlaster to work properly in VirtualBox. I'm not sure what that way is, as of yet, but I haven't spent too much time looking into it. I've had pretty good luck in my OS using 16-bit 44100 sample rate stereo audio, and 16-bit DMA transfers.
I recommend trying to pinpoint the exact moment when things start going wrong, and trying to determine what triggers the audio to start glitching. I'll go ahead and point out that the window for filling the "just finished" half of the buffer is pretty small, so if your code takes more than a few milliseconds after the IRQ is raised, you will get audio glitches due to the soundblaster playing the old audio data, and it will probably be out-of-sync from then on. Are there any other processes running, or threads that might be causing any locking issues?
EDIT2:
The program takes the audio buffer and splits it in half and plays the first half, gets the interrupt notification and then plays the 2nd half, notification, 1st half again, etc.
This is the recommended way to fill the soundblaster buffer, since you only have one buffer to work with. Fill the entire buffer, then tell the soundblaster to play half of the buffer, and wait for the interrupt, then refill the first half while the soundblaster plays the second half. On the next interrupt, you simply fill the last half of the buffer. Depending on the parameters that you send with the Play command, the sound blaster will continue to play audio from the buffer until you send a pause or a stop command. All you need to do is acknowledge the interrupt.