作者开发的一款智能手表SOC上用audio DSP来做音频处理。在录音功能上,客户要做AI转录,因此会把录到的音频数据送给服务器做AI转录。如果送的是PCM数据,数据量明显偏大,因而传给服务器前最好把音频数据做编码,服务器上收到后再做解码,还原出PCM后再去做AI转录。客户指定要用开源的OPUS来做编解码,于是要把OPUS移植到audio DSP,并且用在录音以及录音文件的播放上。本文先简要介绍一下OPUS,然后讲讲怎么将OPUS移植到audio DSP上以及用OPUS来做录音和录音文件播放。
1, OPUS简介
OPUS是一个有损声音编码的格式,由Xiph.Org基金会开发,之后由IETF(互联网工程任务组)进行标准化,目标是希望用单一格式包含声音和语音,且适用于网络上低延迟的即时声音传输,标准格式定义于RFC6716文件。OPUS格式是一个开放格式,使用上没有任何专利或限制。支持的功能主要包括:
- 6 kb/秒到510 kb/秒的比特率;单一频道最高256 kb/秒
2)采样率从8 kHz(窄带)到48 kHz(全频)
3)帧长从2.5毫秒到60毫秒
4)支持恒定比特率(CBR)、受约束比特率(CVBR)和可变比特率(VBR)
5)支持语音(SILK编解码)和音乐(CELT编解码)的单独或混合模式
6)支持单声道和立体声;支持多达255个音轨(多数据流的帧)
7)可动态调节比特率,音频带宽和帧大小
8)良好的鲁棒性丢失率和数据包丢失隐藏(PLC)
2, OPUS移植
给出移植OPUS到audio DSP上的步骤:
1)学习OPUS的基本内容,知道它有哪些功能等。从官网下载OPUS的源码,看明白代码结构以及API的使用(opus_demo.c)。
2)在PC上根据源码做应用程序,能实现OPUS编码和解码功能。我一般在UBUNTU上做应用程序,把源码里相关的文件拿出来,自己写个makefile,然后生成应用程序来调试。由于OPUS文件较多,刚开始编译时会有很多错误,要一个一个的去解决。源码里有好多功能通过开关来控制是否使能,要根据需求来确定是否需要使能。我把OPUS用在录音以及播放录音文件场景上,好多复杂的功能都没使能,这样可以减少code size。OPUS源码支持定点和浮点实现,ADSP上只用定点。源码还支持16位和24位的编解码,我们只用16位的编解码。要确保编解码的结果正确,有一个好的base,方便后面的移植。通过调试可以更好的理解代码和API的使用。
3)根据需求改进代码。我们在ADSP上不会动态分配memory,memory都是事先布局好的。而源码里有多处用了opus_alloc/opus_free来分配和 释放memory,要将其去掉,取而代之的是全局的memory。全局memory的大小可用sizeof()确定。为了隔离OPUS源码以及调用者,我又加了一个wrapper层,这样OPUS源码就以静态库的形式存在。调用者只看到静态库以及相应的头文件。加的几个主要API函数如下:
a) Encoder init
void *opus_encoder_api_init(int sampleRate, int channels, int bitrateBps), 去调用源码里的opus_encoder_create()以及opus_encoder_ctl(), 返回的是encoder的instance。为了方便隔离,强制转成void *, 这样源码里的数据结构就不用暴露给调用者了。
b) Encoder run
int opus_encoder_api_run(void *enc,int frameSize, short *in, unsigned char *out), 去调用源码里的opus_encoder()。encoder instance传进去时用的是void *,在函数内部会转成OpusEncoder *,这样做也是为了隔离。输入的是PCM,输出的是opus码流。
c) Encoder destroy
void opus_encoder_api_destroy(void *enc), 去调用源码里的opus_encoder_destroy(),来destroy encoder instance。
d) Decoder init
void *opus_decoder_api_init(int sampleRate, int channels), 去调用源码里的opus_decoder_create()以及opus_decoder_ctl(), 返回的是decoder的instance。为了方便隔离,同样强制转成void *, 这样源码里的数据结构就不用暴露给调用者了。
e) Decoder run
int opus_decoder_api_run(void *dec, unsigned char *in, int inLen, short *out,), 去调用源码里的opus_decoder()。decoder instance传进去时用的是void *,在函数内部同样会转成OpusDecoder *,这样做也是为了隔离。输入的是opus码流,输出的是PCM。
f) Decoder destroy
void opus_decoder_api_destroy(void *dec), 去调用源码里的opus_decoder_destroy(),来destroy decoder instance。
4) 将改进后的代码移植到ADSP上。OPUS源码做成静态库,wrapper里的API对调用者可见。先前在PC上调试时自己写makefile也是方便这里的移植。由于OPUS文件多,code size大,片内memory放不下,且录音是非常用场景,我就将其code & data放在片外的DDR上。
3, OPUS应用
将OPUS源码移植到ADSP上后就要开始调试录音和播放OPUS码流文件(封装格式是ogg)了。
1) 调试录音
从ADMA获得20ms数据后先做降噪等处理,然后做OPUS编码,得到码流后通过IPC发给AP,AP收到后写进ogg文件里。录音结束后把ogg文件从手表里导出来用PC上支持OPUS解码的播放器放,如声音跟录的一样,就说明录音调试完成。调试过程中会遇到一些问题,我遇到的一个主要问题是OPUS编码时会导致ADSP exception。PC上调试时是没有这个问题的,应该是不同处理器的差异性导致的。经过一步步排查,问题出在opus_encode_native()函数里,具体在下图的位置:
通过函数celt_encoder_ctl()获取celt的mode。分析代码它是用了通用接口的方式获取celt mode(蓝框里mask的代码),做复杂了,简单的方式是通过指针直接获取celt mode(蓝框里unmask的代码),还不exception。
2) 调试播放OPUS码流
播放OPUS码流的流程跟其他格式(比如MP3)是一样的,差异就在码流解码上。OPUS码流每帧的前两个字节表示帧长,根据帧长取得一帧码流,然后放进OPUS decoder里做解码,得到PCM数据。再把PCM数据放进ADMA buffer里就可以通过codec播放出来了。
在OPUS的应用中,虽然ADSP的主频只有200多兆,录音和播放录音文件时均能流畅运行,没出现编解码时的load问题,因而不需要做优化。如果有load问题,则需要做进一步的优化。至于具体怎么做编解码的优化,可以看我前面写的一篇文章(音频的编解码及其优化方法和经验)。
为了更好的跟大家沟通,互帮互助,我建了一个音频讨论群。如您有兴趣,请搜weixin号 david_tym ,共同交流。