| 
  • 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
 

MP3 Player Final Project Documentation

Page history last edited by dhruval.trivedi@... 8 years, 7 months ago

The Project:

 

                    The project is an MP3 player designed for the convenience of people who listen to music while exercising. The design is a box structure with a neck strap to be worn without the use of hands. The MP3 player itself consists of buttons on different sides of the box. Each side of the box facing away from the user houses a button that performs a specific function. The placement of the buttons is constructed for ease-of-use. The top button switches to the next song in the list, the bottom switches to the previous one. The button facing away from the user is the button that controls the "pause" and "play" functions of the player. The placement of the buttons is intuitive and simplistic for the sake of the user. The user will likely be exhausted from exercise while changing songs, so the last action they wish to take is stopping and thinking about the correct inputs to use with their player. 

 

Original design (note that this contains a screen, which I later omitted):

 

 

Here is the backup plan I resorted to when my original plan failed:

 

 

Here is the state diagram of the device (the "display songs feature requires a screen that I chose not to use"):

 

 

Here is the Verplank Diagram I constructed for the player:

 

 

Here is the code I used for my project:

 

PART 1:

 

/*

 * example sketch to play audio file(s) in a directory, using the mp3 library

 * for playback and the arduino sd library to read files from a microsd card.

 * pins are setup to work well for teensy 2.0. double-check if using arduino.

 * 

 * originally based on frank zhao's player: http://frank.circleofcurrent.com/

 * utilities adapted from previous versions of the functions by matthew seal.

 *

 * (c) 2011, 2012 david sirkin sirkin@cdr.stanford.edu

 *                & akil srinivasan akils@stanford.edu

 */

 

// ---- includes and defines ------------------------------------------------

 

// first step is to include (arduino) sd, eeprom and (our own) mp3 libraries.

 

#include <SD.h>

#include <SPI.h>

#include <EEPROM.h>

 

#include <mp3.h>

#include <mp3conf.h>

#include <Bounce2.h>

 

// include the adafruit pcd8544 & gfx libraries for a nokia 5110 graphic lcd.

 

#include <Adafruit_GFX.h>

#include <Adafruit_PCD8544.h>

 

// setup microsd and decoder chip select pins, and the decoder-specific pins.

 

#define sd_cs         17        // 'chip select' line for the microsd card

 

#define mp3_cs        A0        // 'command chip select' connect to cs pin

#define mp3_dcs       A1        // 'data chip select' connect to bsync pin

#define mp3_rst       -1        // 'reset' connects to decoder's reset pin

#define mp3_dreq      A2        // 'data request line' connect to dreq pin

 

// now assign pins for the graphic lcd (carried over from the etch-a-sketch).

 

#define lcd_dc        A4        // 'data/command input' connect to d/c pin

#define lcd_cs        A3        // 'slave chip select' connects to  cs pin

#define lcd_rst       -1        // 'reset' connects to graphic lcd rst pin

 

// 'read_buffer' is the amount of data read from microsd and sent to decoder.

// it's probably best to keep this a factor of 2, up to about 1kb (2kb is the

// max). you might change this if you experienced skips during song playback.

 

#define read_buffer  512        // size (bytes) of the microsd read buffer

#define mp3_vol      200        // default volume. range min=0 and max=254

 

// file names are 13 bytes max (8 + '.' + 3 + '\0'), and the file list should

// fit into the eeprom. for example, 13 * 40 = 520 bytes of eeprom are needed

// to store a list of 40 songs. if you use shorter file names, or if your mcu

// has more eeprom, you can change these.

 

#define max_name_len  13

#define max_num_songs 40

 

// id3v2 tags have variable-length song titles. that length is indicated in 4

// bytes within the tag. id3v1 tags also have variable-length song titles, up

// to 30 bytes maximum, but the length is not indicated within the tag. using

// 60 bytes here is a compromise between holding most titles and saving sram.

 

// if you increase this above 255, look for and change 'for' loop index types

// so as to not to overflow the unsigned char data type.

 

#define max_title_len 60

 

// Define the interrupt button pins

 

#define BUTTON_PIN1 3

#define BUTTON_PIN2 2

#define BUTTON_PIN3 0

 

// ---- global variables ----------------------------------------------------

 

// instantiate a graphic lcd object using the pins that we #define'd earlier.

// comment out the graphics lines to save memory if you're not using the lcd.

 

// Adafruit_PCD8544 lcd = Adafruit_PCD8544(lcd_clk, lcd_din, lcd_dc, lcd_cs, lcd_rst);

Adafruit_PCD8544 lcd = Adafruit_PCD8544(lcd_dc, lcd_cs, lcd_rst);

 

// 'File' is a wrapper of the 'SdFile' data type from the sd utility library.

 

File sd_file;                   // object to represent a file on a microsd

 

// store the number of songs in this directory, and the current song to play.

 

unsigned char num_songs = 0, current_song = 0;

 

// an array to hold the current_song's file name in ram. every file's name is

// stored longer-term in the eeprom. this array is used in 'sd_file.open()'.

 

char fn[max_name_len];

 

// an array to hold the current_song's title in ram. it needs 1 extra char to

// hold the '\0' that indicates the end of a character string. the song title

// is found in 'get_title_from_id3tag()'.

 

char title[max_title_len + 1];

 

// the program runs as a state machine. the 'state' enum includes the states.

// 'current_state' is the default as the program starts. add new states here.

 

enum state { DIR_PLAY, MP3_PLAY, PAUSED };

state current_state = MP3_PLAY;

 

Bounce debouncer1 = Bounce(); 

Bounce debouncer2 = Bounce();

Bounce debouncer3 = Bounce();

//---- module functions -----------------------------------------------------

 

// you must open a song file that you want to play using 'sd_file_open' prior

// to fetching song data from the file. you can only open one file at a time.

 

void sd_file_open() {  

  // first, find the file name (that's stored in eeprom) of the current song.

 

  get_current_song_as_fn();

 

  // then open the file using the name we just found (stored in 'fn' global).

 

  sd_file = SD.open(fn, FILE_READ);

 

  // find the current song's title tag (if present) then print it to the lcd. 

 

  print_title_to_lcd();

}

 

// read a number of bytes from the microsd card, then forward them to the Mp3

// library's 'play' function, which streams them out to the decoder chip.

 

void mp3_play() {

  unsigned char bytes[read_buffer]; // buffer to read and send to the decoder

  unsigned int bytes_to_read;       // number of bytes read from microsd card

 

  // first fill the 'bytes' buffer with (up to) 'read_buffer' count of bytes.

  // that happens through the 'sd_file.read()' call, which returns the actual

  // number of bytes that were read (which can be fewer than 'read_buffer' if

  // at the end of the file). then send the retrieved bytes out to be played.

 

  // 'sd_file.read()' manages the index pointer into the file and knows where

  // to start reading the next batch of bytes. 'Mp3.play()' manages the index

  // pointer into the 'bytes' buffer and knows how to send it to the decoder.

 

  bytes_to_read = sd_file.read(bytes, read_buffer);

  Mp3.play(bytes, bytes_to_read);

 

  // 'bytes_to_read' is only smaller than 'read_buffer' when the song's over.

 

  if (bytes_to_read < read_buffer) {

    sd_file.close();

 

    // if we've been in the MP3_PLAY state, then we want to pause the player.

 

    if (current_state == MP3_PLAY) {

      current_state = PAUSED;

    }

  }

}

 

// continue to play the current (playing) song, until there are no more songs

// in the directory to play. 2 other sd library methods (that we haven't used

// here) can help track your progress while playing songs: 'sd_file.size()' &

// 'sd_file.position()'. you can use these to show say, the percent of a song

// that has already played.

 

void dir_play() {

  if (sd_file) {

    mp3_play();

  }

  else {

    // since 'sd_file' isn't open, the recently playing song must have ended.

    // increment the index, and open the next song, unless it's the last song

    // in the directory. in that case, just set the state to PAUSED.

 

    if (current_song < (num_songs - 1)) {

      if(current_song < 0){

         current_song = 0;  

      }

      current_song++;

      sd_file_open();

      current_state = MP3_PLAY;

    }

 

 

    else {

      current_state = PAUSED;

    }

  }

}

 

void pausePlay(){

  if(current_state == PAUSED){

     current_state = MP3_PLAY;  

  }else if(current_state == MP3_PLAY){

     current_state = PAUSED;  

  }

}

 

void nextSong(){

  //current_state = PAUSED;

  //Mp3.clear_buffer();

  sd_file.close();

  current_state = DIR_PLAY;

  //Serial.println("High");

  //if (current_song < (num_songs - 1)) {

      //current_song++;

      //sd_file_open(); 

  //}

}

 

void previousSong(){

  //current_state = PAUSED;

  //Mp3.clear_buffer();

  sd_file.close();

  //if (current_song > -2) {

      current_song -= 2;

      current_state = DIR_PLAY;

      //sd_file_open();

  //}

}

// ---- setup and loop ------------------------------------------------------

 

// setup is pretty straightforward. initialize serial communication (used for

// the following error messages), mp3 library, microsd card objects, then the

// graphic lcd. then open the first song in the root library to play.

 

void setup() {

  Serial.begin(9600);

  // if using a graphic lcd, initialize with contrast, then setup the screen.

 

  lcd.begin(55);

  lcd.display();

  delay(500);

 

  lcd.clearDisplay();

  lcd.print("Barebones Mp3!");

  lcd.display();

  delay(500);

 

  // initialize the mp3 library, and set default volume. 'mp3_cs' is the chip

  // select, 'dcs' is data chip select, 'rst' is reset and 'dreq' is the data

  // request. the decoder sets the 'dreq' line (automatically) to signal that

  // its input buffer can accommodate 32 more bytes of incoming song data.

 

  // the decoder's default state prevents the spi bus from working with other

  // spi devices, so we initialize it first.

  Mp3.begin(mp3_cs, mp3_dcs, mp3_rst, mp3_dreq);

  Mp3.volume(mp3_vol);

 

  // initialize the microsd (which checks the card, volume and root objects).

 

  sd_card_setup();

 

  // putting all of the root directory's songs into eeprom saves flash space.

 

  sd_dir_setup();

 

  // the program is setup to enter DIR_PLAY mode immediately, so this call to

  // open the root directory before reaching the state machine is needed.

 

  sd_file_open();

 

  attachInterrupt(0, pausePlay, RISING);

  attachInterrupt(1, nextSong, RISING);

  attachInterrupt(2, previousSong, RISING);

 

  //debouncer1.attach(BUTTON_PIN1);

  //debouncer1.interval(100);

  //debouncer2.attach(BUTTON_PIN2);

  //debouncer2.interval(100);

  //debouncer3.attach(BUTTON_PIN3);

  //debouncer3.interval(100);

}

 

// the state machine is setup (at least, at first) to open the microsd card's

// root directory, play all of the songs within it, close the root directory,

// and then stop playing. change these, or add new actions here.

 

// the DIR_PLAY state plays all of the songs in a directory and then switches

// into PAUSED when done. the MP3_PLAY state plays one specific song and then

// switches into PAUSED. this sample player doesn't enter the MP3_PLAY state,

// as its goal (for now) is just to play all the songs. you can change that.

 

void loop() {

  switch(current_state) {

 

    case DIR_PLAY:

      dir_play();

      break;

 

    case MP3_PLAY:

      mp3_play();

      break;

 

    case PAUSED:

      break;

  }

  //debouncer1.update();

 

  // Get the updated value :

  /*int value1 = debouncer1.read();

  if(value1 == HIGH){

    //Serial.println("High");

    pausePlay();

  }*/

  /*

  debouncer2.update();

 

  int value2 = debouncer2.read();

  if(value2 == HIGH){

    //Serial.println("High");

    nextSong();

  }

 

  debouncer3.update();

 

  int value3 = debouncer3.read();

  if(value3 == HIGH){

    //Serial.println("High");

    previousSong();

  }*/

  // EEPROM.write(EEPROM.length() - 1, 250);

  // Mp3.volume(EEPROM.read(EEPROM.length() - 1));

}

 

Part 2:

 

/*

 * example sketch to play audio file(s) in a directory, using the mp3 library

 * for playback and the arduino sd library to read files from a microsd card.

 * pins are setup to work well for teensy 2.0. double-check if using arduino.

 * 

 * originally based on frank zhao's player: http://frank.circleofcurrent.com/

 * utilities adapted from previous versions of the functions by matthew seal.

 *

 * (c) 2011, 2012 david sirkin sirkin@cdr.stanford.edu

 *                akil srinivasan akils@stanford.edu

 */

 

// this utility function reads id3v1 and id3v2 tags, if any are present, from

// mp3 audio files. if no tags are found, just use the title of the file. :-|

 

void get_title_from_id3tag () {

  unsigned char id3[3];       // pointer to the first 3 characters to read in

 

  // visit http://www.id3.org/id3v2.3.0 to learn all(!) about the id3v2 spec.

  // move the file pointer to the beginning, and read the first 3 characters.

 

  sd_file.seek(0);

  sd_file.read(id3, 3);

 

  // if these first 3 characters are 'ID3', then we have an id3v2 tag. if so,

  // a 'TIT2' (for ver2.3) or 'TT2' (for ver2.2) frame holds the song title.

 

  if (id3[0] == 'I' && id3[1] == 'D' && id3[2] == '3') {

    unsigned char pb[4];       // pointer to the last 4 characters we read in

    unsigned char c;           // the next 1 character in the file to be read

 

    // our first task is to find the length of the (whole) id3v2 tag. knowing

    // this means that we can look for 'TIT2' or 'TT2' frames only within the

    // tag's length, rather than the entire file (which can take a while).

 

    // skip 3 bytes (that we don't use), then read in the last 4 bytes of the

    // header, which contain the tag's length.

 

    sd_file.read(pb, 3);

    sd_file.read(pb, 4);

 

    // to combine these 4 bytes together into the single value, we first have

    // to shift each one over to get it into its correct 'digits' position. a

    // quirk of the spec is that bit 7 (the msb) of each byte is set to 0.

 

    unsigned long v2l = ((unsigned long) pb[0] << (7 * 3)) +

                        ((unsigned long) pb[1] << (7 * 2)) +

                        ((unsigned long) pb[2] << (7 * 1)) + pb[3];

 

    // we just moved the file pointer 10 bytes into the file, so we reset it.

 

    sd_file.seek(0);

 

    while (1) {

      // read in bytes of the file, one by one, so we can check for the tags.

 

      sd_file.read(&c, 1);

 

      // keep shifting over previously-read bytes as we read in each new one.

      // that way we keep testing if we've found a 'TIT2' or 'TT2' frame yet.

 

      pb[0] = pb[1];

      pb[1] = pb[2];

      pb[2] = pb[3];

      pb[3] = c;

 

      if (pb[0] == 'T' && pb[1] == 'I' && pb[2] == 'T' && pb[3] == '2') {

        // found an id3v2.3 frame! the title's length is in the next 4 bytes.

 

        sd_file.read(pb, 4);

 

        // only the last of these bytes is likely needed, as it can represent

        // titles up to 255 characters. but to combine these 4 bytes together

        // into the single value, we first have to shift each one over to get

        // it into its correct 'digits' position. 

 

        unsigned long tl = ((unsigned long) pb[0] << (8 * 3)) +

                           ((unsigned long) pb[1] << (8 * 2)) +

                           ((unsigned long) pb[2] << (8 * 1)) + pb[3];

        tl--;

 

        // skip 2 bytes (we don't use), then read in 1 byte of text encoding. 

 

        sd_file.read(pb, 2);

        sd_file.read(&c, 1);

 

        // if c=1, the title is in unicode, which uses 2 bytes per character.

        // skip the next 2 bytes (the byte order mark) and decrement tl by 2.

 

        if (c) {

          sd_file.read(pb, 2);

          tl -= 2;

        }

        // remember that titles are limited to only max_title_len bytes long.

 

        if (tl > max_title_len) tl = max_title_len;

 

        // read in tl bytes of the title itself. add an 'end-of-string' byte.

 

        sd_file.read(title, tl);

        title[tl] = '\0';

        break;

      }

      else

      if (pb[1] == 'T' && pb[2] == 'T' && pb[3] == '2') {

        // found an id3v2.2 frame! the title's length is in the next 3 bytes,

        // but we read in 4 then ignore the last, which is the text encoding.

 

        sd_file.read(pb, 4);

 

        // shift each byte over to get it into its correct 'digits' position. 

 

        unsigned long tl = ((unsigned long) pb[0] << (8 * 2)) +

                           ((unsigned long) pb[1] << (8 * 1)) + pb[2];

        tl--;

 

        // remember that titles are limited to only max_title_len bytes long.

 

        if (tl > max_title_len) tl = max_title_len;

 

        // there's no text encoding, so read in tl bytes of the title itself.

 

        sd_file.read(title, tl);

        title[tl] = '\0';

        break;

      }

      else

      if (sd_file.position() == v2l) {

        // we reached the end of the id3v2 tag. use the file name as a title.

 

        strncpy(title, fn, max_name_len);

        break;

      }

    }

  }

  else {

    // the file doesn't have an id3v2 tag so search for an id3v1 tag instead.

    // an id3v1 tag begins with the 3 characters 'TAG'. if these are present,

    // then they are located exactly 128 bits from the end of the file.

 

    sd_file.seek(sd_file.size() - 128);

    sd_file.read(id3, 3);

 

    if (id3[0] == 'T' && id3[1] == 'A' && id3[2] == 'G') {

      // found it! now read in the full title, which is always 30 bytes long.

 

      sd_file.read(title, 30);

 

      // strip spaces and non-printable characters from the end of the title.

      // you may have to expand this range to incorporate unicode characters.

 

      for (char i = 30 - 1; i >= 0; i--) {

        if (title[i] <= ' ' || title[i] > 126) {

          title[i] = '\0';

        }

        else {

          break;

        }

      }

    }

    else {

      // we reached the end of the id3v1 tag. use the file name as a title.

 

      strncpy(title, fn, max_name_len);

    }

  }

}

 

Part 3:

 

/*

 * example sketch to play audio file(s) in a directory, using the mp3 library

 * for playback and the arduino sd library to read files from a microsd card.

 * pins are setup to work well for teensy 2.0. double-check if using arduino.

 * 

 * originally based on frank zhao's player: http://frank.circleofcurrent.com/

 * utilities adapted from previous versions of the functions by matthew seal.

 *

 * (c) 2011, 2012 david sirkin sirkin@cdr.stanford.edu

 *                akil srinivasan akils@stanford.edu

 */

 

// first step, declare the variables used later to represent microsd objects.

 

File sd_root;                   // the sd partition's root ('/') directory

 

// check that the microsd card is present, can be initialized and has a valid

// root volume. 'sd_root' is a pointer to the card's root volume object.

 

void sd_card_setup() {

  if (!SD.begin(sd_cs)) {

    lcd.clearDisplay();

    lcd.println("sd card failed\nor not present");

    lcd.display();

    return;

  }

  sd_root = SD.open("/");

 

  if (!sd_root) {

    lcd.clearDisplay();

    lcd.println("couldn't mount\nsd root volume");

    lcd.display();

    return;

  }

}

 

// for each song file in the current directory, store its file name in eeprom

// for later retrieval. this saves on using program memory for the same task,

// which is helpful as you add more functionality to the program. 

 

// it also lets users change songs on the microsd card without having to hard

// code file names into the program. ask an instructor if you want to use sub

// directories also.

 

void sd_dir_setup() {

  num_songs = 0;

 

  sd_root.rewindDirectory();

 

  while (num_songs < max_num_songs) {

    // break out of while loop when we check all files (past the last entry).

 

    File p = sd_root.openNextFile();

    if (!p) break;

 

    // only store current (not deleted) file entries, and ignore the . and ..

    // directory entries. also ignore any sub-directories.

 

    if (p.name()[0] == '~' || p.name()[0] == '.' || p.isDirectory()) {

      continue;

    }

 

    // to find the position of the '.' that precedes the file name extension,

    // we have to search through the file name (stored as an array of chars).

 

    // fat16 prefers 8.3 type file names, and the sd library will shorten any

    // names longer than that. so if we have an mp3 or wav file, the '.' will 

    // appear no later than position 8 ('max_name_len'-5) of the file name.

 

    char i;

 

    for (i = max_name_len - 5; i > 0; i--) {

      if (p.name()[i] == '.') break;

    }

    i++;

 

    // only store mp3 or wav files in eeprom (for now). if you add other file

    // types, you should add their extensions here.

 

    if ((p.name()[i] == 'M' && p.name()[i+1] == 'P' && p.name()[i+2] == '3') ||

        (p.name()[i] == 'W' && p.name()[i+1] == 'A' && p.name()[i+2] == 'V')) {

 

      // store each character of the file name (including the terminate-array

      // character '\0' at position 12) into a byte in the eeprom.

 

      for (char i = 0; i < max_name_len; i++) {

        EEPROM.write(num_songs * max_name_len + i, p.name()[i]);

      }

      num_songs++;

    }

  }

}

 

// given the numerical index of a particular song to play, go to its location

// in eeprom, retrieve its file name and set the global variable 'fn' to it.

 

void get_current_song_as_fn() {

  for (char i = 0; i < max_name_len; i++) {

    fn[i] = EEPROM.read(current_song * max_name_len + i);

  }

}

 

// print the title to the lcd. if no title is available, print the file name.

 

void print_title_to_lcd() {

  get_title_from_id3tag();

 

  lcd.clearDisplay();

  lcd.println(title);

  lcd.display();

  Serial.println(title);

}

 

Here is a video of the player being used on a breadboard without any buttons:

 

 

Parts List:

 

     - Arduino Micro

     - SparkFun MP3 Player decoder chip

     - HEF 4050BP voltage shifter

     - 3 0.5" X 0.5" momentary buttons

     - One switch (on/off)

     - Graphical LCD (unused) 

 

Issues:

 

     I was not able to successfully integrate the graphical LCD into the device design due to wiring complications. Additionally, I was not able to complete the final product for similar reasons. I was forced to resort to a cardboard box instead of acrylic plastic due to complications within my design. I learned that it is typically best to use solid-core wire instead of stranded wire for soldering efficiency and circuitry.

 

Future Plans:

 

     I will likely finish the MP3 Player on my own and re-wire everything with solid-core wire. I will create a more refined player based on my design and finally achieve a practical device.

 

Picture:

 

     Here is a picture of my final box, which, unfortunately, did not successfully perform its task:

     

 

Thank you for reading!

Comments (1)

zahraa@... said

at 6:39 pm on Aug 18, 2015

Dear Dhruval,

We like your idea for making a portable and wearable mp3 player for the convenience of people who exercise.
We wish it had the functionality of changing and pausing the songs as you mentioned in your state diagram. Also we wished you had integrate a graphical LCD into the device design that make the final product better. Also using acrylic plastic could make it to look more professional.
Overall, great work this quarter, and we hope you had fun!

Best,
Zahra, David, Dongao, Praveen, Samyuktha, Tian, Xiangyu

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