Design point of view (what are you designing a player for?)
This device is designed for people that don’t want to waste time in learning how to use a device (or don’t have the ability to learn, like somebody drunk), to play the music they want.
Verplank diagram
Photos of paper prototype
State diagram
Project code
/*
* Edited versión by Javier Ernesto Flores Robles, jeflro@gmail.com
* Last edit August 18, 2012
* originally based on 2011 david sirkin sirkin@cdr.stanford.edu work
* based on frank zhao's player: http://frank.circleofcurrent.com/
* utilities adapted from previous versions of the functions by matthew seal.
*
*/
// first step is to include (arduino) sd, eeprom, and (our own) mp3 and lcd libraries.
#include <SD.h>
#include <EEPROM.h>
#include <mp3.h>
#include <mp3conf.h>
//#define ENCODER_OPTIMIZE_INTERRUPTS // not working right
#include <Encoder.h>
#include <nokia_5110_lcd.h>
/*IF we are not using the Graphics or Bitmap capabilities
of the LCD, save code space with these #defines*/
#define NO_GRAPHICS
#define NO_BITMAP
// setup microsd, decoder, and lcd chip pins
#define sd_cs 12 // 'chip select' for microsd card
#define mp3_cs 21 // 'command chip select' to cs pin
#define dcs 20 // 'data chip select' to bsync pin
#define rst 18 // 'reset' to decoder's reset pin
#define dreq 19
// 'data request line' to dreq pin
//LCD PINS
#define LCD_PWR 10
#define LCD_SCE 16
#define LCD_RESET 17
#define LCD_DC 4
#define LCD_BL 13
//Interrupts PINS
#define redButton 8
#define encButton 7
// Color PINS
#define redPin 9
#define bluePin 14
#define greenPin 15
//eeprom variables:
#define eeVol 601
#define eeLights 600
#define eeContrast 602
// read_buffer is the amount of data read from microsd, then sent to decoder.
#define read_buffer 512 // size of the microsd read buffer
int mp3_vol; // default volume: 0=min, 254=max
// 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
// next steps, declare the variables used later to represent microsd objects.
Sd2Card card; // top-level represenation of card
SdVolume volume; // sd partition, not audio volume
SdFile sd_root, sd_file; // sd_file is the child of sd_root
// declare lcd object as well
Nokia_5110_lcd lcd(LCD_PWR, LCD_DC, LCD_SCE, LCD_RESET);
// declare the encoder
Encoder myEnc(6, 5);
// store the number of songs in this directory, and the current song to play.
unsigned char num_songs = 0, current_song = 0, mp3_selected=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 for sd_file.open().
char fn[max_name_len];
// 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, IDLE, PAUSE, MENU, SETTINGS,CHOOSE };
volatile state current_state = MENU;
volatile state paused_state = MENU;
//menu states
int selected = 0;
enum menu_state {MENU_PLAY_ALL,MENU_CHOOSING, MENU_SETTINGS };
// settings states
int sett_selected = 0;
enum sett_state {SETT_BGLON, SETT_BGLOFF};
void rgbColor (int Re, int Gr, int Bl) {
if (EEPROM.read(eeLights)>0) {
analogWrite( redPin ,255-Re);
analogWrite(bluePin , 255-Bl);
analogWrite(greenPin, 255-Gr);
} else {
analogWrite( redPin ,255);
analogWrite(bluePin , 255);
analogWrite(greenPin, 255);
}
}
// you must open any 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() {
map_current_song_to_fn();
sd_file.open(&sd_root, fn, FILE_READ);
// if you prefer to work with the current song index (only) instead of file
// names, this version of the open command should also work for you:
// sd_file.open(&sd_root, current_song, FILE_READ);
}
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
// send read_buffer bytes to be played. Mp3.play() tracks the index pointer
// within the song being played of where to get the next read_buffer bytes.
bytes_to_read = sd_file.read(bytes, read_buffer);
Mp3.play(bytes, bytes_to_read);
// bytes_to_read should only be less than read_buffer when the song's over.
if(bytes_to_read < read_buffer) {
sd_file.close();
current_state = IDLE;
}
}
long oldPosition = -999;
void checkVol () {
long newPosition = myEnc.read();
if (newPosition != oldPosition) {
rgbColor(255,30,30);
mp3_vol += newPosition - oldPosition;
if (mp3_vol < 1 )
mp3_vol = 1;
else if (mp3_vol > 255)
mp3_vol = 255;
EEPROM.write(eeVol,mp3_vol);
Mp3.volume(mp3_vol);
oldPosition = newPosition;
}
}
// continue to play the current (playing) song, until there are no more songs
// in the directory to play.
void dir_play() {
lcd.writeString( 0, 0, "Playing: ", MODE_NORMAL);
lcd.writeString( 0, 1, fn, MODE_NORMAL);
checkVol();
if (current_song < num_songs) {
mp3_play();
// if current_state is IDLE, then the currently playing song just ended.
// in that case, increment to get the next song to play, open that file,
// and return to the DIR_PLAY state (which will then play that song).
// if we played the last part of the last song, we don't do anything,
// and the current_state is already set to IDLE from mp3_play()
if (current_state == IDLE && current_song < (num_songs - 1)) {
current_song++;
sd_file_open();
current_state = DIR_PLAY;
}
}
}
//The functions for the buttons
void rightButtonPressed (){
oldPosition = myEnc.read();
// Serial.println("right int");
static unsigned long last_interrupt_time = 0;
unsigned long interrupt_time = millis();
// If interrupts come faster than 200ms, assume it's a bounce and ignore
if (interrupt_time - last_interrupt_time > 200)
{
// Serial.println("button");
switch(current_state) {
case MENU:
// Serial.println("menu push");
lcd.clear();
if (selected == MENU_PLAY_ALL){
current_state = DIR_PLAY;
oldPosition = myEnc.read();
}
else if (selected == MENU_SETTINGS){
myEnc.write(sett_selected*4);
current_state = SETTINGS;
}else if (selected == MENU_CHOOSING){
myEnc.write(mp3_selected*4);
current_state = CHOOSE;
}
break;
case DIR_PLAY:
paused_state = current_state;
current_state = PAUSE;
break;
case MP3_PLAY:
paused_state = current_state;
current_state = PAUSE;
break;
case IDLE:
break;
case PAUSE:
current_state = paused_state;
break;
case CHOOSE:
sd_file.close();
Mp3.clear_buffer();
current_song = mp3_selected;
sd_file_open();
current_state = DIR_PLAY;
break;
case SETTINGS:
if (sett_selected == SETT_BGLON){
digitalWrite(LCD_BL, HIGH);
EEPROM.write(eeLights,1);
rgbColor(150,20,255);
}
else if (sett_selected == SETT_BGLOFF){
digitalWrite(LCD_BL, LOW);
EEPROM.write(eeLights,0);
rgbColor(0,0,0);
}
break;
}
}
last_interrupt_time = interrupt_time;
}
void leftButtonPressed (){
static unsigned long last_interrupt_time = 0;
unsigned long interrupt_time = millis();
// If interrupts come faster than 200ms, assume it's a bounce and ignore
if (interrupt_time - last_interrupt_time > 200)
{
switch(current_state) {
case MENU:
break;
case DIR_PLAY:
current_state = MENU;
break;
case MP3_PLAY:
current_state = MENU;
break;
case IDLE:
break;
case PAUSE:
break;
case SETTINGS:
current_state = MENU;
break;
case CHOOSE:
current_state = MENU;
break;
}
}
last_interrupt_time = interrupt_time;
}
void settings() {
if (myEnc.read()%4 == 0){
if (myEnc.read()/4 > sett_selected)
sett_selected++;
else if (myEnc.read()/4 < sett_selected)
sett_selected--;
if (sett_selected < SETT_BGLON )
sett_selected = SETT_BGLON;
else if (sett_selected > SETT_BGLOFF)
sett_selected = SETT_BGLOFF;
myEnc.write (sett_selected*4);
}
if (sett_selected == SETT_BGLON)
lcd.writeString( 0, SETT_BGLON, "Lights ON ", MODE_INVERSE);
else
lcd.writeString( 0, SETT_BGLON, "Lights ON ", MODE_NORMAL);
if (sett_selected == SETT_BGLOFF)
lcd.writeString( 0, SETT_BGLOFF, "Lights OFF ", MODE_INVERSE);
else
lcd.writeString( 0, SETT_BGLOFF, "Lights OFF ", MODE_NORMAL);
}
void menu (){
// lcd.clear();
if (myEnc.read()%4 == 0){
if (myEnc.read()/4 > selected)
selected++;
else if (myEnc.read()/4 < selected)
selected--;
if (selected < MENU_PLAY_ALL )
selected = MENU_PLAY_ALL;
else if (selected > MENU_SETTINGS)
selected = MENU_SETTINGS;
myEnc.write (selected*4);
}
if (selected == MENU_PLAY_ALL)
lcd.writeString( 0, MENU_PLAY_ALL, "Play all ", MODE_INVERSE);
else
lcd.writeString( 0, MENU_PLAY_ALL, "Play all ", MODE_NORMAL);
if (selected == MENU_SETTINGS)
lcd.writeString( 0, MENU_SETTINGS, "Settings ", MODE_INVERSE);
else
lcd.writeString( 0, MENU_SETTINGS, "Settings ", MODE_NORMAL);
if (selected == MENU_CHOOSING)
lcd.writeString( 0, MENU_CHOOSING, "Choose a song ", MODE_INVERSE);
else
lcd.writeString( 0, MENU_CHOOSING, "Choose a song ", MODE_NORMAL);
}
int pastState = current_state;
void celarScrn() {
if (pastState != current_state)
{
pastState = current_state;
lcd.clear();
}
}
void choose(){
celarScrn();
if (myEnc.read()%4 == 0){
if (myEnc.read()/4 > mp3_selected)
mp3_selected++;
else if (myEnc.read()/4 < mp3_selected)
mp3_selected--;
if (mp3_selected > num_songs*5 )
mp3_selected = 0;
else if (mp3_selected > num_songs-1)
mp3_selected = num_songs-1;
myEnc.write (mp3_selected*4);
}
for(int i=0; i <num_songs; i ++) {
lcd.gotoXY(0,i);
for (int j = 0; j < max_name_len; j++){
if (EEPROM.read(i * max_name_len + j) == '\0') {
break;
}
if (mp3_selected == i)
lcd.writeChar(EEPROM.read(i * max_name_len + j), MODE_INVERSE);
else
lcd.writeChar(EEPROM.read(i * max_name_len + j), MODE_NORMAL);
}
}
}
// setup is pretty straightforward. initialize serial communication (used for
// the following error messages), microsd card objects, mp3 library, and open
// the first song in the root library to play.
void setup() {
Serial.begin(9600);
Serial.println("serial on");
pinMode(SS_PIN, OUTPUT); //SS_PIN must be output to use SPI
pinMode(LCD_BL, OUTPUT);
pinMode(redPin, OUTPUT);
pinMode(bluePin, OUTPUT);
pinMode(greenPin, OUTPUT);
// the default state of the mp3 decoder chip keeps the SPI bus from
// working with other SPI devices, so we have to initialize it first.
Mp3.begin(mp3_cs, dcs, rst, dreq);
// initialize the microsd (which checks the card, volume and root objects).
sd_card_setup();
//initialize the LCD
int contrast = EEPROM.read(eeContrast);
if (contrast == 0)
contrast = 50;
lcd.init(contrast); //parameter is contrast value, between 0 [low] and 127 [high]
lcd.clear();
// 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 raises the dreq line (automatically) to signal that
// it's input buffer can accommodate 32 more bytes of incoming song data.
// we need to set the SPI speed with the mp3 initialize function since
// it is the limiting factor, so we call its init function again.
Mp3.begin(mp3_cs, dcs, rst, dreq);
// 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();
//Test that LCD still works with other SPI devices
pinMode(redButton, INPUT_PULLUP);
pinMode(encButton, INPUT);
attachInterrupt(2, rightButtonPressed, FALLING );
attachInterrupt(3, leftButtonPressed, RISING );
if (EEPROM.read(eeLights)>0) {
digitalWrite(LCD_BL, HIGH);
} else
rgbColor (0,0,0);
mp3_vol = EEPROM.read(eeVol);
if (mp3_vol == 0)
mp3_vol= 175;
Mp3.volume(mp3_vol);
}
// 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 IDLE when done. the MP3_PLAY state plays one specified song, and then
// switches into IDLE. this example program 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 MENU:
celarScrn();
rgbColor(20,250,250);
lcd.writeString( 0, 5, "menu ", MODE_NORMAL);
// lcd.update();
menu();
break;
case DIR_PLAY:
celarScrn();
rgbColor(20,255,30);
lcd.writeString( 0, 5, "dir play ", MODE_NORMAL);
// lcd.update();
dir_play();
break;
case MP3_PLAY:
lcd.writeString( 0, 5, "mp3 play ", MODE_NORMAL);
// lcd.update();
mp3_play();
break;
case IDLE:
lcd.writeString( 0, 5, "idle ", MODE_NORMAL);
// lcd.update();
break;
case PAUSE:
lcd.writeString( 0, 5, "pause ", MODE_NORMAL);
// lcd.update();
break;
case SETTINGS:
rgbColor(255,255,255);
lcd.writeString( 0, 5, "settings ", MODE_NORMAL);
// lcd.update();
settings();
break;
case CHOOSE:
rgbColor(0,0,255);
lcd.writeString( 0, 5, "choosing ", MODE_NORMAL);
// lcd.update();
choose();
break;
}
}
Video of the final working player in use
short video of the making process