这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » STM32 » 【STM32F469I-DISCO】基于touchgfx的音乐播放器

共3条 1/1 1 跳转至

【STM32F469I-DISCO】基于touchgfx的音乐播放器

工程师
2025-04-23 08:40:48     打赏

【前言】

我一直想实现音乐播放器,但是一直没有成功,在西安邮电大学的严学文老师的课程中,刚好有一个基于touchgfx的音乐播放课程,碰上EEPW刚好有他教学用的开发板【STM32F469I-DISCO】,借此良机,我一步一步的跟着老师的课程,终于把基于touchgfx的音乐播放器实现了,在此分享一下成果与经验。

【实现的功能】

1、使用 touchGFX4.25.0,编写音频播放器界面,结合 sd 卡和 cs43l22 音频芯片, 可播放存储在 sd 卡上的 wav 文件; 

2、显示正在播放的歌曲名、文件大小、时长、采样率; 

3、 可通过按键切换播放曲目; 

4、 显示和调节播放进度; 

5、按键实现暂停、播放、静音等功能; 

6、 滚动条实现音量调节功能。

【界面设计】

image.png

这方面的内容我这篇文章中有详细介绍:【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);
	}
}

主要的代码解读如上,源代码我将附到工程最后。

【实现效果】

play.gif

【附源代码】

https://share.eepw.com.cn/share/download/id/395134




关键词: STM32F469     TouchGFX     Music    

院士
2025-04-23 11:49:08     打赏
2楼

谢谢分享,给楼主点赞。


助工
2025-04-23 13:48:56     打赏
3楼

谢谢分享,给楼主点赞


共3条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]