Picante: Audio

My plan was always to do a SID-style synthesizer so I could make C64-style sounds on the picante.

I've managed to make a thing!





This is a software synth running on a RaspberryPi Pico attached to an Adafruit UDA1334 I2S module (recorded through the mic jack on my PC).

Getting the UDA1334 working

The official Adafruit guide was useful, but not perfect as it didn't cover the Pi Pico.  The micropython examples on how to play a tone and how to play a wav from an sd card were really useful though.

First, I got the play tone working, then I adapted the sd card (non-blocking) interrupt to play a continuous tone (which was just beautiful and didn't make my ears bleed at all).

Starting on the Synth

I then had to do a LOT of thinking about how I could make this synth fast enough and low-RAM enough for a Pi Pico.  After trying out a few combinations of parameters, I settled on a sampling rate of 16kHz and 16 bits per sample.  I wanted the synth to work on different waveforms (sine, square, triangle, sawtooth, noise), so I went for a standard interface of taking a uint16 for 'phase' and giving out the corresponding int16 sample value.  For square, triangle and sawtooth, the implementation was a fairly trivial calculation.

The Sine Waveform

To implement the sine waveform, I decided a raw look-up table would take up too much RAM, but a polynomial would be too CPU-intensive.  So I went for a horrible hybrid:

#define INTERP_BITS 11
#define INTERP_MASK ((1<<INTERP_BITS)-1)

void setupSineLUT() {
// initialise sine look-up table
sineLUT[ 0] = 0;
sineLUT[ 1] = 6392;
sineLUT[ 2] = 12539;
sineLUT[ 3] = 18204;
sineLUT[ 4] = 23169;
sineLUT[ 5] = 27244;
sineLUT[ 6] = 30272;
sineLUT[ 7] = 32137;
sineLUT[ 8] = 32767;
sineLUT[ 9] = 32137;
sineLUT[10] = 30272;
sineLUT[11] = 27244;
sineLUT[12] = 23169;
sineLUT[13] = 18204;
sineLUT[14] = 12539;
sineLUT[15] = 6392;
sineLUT[16] = 0;
}
int16_t sine(uint16_t phase) {
uint16_t idx = (phase >> INTERP_BITS) & 0xf;
int32_t delta = sineLUT[idx+1] - sineLUT[idx];
int32_t interp = phase & INTERP_MASK;
int32_t interpDelta = delta*interp;
int16_t value = sineLUT[idx] + (interpDelta>>INTERP_BITS);
if (phase & 0x8000) {
value = -value;
}
return value;
}

I chose 16 look-up values because that's the minimum that looked reasonable smooth on a LibreOffice Calc plot.

With 16 look-up values for half the wave-cycle, I can take the top bit to tell me whether the output should be negative or positive.  The next four most-significant bits tell me the index from the look-up-table to start with, and the final 11 bits tell me how much to interpolate by.

The Noise Waveform

Noise is just random, right? ...right?

Not on the C64 it's not.  You can actually change the pitch of the noise from a high hiss to a low crunch.  Playing around with random number generation achieved no usable results.

Then I found this page from the people that make SIDPLAY.  With some jiggery-pokery I managed to get something to work that sounds kinda like the C64, but it needs a bunch more work:


It's extra-problematic because the noise waveform requires internal state, which the other waveforms don't need.  So in the video you can hear the noise go double-speed when there are two noise waveforms playing.  :/

Pushing the Envelope

The SID implements an ADSR envelope (Attack-Decay-Sustain-Release).  Both the basic amplitude of the voice and its envelope mean multiplying the sample from the waveform by a number between 0 and 1.  To avoid floating point operations (because the Pico does them in software) I decided to use the integer range 0->32767 to represent 0->1.  Then it's an integer multiply-shift operation.  This is slightly inaccurate if the envelope factor is actually 1, but I don't care enough to do anything about it.

Frequency Modulation

Now this was a DOOZY to get my head around.  There were quite a few pages which I found useful, but I still spent several days pounding my head against the keyboard before I finally got where I wanted.  Things it took me a while to properly grasp:

Modulator amplitude does NOT relate to carrier amplitude.  Modular amplitude relates to how much the carrier frequency is changed by the modulator.

Modulator frequency should be an even multiple of the carrier frequency for nice sounds.  This means that when the primary note (frequency) changes, so should the modulator frequency.


Linear modulation (adding to/subtracting from the carrier frequency) is what is used for nice instruments.  Exponential modulation (multiplying the carrier frequency) also exists and can make very interesting sounds.


Not done yet...

With the more outlandish synth sounds, there were really high-pitch artefacts in the background of the notes.  I remembered the C64 having some filters you could set up and I figured a low-pass filter would get rid of those artefacts nicely.

It took some working through, but the method on this page is really fast and did everything I needed the filter to do.

In summary

This has been an epic project which still needs more work, but I'm really happy with the progress I've made.  I think I'm ready for making some kind of demo game and getting the messy code repo ready for some kind of release.








Comments

Popular posts from this blog

Micro:Bit and SPI display

DCS World with TrackIR under Ubuntu

Cardboard Mock-up of USB Joystick