【前言】
我一直想实现音乐播放器,但是一直没有成功,在西安邮电大学的严学文老师的课程中,刚好有一个基于touchgfx的音乐播放课程,碰上EEPW刚好有他教学用的开发板【STM32F469I-DISCO】,借此良机,我一步一步的跟着老师的课程,终于把基于touchgfx的音乐播放器实现了,在此分享一下成果与经验。
【实现的功能】
1、使用 touchGFX4.25.0,编写音频播放器界面,结合 sd 卡和 cs43l22 音频芯片, 可播放存储在 sd 卡上的 wav 文件;
2、显示正在播放的歌曲名、文件大小、时长、采样率;
3、 可通过按键切换播放曲目;
4、 显示和调节播放进度;
5、按键实现暂停、播放、静音等功能;
6、 滚动条实现音量调节功能。
【界面设计】
这方面的内容我这篇文章中有详细介绍:【STM32F469I-DISCO】播放器界面设计-电子产品世界论坛
【音乐文件的读取】
音乐文件的读取包话SD卡的驱与文件系统的移植,我在两篇帖子进行了详细的分享:
【STM32F469I-DISCO】移植SD卡驱动-电子产品世界论坛
【STM32F469I-DISCO】移植FATFS-电子产品世界论坛
【音频硬件驱动】
开发板上的硬件是CS4L22,我也有专门的文章来分享:
【STM32F469I-DISCO】驱动音频CS43L22-电子产品世界论坛
【TouchGFX控制音频播放】
在上面的基础上,接下来就是如何编写touchgfx的逻辑驱动了。
1、把指定目录下面的音频文件搜出来,以获取音乐文件总数,同时把获取到的文件放到一个数据中,用来读取音乐文件的数组,其代码如下:
** * @brief List up to 25 file on the root directory with extension .BMP * @param None * @retval The number of the found files */ uint32_t Storage_GetDirectoryWavFiles (const char* DirName) { FATFS fs; FILINFO fno; DIR dir; uint32_t counter = 0, index = 0; FRESULT res; /* Open filesystem */ if(f_mount(&fs, (TCHAR const*)"",0) != FR_OK) { return 0; } /* Open directory */ res = f_opendir(&dir, (TCHAR const*)DirName); if (res == FR_OK) { for (;;) { res = f_readdir(&dir, &fno); if (res != FR_OK || fno.fname[0] == 0) break; if (fno.fname[0] == '.') continue; if (!(fno.fattrib & AM_DIR)) { do { counter++; } while (fno.fname[counter] != 0x2E); /* . */ if (index < MAX_BMP_FILES) { if ((fno.fname[counter + 1] == 'W') && (fno.fname[counter + 2] == 'A') && (fno.fname[counter + 3] == 'V')) { if(sizeof(fno.fname) <= (MAX_BMP_FILE_NAME + 2)) { // 增加边界检查 if (index < MAX_BMP_FILES) { // 使用 strncpy 确保字符串以空字符结尾 strncpy(pDirectoryFiles[index], fno.fname, MAX_BMP_FILE_NAME - 1); pDirectoryFiles[index][MAX_BMP_FILE_NAME - 1] = '\0'; // 确保字符串以空字符结尾 index++; } } // 打印文件名 printf("File name: %s\r\n", fno.fname); } } counter = 0; } } } f_mount(NULL, (TCHAR const*)"",0); if (index > 0 && pDirectoryFiles[0][0] != '\0') { printf("file no %s\r\n", pDirectoryFiles[0]); } else{ printf("pDirectoryFiles appen false!.\r\n"); } return index; }
在严老师的教程中是使用malloc来申请内存的,但是在stm32cubeide中,好象对malloc的支持不是太好,所以在这里我修改成了静态数组的分配。在此函数中,主要是逐一读取文件的文件名,判断他是的文件名是否为WAV结束,如果就把他的文件名存入数据pDirectoryFiles数组中。这个全局变量供在播放整个过程进行数据交换用。
2、获取指定次序的文件数据,在函数READ_WAV_FILE_FROM_SD为指定序号的文件读取,把他的文件名、大小、以及采集率读取出来,以用作显示播放进度,调整采样率:
void READ_WAV_FILE_FROM_SD(uint8_t file_no) { if(FATFS_LinkDriver(&SD_Driver, SD_Path) != 0) { Error_Handler(); } else { /*##-3- Initialize the Directory Files pointers (heap) ###################*/ f_mount(&SD_FatFs, (TCHAR const*)SD_Path, 0); } // 修改: 传递正确的指针类型 ubNumberOfFiles = Storage_GetDirectoryWavFiles("/Media"); printf("ubNumberOfFiles %d\r\n", ubNumberOfFiles); if (ubNumberOfFiles == 0) { Error_Handler(); } else { // 增加边界检查 if (file_no < ubNumberOfFiles) { sprintf ((char*)str, "Media/%-11.11s", pDirectoryFiles[file_no]); printf("file %s\r\n", str); // 添加调试信息 printf("pDirectoryFiles[%d] = %s\r\n", file_no, pDirectoryFiles[file_no]); // 添加调试信息 Storage_OpenReadFileSize(uwInternalBuffer, (const char*)str);//将指定文件名的文件头读取到buffer; printf("read wav file success!"); } else { Error_Handler(); } } }
3、为了播放流畅,我们需要一次从SD卡中读取8k音频文件到缓存中,其功能函数如下:
/*每次读取wav文件中8KB数据*/ uint32_t Storage_OpenRead_8KB_File(uint8_t *Address, const char* WavName) { size = 8*1024;//每次读取8KB uint32_t WavAddress; WavAddress = (uint32_t)sector; if(filSizeAlreadyRead< uwWavlen)//如果已读取的文件长度小于整个wav文件的长度 { do { i1 = 256*2;//SD卡的读取按照扇区来操作,每次分为固定的512字节 size -= i1; f_read (&F1, sector, i1, (UINT *)&BytesRead);//读取512字节 for (index1 = 0; index1 < i1; index1++) { *(__IO uint8_t*) (Address) = *(__IO uint8_t *)WavAddress;//读取的512字节数据存储在Address地址 WavAddress++; Address++; } WavAddress = (uint32_t)sector;//sector为512字节的数组 } while (size > 0);//直到读取完8KB为止 filSizeAlreadyRead=filSizeAlreadyRead+8*1024;//读取完8KB Address-=8*1024;//读取8KB之后,缓存区地址回归数组的初始位置 } if(filSizeAlreadyRead>=uwWavlen) { f_close (&F1);//整个wav文件读取完毕之后,关闭文件 playOneSongOverFlag=1;//全局变量表示这首歌曲读取播放完了 } return 1; }
4、播放音乐函数,在开始播放时,由touchgfx传入播放音频的数组缓冲,以及文件长度,在功能函数中,使用GetData读取内存缓存地址,并执行BSP_Audio_Play执行播放并修改状态机参数:
/** * @brief Starts Audio streaming. * @param None * @retval Audio error */ AUDIO_ErrorTypeDef AUDIO_Play_Start(uint32_t *psrc_address, uint32_t file_size) { uint32_t bytesread; buffer_ctl.state = BUFFER_OFFSET_NONE; buffer_ctl.AudioFileSize = file_size; buffer_ctl.SrcAddress = psrc_address; bytesread = GetData( (void *)psrc_address, 0, &buffer_ctl.buff[0], AUDIO_BUFFER_SIZE); if(bytesread > 0) { BSP_AUDIO_OUT_Play((uint16_t *)&buffer_ctl.buff[0], AUDIO_BUFFER_SIZE); audio_state = AUDIO_STATE_PLAYING; buffer_ctl.fptr = bytesread; return AUDIO_ERROR_NONE; } return AUDIO_ERROR_IO; }
5、AUDIO_Play_Process提供持续播放音乐的能力,共代码如下:
/** * @brief Manages Audio process. * @param None * @retval Audio error */ uint8_t AUDIO_Play_Process(void) { uint32_t bytesread; AUDIO_ErrorTypeDef error_state = AUDIO_ERROR_NONE; switch(audio_state) { case AUDIO_STATE_PLAYING: if(buffer_ctl.fptr >= buffer_ctl.AudioFileSize) { /* Play audio sample again ... */ buffer_ctl.fptr = 0; error_state = AUDIO_ERROR_EOF; } /* 1st half buffer played; so fill it and continue playing from bottom*/ if(buffer_ctl.state == BUFFER_OFFSET_HALF) { AudioCurrentTime+=(float)AUDIO_BUFFER_SIZE/(8*(float)AudioFre);//ÿ�δ�sd��ȡ��8KB���ݣ��ۼƲ��ŵ�ʱ�� bytesread =8*1024;//ÿ�δ�sd����ȡ8KB Storage_OpenRead_8KB_File(&buffer_ctl.buff[0], pDirectoryFiles[corruntPlayingFileNo]);//��sd��ȡ��8KB��ŵ�buffer_ctl.buff��������ǰһ�� if( bytesread >0) { buffer_ctl.state = BUFFER_OFFSET_NONE; buffer_ctl.fptr += bytesread; } } /* 2nd half buffer played; so fill it and continue playing from top */ if(buffer_ctl.state == BUFFER_OFFSET_FULL) { AudioCurrentTime+=(float)AUDIO_BUFFER_SIZE/(8*(float)AudioFre); bytesread =8*1024; Storage_OpenRead_8KB_File(&buffer_ctl.buff[AUDIO_BUFFER_SIZE /2], pDirectoryFiles[corruntPlayingFileNo]);//��sd��ȡ��8KB��ŵ�buffer_ctl.buff�������ĺ�һ�� if( bytesread > 0) { buffer_ctl.state = BUFFER_OFFSET_NONE; buffer_ctl.fptr += bytesread; } } break; default: error_state = AUDIO_ERROR_NOTREADY; break; } return (uint8_t) error_state; }
在此功能函数,首先判断是否播放进度,如果读取的文件长到达最大值,状态传为播放结束。如果为DMA传为半传输中断或者传输完毕,进行DMA的传输地址转换并从SD读取下一个8k的数据。
6、在TouchGFX的screenView.cpp中首先引入全局变量,来获取显示的基本信息:
#include "main.h" extern TIM_HandleTypeDef htim2;//在main.C中定义的定时器2的结构体 extern __IO uint32_t uwVolume ;//在audio_play.c函数中定义的音量变量 extern uint8_t ubNumberOfFiles;//在"fatfs_storage.c"中定义的wav文件数目 extern char pDirectoryFiles[MAX_BMP_FILES][MAX_BMP_FILE_NAME];//存放音频文件名的数组 extern uint32_t uwWavlen ;//文件大小(BYTE) extern uint8_t *uwInternalBuffer;//临时缓存地址,并不是音频播放缓存的地址 extern uint32_t AudioFre;//音频文件的采样频率 extern uint8_t corruntPlayingFileNo;//当前播放的文件编号 extern uint32_t filSizeAlreadyRead;//已经读取的文件大小 extern float AudioCurrentTime;//当前wav文件已经播放了多少时间(秒) extern uint8_t playOneSongOverFlag;//当前wav文件是否已经播放完成的标志位 extern "C" uint32_t READ_WAV_FILE_FROM_SD(uint8_t);//在"fatfs_storage.c"中定义的读取第几个wav文件的函数 uint32_t count=0;//定期刷新屏幕里面用到的计数器 uint8_t showPlayListFlag=1,playModeFlag=0;//是否显示播放列表的标志位
7、在屏幕截入时执行音频的初始化,并从SD卡中读取第一首歌的数据:
void Screen1View::setupScreen() { Screen1ViewBase::setupScreen(); BSP_AUDIO_OUT_Init(OUTPUT_DEVICE_BOTH, uwVolume, AudioFre/2);//初始化音频播放芯片 PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo);//播放第一首歌 }
8、播放音乐按键按下后的首先停止播放,停止定时器2,读取SD卡中的文件到缓冲区,获取到采样率,对CS4L22进行初始化,然后开启定时器对播放AUDIO_Play_Process进第周期调用。接下来把播放的内容刷新到屏幕上,代码如下:
void Screen1View::PLAY_WAV_FILE_FROM_SD(uint8_t file_no) { BSP_AUDIO_OUT_Stop(1);//停止播放 HAL_TIM_Base_Stop_IT(&htim2);//关闭定时器2中断 HAL_TIM_Base_Stop(&htim2);//关闭定时器2 filSizeAlreadyRead=0;//已读取文件长度置为0 AudioCurrentTime=0;//已播放时间置为0 READ_WAV_FILE_FROM_SD(file_no);//搜索并读取第corruntPlayingFileNo个wav文件 BSP_AUDIO_OUT_Init(OUTPUT_DEVICE_BOTH, uwVolume, AudioFre/2);//初始化音频播放芯片 AUDIO_Play_Start((uint32_t *)uwInternalBuffer, (uint32_t)uwWavlen);//开始播放 HAL_TIM_Base_Start_IT(&htim2);//开启定时器2中断 HAL_TIM_Base_Start(&htim2);//开启定时器2,定时将sd卡数据,传输到音频播放缓存区 Unicode::strncpy(textPlayingBuffer,pDirectoryFiles[corruntPlayingFileNo],20); textPlaying.invalidate();//更新显示当前播放的文件名 Unicode::snprintf(textFileNOBuffer,10,"%d",ubNumberOfFiles); textFileNO.invalidate();//更新显示搜索到的wav文件数目 Unicode::snprintf(textFileSizeBuffer,10,"%d",uwWavlen/1024); textFileSize.invalidate();//更新显示当前播放的wav文件的大小(KB) Unicode::snprintf(textAudioFreqBuffer,10,"%d",AudioFre); textAudioFreq.invalidate();//更新显示当前播放的wav文件的采样率 }
9、在touchgfx的Tick函数,每20毫秒刷新一次数据,并根据播放状态机来执播放下一步等:
void Screen1View::handleTickEvent() { count++; if((count%20)==0)//定期刷新 { //双通道*2,采样率是16位,两个字节再*2,每秒钟合计播放AudioFre*4字节 Unicode::snprintf(textTotalTimeBuffer1, 10, "%d",(uwWavlen/(4*AudioFre))/60);//已播放的分钟 Unicode::snprintf(textTotalTimeBuffer2, 10, "%d",(uwWavlen/(4*AudioFre))%60);//已播放秒钟 textTotalTime.invalidate();//更新显示当前播放的音频文件总时间 Unicode::snprintf(textAlreadyPlayTimeBuffer1, 10, "%d",(uint32_t)AudioCurrentTime/60); Unicode::snprintf(textAlreadyPlayTimeBuffer2, 10, "%d",(uint32_t)AudioCurrentTime%60); textAlreadyPlayTime.invalidate();//更新显示当前音频文件已经播放的时间 gauge.setValue((uint8_t)(AudioCurrentTime*100/(uwWavlen/(4*AudioFre))));//更新播放进度的gauge仪表指示器 Unicode::snprintf(textFileSizeAlreadyReadBuffer, 10, "%d",filSizeAlreadyRead/1024); textFileSizeAlreadyRead.invalidate();//更新显示已读取的文件大小 if(playOneSongOverFlag==1)//如果当前歌曲播放完成了 { playOneSongOverFlag=0;//播放完成与否标志位置零 if( playModeFlag==0) { if(corruntPlayingFileNo<(ubNumberOfFiles-1))//如果不是最后一首歌播放完 { corruntPlayingFileNo++; PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo);//顺序播放下一首歌曲 } else//如果最后一首歌播放完了 { corruntPlayingFileNo=0; PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo);//从头播放第一首歌 } } else if(playModeFlag==1) PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo); else { corruntPlayingFileNo=count%ubNumberOfFiles ; PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo); } } } }
10、播放下一首歌,根据当前播放序号来执PLAY_WAV_FILE_FROM_SD。这个功能函数还需要判断是否是最后一首歌。其代码如下:
void Screen1View::funcNextSong(){ if( playModeFlag==0) { if(corruntPlayingFileNo<(ubNumberOfFiles-1))//如果不是最后一首歌播放完 { corruntPlayingFileNo++; PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo);//顺序播放下一首歌曲 } else//如果最后一首歌播放完了 { corruntPlayingFileNo=0; PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo);//从头播放第一首歌 } } else if(playModeFlag==1) { PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo); } else { corruntPlayingFileNo=count%ubNumberOfFiles ; PLAY_WAV_FILE_FROM_SD(corruntPlayingFileNo); } }
主要的代码解读如上,源代码我将附到工程最后。
【实现效果】
【附源代码】
https://share.eepw.com.cn/share/download/id/395134