VOGONS


First post, by byb

User metadata
Rank Newbie
Rank
Newbie

Hello,
I just created an account, and I hope I am not breaking any rules since this is a great forum.

I'm interested in recreating PC Speaker output using a microcontroller, specifically using a low-cost RPI Pico. I have documented my experimenting so far on the micropython forum, but unfortunately I haven't gotten any feedback, so I thought I would try here. Rather than copy and paste the entire contents, I'll just provide a link:
https://forum.micropython.org/viewtopic.php?f=21&t=12500

Currently, I cannot mimic the output exactly. I'm wondering where I might be going wrong. I'm essentially setting the Pico to use pulse width modulation on the output pins and chose a duration 7 ms for each tone.

  • What is the frequency response of the PC speaker? According to my imperfect measurements, it seems the lowest note is 76.3 Hz and the highest note is around 1,076 Hz or 2,178 Hz.
  • I just got a Hantek USB oscilloscope (Thanks to Adrian's Digital Basement's review), and am thinking about capturing the audio waveform from my PC (Keenwave/Windows 10 plays emulates the PC Speaker), and then comparing that against my RPI Pico implementation.
  • Has anyone ever tried this before?
  • Is there any documentation, such as a programmer's reference/composers from the 1980s which details how PC Speaker sounds were composed? Nostalgia Nerd created a decent primer video years ago touching on this subject:, and highlighted two interesting items (the Windows 3.1 PC Speaker sound driver, and Access Software's Real Sound) https://www.youtube.com/watch?v=ts069msIzg0

I realize this is probably not a topic that many people are interested in, but this would open up a new dimension of interaction for microcontroller projects.

Reply 1 of 3, by Tiido

User metadata
Rank l33t
Rank
l33t

PC speaker output comes from one section of the i8254 programmable interval timer, and the output frequency is (3.579545MHz / 3) / 16bit divider value that game etc. programs. Consult the i8254 datasheet for exact details as there are few operating modes, square wave is most useful but rate generator mode can also produce useful sounds. Lowest freq is ~18.2Hz and highest is ~596KHz.

The output of the timer is gated by another signal that comes from keyboard controller of the PC, allowing it to be muted or enabled. This is used to play samples sometimes when done fast enough, using another section in system timer that can produce periodic interrupts which handler will toggle this gate bit according to input data.

There are also some analog effects at play, coming mostly from the type of speaker used. The digital signal is connected to a speaker through a simple inverting driver in most implementations.

T-04YBSC, a new YMF71x based sound card & Official VOGONS thread about it
Newly made 4MB 60ns 30pin SIMMs ~
mida sa loed ? nagunii aru ei saa 😜

Reply 2 of 3, by byb

User metadata
Rank Newbie
Rank
Newbie

Thank you! I knew this was the right place to ask. It was suggested by another acquaintance that the PC Speaker was either handled by the 8253 or 8254 on the 8080, with the system timer there running at 1.19 MHz... maybe by the time the 386 came out the speed had increased? I'm hesitant to add another IC since the RPI Pico should be able to handle this. I'm just trying to reproduce simple square waves... I have no idea about what you mean by "rate generator", I mean, I read it on the datasheet and part of me thinks I should get some ICs and a crystal to experiment with them on a breadboard. Ultimately, it seems that harvesting the frequency information of PC speaker music from games by various publishers would need to be done, but not until I can accurately reproduce the sounds.

Now this page makes a little more sense: https://moddingwiki.shikadi.net/wiki/AudioT_Format page, but it currently is down for me and I cannot find it on archive.org, so I cannot verify the information. I also remember looking at the source code for how sounds were played, but I cannot find it now, not that I understood it anyway. From memory the Apogee format stores the frequencies as 8-bit values. I remember the text saying they were inverted (255 = lowest frequency, 1=highest frequency, 0=pause) and multiplied by 60 to get a 16 bit value. Human hearing works from 20 Hz to 18,000/22,000 Hz (depending upon age)... 596 KHz is huge. It seems that only about 27 % of the range is audible to humans. Programmers must have know this and decided they could get away with an 8 bit value. Maybe they also knew that the small PC speakers didn't react well to very low frequencies and decided to use 60, instead of say 83 or a higher value to get lower frequency ranges. Regardless, I remember from looking at the keenwave editor that most of the sounds were in the middle range.

It dawned on me that the RP2040 only has 16-bit PWM, which would only be "good" up to 65,535 Hz (or maybe half this, but that would still be enough for human hearing). https://docs.micropython.org/en/latest/librar … itations-of-pwm

Therefore, here are my calculations:

Lowest frequency = 3.579545 MHz / 3 = 1,193,181 / (255 * 60 = 15,300) = 77.98 Hz
Highest frequency = 3.579545 MHz / 3 = 1,193,181 / (1 * 60 = 60) = 19,886.35 Hz

Here's a simple Python script to output all the possible frequency values:

for x in range(1,255):
print((3579545/3)/(x*60))

The values 1-19 are all above 1,000 Hz, all other values are below that. I should really make a nice graph.
19886.361111111113
9943.180555555557
6628.787037037037
4971.590277777778
3977.2722222222224
3314.3935185185187
2840.90873015873
2485.795138888889
2209.595679012346
1988.6361111111112
1807.8510101010102
1657.1967592592594
1529.7200854700857
1420.454365079365
1325.7574074074075
1242.8975694444446
1169.7859477124184
1104.797839506173
1046.6505847953217

With this formula in hand, I tried it on the RPI Pico:

import machine
import time
speaker = machine.PWM(machine.Pin(16))
# Play Wolfenstein3D treasure sound #36
# These are the correctly decoded hex values
base = [69, 66, 64, 63, 62, 61, 60, 58, 57, 56, 54, 52, 51, 50, 49, 47, 46, 45, 44, 44, 45, 47, 49, 50, 52, 54, 58, 59, 60, 59, 57, 55, 52, 50, 46, 42, 40, 39, 38, 37, 36, 36, 35, 35, 37, 38, 40, 41, 43, 44, 44, 43, 42, 40, 34, 32, 31, 30, 29, 27, 26, 26, 26, 25, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 19, 19, 19, 19, 19, 0, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19, 0, 0, 0, 19, 19, 19, 19, 19, 19, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 0, 0, 0, 16, 16, 16, 16, 16, 16, 16, 16, 16, 0]
actual = []
tune = []
for x in base:
if x != 0:
tune.append(int(round((3579545/3)/(x*60))))
actual.append((3579545/3)/(x*60))
else:
tune.append(0)
actual.append(0)
print(tune)
print(actual)

def play_note(note):
if note == 0:
speaker.duty_u16(0)
else:
speaker.duty_u16(int(65535/2))
speaker.freq(note)
time.sleep(.0085)

for note in tune:
play_note(note)
speaker.duty_u16(0)

The "Actual" values list (the floating point values), although it seems the Pico is limiting the precision.
For example, on my PC, Python calculated value 69 as 288.2081320450886, not 288.2081

[288.2081, 301.3085, 310.7244, 315.6565, 320.7477, 326.0059, 331.4393, 342.8683, 348.8835, 355.1136, 368.2659, 382.43, 389.9286, 397.7272, 405.8441, 423.114, 432.3122, 441.9191, 451.9627, 451.9627, 441.9191, 423.114, 405.8441, 397.7272, 382.43, 368.2659, 342.8683, 337.0569, 331.4393, 337.0569, 348.8835, 361.5702, 382.43, 397.7272, 432.3122, 473.4848, 497.159, 509.9067, 523.3253, 537.4691, 552.3989, 552.3989, 568.1817, 568.1817, 537.4691, 523.3253, 497.159, 485.0332, 462.4735, 451.9627, 451.9627, 462.4735, 473.4848, 497.159, 584.8929, 621.4488, 641.4955, 662.8787, 685.7366, 736.5318, 764.86, 764.86, 764.86, 795.4544, 828.5983, 828.5983, 828.5983, 828.5983, 828.5983, 828.5983, 828.5983, 828.5983, 828.5983, 828.5983, 828.5983, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 0, 0, 0, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 0, 0, 0, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 1046.651, 0, 0, 0, 0, 0, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 0, 0, 0, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 1242.898, 0]

The "Actual" values wouldn't play because the RPI Pico PWM interfaces can only accept integers, not floats. So I converted them to integers. Interestingly, the int() command always rounds down, so this is why I used the round() command them to get a little closer to the actual frequency.
"Tune"

[288, 301, 311, 316, 321, 326, 331, 343, 349, 355, 368, 382, 390, 398, 406, 423, 432, 442, 452, 452, 442, 423, 406, 398, 382, 368, 343, 337, 331, 337, 349, 362, 382, 398, 432, 473, 497, 510, 523, 537, 552, 552, 568, 568, 537, 523, 497, 485, 462, 452, 452, 462, 473, 497, 585, 621, 641, 663, 686, 737, 765, 765, 765, 795, 829, 829, 829, 829, 829, 829, 829, 829, 829, 829, 829, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1047, 1047, 1047, 1047, 1047, 1047, 0, 0, 0, 1047, 1047, 1047, 1047, 1047, 1047, 1047, 1047, 0, 0, 0, 1047, 1047, 1047, 1047, 1047, 1047, 0, 0, 0, 0, 0, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 0, 0, 0, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 0]

When this played I did a double-take. It is very close my computer's playback. The higher frequencies at the end aren't quite right.

This is the closest I've gotten, but not exact because of the loss of precision from int to float. Part of it could be due to the emulated PC speaker being played back through my PC sound device compared to the 1 cm PC speaker; I don't have a DOS machine which I can test the playback with the same tiny 1 cm PC speaker I bought for 30 cents. It was also suggested to me in the micropython thread that I could use the RP2040's PIO interface since it has a 64-bit counter, which should enable more precise timing so I don't throw away everything after the decimal point, resulting in more accurate playback. I think I just need a bit or two more of precision.

I'll do some more comparisons of other tunes and see if my oscilloscope can count the frequencies. I'm just very glad to get farther along.

Reply 3 of 3, by Tiido

User metadata
Rank l33t
Rank
l33t

The speed has never changed, it would break compatibility with basically everything if it did, system timer would no longer be accurate and operating systems will fail in some ways, and pitch of all PC speaker ops will also be off.
Each timer in the chip has several operating modes, the two useful modes for sound generation are square wave mode and rate generator mode. Squarewave mode produces 50% duty cycle squares and rate generator only produces a short pulse every time counter overflows, allowing to produce a narrow duty cycle waveform that can be useful for SFX stuff. I'm not sure how many games actually use the rate generator mode but all definitely use squarewave mode.

If you are targetting some specific games you'll definitely want to approach the values the games will produce and try to get maximal accuracy to those. with hardware at hand.

But as far as generic PC speaker emulation goes, you definitely want to use some counter feature running at a high enough clock. A phase accumulator based setup should be able to get very close results as long as freqs requested don't get too close to the nyquist limit and will take minimal CPU time on the Pi also. You then only have to translate original divider value into phase accumulator increment value that produces close enough freq.

PCspeakerClock = 3579545 / 3 'PC speaker clock is 1/3 of NTSC subcarrier freq
Freq = PCspeakerClock / PCspeakerDivider 'Turn PC speaker divider value into a real frequency
PhaseInc = PhaseClock / Freq 'calculate phase increment value out of clock and needed freq

Loop this at PhaseClock frequency :
PhaseAcc = (PhaseAcc + PhaseInc) AND (2^AccSize) 'accumulator wraps around within its size constantly
Output = PhaseAcc SHR (AccSize-1) 'Output is top bit of the accumulator

This will give as accurate result as PhaseClock and accumulator width allows

T-04YBSC, a new YMF71x based sound card & Official VOGONS thread about it
Newly made 4MB 60ns 30pin SIMMs ~
mida sa loed ? nagunii aru ei saa 😜