• Please review our updated Terms and Rules here

Getting rid of the carrier wave (whining) on PWM for PC speaker

carlos12

Experienced Member
Joined
May 10, 2020
Messages
183
Location
Madrid, Spain
Hi there! A time ago I managed to reproduce music through the PC speaker. I just use this code:

Code:
        asm les     di,dword ptr[Voice.Sample]
        asm add     di,[SpeakerPCMCounter]
       
        asm mov     al,es:[di]
        asm out     42h, al

This is embedded on an Int 08h handler. I also ommited the PIT programming part. Well, it works reasonably well on anything barely superior to 8088 at 4.77 mhz, and not very bad on this one (somewhat slower and with a lower pitch). The sample is 8 bit mono and 7 khz. I chose that rate in order to be usable also by Mickey Sound Source.

The problem is the disgusting whining, that carrier wave. I knew that there's a method to reduce or even eliminate the carrier wave: just oversampling. But that would need to fire the PIT to 14 khz (for not even making the whining completely unaudible, as we need more than 16 khz). 14 khz is, at least in my experience, just too much for the poor 8088 and 8086. 21 khz (x3 oversampling) is something I don't even think about.

So I was wondering if is there another method to at least mitigate the whining without requiring that much CPU power.

Access' Crime Wave plays decently well on an 8086/8 mhz, and it's able to play music while doing the MCGA presentation. There's a slight carrier wave but not very noticeable (at least it's 100 times less annonying than mine). Also, Italy 90 (in my opinion a quite mediocre soccer game) reproduces music, quite feeble, but without a very noticeable ringing. Does anybody know what's their secret? Maybe that was the part that justified the patent by Real Sound? The official current DosBox build (0.74 today) does not emulate the carrier wave, but Dosbox-X does. 86Box also doesn't do it as of today.

Thank you all!
 
You may have to re-think how to drive the PWM signal. Interrupts may have just too much overhead. Michael Mahon pushed a 1 MHz 6502 to a pulse rate of 22 KHz: http://michaeljmahon.com/RTSynth.html

His runs entirely using scheduled code to interleave PWM generation with program logic. It is very impressive to hear in real life.
 
It should be possible to fire the PIT at 14kHz on a 4.77MHz 8088. In fact (with some trickery) it should even be possible to get pretty close to 21kHz. What are you doing in your interrupt routine? Can you do less and/or optimise it? Do you need to have enough cycles left over to do stuff in the foreground as well?
 
Thank you for your ideas, resman and reenigne!

Basically my interrupt routine is the code I posted first, plus this: SpeakerPCMCounter++;

and a few instructions to let the PIT the way it used to be when the sample ends.

Maybe my bottleneck is the les di, plus the add di,mem16, which happens on every byte of the sample. Too taxing for such processors... Mabe I should find a way to avoid them.

Do you need to have enough cycles left over to do stuff in the foreground as well?
I plan to have a more or less static presentation screen while the music plays. With 7 khz, in my tests on the gameplay (not the presentation), the computer freezes completely while playing the sample on an 8088 4.77 mhz (both emulators and real hardware), backing to normal when ends. But it does way better on an 8086 @ 8 mhz, just produces a little slowdown. On AT or higher, there's no noticeable slowdown. Anyway, that was only a test, the music during the gameplay will be done exclusively with Adlib and Tandy 3 voices.

For the SFX I'll reproduce the sample on 1 bit, so it is louder and also avoids the whining.

Out of curiosity, how did you avoid the whining on the 8088mph mod player? Thanks again!
 
Those 4 instructions take about 95 cycles. "inc word[SpeakerPCMCounter]" is probably another 37. It takes roughly 84 cycles to get from the instruction before the IRQ fires to the first instruction of the interrupt handler, and another roughly 44 cycles for an IRET. The End-of-Interrupt sequence ("mov al, 0x20", "out 0x20, al") takes 17 cycles. That adds up to 277 cycles, or 17.2kHz. You might be calling back into a previous interrupt handler instead of doing that directly, which can take a lot of time (especially if it's code that assumes it's only going to run 18.2 times per second). I'm guessing you're PUSHing all the CPU registers at the start of the handler and POPping them at the end. I'm also guessing that you're doing something to set up the DS register correctly to be able to access [Voice.Sample] and [SpeakerPCMCounter]. And detecting when you've reached the end of the sample. All of these things can be elided, optimised, or worked-around depending on how much trouble you want to go to.

The 8088 MPH mod player outputs samples at 16.6kHz (one every 288 CPU cycles). So it's a slightly audible carrier depending on the age of your ears, but higher pitched than the CRT's 15.7kHz horizontal flyback. It doesn't use interrupts at all, though, so it'll only play at the right speed on a 4.77 MHz 8088. https://www.reenigne.org/blog/8088-pc-speaker-mod-player-how-its-done/ is my writeup of all the trickery needed to mix 4 channels with independently variable frequencies and instruments, while decompressing the song and updating the screen, all in 288 cycles per sample.
 
I'm using Turbo C, so it generates automatically the code for the interrupt. My guess is yes, it pushes all the registers. I would have to generate assembler output to check it out, but I'm pretty sure it does... My code is inserted directly on the interrupt handler (void interrupt new_int8()), so at least I'm saving one call + stack + ret.

I'm going to study what optimizations I could do to save some cycles in order to being able to play a 14 or more khz sample.

Thanks!
 
Switching to pure assembly would definitely be a good first step. Turbo C will generate code that works for the general case* at the expense of being much slower for your specific case. Using the assembly code that Turbo C generates may be a good starting point, though.

* Well, usually - under some circumstances, Turbo C can generate buggy code for interrupts which can crash depending on what you do in the interrupt handler and what other interrupts are occurring on the machine.
 
Back
Top