I finished bringing the Loser Corps port on the Pico on par with the ESP32, and then added sound output supporting 4 sounds plus music at the same time, with music coming from a MOD player I wrote from scratch for the Pico.

Sound Hardware

First, let's look at the hardware side. This is my homemade sound amplifier board:

Homemade amplifier using an  LM386
Homemade amplifier using an LM386

The input is at the 3-pin female header at the top (VCC, GND and input) and the output is at the 2-pin male header on the left (GND and output). This board is very similar to what I had in the last post in the breadboard, I just changed some capacitor values and moved the potentiometer for the volume control to go between the input coupling capacitor (now 2.2 μF) and the LM386. Here is the updated schematic:

Homemade amplifier using an `LM386`
Homemade amplifier using an `LM386`

One thing I didn't mention in the last post is that I tried using generic potentiometers for the volume control, but the metal casing added a lot of noise (this may be be a common thing, or it might be just because I live pretty close to some big radio towers, I have no idea). I ended up buying the sound table potentiometers you see in the photos, they're more expensive but work pretty well.

Sound Code

Now, the software side. The sound code is based on the setup I had on the last post, with the addition of MOD music playing. It can play music plus up to 4 other sounds at the same time.

The music is played with a simple MOD player I wrote from scratch for the Pico. The player supports only 4 channels (easily changed in a #define) and is currently missing a lot of MOD effects like tremolo and vibrato, but it's good enough to play a really good Axel F MOD pretty close to what VLC does, so I'm happy with it. I also tested it with the MOD files from Flashback (the game from 1992) and a few other MODs from modarchive.org.

The player modulates notes by streching or compressing the input samples to the required sample rate, always choosing the nearest input sample (think scaling an image using the "nearest" algorithm). It causes aliasing but it's very fast -- I use a fixed point number (20.12) to store the index of the current sample, and add fi/fo to it after playing each sample, where fi is the sample rate of the note and fo is the sample rate of the output sound (22050Hz). So if the MOD requests a sample to be played at 22050Hz, fi/fo would be 1 and the sample would be played without any modulation (i.e., each input sample would be sent exactly once to the output); if the frequency requested is 11025Hz (half), then fi/fo would be 0.5 and each input sample would be sent twice since it would take two additions to the sample index to advance to the next sample.

Of course things are a little more complicated than that, because MOD files store the sample frequency as periods of the original Amiga clock (7.09379 MHz), so there's some annoying math involved (the annoying part is in making sure we get a reasonable precision while avoiding overflow). To get the gory details, see the file game/lib/mod_player.c on Github.

I might improve the MOD player in the future, but I'm pretty happy with it for now.

Other than the MOD player, I added some sound effects. There's a small "bump" sound then the player character hits the ground (I recorded it by gently tapping a microphone some 20 years ago when making the game on PC) and a generic explosion sound when a shot hits something (taken from a free sound library from 20 years ago).

Other Code Improvements

And finally, not related to sound at all, a small improvement: I added some tile caching when drawing the screen to improve the speed, making sure the game never drops under 60 frames per second. Some parts of the map use too many different tiles, which used to cause problems because the CPU couldn't stream data from the flash fast enough to draw the entire screen in 16 miliseconds. So I copy some of the most-used tiles to RAM at the start, and that fixes the problem.

As usual, the code is on Github.