为什么要做

一直有做音频合成的需求,但是在一些低端机器上总是出现问题。
其一是,长音频合成的时候经常OOM,导致在小内存机器上有限制。
其二是,CPU不给力的时候合成太慢。
所以想要一种优雅的方式解决音频合成,想了很久了,最近有点时间想要改进以上问题。

如何改进

改进合成过程的思路其实有两种,1、时间换空间,2、空间换时间。什么意思呢?时间换空间就是不要求合成速度,只要能把内存省下来就行,而空间换时间就是全部读到内存要最大的合成效率。

一、文件读写

解码音频至PCM文件后,读入内存会占据很大空间,如果全部读入内存,音频越长越大,内存就越大。

这显然不能满足小内存机器的需求,因为这两天看到了Win平台下的Mapping File方式,想起了Java中有一些类似的方式处理IO
RandomAccessFile
MappedByteBuffer

源自:http://javarevisited.blogspot.jp/2012/01/memorymapped-file-and-io-in-java.html

有了这个特性我们就可以对文件进行指定位置读写
所以节省内存的大体思路是,分段(以某个内存大小为上限)读入文件,然后逐段混音,拼接输出文件。IO读取的速度肯定比内存读取慢得多,所以一次读多少,读多少次是这个方式的平衡点。

二、合成效率

之前的合成部分在Java层做的,打算移到Native层去做,当然,这样的效率提高很有限,所以合成算法方面也打算根据时间复杂度再次改进。

三、合成逻辑

这次改进的合成逻辑打算以时间线为基准,类似逐帧合成输出。
画图吧,画图清晰一点

1
2
3
4
5
6
7
8
9
10
11
12
13
源文件
↓↓↓↓↓↓
Audio 1 Audio 2
|_________| |_________|
Audio 0
|__________________________________|
Time -->

合成后
↓↓↓↓↓↓
Audio 1+0 Audio 0 Audio 2+0 Audio 0
|_________|___________|_________|__|
Time -->

这是有一段比较长的音频的情况,看起来像是好多段往一段音频上叠加一样。实际情况却复杂的多,很多是下面这种情况或更乱

1
2
3
4
5
6
7
8
9
10
11
12
  Audio 1               Audio 2
|_________| |_________|
Audio 0
|_________________________|
Time -->
/////////////////////////////////////////////////////////////

Audio 1 Audio 2
|_________| |_____________________|
Audio 0 Audio 3
|_____________| |_____________________|
Time -->

所以当你指定合成时间位置的时候,这里面还有个补空白音频的问题。以上面第二种情况来说,合成后的音频就应该是如下

1
2
3
4
5
6
7
8
9
10
11
合成前
↓↓↓↓↓↓
Audio 1 Audio 2
|_________| |_____________________|
Audio 0 Audio 3
|_____________| |_____________________|
合成后
↓↓↓↓↓↓
Audio 1+0 0 Black Audio 3 Audio 2+3 Audio 2
|_________|___|_________|_________|___________|_________|
Time -->

具体流程

建立文件队列层

将所有文件划入队列层

根据当前可用内存动态和音频队列最大叠加层数,划定读取数据的区块大小

读入当前时间段内的每层所需的文件,没有文件的位置补0

Mix

输出(加一个输出文件长度控制,如果指定了合成后长度,就不再合成,直接退出,没有指定,就合成到最后一句指定位置的文件末尾。)

下面具体实现难点

1、文件读取的元大小如何确定?
根据目前手机的可用内存和当前音频长度所决定,可以在一个内存范围中给出几个可用数字,或者直接计算。

2、文件读取逻辑是怎样的?
每层音轨一个读取入口,读取指定开始点和结束点,由层方法放回当前读取的数据集。(short数组)没有文件的地方补0。

3、以什么方式实现合成这个功能块?
这个功能属于应用级的服务程序,用来提供应用内音频合成的服务。由于涉及文件操作又相对比较耗时,所以这个功能的选择是独立进程的Service,利用多线程进行合成。使用bindService方式来加载,首次启用就不用第二次加载,随应用进程终止。利用IADL进行进程间通讯,用来控制合成操作、数据传输、取消合成及进度反馈等。

音频混合相对于视频来说相对比较简单,方法也有很多种,基于不同应用场景也有不同的做法。这里笔者写的方法是针对部分低内存的机器进行的兼容探索,并不具备普适性。线上场景下多方案备选才是比较稳妥的,毕竟小内存的只是一部分。

对于音频叠加的算法,涉及到音频文件结构,有时间会单独写一篇文章大家讨论。