【前言】
我一直想实现音乐播放器,但是一直没有成功,在西安邮电大学的严学文老师的课程中,刚好有一个基于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
我要赚赏金
