| 
  • If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

  • You already know Dokkio is an AI-powered assistant to organize & manage your digital files & messages. Very soon, Dokkio will support Outlook as well as One Drive. Check it out today!

View
 

Barebones MP3 Player

Page history last edited by David S 11 years, 9 months ago

What's Under the Hood 

To understand what’s going on under-the-hood of the barebones MP3 player, open the program and follow along. During this review, read the program’s comments to understand how and where to extend its functionality for your own players.

 

Program Structure

The Barebones MP3 Player program is structured into three modules:

 

  1. Song: the constants, pinout settings and functions to choose, open and play songs.
  2. Utilities: the routines that interact with the microSD card and serial (debug) output.
  3. Id3Tag: which finds (or at least tries to find) a song’s title, embedded in its id3 tag.

 

Each of these modules appears in a separate tab across the top of the interface. Let’s discuss them in turn.

 

1. Song Module

The Song module is where most of the MP3 player behavior takes place. Click on the Song tab.

 

The setup() Function

When the player starts up, it enters the setup function, which first initiates the serial connection, and then makes 5 function calls:

 

  • sd_card_setup: initializes communication with the microSD card file system (in the Utilities module).
  • Mp3.begin: sets up the player to work with the chip select, reset and data request pins (in the Mp3 library).
  • Mp3.volume: sets the player’s initial output volume (in the Mp3 library).
  • sd_dir_setup: finds the audio files on the microSD card and stores their names in EEPROM (in the Utilities module).
  • sd_file_open: opens the file on the microSD card for the song that’s about to play (in the Song module).

 

 

We’ll cover these functions in more detail in a moment, but for now, that’s all the setup that the player needs.

 

The loop() Function

Next, the player enters the loop function, which includes (only!) a switch statement that references the current_state of the player. current_state is a variable of type state, an enumeration of all possible player states.

 

As this suggests, the player is structured as a state machine, implemented through the switch statement. It has 3 initial states:

 

  • DIR_PLAY: plays all of the songs in a directory by calling dir_play, then stops.
  • MP3_PLAY: plays only a single chosen song by calling mp3_play, then stops.
  • IDLE: just stops.

 

 

Try to find where in the Song module state and current_state are initialized, and what that state is. Given that, how do you think the player will behave when it starts up?

 

The other functions in the Song module, which we briefly mentioned earlier, are sd_file_open, mp3_play and dir_play.

 

The sd_file_open() Function

This function opens the file on the microSD card associated with the song that you want to play. It does so using 2 function calls:

 

  • map_current_song_to_fn: finds the file name of the current song (in the Utilities module).
  • sd_file.open: opens the file that was just found and returns a handle to it (in the Arduino SD library).

 

The returned handle sd_file is a variable of type SdFile, a data structure in the Arduino SD library that represents a file. Take a moment to find where in the Song module sd_file is defined.

 

So how does the player know what song you want to play? It keeps track of the variable current_song, initialized earlier in the Song module.

 

The mp3_play() and dir_play() Functions

The mp3_play function plays all the way through a single song and then stops. It makes 3 new function calls along the way:

 

  • sd_file.read: reads an array of bytes from a buffer (more on this in a moment) using the sd_file handle (in the Arduino SD library).
  • Mp3.play: sends those bytes to the library’s play function, which forwards them on to the Mp3 decoder chip (in the Mp3 library).
  • sd_file.close: closes the file that the sd_file handle references (in the Arduino SD library).

 

The program logic of mp3_play is pretty straightforward: when there are no more bytes_to_read, close the file and change the player state to IDLE.

 

 

The dir_play function plays all of the songs in a directory. As you might expect, it calls the mp3_play function (and the sd_file_open function) for each song to play.

 

The program logic of dir_play deserves some explanation. mp3_play closes the file and sets the current_state to IDLE when a song is finished playing. dir_play therefore has to recognize when the current_state is IDLE AND there are still unplayed songs in the directory. In that case, it opens the next song file, and resets the current_state back to DIR_PLAY.

 

Decoder, mp3_play() and Mp3.play() Functions

The VS1033d decoder chip and the (local) mp3_play and (library) Mp3.play functions cooperate to help the player play songs without interruption. Here’s how.

 

The decoder receives audio data sent to it over a serial connection and stores it on a 2,048 byte input buffer. If you just send the chip 2,048 bytes of data, it will play, but then the input buffer will be empty. And because it takes a moment to fetch new audio data, letting the input buffer fully empty before refilling it leads to breaks in playback.

 

To address that, the decoder chip has an output pin called data request (or dreq) that indicates when the input buffer can accept just 32 bytes of data. By fetching new audio data every 32 bytes instead of every 2,048 bytes, the input buffer can be kept nearly full, all of the time.

The Mp3 library’s Mp3.play function does just that, but it doesn’t fetch each batch of new audio data directly from the microSD card. Instead, it gets it from an output buffer maintained by the local mp3_play function.

 

That’s because the microSD card is a lot slower than SRAM. Therefore, we prefer to read data from the card fewer times, but to capture more data on each read. And that’s what mp3_play does.

 

Here’s what happens: mp3_play reads 512 bytes at a time from the microSD card and stores that in the read_buffer variable. Mp3.play then reads 32 bytes at a time from read_buffer, and forwards that on to the decoder chip whenever the dreq line goes high. This continues until there are no more bytes to be read from the read_buffer.

 

2. Utilities Module

The Utilities module is where most of the microSD card file and directory tasks take place. Click on the Utilities tab.

 

The sd_card_setup() Function

This function is only called once, when the player begins, from the setup function of the Song module. It makes several new function calls, all of which are located in the Arduino SD library:

 

  • SD.begin: checks that the microSD card is responding as expected.
  • SD.open: opens the top-level directory ("root", or "/") on the card.


 

Note how SD.begin sends sd_cs as a parameter. This is the microSD card’s chip select line, which is set at the top of the Song module, along with all of the other pin definitions. sd_root is a File type object that points to the top-level directory of our microSD card.

 

The sd_dir_setup() Function

The FAT16 file system installed on the microSD card makes organizing directories and files straightforward, but adds overhead. As a result, retrieving data from the microSD card is slower than reading the same data from EEPROM. sd_dir_setup copies the song file names from the card to EEPROM, which improves performance.

 

This function is also only called once, when the player begins, from the setup function of the Song module. It makes 2 new function calls, both located in the Arduino SD library:

 

  • sd_root.rewindDirectory: moves (rewinds) the file index pointer for the directory back to the first file.
  • sd_root.openNextFile: increments the file index pointer and then reads the name of the current file in the directory.

 

At the top of sd_dir_setup, we define a local variable p of type File. This is a data structure that represents a file in the Arduino SD library. p.name is therefore an array (of bytes) that holds the name of the file p.


 

Programs that run on PCs have a lot of computational resources to figure out the file type from characteristics of the file itself. Since we have little resources, we just check whether the file extension is .MP3 or .WAV (although you can add others).

 

The sd_dir_setup function loops over all of the files in the sd_root directory, noting which ones have an .MP3 or .WAV file extension, and copies those names into EEPROM. This creates our handy list of file names that we can access without having to read the file system again.

 

The map_current_song_to_fn() Function

Because the current_song is just a number that represents a position in the song list that’s stored in EEPROM, this function returns the file name of the current_song.

 

The other 2 functions in the Utilities module, print_title_to_serial and print_status_info are for debugging and are pretty straightforward. Take a look at them now, so you know what to expect when you enable DEBUG mode (a #define in the Song module).

 

3. Id3Tag Module

The Id3Tag module consists of one function, get_title_from_id3tag, which scans the song file for one of several types of Id3 tag. Specifically, it’s looking for the song’s title. Click on the Id3Tag tab.

 

 

Id3 tags started out as simple metadata tacked on to the end of song files, describing the title, artist and length. But several iterations of the standard tried to address shortcomings in earlier versions and added more metadata. Now, older types of tags often co-exist with newer types of tags (even within the same file) in several different file locations.

 

To get around that, the get_title_from_id3tag function scans through the file, byte-by-byte, looking for a 3-4 letter header that indicates that a general Id3 tag (“ID3” or “TAG”) or a title-specific tag (“TT2” or “TIT2”) exists. It only makes 1 unfamiliar function call from the Arduino SD library:

 

  • sd_file.seek: moves to a specific position within the file (to read data someplace other than the beginning of the file if desired).

 

Once a tag is found, the function reads the next few characters, which represent the length of the title field. These characters are converted to an integer by shifting bits into their correct “digits” positions.

 

The function then stores the title in a global variable named fn, which is defined in the Song module. 

 

Please read through the module now; it’s heavily commented. Ask a member of the teaching team if you have questions about how something works! 

Comments (0)

You don't have permission to comment on this page.