1

I have a project involving using an AVR microcontroller (ATMega328PB), connected to a keyboard and small speaker, where each key should play a specific tone, like a piano. I have used the keyboard to call inputs to activate LEDs, but I don't know how to best play tones through the speaker. Can I play mp3 files of the tones? Or is it better/easier to have the buzzer play sine waves generated in a program? I am programming the board in C. Thank you!

I've worked with the board already, tried inputting the mp3 files in the same directory and using , but I can't figure out how to get them to be played through the programming.

  • 1
    I doubt MP3 is going to be practical - they are compressed and would require a lot of code to read and decompress them. You probably want raw sample values in an array. – pmacfarlane May 25 '23 at 11:16
  • This library will be very useful if you are just looking for a simple square wave output and you're OK with using pin PD3: https://github.com/pololu/pololu-buzzer-arduino – David Grayson May 25 '23 at 23:16

2 Answers2

0

I think you have to clarify what exactly type of sound you want.

If simple single-tone square wave is enough, then you can use a timer in CTC mode, toggle output on compare match. You can get sound output into the amplifier from corresponding OC pin. If you want sound frequency to match closer, I recommend to use TIMER1, since it has 16 bit resolution and allows to select frequency more accurate.

If you want to generate some more complex wave, make fading sound (ding), polyphony etc, then you have to use some kind of DAC and feed the waveform to it. ATMega328 has no DAC on the board, so, either you need some external DAC, or you may put one of the timers to work on the high frequency (more than 20kHz) in PWM mode, and connect its output to some kind of a filter (e.g. RC-filter), and then feed the output of the filter to the amplifier.

I recommend to use one of 8 bit timers for that. You need update OCR value at each cycle, so it is better to do it in the timer overflow interrupt.

Note, this approach is computational extensive. You have less than 256 CPU cycles to prepare next sample value (that's besides all other code you want to run in parallel). Floating point arithmetic should be avoided. If you want to generate some complex waveforms, or feed the sound from some external source (e.g. reading wav from a SD-card), then I recommend to use a circular buffer, which will be filled in main code, and interrupt will only read next value from the buffer into OCR register.

ATmega328 has no CPU power, neither RAM enough to decode MP3 or any other highly compressed format (also no enough internal flash, where those MP3s to be stored). If you want to use MP3 playback, then you need some external decoder.

AterLux
  • 4,566
  • 2
  • 10
  • 13
0

Starting with some assumptions,

  1. By keyboard you mean a piano-like keyboard, with which you are able to get the key number that has been pressed.
  2. The speaker is a conventional speaker attached to a DAC and amp.
  3. You have small sound tracks that you can play against each key, in .mp3 files.

As indicated in one of the comments, playing an mp3 would be unnecessarily complicated. Instead you can convert the mp3 files into .WAV files, which can be easily read and processed as raw audio signals.

The first thing that you'd wanna make sure is that all the WAV files can be stored into the RAM of the ATMega328. Because reading from the SD card each time would cause a lot of delay. If the files are too big, you can consider:

  1. Reducing size (hence quality) of each file
  2. Reducing the total number of files (have 8 keys instead of 24?)
  3. Having a function that generates the sound signal itself

Now, assuming that there are 8 files and they are stored in an SD card, they can be read and stored in an array like this before going into a loop

#define NUM_FILES      8     // maximum number of keys
#define MAX_FILE_SIZE  4096  // maximum size of sound files

uint8_t key_sounds[NUM_FILES][MAX_FILE_SIZE];

int main(void)
{
    // load sounds from SD card to RAM
    for (uint8_t file = 0; file < NUM_FILES; file++)
    {
        // assuming your files are named like "1.wav"
        char file_name[6] = {0};
        sprintf(file_name, "%02u.wav", file);
        
        // load data from file_name and copy to relevant key_sounds[]
        int err = load_from_sd_card(file_name, key_sounds[file]);
        if (err != 0)
        {
            printf("\rError happened!\n");
            return;
        }
    }
    
    // .... rest of your code, maybe an infinite loop

For your keys, you could have an ISR that reads which key is pressed, and set a global variable uint8_t key_pressed = 0U.

void your_isr()
{
    key_pressed = read_pressed_key()
}

and for your infinite loop, you can have code that continuously checks if the value of key_pressed has changed from 0 or not, and when it does, it plays the appropriate sound.

while (true)
{
    if (key_pressed != 0U)
    {
        // in case key_pressed changes during playback
        uint8_t key = key_pressed;
        
        // get address of sound data for that key
        uint8_t * p_sound = key_sounds[key_pressed];
        
        // send the address of sound data to playback function
        play_sound(p_sound);
        
        // reset key_pressed
        key_pressed = 0U;
    }
}

Do note that this is a very simplified explanation, the code for playing the sound and loading data from SD card is not shown here.