Video playback on low-end MS-DOS machines

Playing video on MS-DOS… it kinda was a thing with things such as the GRASP/GLPro and FLIC formats back in the day. But not on REALLY low-end machines, such as 8088s and CGA. Until the demo 8088 Corruption was released in 2004. And then its successor 8088 Domination in 2014, which I already discussed earlier.

It can play videos at very acceptable quality on an 8088 at 4.77 MHz with (composite) CGA and a Sound Blaster 2.0.

Wait, a Sound Blaster 2.0? That is a bit of an anachronism, is it not? Yes, but for two good reasons:

  1. The Sound Blaster was the first commonly available sound solution for the IBM PC that could take advantage of the DMA controller for sample playback, thereby reducing CPU load to near-zero.
  2. As discussed here earlier, the early Sound Blasters had some shortcomings that meant they weren’t capable of seamless playback. The 2.0 version was the first to solve this (this was fixed by implementing new commands in the firmware of the DSP. Creative also sold upgrades for SB 1.x cards to get the new 2.0 firmware, solving this issue).

Okay, so the Sound Blaster 2.0 was the first *good* solution for digital audio playback. But, one may argue… An IBM PC with a 4.77 MHz 8088 and CGA is not in the ‘good’ category either, is it? The point is “let’s see what we can do anyway”. What if we also apply this to audio?

I’ve had this idea for a while. Since I had done a streaming audio solution on PC speaker on my IBM 5160 PC/XT, I wanted to add video as well. It would make sense to combine the two, as I already wrote at the time. And the past few days I had a bit of spare time, so I decided give it a quick-and-dirty try.

The code for the XDC encoder and decoder are available on Github, so I just made my own fork:

https://github.com/Scalibq/XDC

PC Speaker first

As you can see from the commits, I added my code for auto-EOI to shave off a few cycles with each interrupt. For the Sound Blaster and ‘quiet’ options, this is not required, because there is only one interrupt per frame. But for the PC speaker and various other sound sources that don’t support DMA, you need an interrupt for every individual sample, so I enable auto-EOI in those cases.

I then added some PC speaker routines to enable PWM playback. I use a simple translation table that translates the unsigned 8-bit PCM samples in the XDV file to the correct PWM values. Note that the PWM values are dependent on the sample rate, so the table is initialized at runtime, when the sample rate of the file is known.

And that pretty much took care of things already. PC speaker sound is now working. There is extra CPU overhead of course, so you’ll either need a faster machine to play the same content, or you need to encode your content with a lower sample rate, and perhaps also reduce the frame rate and/or the MAXDISKRATE value when encoding. I would not recommend PC speaker on a 4.77 MHz machine at all, but on a turbo XT at 8 or 10 MHz, you should be able to get away with it reasonably well, as long as you keep the sample rate low enough, say between 8 and 11 kHz. It will be a bit of trial-and-error to find what works best for your specific machine.

Then Covox…

Now that I had a solution that could bit-bang samples, I figured I might as well add some other common devices. One is the Covox, which was trivial, as the 8-bit unsigned PCM samples could be sent to the Covox as-is. This makes it slightly faster than the PC speaker solution. It also sounds better. The quality is almost as good as the Sound Blaster. The main issue is that the interrupt-driven sample playback is more susceptible to jitter than DMA transfers are. On a fast enough machine that won’t be an issue, but on 8088s you may hear some minor jitter.

And Tandy…

Tandy is another interesting target. XDC already supports Tandy video modes, so adding Tandy audio would make sense. It can do PC speaker playback of course, but PWM also gives an annoying carrier wave. Sample playback on the SN76496 chip does not have that issue. You can get 4-bit PCM from the sound chip’s volume register. The volume is non-linear however, so you can’t just take the high nibble from the 8-bit source samples (it works, but it’s suboptimal). Instead you should use a translation table from the linear 8-bit samples to the correct non-linear 4-bit volume settings. That’s simple enough. I could just use the same approach as for the PC speaker, but with a different table.

The Tandy threw me a bit of a curveball though. I had done PCjr sample playback before, so I thought I could just take that code as a starting point. But it gave me a weird low base note, and very loud at that. Strange, that wasn’t there before?

Where the PCjr uses an actual Texas Instruments SN76496 chip, Tandy opted for a clone instead: an NCR 8496. No big deal, you may think. But there’s a catch: for sample playback you set the SN76496 to a period value of 0. This is an undocumented feature that results in an output of 0 Hz, so effectively the output is constantly high. You can modulate it with the volume register, and presto: we have a 4-bit DAC.

The NCR 8496 however, didn’t get that memo. When you write a period of 0 to it, it interprets it as a value of 400h instead, which is one higher than the maximum period value of 3FFh you can normally set (which is very similar to the various 6845-implementations that interpret a hsync width of 0 differently). So this results in a tone of (base frequency)/1024, instead of 0 Hz. That explains the loud base note I was hearing.

But wait, sample playback works on Tandy, doesn’t it? Didn’t Rob Hubbard use a sample channel in some of that excellent music he made for Tandy games? Well yes:

So how does that work then? I decided to study the code and see what it did exactly. And indeed, I found that it set a period of 1 instead of a period of 0. On the NCR 8496 this results in the highest possible frequency it can play, rather than the lowest possible frequency that you got with a period of 0. Since this frequency is well beyond the audible spectrum (over 100 kHz), you won’t actually hear it. But you can hear the modulation with the volume register, giving you the same DAC-like effect (this same trick is also used on the SAA1099 sound chip by the way, found on the CMS/GameBlaster, for example in the game BattleTech: The Crescent Hawk’s Revenge).

So once I figured out how the NCR 8496 and SN76496 are different, it was an easy enough fix. Apparently all my code was correct after all, I should just have set a period of 1 instead of a period of 0.

Sound Blaster..

Sound Baster? That was already supported, wasn’t it? Well yes, the 2.0 version. But as I discussed earlier, you can implement streaming playback on earlier Sound Blasters as well. It will not be entirely glitch-free, but you can get acceptable results.

In this case I just went for a quick-and-dirty approach. The thing is that the audio buffer size is directly related to the frame rate. That is, an audio buffer has the exact length of one frame. That is what makes the magic work in the original XDC player: the SB will generate an interrupt when the audio buffer has completed. The interrupt handler will then display a new frame. This means that the audio buffers are extremely short.

As I said earlier, that is suboptimal, because you get a glitch everytime you start a new buffer. So ideally you want the largest possible buffers, to reduce glitching to a minimum. However, for now I just went for a quick-and-dirty solution that just restarts the audio buffer at every interrupt. It was trivial to add this to the existing code. It will result in a glitch at every frame. However, this glitching is because of the time it takes to start a new buffer on the DSP. When I wrote the article, the assumption was that you use high sample rates. As I wrote in the article, sending a new command to the DSP took about 316 CPU cycles on an 8088 at 4.77 MHz, while a single sample at 22 kHz took 216 CPU cycles. Clearly, at lower sample rates, each sample takes longer, while the DSP time should remain constant. So that means the glitches become less obvious at lower rates, and perhaps won’t be noticeable at all, given a low enough rate.

I may revisit this code at some point, and modify the buffering of audio in a way that I can apply some of the ideas in my earlier article, by using a large DMA buffer. But for now at least SB 1.x works. And the code checks the DSP version, so it only uses this fallback when a 1.x SB is detected. SB 2.0 and higher should still work as they always did, so they are not compromised in any way by this addition.

So now what?

Well, one way to use this is to create new content, targeted specifically at lower sample rates that work better on these newly supported sound devices.

Another way is to run existing content on faster machines. 8088 Domination uses 22 kHz audio, which is a pretty bad case for bit-banged devices. But a reasonably fast 286 will have no problem with that. So you should be able to play back the 8088 Domination content as-is on a fast enough machine.

A third method is to downgrade the existing content. The audio buffers are stored at the end of each frame. It is trivial to downsample the buffers in-place. A turbo XT should be able to play the video content on PC speaker/Covox when the audio is downsampled from 22 kHz to something in the range of 8-11 kHz I would think.

And when you modify the audio anyway, you could also pre-apply the translation table, so it won’t have to be done at runtime, shaving off some precious CPU-cycles per sample, which will definitely improve performance. Some extra feature flags could be introduced to the feature-byte of the XDV-format to mark PWM-encoded or Tandy-encoded audio.

Anyway, feel free to experiment and have fun. See the next post for some of my results, and a modified version of 8088 Domination to toy around with.

This entry was posted in Oldskool/retro programming, Software development and tagged , , , , , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

7 Responses to Video playback on low-end MS-DOS machines

  1. Pingback: Some results from the modified XDC movie player for 8088/CGA | Scali's OpenBlog™

  2. maximzhao says:

    Hello from the world of Sega Master System emulation! We have an SN76489 variant and also play back samples. Have a look at “pcmenc” for optimising the data for higher-definition playback. The use of a very high frequency setting for sample playback works because at such high frequencies, the low-pass filter applied to the chip’s outputs (to smooth the 0-1 transitions down to normal waveform shapes) starts to act as a charge pump, maintaining a DC offset related to the amplitude of that (ultrasonic) high frequency waveform. The use of 0 vs 1 as the wavelength parameter doesn’t seem to make much difference.

    • Scali says:

      Ah yes, I read about this approach of using all three channels to get more than 4-bit accuracy. I wasn’t aware that there was already an encoder tool available for this. Thanks, this may be a good addition for a future update!

  3. Pingback: Futureseek Daily Link Review; 3 January 2024 | Futureseek Link Digest

  4. Zack Harrison says:

    This is awsome extra functionality, however I encountered a bit of an issue with the pc speaker support when I ran it on my IBM thinkpad 340 486slc2 laptop where there is strange static sounds that sounds like that of data being interpreted as audio playing over the sound, making it almost inaudable. I tested this in dosbox and the issue is still somewhat present (although to a much lesser extent) but persisted even with very low sample rates (8000hz). I havn’t tested low samples rates on real harware yet and will but though I may as well adress the core issue at hand first. The current sample rate used on the 486 was 41000 which it should be capable of playing back as it is a 50MHz CPU, way more powerful that an 8088. Am I doing something wrong? Anything, thanks for the amazing work and making this possible!

    • Zack Harrison says:

      Don’t worry, I fixed it. Its cause I was using MSDOS 6.22 but it works in MSDOS 4.0

      • Scali says:

        Oh right. Perhaps it’s not specifically the DOS version that is the problem, but rather the configuration. With newer versions of DOS and more advanced CPUs, you can use himem.sys, EMM386, UMBs and that sort of thing. It might be that some of that is messing with the timing, which may distort the audio.

Leave a comment