dash1982 发表于 2013-8-18 21:09:29

基于VS1003和SD卡的MP3播放器

本帖最后由 dash1982 于 2013-8-18 21:21 编辑

    最初是照着《VS1003+SD卡播放MP3文件》这个帖子来做MP3播放器的,但按照该帖制作出来的播放器只能识别FAT16分区的SD卡,而且很挑卡(比如无法识别使用卡槽转换的TF卡/小卡); 对于码流过高的mp3文件,会有“咔咔”的爆音(click noise)出现。
在原帖的基础上,参考相关资料,做出来下面的播放器雏形,主要功能如下:
    1. 320kbps的mp3解码无压力,更高的就没试过了。
    2. 支持FAT16/FAT32的SD卡(不挑小卡大卡)。
    3. 自动扫描SD卡目录并生成播放列表。
    4. SD卡有新拷贝MP3文件时,自动更新播放列表。

硬件:
    我的板子是Arduino nano w/ATmega328, 淘宝上38块钱的货。VS1003模块和SD模块也都是淘宝上买来的现成模块。程序跑起来后还能剩下大概600字节的RAM,便于后续添加更多的功能。
    连线图参考《VS1003+SD卡播放MP3文件》 这个帖子就可以。SD卡片选线可能会不同,修改ino文件的第659行:
if (!sd.begin(4, SPI_FULL_SPEED)) sd.initErrorHalt();
    把4改成你自己的片选线即可

软件:
    ino文件在arduino1.0.5 编译、运行均无问题, 操作系统是ArchLinux。其他系统/IDE版本未验证
    依赖于SdFat库,下载地址在:
https://code.google.com/p/sdfatlib/downloads/list
    我下载的版本是
sdfatlib20130629.zip
    下载完毕后直接拷到Arduino的library目录即可。其他版本未验证。
    注:SdFat代码挂在code.google上,由于某邮笑长FBX的缘故,可能需要借助于翻/墙才能顺利下载。

局限、注意事项和未来可能的改进
    1. 扫描SD卡目录我设置成了最多扫描4级目录,埋藏太深的目录会被跳过。这是因为在扫描目录时使用的是自己写的函数,使用的递归层数太多会导致内存的耗尽。未来可以应该通过对SdFat库的修改来突破这个限制。
    2. SD的片选线是4,如果你的片选不是4,需要修改ino文件的第659行
    3. 可能是由于nano板供电的原因,我的VS1003模块在外接大功率音箱时偶尔会不响,用耳机无此问题。
    4. 运行此程序后会在SD卡根目录自动生成"PL_OK.TXT"和"PLT.TXT"两个文件,PL_OK.TXT记录着MP3文件的目录结构,而PLT.TXT是生成供程序使用的实际播放文件。
    5. 文件太多时,初次运行生成播放文件可能要比较耗时。
    6. 后续工作的一些想法: 增加控制按钮(音量调节、挑歌),增加随机播放,支持录音,支持WMA播放,支持红外控制等等。

/*
* This sketch will list all files in the root directory and
* then do a recursive list of all directories on the SD card.
* Only the mp3 files will be listed in the original playlist.
* Then we use original Playlist to generate the actual Playlist.
* Then feeding VS1003 with the bytes read from the mp3 file.
*
*/
#include <SdFat.h>
#include <SdFatUtil.h>
#include <SdFile.h>
#include <SPI.h>

// SD FileSystem Object
SdFat sd;
SdFile file;                // General Operating file

short songnumbers = 0;         // songs in SD Card
short open_count = 0;        // open_count is for holding the opencounts in genOrigListFile()
short totalsongs = 0;   // totalsongs in playlist.

// Variable for holding the latest timestamp
uint16_t latestWriteTime = 0;
uint16_t latestWriteDate = 0;

/* VS1003 Part */
//set vs1003 pins
short xCs=9;
short xReset=8;
short dreq= 7;
short xDcs=6;

// Vs1003 Reset, for Initialize the vs1003
void Mp3Reset(){
int volume = 0x30;

digitalWrite(xReset,LOW);
delay(100);
digitalWrite(xCs,HIGH);
digitalWrite(xDcs,HIGH);
digitalWrite(xReset,HIGH);
commad(0X00,0X08,0X04); //Write into the MODE
delay(10);
if(digitalRead(dreq) == HIGH)
{
    commad(0X03,0XC0,0X00);//Set the clock of VS1003
    delay(10);
    commad(0X05,0XBB,0X81);//Set VS1003 to 44kps Sterero
    delay(10);
    commad(0X02,0X00,0X55);//Set the heavy sound
    delay(10);
    commad(0X0B,volume,volume);//highest volume:0x0000, lowest volume:0xFEFE
    delay(10);
    SPI.transfer(0);
    SPI.transfer(0);
    SPI.transfer(0);      
    SPI.transfer(0);
    digitalWrite(xCs,HIGH);
    digitalWrite(xReset,HIGH);
    digitalWrite(xDcs,HIGH);
    digitalWrite(4,LOW);
}
}

// for sending SPI commands
void commad(unsigned char addr,unsigned char hdat,unsigned char ldat)
{
if(digitalRead(dreq)==HIGH)
{
    digitalWrite(xCs,LOW);
    SPI.transfer(0X02);
    SPI.transfer(addr);
    SPI.transfer(hdat);
    SPI.transfer(ldat);   
    digitalWrite(xCs,HIGH);
}
}


//PlayMP3 pulls 32 byte chunks from the SD card and throws them at the VS1003
//We monitor the DREQ (data request pin). If it goes low then we determine if
//we need new data or not. If yes, pull new from SD card. Then throw the data
//at the VS1003 until it is full.
void playMP3(char* fileName)
{
//Open the file in read mode.
if (!file.open(fileName, O_READ))
{
    Serial.print("Failed to Open:");
    Serial.println(fileName);
    return;
}

Serial.println("Track open");

//Buffer of 32 bytes. VS1053 can take 32 bytes at a go.
uint8_t mp3DataBuffer;
//track.read(mp3DataBuffer, sizeof(mp3DataBuffer)); //Read the first 32 bytes of the song
uint8_t need_data = 0;

Serial.println("Start MP3 decoding");
while(1)
{
    while(!digitalRead(dreq))
    {
      //DREQ is low while the receive buffer is full
      //You can do something else here, the buffer of the MP3 is full and happy.
      //Maybe set the volume or test to see how much we can delay before we hear audible glitches

      //If the MP3 IC is happy, but we need to read new data from the SD, now is a great time to do so
      if(need_data == 0)
      {
      //Try reading 32 new bytes of the song
      if(!file.read(mp3DataBuffer, sizeof(mp3DataBuffer)))
      {
          //Try reading 32 new bytes of the song
          //Oh no! There is no data left to read!
          //Time to exit
          break;
      }
      need_data = 1;
      }   
    }

    //This is here in case we haven't had any free time to load new data
    if(need_data == 0)
    {
      //Go out to SD card and try reading 32 new bytes of the song
      if(!file.read(mp3DataBuffer, sizeof(mp3DataBuffer)))
      {
      //Oh no! There is no data left to read!
      //Time to exit
      break;
      }
      need_data = 1;
    }

    //Once DREQ is released (high) we now feed 32 bytes of data to the VS1053 from our SD read buffer
    digitalWrite(xDcs, LOW); //Select Data
    for(int y = 0 ; y < sizeof(mp3DataBuffer) ; y++)
    {
      SPI.transfer(mp3DataBuffer); // Send SPI byte
    }

    digitalWrite(xDcs, HIGH); //Deselect Data
    //We've just dumped 32 bytes into VS1053 so our SD read buffer is empty. Set flag so we go get more data
    need_data = 0;
}

//Wait for DREQ to go high indicating transfer is complete
while(!digitalRead(dreq));
//Deselect Data
digitalWrite(xDcs, HIGH);

//Close out this track       
file.close();

Serial.print("Track ");
Serial.print(fileName);
Serial.println(" done!");
}

//***************************************************************
//                 SD Card Operation Functions Start
//***************************************************************

/* This function is for get all of the songs in SD card */
/* return value: how many mp3 files in the SD Card */
short getSongsinSD(SdBaseFile * dir, int numTabs) {
// entry is for recording the file information
SdFile entry;
// length is 8(filename)+1(.)+3(suffix)+1(NUL) == 13
char filename;

// open Next item, until the end of the world.
while(entry.openNext(dir, O_READ)) {   
    // If this entry is a sub-directory, go to sub directories to get numbers.
    if(entry.isSubDir())
    {
      // Get the current index, use this index for recursively print the sub-directories
      uint16_t index = dir->curPosition()/32 -1;
      SdBaseFile s;
      if(s.open(dir, index, O_READ))
      {
      if(numTabs <=2)
      {
          getSongsinSD(&s, numTabs + 1);
      }
      }
      entry.close();      
    }   
    else // Common files will directly be displayed.
    {
      // Calculate how many MP3 exists in SD card.
      entry.getFilename(filename);      
      if((filename == 'M') &&
      (filename == 'P') &&
      (filename == '3'))
      {
      // An mp3 item will add 1 of songnumbers.
      songnumbers++;
      }   
      entry.close();
    }
}
return songnumbers;
}


/* This function is for generating the original mp3 list */
/* mp3 list file will use the latest timestamp in SD Card */
short genOrigListFile(const char *f, SdBaseFile * dir, int numTabs) {
// entry is for recording the file information
SdFile entry;
// length is 8(filename)+1(.)+3(suffix)+1(NUL) == 13
char filename;

/* Create file from "f" for recording the result */
open_count++;
if(!file.isOpen())
{
    /* Create one */
    if(!file.open(f, O_CREAT | O_TRUNC | O_WRITE))
    {
      /* Create failed will return 3 */
      return 3;
    }
}

// Set the file's current position to zero
dir->rewind();

// open Next item, until the end of the world.
while(entry.openNext(dir, O_READ)) {   

    // If this entry is a sub-directory, Print itself and sub-entries
    if(entry.isSubDir())
    {   
      // Use spaces for indicating the directory layers.
      for(uint8_t i=0; i < numTabs; i++) {
      //Serial.print("");
      file.write("");   
      }

      memset(filename, '\0', 13);
      entry.getFilename(filename);

      uint8_t lastchar = strlen(filename);
      //filename='\0';
      filename='/';
      file.write(filename);
      file.write("\r\n");

      dir_t dir_time;
      entry.dirEntry(&dir_time);

      if((dir_time.lastWriteDate > latestWriteDate))
      {
      latestWriteDate = dir_time.lastWriteDate;
      latestWriteTime = dir_time.lastWriteTime;
      }
      if((dir_time.lastWriteDate == latestWriteDate))
      {
      if(dir_time.lastWriteTime > latestWriteTime)
      {
          latestWriteTime = dir_time.lastWriteTime;
      }
      }

      // Get the current index, use this index for recursively print the sub-directories
      uint16_t index = dir->curPosition()/32 -1;
      SdBaseFile s;
      if(s.open(dir, index, O_READ))
      {
      if(numTabs <=2 )
      {
          genOrigListFile(f, &s, numTabs + 1);
      }
      }
      entry.close();      
    }   
    // Common files will directly be displayed.
    else
    {      
      // Calculate how many MP3 exists in SD card.
      memset(filename, '\0', 13);
      entry.getFilename(filename);
      if((filename == 'M') &&
      (filename == 'P') &&
      (filename == '3'))
      {

      // Use spaces for indicating the directory layers.
      for(uint8_t i=0; i < numTabs; i++) {
          //Serial.print("");
          file.write("");   
      }
      //entry.getFilename(filename);
      file.write(filename);
      file.write("\r\n");

      dir_t dir_time;
      entry.dirEntry(&dir_time);

      if((dir_time.lastWriteDate > latestWriteDate))
      {
          latestWriteDate = dir_time.lastWriteDate;
          latestWriteTime = dir_time.lastWriteTime;
      }
      if((dir_time.lastWriteDate == latestWriteDate))
      {
          if(dir_time.lastWriteTime > latestWriteTime)
          {
            latestWriteTime = dir_time.lastWriteTime;
          }
      }
      }         

      entry.close();
    }
}
// open_count will decrease for avoiding open it several times.
open_count--;
// Close opened file at the end of all routine, failed if we received 4.
if(open_count == 0)
{
    // update the timestamp.
    dir_t dir_play;
    file.dirEntry(&dir_play);

    // Update the timestamp for generated file. later we will use this timestamp for updating.   
    if (!file.timestamp(T_WRITE, (1980+(latestWriteDate>>9)), ((latestWriteDate>>5)&0XF), (latestWriteDate & 0X1F), (latestWriteTime >> 11), ((latestWriteTime >> 5) & 0X3F), ((latestWriteTime & 0X1F)*2)))
    {
      Serial.println("set Write time Failed");
    }

    file.sync();         

    if(!file.close())
    {
      return 4;
    }
}

return 0;
}


/* This function will get raw file list from Original file
* then output with the full name to the dest file */
short genPlayList(const char *src, const char *dest)
{
char str;
short n=0;
short SpaceCounts, i, j, k;
char *dirname = {
    0};
// 10 layers means (8+1)*10layer+13(file)
char *prefix = (char *)malloc(9*4 + 13);


for( j = 0; j < 4; j++)
{
    // malloc size is 8(dir name) + 1("/") + 1(NULL) = 10
    dirname = (char *)malloc(10);
}


file.open(src, O_READ);
if(!file.isOpen())
{
    return 1;
}

// pl == PlayList file
SdFile plfile(dest, O_CREAT | O_TRUNC | O_WRITE);
if(!plfile.isOpen())
{
    return 2;
}


while((n = file.fgets(str, sizeof(str))) > 0)
{
    // Change the '\r' to the NUL, means the end of the string.
    short len = strlen(str) -1;

    if(str == '\n')
    {   
      str = 0;
    }
    if(str == '\r')
    {   
      str = 0;
    }

    SpaceCounts = 0;
    for( i = 0; i < strlen(str); i++)
    {
      if(str == ' ')
      {
      SpaceCounts++;
      }
    }


    // Directly print the MP3 files under the root directory
    if((!strncmp(str+strlen(str)-3, "MP3", 4)) && (SpaceCounts == 0))
    {
      plfile.write(str);
      plfile.write("\r\n");
    }

    // Processing directory entries
    if(str == '/')
    {
      // Remove the SpaceCounts and copy the rest of the name into string array dirname[]
      strcpy(dirname[(SpaceCounts/2)], (char *)(str+SpaceCounts));
    }

    // Print the MP3 which locates in sub-directories
    if((SpaceCounts >= 2) && (str != '/'))
    {
      memset(prefix, '\0', 9*4 + 13);
      for( k = 0; k <= (SpaceCounts/2); k++ )
      {
      strcat(prefix, dirname);
      }
      strcat(prefix, str+SpaceCounts);
      plfile.write(prefix, strlen(prefix));
      plfile.write("\r\n");
      delay(10);
    }

} // end of while()


for(j = 0; j < 3; j++)
{
    free(dirname);
}
free(prefix);

// Generate the real player file.
if(!file.close())
{
    return 5;
}
if(!plfile.close())
{
    return 6;
}

return 0;
}

/* This function is for scanning all of the directories and sub-items
* to fetch the latest timestamp, we will use the latest timestamp for
* judging update playlist or not
*/
void getLatestStamp(SdBaseFile * dir, short numTabs) {
// entry is for recording the file information
SdFile entry;
// length is 8(filename)+1(.)+3(suffix)+1(NUL) == 13
char filename;

// open Next item, until the end of the world.
while(entry.openNext(dir, O_READ)) {   
    // If this entry is a sub-directory, Print itself and sub-entries
    if(entry.isSubDir())
    {
      // Get the latest timestamp of the directory.
      dir_t dir_time;
      entry.dirEntry(&dir_time);

      if((dir_time.lastWriteDate > latestWriteDate))
      {
      latestWriteDate = dir_time.lastWriteDate;
      latestWriteTime = dir_time.lastWriteTime;
      }
      if((dir_time.lastWriteDate == latestWriteDate))
      {
      if(dir_time.lastWriteTime > latestWriteTime)
      {
          latestWriteTime = dir_time.lastWriteTime;
      }
      }

      // Get the current index, use this index for recursively print the sub-directories
      uint16_t index = dir->curPosition()/32 -1;
      SdBaseFile s;
      if(s.open(dir, index, O_READ))
      {
      if(numTabs <=2) getLatestStamp(&s, numTabs + 1);
      }
      entry.close();      
    }

    // Common files goes here.
    else
    {      
      // Calculate how many MP3 exists in SD card.
      entry.getFilename(filename);      
      if((filename == 'M') &&
      (filename == 'P') &&
      (filename == '3'))
      {
      // Get the latest timestamp of the directory.
      dir_t dir_time;
      entry.dirEntry(&dir_time);

      if((dir_time.lastWriteDate > latestWriteDate))
      {
          latestWriteDate = dir_time.lastWriteDate;
          latestWriteTime = dir_time.lastWriteTime;
      }
      if((dir_time.lastWriteDate == latestWriteDate))
      {
          if(dir_time.lastWriteTime > latestWriteTime)
          {
            latestWriteTime = dir_time.lastWriteTime;
          }
      }
      }   
      entry.close();
    }
}
}


/* This function will get the file's timestamp, we want to
* fetch the orginal playlist's timestamp, use it to judging update
* playlist or not.
*/
short getfileStamp(const char *f, uint16_t *date, uint16_t *time)
{
SdFile localfile(f, O_READ);
if(!localfile.isOpen())
{
    Serial.println("Open Local File Failed!");
    return 2;
}

// Get the latest timestamp of the directory.
dir_t dir_time;
localfile.dirEntry(&dir_time);

*date = dir_time.lastWriteDate;
*time = dir_time.lastWriteTime;

if(!localfile.close())
{
    return 4;
}
return 0;
}

/* This function is for getting the numbers in PlayList */
short getListLength(char *playlistfile)
{
short tmpnum = 0;
char line;
int n;

delay(100);
// Read Items from the Playlist file.
// open PlayList file
SdFile rdfile(playlistfile, O_READ);
if (!rdfile.isOpen())
{
    Serial.println("Open PlayList Failed!");
}

// read lines from the file
while ((n = rdfile.fgets(line, sizeof(line))) > 0)
{
    if (line == '\n')
    {
      tmpnum++;
    }
    else
    {
      ;
    }                              
}       
rdfile.close();
return tmpnum;
}


void selectSong(char *playlistfile, char *song, short select_number)
{
short tmpnum = select_number;
char line;
int n;

delay(100);
// Read Items from the Playlist file.
// open PlayList file
SdFile rdfile(playlistfile, O_READ);
if (!rdfile.isOpen())
{
    Serial.println("Open PlayList Failed!");
    return;
}

// read lines from the file
while ((n = rdfile.fgets(line, sizeof(line))) > 0)
{
    if (line == '\n')
    {
      tmpnum--;
      //songs_in_list++;
    }
    else
    {
      ;
    }
    if(tmpnum == 0)
    {
      strncpy(song, line, strlen(line)-1);
      song='\0';
    }

}       
rdfile.close();
}



void setup()
{
bool updateflag = 0;       

delay(10);
Serial.begin(9600);

// See Free RAM       
PgmPrint("Free RAM: ");
Serial.println(FreeRam());

// set 1003
Serial.println("Set VS1003...");
SPI.begin();
SPI.setBitOrder(MSBFIRST); //send most-significant bit first
SPI.setDataMode(SPI_MODE0);
SPI.setClockDivider(SPI_CLOCK_DIV16);
pinMode(7,INPUT);
pinMode(8,OUTPUT);
pinMode(6,OUTPUT);
pinMode(9,OUTPUT);
digitalWrite(4,HIGH);
Mp3Reset();
Serial.println("DONE VS1003...Begin SD Card");


// initialize the SD card at SPI_HALF_SPEED to avoid bus errors with
// breadboards.use SPI_FULL_SPEED for better performance.
if (!sd.begin(4, SPI_FULL_SPEED)) sd.initErrorHalt();

// Start at beginning of the root directory.
// rewind is used for set current position to zero.
sd.vwd()->rewind();

// Testing Functionality:
// How many MP3 sons in SD Card
short numbers = getSongsinSD(sd.vwd(), 0);
Serial.print("Songs in SD Card is ");
Serial.println(numbers);
// See Free RAM       
PgmPrint("Free RAM: ");
Serial.println(FreeRam());


// 1. If no playlist file exists, scan the whole SD to generate one
// 2. Else Check the timestamp of the playlist to see if update is needed
// 3. If update is needed, generate playlist again.
sd.vwd()->rewind();
if(!sd.exists("PL_OK.TXT"))                // Condition 1
{
    Serial.println("No PL_OK.TXT found!");
    updateflag = 1;
}
else                                        // Condition 2
{
    Serial.println("PL_OK has been created, need to check its timestamp!");
    // Set global Variables to 0 for avoiding potential mis-using
    latestWriteTime = 0;
    latestWriteDate = 0;
    sd.vwd()->rewind();
    getLatestStamp(sd.vwd(), 0);
    // Get the PlayList File's timestamp, for comparing.
    uint16_t ldate = 0;
    uint16_t ltime = 0;
    short rvalue = getfileStamp("PL_OK.TXT", &ldate,&ltime);
    if( (ldate == latestWriteDate) && ( ltime == latestWriteTime) )
    {
      Serial.println("PlayList is the newest version, update not needed!");
    }
    else
    {
      Serial.println("Need Update PlayList!");
      updateflag = 1;
    }
}

sd.vwd()->rewind();
if(updateflag)                                // Condition 3, update playlist or not
{
    // Call genOrigListFile() to fetch all of the MP3 and directory layer information
    short error_reason = genOrigListFile("PL_OK.TXT", sd.vwd(), 0);
    if(error_reason != 0)
    {
      Serial.print("genOrigListFile error number is ");
      Serial.println(error_reason);
    }


    // Call genPlayList() to generate the actual PlayList file
    error_reason = genPlayList("PL_OK.TXT", "PLT.TXT");
    if(error_reason != 0)
    {
      Serial.print("genPlayList error number is ");
      Serial.println(error_reason);
    }
    Serial.println("Generate OrigiList and PlayList Done!");
}
else
{
    Serial.println("No Update!");
}


totalsongs = getListLength("PLT.TXT");
Serial.print("Songs in PLT.TXT is");
Serial.println(totalsongs);

delay(1000);
PgmPrint("Free RAM: ");
Serial.println(FreeRam());


Serial.println("Done setup()");
}


void loop() {
PgmPrint("Free RAM: ");
Serial.println(FreeRam());

short i = 0;
char song;
for (i = 1; i<totalsongs; i++)
{
    selectSong("PLT.TXT",song,i);
    sd.vwd()->rewind();
    playMP3(song);
}
}

Fortware 发表于 2013-8-19 13:50:40

LZ自己写的代码?

dash1982 发表于 2013-8-19 14:55:47

Fortware 发表于 2013-8-19 13:50 static/image/common/back.gif
LZ自己写的代码?

是的,VS1003部分代码有参考sparkfun的例程

Fortware 发表于 2013-8-20 15:27:28

表示很不错{:soso_e179:}

dash1982 发表于 2013-8-20 21:11:54

Fortware 发表于 2013-8-20 15:27 static/image/common/back.gif
表示很不错

谢谢鼓励:) 还在继续改动中,现在只能从头到尾一首一首听,下个版本会加上跳歌、随机播放、音量调节等功能。

3Dmaker 发表于 2014-12-19 18:38:18

好熟悉的linux风格

Mr灬自在 发表于 2016-5-14 17:45:36


现在听到声音了,但是最大声时,声音是断断续续 ,就想光盘卡一样的
页: [1]
查看完整版本: 基于VS1003和SD卡的MP3播放器