嵌入式平台音频播放器设计(基础篇)

一、目的

相信不少同学都见过以前那种很小的MP3播放器(暴露年龄),高级一点的还带一个小的单色液晶屏,想必理工科男都想自己设计一款这样的一款播放器。

那么如何才能设计实现一个简单的音乐播放器呢?

本文不讲述跟硬件相关的知识点,例如mcu选型、codec选型,着重于软件设计相关的内容。

二、分析

当我们播放一首歌曲时,首先我们需要考虑歌曲哪里来,是本地文件系统中读取音频文件还是从网络上下载音频流(例如HTTP流或者现在比较流行的m3u8流)还是其他动态输出音频数据的设备或软件实体?为了描述方便,我们可以称之为预处理,即负责原始数据的获取。

接下来,我们需要分析这个音频的基本特征,一般需要考虑这个音频流是否需要解包、是否需要解码、是否需要重采样,例如大家常见的MP3其实是MPEG 1 Layer 3的一种规范定义的音频压缩方式,还有在windows上比较流行的WAV以及WMA文件。同样为了描述方便,我们可以将这一过程统称为解码,即负责原始数据的解包解码。

最后,就是PCM数据播放了,一般情况下解码端会输出音频的基本信息,例如采样率、通道数、量化位数,播放设备的开启需要这些信息,我们称之为播放设备管理(暂停、恢复、终止)。

关于基本音频特征的相关知识点我会在后续的博文中一一讲解。

至此,通过上面的分析我们可以得到这样一个简单的播放器软件框架,如下图:

播放器草图

图中每个圆形实体分别代表用户、预处理、解码、播放模块。

用户模块去其实就是开发者使用播放器提供的相关接口的用户业务层代码,一个基本的播放器必须提供播放、暂停、恢复、停止四个接口,另外还需要提供一个播放完成的通知接口,图中我们以事件反馈的方式提供。

为了方便,我们可以将这个整个播放过程使用单线程这种方式来实现,但是此种方式会有很大的缺点:

  • 所有处理过程耦合在一起,不利于后续的功能的扩充,例如现在需要在解包解码与播放之间添加一个重采样的过程,这个时候不仅解包模块需要修改,播放模块也要修改,不符合模块解耦的设计要义。
  • 嵌入式设备的处理能力有限,无法在一个while循环里面在给定时间内完成所有的操作,这就导致用户感官上的卡顿体验,例如播放的过程中就不能进行预处理和解码,一般情况下播放必须按照采样率规定的时钟进行播放,才播放一段PCM数据期间,其他各个模块都不能继续执行。

鉴于此我们在上图实现时可以采用多线程方式,那么这个时候就需要线程间的同步。

为了实现简单,我们可以采用pipeline配合消息传递机制来进行线程间同步。举例来讲,当预处理器收到用户播放请求时,预处理器开始工作并在解析出音频特征信息后,例如知道需要播放的音频是MP3还是AAC,那么将此信息传递给解包解码线程,由它负责处理后续的操作,当解包解码线程解析出PCM参数时,传递给播放线程,由播放线程负责后续的播放,即预处理驱动解码,解码再驱动播放。

在上图中我们可以看到蓝色箭头代表音频数据的流动,并且可以看到消息也是从前往后往后流动的,上一级控制下一级,这种方式在实现上也很简单,通常我们会是使用消息队列、邮箱或者其他线程间通信机制。

至此,我们其实就已经将一个音频播放器的软件架构就已经设计好了,后面无非就是具体实现。

但是如果仔细推敲这个设计,你会发现其实这种流水线式消息传递是存在问题的,举例来说如果用户需要暂停播放,这个时候暂停的消息是直接控制播放设备的,即不往播放设备中写数据,则设备不发声;某个时刻由于预处理过慢,还没有获取到音频信息或者解包解码线程还没有拿到足够的数据解析出PCM信息,那么播放线程是不工作的,也就是说用户的暂停操作不能及时得到处理,虽然感官上设备不放音,呈现给用户的时设备没有在放音,就可以认为是已经暂停。

有些同学就说既然是因为暂停的消息直接控制播放设备才导致这样的问题,那么我为什么不将暂停消息发给其他模块呢?

其实这边涉及到一个缓冲问题。

上图中我们看到各个模块间不仅有消息传递也有音频数据传递,如果暂停请求不直接去控制播放设备,由于播放设备是从解码和播放之间的数据缓冲中拿数据,那么不控制播放设备,播放设备就可以从这个缓冲中取出数据去播放,如果这个缓冲很大,用户的暂停请求就不能快速得到响应。

基于以上分析,这种设计适合于那种单个播放对象的场景。如果某类场景中需要两个播放对象间快速切换,例如A在播放,此时需要暂停A,立刻去播放B,那么B的播放就会受到A的暂停响应速度的影响。有读者会问那什么样场景中会有这样的需求呢,请关注后续的博文。

但是这种解耦设计的方式是可取的,只是在消息传递方式上需要再次斟酌。

 

接下来让我们再分析分析每个模块的设计实现。

预处理上面也说了是负责数据的获取,那么这个数据有可能是本地文件流也有可能是网络流(通过HTTP或者其他网络协议获取)。那么我们就可以对它进行抽象,例如我们可以设计这样一套接口:

typedef struct {
    char *target;
    int (*throw_info)(char *info, void *userdata);
    int (*throw_data)(const char *buf, int len, void *userdata);
    void *userdata;
} play_preprocessor_cfg_t;

typedef struct play_preprocessor {
    const char *type;
    int (*init)(play_preprocessor_t *pp, play_preprocessor_cfg_t *cfg);
    play_preprocessor_err_t (*poll)(play_preprocessor_t *pp, int timeout);
    void (*destroy)(play_preprocessor_t *pp);
    void *priv;
} play_preprocessor_t;

开发者也可以实现特定预处理器的这些接口,并且在获取音频特征时通过throw_info接口抛出此信息,通过throw_data接口抛出原始音频数据。

另外解码解包和播放模块也可以实现类似的接口设计,这样整个播放器就可以支持插件配置,完成各种各样的播放请求。

 

关于如何优化消息传递方式,由于涉及到公司具体实现方案,不方便详细描述,但是我们可以往状态机的方向上思考思考,相信你肯定会有一个不错的解决方案。

至此一个简单播放器框架就设计完成了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值