同样叠个甲,如果出现了本指南没有覆盖到的错误,还是需要求助有经验的同学。希望大家作业完成顺利。当然,如果有大佬觉得这篇指南的内容过于幼稚简单,受限于本人水平,还请多多见谅。

上一篇文章指路: THU-2023年春嵌入式课程实验指南——大作业Part1

题目要求概述

经过Part1的学习,读者基本上对于嵌入式的开发流程以及WAV音频格式有了一个大致的了解,这一次的Part2和Part3的内容就是在Part1的基础上,完成一个音乐播放器的开发。

由于Part3的附加功能QT界面的开发,需要将Part2的代码迁移到QT当中,进行相应的适配和重构,所以这里就不再分开讲解,直接讲述QT项目下的代码结构和功能实现。

这里需要支持的功能如下:

  • 1、播放wav格式的音频
  • 2、支持音量的调节
  • 3、支持上下首歌曲切换
  • 4、支持MP3格式音频的播放
  • 5、支持0.5,1.0,1.5,2.0四种播放速度
  • 6、支持快进快退10s
  • 附加:实现能够在开发板上启动的GUI

部署环境简介

  • 硬件环境
    • 电脑系统
      • 本实验中电脑系统为Windows 10、Windows 11和macOS均可顺利运行。
    • 开发板系统
      • 自9字班开始使用的新设备盒,注意开发板的启动模式选择EMMC启动
  • 软件环境
    • xshell版本: Xshell 7(Build 0122)
    • xftp版本: Xftp 7(Build 0119)
    • vmware版本: 16.2.2 build-19200509
    • ubuntu版本: 18.04.6 LTS (即课程提供的ubuntu)
    • QT版本: 5.14.1 (即课程提供的ubuntu当中内置的QT)

实现流程

流程概述

由于本次实验需要使用QT完成附加功能GUI界面,因此需要先对前两次实验完成的代码结构进行重构,将其模块化,方便后续功能的添加以及前端界面的调用。

代码重构之后,依次完成QT界面设计与槽函数的连接,文件导入,音频播放、暂停、继续、停止功能的逻辑,解码转换MP3格式音乐,重载QListWedgit组件实现歌曲切换逻辑,实现滑动条控制的音量调节,通过修改文件读取头实现快进快退,通过计时器播放进度条逻辑设计与播放结束逻辑,通过抽帧和插值技术完成倍速播放功能。总计代码量近1200行,不包含引用的minimp3.h头文件与QT的ui文件与资源文件。

实验操作

可以结合参考代码阅读实验流程,在实验操作流程当中遇到问题,可以先看下一节中的问题与解决,如果没有覆盖到读者的问题,那读者还需要去咨询有经验的同学。

  • 1、QT界面设计与槽函数的连接:这个部分需要考虑开发板的长宽比例(测量约为15.5cm:8.8cm)来提前固定QT界面的大小,这里使用的是800:460(这里是一个近似的比例,主要时方便元素排版设计),能够避免设计元素在开发板上显示的拉伸变形。其他的前端工作主要是控件的排版以及相关图片、图标等的插入,一些次要工作的例如按钮都添加悬浮动画重新设计标题栏等这里不赘述。除此之外,还需要还需要使用QT的信号与槽机制,将每个控件的信号与对应的槽函数进行连接,实现控件的操作。下图即为QT设计的页面ui:

gui展示

其中播放栏中的按钮从左到右依次为后退10s,播放暂停,快进10s,停止,导入歌曲,音量选择,倍速选择

其中播放列表右侧的按钮从上到下依次为 上一曲(向上箭头),下一曲(向下箭头)

  • 2、文件导入:使用QFileDialog打开文件对话框,将选取的文件信息插入到QListWidget控件当中,这里通过重载QListWidget组件,在其构造函数当中读取、存储文件的路径、文件名等基础信息,同时将信息展示在前端播放列表当中。

  • 3、音频播放、暂停、继续、停止功能的逻辑:

    • 这个部分的前端显示逻辑通过变量playing播放进度条的数值两个部分联合判断播放情况:对于playing为true的,调用播放逻辑的pause,对于play为false情况,如果进度条数值为0表示是新音乐,调用播放逻辑的play,不为0表示是之前被暂停的,调用播放逻辑的continue。除此之外,通过对于QListWidget组件的重载,给每个item添加上播放的标记信息,用于在播放列表显示乐曲的播放状态。
    • 对于这个部分的播放逻辑,在Part2的部分,助教会提供一个留空的代码文件,补充十个lasound函数调用(直接参看代码或者csdn搜索lasound播放音乐可以获得结果),在补全代码后,需要将这部分的代码迁移到QT当中,同时分成读取文件头open_music_file初始化pcm设备init_pcm以及播放play三个模块,这里通过使用playingisplaying两个变量分别表示是否播放了音乐(用于开始和停止)是否正在播放音乐(用于暂停和继续),随后在pause、continue和stop三个函数当中进行修改这两个播放状态来完成音乐的暂停、继续和停止的功能。
    • 需要注意的是QT的主线程是用来绘画UI界面,因此播放的时候需要开启一个新的线程进行操作
  • 4、实现歌曲切换逻辑:这个部分是建立在前面的播放和停止的逻辑以及QListWidget组件之上

    • 在前端的显示上,通过设置QListWidget的currentRow参数,加一减一以及边界判断,即可在播放列表显示上实现歌曲的切换,最后一首歌往后切会回到第一首歌的循环切换。除此之外,这里还实现了双击QListWidget中单个Item项的方式来进行歌曲的选择
    • 在播放的逻辑上,清除当前歌曲的播放状态,通过getcurrentRow获取现在需要播放的新歌曲,调用播放按钮的槽函数即可实现播放
  • 5、解码转换MP3格式音乐

    • 这个部分使用了minimp3这个单文件的音频转码库,虽然这个库可以将MP3直接转化为PCM设备能够接受的字符流,但是考虑到WAV和MP3需要复用一样的播放代码,所以这里采取的方式是使用minimp3先将MP3文件转化为临时的WAV文件,再调用前面的播放函数进行播放
    • 这个MP3转成WAV的实现逻辑主要是学习了博客园的相关文章,将在minimp3的github中minimp3.h文件下载并放进QT项目当中,随后在项目当中实现对于获取到的MP3文件,使用mp3dec_decode_frame函数读取MP3的文件头信息,使用realloc函数读取其文件数据流信息,再将这些信息根据WAV文件头的格式依次写入到临时的.temp.wav文件当中
  • 6、实现音量调节与界面滑条之间的同步:

    • 这个部分的显示逻辑,只有在程序开始是读取Player的volume信息进行初始化显示,随后的音量调节部分都是通过对于QSlider的调节触发设置的槽函数,槽函数当中直接获取QSlider的value,随后调用播放逻辑当中的音量设置。这里通过使用QSlider组件实现了音量调节的连续性
    • 在音量调节的实现上,这里可以参考代码的实现,这里主要介绍几个主要函数的功能以及简单的流程:
      • 初始化混音器:snd_mixer_open()打开混音器,snd_mixer_attach()将混音器连接到音频设备,snd_mixer_selem_register()为混音器注册简单元素类,snd_mixer_load()加载混音器设置。attach所连接的音频设备名称获取方式可以通过在命令行输入aplay -l即可获取到音频设备名称为hw:o,混音器名称为Playback
      • 创建简单元素ID并查找混音器元素:snd_mixer_selem_id_alloca()分配内存,snd_mixer_selem_id_set_index()snd_mixer_selem_id_set_name()设置ID的索引和名称。snd_mixer_find_selem()根据简单元素ID找到对应的混音器元素。
      • 获取音量范围:snd_mixer_selem_get_playback_volume_range()获取当前音量范围,以便在设置音量时使用正确的值。
      • 设置音量:根据按键内容修改音量变量,然后使用snd_mixer_selem_set_playback_volume_all()将调整音量应用到设备上。
  • 7、通过修改文件读取头实现快进快退

    • 这个部分的前端逻辑很简单,点击快进或者快退按钮,直接调用播放逻辑当中的快进与快退函数,随后设置将当前的时间进度设置跳转到对应的数值
    • 在播放逻辑部分,这里使用了10 * rate * num_channels * bits_per_sample / 8的公式来计算10s内正常速度读取的数据量,随后使用ftell函数获取当前文件指针读取的位置,返回值是数据量,随后简单判断一下快进和快退的边界情况, 确定下来需要切换的数量量,使用fseek函数设置文件指针的新的读取位置。这个时候播放函数在再次从文件中读取buffer的时候就会从新的文件指针的位置进行读取,从而实现了快进与快退。
  • 8、通过计时器播放进度条逻辑设计与播放结束逻辑:这里的主要工作是获取音乐的总秒数,随后通过当前播放的秒数去计算时间进度条的进度,然后将两个总秒数转化为时间格式显示出来。

    • 总秒数的获取,是在打开音乐文件的时候,通过fseek(fp, 0, SEEK_END);将文件指针放在文件的末尾,随后使用fsize = ftell(fp);读取到文件总的大小,再使用公式fsize/(rate * num_channels * bits_per_sample / 8);用总的文件大小除去每秒钟处理的数据量,就可以得到总的时间。
    • 对于当前的播放的秒数的获取,在每次音乐开始播放的时候将会初始化当前时间为0s,随后开启计时器,设置每1s触发一次时间函数,在时间函数当中,当前的秒数每次添加上播放的倍数speed*1,作为最新的秒数
    • 播放结束逻辑是当当前播放的秒数大于总的秒数的时候,调用停止按钮的槽函数,终止播放。
  • 9、(这个部分推荐看实验优化的第一条)通过基础的抽帧和插值思路完成倍速播放功能:倍速播放这个部分单纯通过修改采样率会导致音调随着变化,而实现或者调用短时傅里叶等高级时间拉伸算法又过于复杂,因此这里使用效果相对粗糙的抽帧技术实现大于1的倍速,使用插值技术实现小于1的倍速。

    • 抽帧技术的实现,这个部分的大概思路就是将需要输入PCM设备的播放数据进行切分,然后删除其中的部分数据。考虑到实现的复杂度,这里采用的是相对简单的没有平滑处理的等距抽帧的方式,于是引入了两个调整参数——k是切分的数量,m是相邻删除区域的间隔。在调整k和m的过程当中,有如下发现:m不变的情况下,不断增大k的值,会出现音质先急速下降,又缓慢回复的过程,同时伴随着音调从一开始的基本正常到k接近rate之后变得越来越高;k不变的情况下,不断增大m的值,可以发现音调基本没有变化,但是音质在缓慢地提高。这个过程可以通过音频波的形态进行理解,由于k值的增大,一个周期内数据被删除的越多,处理一个周期的时间长度变小,相对于的频率、音调就会变高;对于m值的增大,被删除的块更加集中,音频波的断裂数量相对越来越少,由此产生的杂音就相对较少。
    • 插值技术的实现,这个部分的大概思路就是需要将输入PCM设备的播放数据进行细分和扩充。考虑到实现的复杂度,这里采用的是相对简单的没有平滑处理的克隆的方式。同样的引入了两个调整参数——k是切分的数量,m是相邻扩充区域的间隔。和抽帧相似的规律,在m不变的情况下,不断增加k,音调从开始的不变,最后变为低音; 在k不变的情况下,不断增加m,音质的杂音也会相对减少。
    • 考虑到上述量种情况相似的规律,于是K需要尽可能取得小,k_per_size * m则需要尽可能取得大。为了进一步扩大 k_per_size * m的值,这里还将周期的大小增加了1倍,以取得相对更优的倍速效果。

实验中遇到的问题与解决方法

  • 1、音乐播放时候遇到开发板外放的音量无法调整,但是个开发板连接耳机的音量和虚拟机上的音量可以调解:这个问题在于混音器的设置出现了问题,需要注意想要在开发板外放时候能够正常调节音量,需要配置snd_mixer_attach(mixer, "hw:0");当中的hw:0snd_mixer_selem_id_set_name(sid, "Playback");当中的Playback,但是这个配置无法在虚拟机上进行播放,在虚拟机上进行音量调节需要修改配置为snd_mixer_attach(mixer, "default");当中的defaultsnd_mixer_selem_id_set_name(sid, "Master");当中的Master

  • 2、解码MP3格式音乐:这个地方和GUI库的选择部分遇到了类似的问题,因为使用ffmpeg这个较为成熟的音频转化库也会遇到交叉编译链当中不存在这个工具需要自己手动配置进去的问题。解决方法:这里通过在网上查阅单头文件转化MP3的开源仓库,谷歌推荐了minimp3这个仓库,通过简单的阅读和资料的查阅,这个minimp3不需要额外的库环境,可以通过交叉编译,所以最后选这了minimp3这个音频转化库

  • 3、QT编译需要alsa库:在将播放的部分代码迁移到QT后,点击编译一直报错,显示无法使用ALSA库的函数文件。解决方法:这是因为编译指令需要带有-lasound,这个需要在QT的pro文件当中配置,指定链接alsa库的选项,例如pro文件最后添加上LIBS += -lasound即可解决问题。如果是在Part2的编译中,需要在编译指令后加入-lasound,例如$CC Music_App.c -o Music_App -lasound

  • 4、获取音乐播放的总时长:虽然正确使用了音频数据大小/(rate * num_channels * bits_per_sample / 8)这个公式,但是在辨别谁才是真正的音频数据的大小的时候,遇到了很多的问题,尝试了sub_chunk2_size、buffer_size等数据都显示不正确。解决方法:这个地方需要获取的音频数据其实是整个文件的数据大小,这个可以从播放音频的时候使用fread的读取函数可以观察到,于是查阅资料找到可以读取整个文件长度的实际大小的操作fseek(fp, 0, SEEK_END);获取文件头指向文件末尾,fsize = ftell(fp);获取以及读取的数据,这样返回的数据就是整个文件的数据。

  • 5、倍速技术选择:在倍速上先是尝试了调解采样率的操作后发现音调变化过大,使用密集抽帧和密集插值的方式也没有太差差别,音质反而还差了。所以这个问题花费了很长的时间去调研,了解到想要实现变速不变调,还不损音质,谷歌推荐可以考虑使用卷积插值、短时傅里叶等时间拉伸算法,但是这些算法相关的论文虽然是开源的,但是实现这些算法的代码并没有开源,而已经实现了这些时间拉伸算法的成熟音频库,却因为不在交叉编译链当中而无法使用。而直接从论文复现代码难度过大。解决方法:这里最后还是采用了抽帧和插值思路的方式,但是相比最初的尝试,这里的实现给其中加入了调整参数,花费了一个下午的时间调参炼丹出来了一个相对较优的结果,能够做到音调不变,杂音相对较少。

  • 6、ftp传输中断异常:虽然助教介绍开发板说有4G的存储,但是实际上剩余的物理内存大约只有50MB(槽点太多),当放入的文件大小超过这个大小的时候,传输会中断,文件会被损坏,这里的解决方式主要是选用小的音频文件,也可以使用U盘挂载在开发板上,然后直接读取U盘上的音频文件,这样就不会出现内存不足的问题。

实验优化

1、支持更多的音频播放格式和使用成熟的时间拉伸算法:在作者实现了这个简陋的抽帧和插值思路的倍速代码后,有超级大佬研究出了将ffmpeg库编译到交叉编译环境的操作(啥都不懂的助教当然也不懂这个),这里可以直接使用ffmepg库的音频转码以及倍速播放api完成相关的功能,实现效果非常稳定。这里指路超级大佬的配置交叉编译指导文章

2、音乐播放的时间进度:由于计时器每秒更新一次进度,所显示的时间进度可能与实际播放进度存在0.5-1秒的误差。因此,拖拽进度条切换进度可能会有较大误差。优化思路是增加计时器的更新频率,缩小更新时间间隔,以尽可能减小误差。然后启用进度条的编辑功能,类似于快进和快退的方式实现自由的进度修改。

3、导入文件预转码MP3文件:这是因为在播放MP3前需要先将其转码为WAV,相对比较耗时,大约需要等待1s左右的时间,这个部分可以考虑在导入文件的时候,开一个新的线程,给每个MP3先预转码一个WAV文件,实现用空间换时间。

4、UI上的CD机图片可以新增旋转动画设计:这个部分在代码上的实现采用的是QPixmap格式的图片进行设置,QPixmap格式的图片可以设置一个transformed的旋转操作,只要给播放进度条设置一个监督触发函数,播放的时候触发设置QPixmap进行旋转就可以实现播放时候的一边旋转的CD机动画

实验执行流程

QT项目在开发板上的启动流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
## 先将QT项目文件夹拷贝一个副本(部分同学执行了下面的编译操作后项目变成了只读,所以最好拷贝用副本操作),副本文件夹下,打开终端输入以下指令加载交叉编译环境
source /opt/st/myir/3.1-snapshot/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
## 随后用qmake编译QT项目
qmake xxx.pro
## 最后用make指令编译得到可执行文件
make

## 将可执行文件通过ftp传递到开发板上,在启动开发板后,在xshell当中运行以下指令关闭开发板的默认桌面进程,这样自己的QT程序启动了才能显示出来
killall mxapp2

## 随后依次输入以下指令赋予自己QT程序权限,并执行
chmod +x 可执行文件名
./可执行文件名