March 17, 2014

Building a WAV player, part 2

This is the continuation of my last post. Here is the source code we had last time, minus the huge array which you can find in the previous post:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdint.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#define F_CPU 8000000UL  // 8 MHz
#include <avr/delay.h>
#include <avr/interrupt.h>
volatile uint16_t sample = 0;
volatile int sample_count;
const uint16_t helloworld_length= 25000;

void init_pwm(void)
{
 CLKPR = (1 << CLKPCE);
 CLKPR = 0;
    // use OC1A pin as output 
    DDRB = 1 << PB1;

    /*
    * Use timer 1 for fast PWM, 8bit
    */
    
    /*
    * Prescaler: clk/1 = 8MHz
    * PWM frequency is 8MHz / 256 = 31.25kHz
    */
    
 TCCR1A = (1 << COM1A1)| (1 << WGM10);
 TCCR1B = (1 << WGM12) | (1 << CS10);
   
    OCR1A = 0;
 
    /* Setup Timer0 which changes the pwm duty cycle in its overflow interrupt */
 
    TCCR0B |=(1<<CS00);
    TCNT0=0;
    TIMSK0|=(1<<TOIE0);
    sample_count = 4;
    sei(); //Enable interrupts
}



ISR(TIMER0_OVF_vect)
{
  
         sample_count--;
         if (sample_count == 0)
            {
             sample_count = 4;         
             OCR1A = pgm_read_byte(&helloworld[sample++]);
             if(sample>helloworld_length){
     sample=0;
    }    
            }
}



int main(void)
{
   init_pwm();
   while(1);//do nothing
}


Now what does the code do? For definiteness, let us say the speaker is connected between 5V and ground. The huge array in the previous post is a small WAV file - a really low-quality one, at a sample rate of only 8 KHz and 8-bit encoding. For comparison, CD quality is 44 KHz at 16 bit encoding. A WAV file is just a long list of numbers, encoding the form of the soundwave we want to generate. Basically, the n-th number in our array is the sound pressure after n/8000 seconds since we use a 8 KHz WAV.

To reproduce the soundwave, we now just have to apply the correct votlages at the correct times: If the n-th number in our array is 0, there should be no voltage over the speaker, if it is 255, there should be 5V, if it is 128, we want 2.5V and so on. Our microcontroller cannot directly produce these voltages, and even if it could, it could not drive the speaker from these voltages. You could plug the number into a digital-analog-converter and then use an amplifier behind the converter to drive the speaker - this is the usual route, but requires more hardware.

We go another route, faking the voltages with PWM: When the number in our WAV file is 128, we will drive the transistor such that it is open half the time. With a suitable capacitor from the ground of the speaker to 5V (for me, 22 mikrofarad seem to work best), this is nearly the same as applying 2.5 V to the speaker directly.

We have to change the PWM duty cycle 8000 or even 16000 times per second, so each sample from the WAV file will be used in the duty cycle only a few times. Let us check the math for 8000 samples per second: We want to vary the PWM duty cycle between 0 and 255, so the length of one complete cycle has to be at least 256 clock cycles. If we want to use each sample only 4 times, we need 1024 clock cycles. Since we have to do it 8000 times per second, we already need an 8 MHz clock.

The above program does exactly that: the first two lines in the pwm_init() function


CLKPR = (1 << CLKPCE);
CLKPR = 0;

set the clock rate to 8 MHz. The next four lines set Pin B1 as output, which is the pin we can directly attach to timer 1 as a PWM pin, and set up timer 1 such that it is indeed in PWM mode and sets the duty cycle to 0 - the OCR1A register contains the duty cycle. Then we set up timer 0 to create an interrupt every 256 cycles. In the interrupt handler


ISR(TIMER0_OVF_vect)
{
  
         sample_count--;
         if (sample_count == 0)
            {
             sample_count = 4;         
             OCR1A = pgm_read_byte(&helloworld[sample++]);
             if(sample>helloworld_length){
     sample=0;
    }    
            }
}

we count down from 4 since we want to use each number in the WAV file four times. Once we have reached zero after 4 interrupt handlers, i.e. after 1024 clock cycles, we pass to the next number of the WAV file and make it our dutycycle for the next four PWM cycles, and so on. Finally, when we have reached the end of the WAV file, we start again at the beginning. The pgm_read_byte function is necessary since we had to store the helloworld-array in the flash memory since it is way too large for the RAM; this is what the PROGMEM keyword in the declaration of the array was for. The function is contained in the pgmspace.h header.

Now we can play actual soundfiles, but we are extremely restricted by memory: Even at 8 KHz, we can realistically at most fit 3.5 seconds of sound into 32 kB flash memory; that is not terribly useful. The easiest way for accessing more memory is via SD cards; in the next post, I will explain how to attach an SD card to our simple player.

2 comments: