Mimic PC Speaker Output (not Adlib or Sound Blaster)

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
Post Reply
bai-yi-bai
Posts: 2
Joined: Mon Jun 06, 2022 10:17 am

Mimic PC Speaker Output (not Adlib or Sound Blaster)

Post by bai-yi-bai » Mon Jun 06, 2022 10:37 am

Hello,
I was encouraged to post here by Peter Hinch.

I would like to make a library which plays all sorts of PC speakers sounds from DOS games (not Adlib or Sound Blaster), starting with Apogee Software's Wolfenstein 3d. Peter said, "The way I would approach this is to use the PIO to emulate the original Assembler code in your link. Using the FIFO would enable a nonblocking driver."

I have written a small uasyncio code to output a series of frequencies using PWM/duty_u16. I wrote about it here:
https://github.com/gurgleapps/musicode/issues/1

Example video: https://www.youtube.com/watch?v=5v36e4_ ... Zdenda1990

Shareware link: https://archive.org/details/Wolfenstein3d

I tried decoding the audio format, but I don't seem to be able to get the frequencies correct. My code seems to play notes in the right sequence. https://moddingwiki.shikadi.net/wiki/AudioT_Format

I ended up resorting to using a tool called keenwave to playback the sounds for reference. Using the .dat output from keenwave, I confirmed that my Python code did extract the sound chunks correctly. There's a link in this thread to download an old version: http://keenmodding.org/viewtopic.php?t=6223

I'm not sure if this is a problem with the cheap speaker I'm using, if I converted the values incorrectly (LSB invert the values... how?), or if something wrong my with timing. I also couldn't get it running using the Python winsound library. 140 Hz should be ~7 ms, but it isn't right. I also tried using Audacity to examine a waveform, but that was not helpful.

# Hex values for sound 36 in Wolfenstein 3D... (I'm not sure where this stands on copyright law)
# 95 00 00 00 46 02 45 42 40 3F 3E 3D 3C 3A 39 38 36 34 33 32 31 2F 2E 2D 2C 2C 2D 2F 31 32 34 36 3A 3B 3C 3B 39 37 34 32 2E 2A 28 27 26 25 24 24 23 23 25 26 28 29 2B 2C 2C 2B 2A 28 22 20 1F 1E 1D 1B 1A 1A 1A 19 18 18 18 18 18 18 18 18 18 18 18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 13 13 13 13 13 13 00 00 00 13 13 13 13 13 13 13 13 00 00 00 13 13 13 13 13 13 00 00 00 00 00 10 10 10 10 10 10 10 10 10 10 10 00 00 00 10 10 10 10 10 10 10 10 10 00

Here's the song using gurgleapp's non-async version, since this doesn't have notes assigned to the frequencies

Code: Select all

tune = [0, 0, 0, 4200, 120, 4800, 4680, 4560, 4500, 4320, 4200, 4080, 3960, 3840, 3720, 3600, 3480, 3420, 3360, 3300, 3240, 3240, 3240, 3240, 3360, 3480, 3540, 3600, 3720, 3780, 3840, 3900, 3900, 3900, 3840, 3840, 3840, 3720, 3600, 3540, 3480, 3420, 3360, 3300, 3240, 3180, 3060, 3000, 2940, 2820, 2760, 2700, 2640, 2640, 2580, 2580, 2580, 2580, 2580, 2580, 2580, 2640, 2880, 2940, 3060, 3060, 3120, 3060, 3060, 3000, 2940, 2880, 2820, 2640, 2520, 2400, 2280, 2160, 2040, 1920, 1860, 1800, 1800, 1740, 1740, 1680, 1680, 1680, 1740, 1800, 1920, 1980, 2040, 2100, 2100, 2160, 2100, 2040, 1920, 1860, 1740, 1620, 1500, 1440, 1260, 1140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1080, 1080, 1080, 1080, 1080, 1080, 1080, 1080, 0, 0, 0, 1080, 1080, 1080, 1080, 1080, 1080, 1080, 0, 0, 1080, 1080, 1080, 1080, 1080, 1080, 1080, 0, 0, 0, 0, 0, 0, 0, 0, 960, 960, 960, 960, 960, 960, 960, 960, 960, 0, 0, 0, 960, 960, 960, 960, 960, 960, 960, 960, 960, 0, 0, 0, 960, 960, 960, 960, 960, 960, 960, 960, 1020, 1020, 0, 0, 0, 0, 0, 0, 900, 900, 900, 900, 900, 900, 900, 900, 900, 0, 0, 0, 900, 900, 900, 900, 900, 900, 900, 900, 0, 0]

speaker = machine.PWM(machine.Pin(8))

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

for note in tune:
    play_note(note)
speaker.duty_u16(0)
Now that I think about it, I could possibly use Keenwave to manually create my own sound, using the interface to assign maximum and minimum values and compare their hexadecimal values. Maybe I can use a tuning app on my smartphone to determine the frequency to see if the problem is with my equipment (and I really need an amplifier and a better speaker).

bai-yi-bai
Posts: 2
Joined: Mon Jun 06, 2022 10:17 am

Re: Mimic PC Speaker Output (not Adlib or Sound Blaster)

Post by bai-yi-bai » Thu Jun 09, 2022 5:08 am

Update

Here's what I found:
  • I had incorrectly converted the hex values to frequencies. I'd forgotten that the value 0 = rest.
  • I chose one sound chunk to experiment with, set aside all the audio header/data extraction code, and relocated the code to the Pico so I can more quickly test tuning the frequencies.
  • I wrote a simple algorithm to calculate the inverted values to frequencies. It may not be correct.
  • I tried using Keenwave's sound chunk editor to create one sound each with only the highest and lowest pitch values. I played that on my PC through the Windows 10 sound system, which offers DOS program PC Speaker output. Then I recorded those on my smartphone and used a pitch finder to determine what the lowest and highest pitches are.
    The lowest note is FF (255): 76.3 Hz D#2
    The highest note is 01 Hex (and decimal) is either 1,076 Hz or 2,178 Hz (I couldn't get an accurate reading)
  • This lowest frequency tells me that there must be a frequency "floor" which I need to account for in my code, otherwise they will be inaudible.
Problems
  • I'm not sure what the frequency response range of a DOS program outputting sound to a PC Speaker. Is it 1,000 Hz or 2,000 Hz? Or is it larger?
  • I'm not sure I'm doing the calculations correctly. There is something about "taking the inverse frequency and multiplying by 60" to get a 16 bit number... I'm working with the 8-bit data, multiplying by 60 doesn't add any additional data, it just stretches out the distance between the pitches further and increases the frequency range.
  • I might also be missing something special about PC timing.
  • Assuming 8 bits, there are 254 steps between the highest pitch and the lowest pitch. I'm also assuming that this is as linear scale. I calculate the step size depending upon set frequency range. With a 76-1076 Hz range, this gives approximately 3.95 Hz between each of the 254 pitches.
Result
I do get output which does match the progression of notes, and my timing is close enough, but the timbre (the character of the sound), doesn't match my PC's emulated PC Speaker output. Some notes are very close. This could be because I'm just using a very cheap 1 cm speaker, which is the fraction the size of PC speakers from 30 years ago. It also isn't being driven correctly, I don't know the rating of the speaker (4 or 8 Ohm). It's running at 3.3 V w/100 Ohm resistor, instead of say 5 V w/33 Ohm resistor. I'll try getting a dedicated LM368 IC to see if that will help.

Code: Select all

import machine
import time

# Play Wolfenstein3D treasure sounds
# 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]

# Tune these values
y = 76 # lowest frequency
z = 1970 # highest frequency

# Determine the step size
stepsize = (z-y)/254

print(stepsize)
tune = []
for x in base:
    #print(x)
    result = int((z - ((x-1)*stepsize))-y)
    # print(result)
    if x==0: # If the value is 0, it means it's a rest, don't play anything
        tune.append(0)
    if x!=0:
        tune.append(result)
print(tune)

speaker = machine.PWM(machine.Pin(8))

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

Post Reply