The PROGMEM directive tells the compiler to store these data into the Flash memory and to avoid copying them to RAM at startup (there would be no space for them!)

This creates a small complication, because AVR microcontrollers use the Harvard architecture: data and code have separate memory addressing spaces. It means that the above sampled audio data cannot be accessed with normal instructions.

(sorry for the limited width; I know this blog format is not well suited to code listings, but I am a guest here)

The most interesting thing here is the 16.8 pointer. What does that mean?

To resample while playing, we need to use a fractional increment for the pointer into the sampled sound data; in this case I used 16 bits of integer part (wavePtr) and 8 bits of fractional part (wavePtrFrac).

In other terms, the integer part points to the actual sample into the data (we cannot read between the data!) while the fractional part keeps track of the residual fraction that will be added on the next step.

The speed at which the sampled data is read is determined by another fractional value, this time with 8 bits of integer part and 8 bits of fractional part: waveStep. In short:

wavePtr = wavePtr + waveStep

using fixed-point values in 16.8 format.

Playing out a voice

C does not offer 24-bit fixed-point arithmetic, so it must be built using existing instructions (I could use 32-bit operations, but that would be horribly inefficient on an 8-bit processor).

To get the current sample for a voice, I use the pseudo-function call pgm_read_byte_near() to get from the sampled data array in Flash memory the value pointed by wavePtr:

//get current sample
sample = pgm_read_byte_near(vp->wavePtr);

Then I advance the 16.8 pointer by the fractionary 8.8 step(that determines the frequency (i.e. the musical note being played) and keep the 0.8 fractional rest:

WAVE_CYCLE is the number of samples making up a single cycle of the digitized wave. In practice, it continues to play the same cycle.

The sum of six voices

When I had the six ‘sample’ values for the six voices, my first idea was to add them up and send the result to a single PWM output (working as DAC). But I had a problem.

Each 8-bit voice can have 256 possible values, so the sum of six voices can have:

256 * 6 = 1536 possible values

If I wanted to use a PWM, I needed a timer that could count from 0 to 1535. No problem here: the timer 1 on the ATmega368P can count up to 65535.

If you remember the previous post, you may have guessed where my problem was.

The timer cannot count faster than the CPU clock, that is 16 MHz. So the PWM frequency, the maximum rate at which I could play the digitized samples, would have been:

16 MHz / 1536 = ~10417 Hz

Alas, 10.4 kHz is well within the audible range: a loud high-pitched sound would be audible. Filtering it out without cutting off some of the music ‘brillance’ would be quite problematic (more about filters in a future post).

Raising the PWM frequency

A better idea would have been to double the PWM frequency, using 768 possible values instead of 1526:

16 MHz / 768 = ~20833 Hz

A 20 kHz signal is easier to filter out without disturbing the music too much (we are not talking Hi-Fi here). But 768 possible values are not enough for six 8-bit voices!

I could have added the six values up and then thrown away the least significant bit (i.e. divided the result by 2)

(256 * 6) / 2 = 768 possible values

The first compromise

Actually, I initially decided for a different approach to get a bit more time for computations and a bit more quality: I set a PWM frequency of about 16 kHz, using 1024 (10 bits) as PWM count.

Then I reduced the volume of the sampled sound from 256 to 170 possibles value, so that the sum of the six voices could not exceed 1024:

1024 / 6 = 170 (discarding the fractional part)

It worked, as you can hear from the audio clip at the start of this post, but my son said:

“Dad, can you take out that whistling sound?”

Despite my filter, the 16 kHz from the PWM output was still audible. I could not hear it, but he could.

It was the start of a long optimization voyage… but that is another story.