Corsi base e avanzato per la creazione ebook: Genova e Torino La tua libreria
Scegli la tua newsletter
facebook

An earlier play-v6 version

I started working on play-v6 in the first days of December 2013 and in about three weeks I could play “Jingle Bells”: in time for Christmas :-)

That early version (450 git commits ago) had lots of limitations but reproduced my sampled guitar string sound on six voices.

My test actually used three voices:

Let us see how a voice was played.

The guitar sound in memory

The guitar string sound exported from Audacity was stored in a constant byte array in the code:

PROGMEM const int8_t samples[] = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

// [ ... ]

0xdf, 0xdb, 0xd8, 0xd5, 0xd2, 0xcf, 0xcc, 0xc9, 0xc5, 0xc3, 0xc0, 0xbd,
0xba, 0xb8, 0xb5, 0xb3, 0xb2, 0xb0, 0xaf, 0xae, 0xad, 0xad, 0xac, 0xab,
0xa9, 0xa9, 0xa7, 0xa6, 0xa6, 0xa6, 0xa6, 0xa7, 0xa7, 0xa8, 0xa9, 0xab,
0xad, 0xae, 0xb0, 0xb2, 0xb5, 0xb8, 0xbb, 0xbe, 0xc1, 0xc4, 0xc6, 0xc9,
0xcb, 0xcd, 0xcf, 0xd2, 0xd5, 0xd8, 0xdb, 0xde, 0xe2, 0xe5, 0xe8, 0xec,
0xef, 0xf3, 0xf6, 0xf9, 0xfd, 0x01, 0x05, 0x08, 0x0b, 0x0e, 0x10, 0x12,

// [ ... ]

};

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.

A fixed-point voice pointer

You may remember the theory about how to play a digitized sound at a different frequency. Here is an actual implementation from my early player.

This structure represents the playing status of a voice:

//status of a single voice, will point to wave samples in PROGMEM
typedef struct Voice
{
const int8_t   *wavePtr;        //current position (16.8)
uint8_t         wavePtrFrac;    //(no need for alignment padding, access is 8-bit)
const int8_t   *loopHere;       //loop back a PWM_CYCLE when wavePtr gets past this point
uint16_t        waveStep;       //wavePtr advance step (8.8), determines pitch
} Voice;

(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:

//advance to next sample (16.8 wavePtr + 8.8 waveStep):
//first add .8 fractional remainder from previous cycle to get 8.8 step
step = (uint16_t)vp->wavePtrFrac + vp->waveStep; //(0.8 + 8.8, no overflow)
//keep .8 for next cycle
vp->wavePtrFrac = step & 0xff;
//add 8. to wave pointer (16. + 8.)
vp->wavePtr += (step >> 8); //(logical shift is ok: step bit 15 is 0)

Last, I check for the end of the data array (the end of the guitar string sound) and, in that case, continue to play the same note. That is done to prolong the trailing string sound that I had to cut short for lack of memory space.

</pre>
//if end reached, enter 1-cycle loop
if (vp->wavePtr >= vp->loopHere)
{
vp->wavePtr -= WAVE_CYCLE;
}

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.

 

27. June 2015 by Erix
Categories: Arduino, Firmware / embedded, Learning | Leave a comment

Leave a Reply

Required fields are marked *