这篇文章是系列文章的第3篇。
第一篇在这里声波配网原理。
关于声波传输编码的部分在这里声波传输编码。
前面说过,声波传输的过程可以理解成对称加解密的过程,因而和声波传输编码的三个步骤对应,声波传输解码也分为下面这样的三步:
1.从正弦波音频信号到对应的频率数组
2.根据频率数组反映射到数字数组
3.根据码表查找对应的字符
首先我们先来看看最复杂的这步:从正弦波音频信号到对应的频率数组。这一步涉及的操作比较多,写代码的时候容易,整理成文章还是有点麻烦,我尽量让我的叙述有逻辑,成系统。
下面的6个点基本囊括了这一步要做的事情。
1.音频信号的采集
2.开始音节
3.快速傅里叶变换
4.解析窗口大小的选取
5.对齐音节
6.顺序解析
7.结束音节
1.音频信号的采集
这个要根据自己的需求来定,选择不同的平台做解码的工作,音频信号的采集就在相应的平台完成。如果是Android 平台的宝宝的话,用 AudioTrack 就可以了。当发送端播放声音的时候,是在源源不断的产生音频信号的,而解码端在音频信息中解析到我们约定好的开始音节频率之后,要进行一些操作(需要在开始标志位置附近来回扫描计算确定最佳开始解析的位置),可以形象地把它称为对齐音节。在对齐音节的过程中,声音产生的速率是远远大于我们消费声音的速率的,而一旦我们对齐音节之后,开始音节之后的音节就是携带信息的音节,因而在我们没有处理完开始音节的时候,下一个音节的数据既不能丢弃,也不能处理。
这种情况很明显就需要用到经典的生产者消费者模式来平衡录制音频的线程和解析线程之间的速率,防止丢失数据(缓冲区)。
而关于录制时采样率,声道数,数据位数的选取都应该和编码端一致。
2.开始音节:
在声波配网的原理篇,我们说过,把一段携带一个字符信息的单频率的正弦波叫做一个音节。而开始音节的作用是告诉解码端,声波已经开始播放,这个音节结束之后的音频信号就是携带信息的声波,可以开始解码。我们需要定一个阈值,当开始频率的振幅超过一定的阈值就认为识别到了音频的开始位置,可以开始解码。
3.快速傅里叶变换
快速傅里叶变换的作用在于从一个解析窗口(缓冲区)中解析出里面的音频信号包含的频域信息。快速傅里叶变换的准确率和效率直接影响着我们解码成功的准确率和效率,因而推荐使用经典的库 Mark Borgerding 的 kiss_fft 。然后将我们关心的频率【编码时采用的98个频率】的振幅大小记录下来后续进行分析。
但我们要考虑到一点,我们在利用快速傅里叶变换计算频率的时候,实际上是根据相位得到周期,然后再得到频率,先不说计算机关于浮点数的存储就是近似值。就算是能精确存储浮点数,通过2*pi/T计算来的频率也是一个在我们真正的频率附近的值。因而如果我们的目标频率是1700Hz,那么1699,1701也应该看作是音频信号中有我们的目标频率的标志,这个左右挪动的范围究竟要定成多大了?看心情哈哈哈~~~
改动这个参数,看这个参数改变后的具体效果,可以自定。
蠢作者定的是15,以1700为目标频率的话,傅立叶变换解析出的1685-1715之间的频率,我都认为是目标频率。这个阈值影响着我们在编码时对各个字符对应的频率设定,定成15意味着相邻两个字符对应的频率间隔至少要大于30。
4.解析窗口大小的选取:
由于每个音节持续时间内,一个正弦波信号都重复了很多个周期,所以当周期重复的次数(包含的采样点的多少)达到一定值后,并不影响快速傅里叶变换计算出的振幅大小。(理论上如果采样点是连续而不是离散的,能完整的还原声音信息,只要包含正弦波信号的一个周期,那么计算出的振幅就是固定的。)但由于我们在实际中是用离散的采样点记录声音信息,而且声波在从发送端设备播放,到在空气中传输,再到接收端设备接收,中间还有环境噪音的影响。如果重复的周期数太少,计算出的频域信息就会是不准确的。
网上流传的解决方案,都选择了让解析窗口略大于编码的音节大小。这也是导致解析了一段音频后,解码成功率就会下降的根本原因。
让解析窗口大于音节窗口的原因很简单,这样解析窗口就能包括一个音节的数据,通过傅里叶变换出来的结果就是准确的。而由于解码的时候,我们开始解析的位置其实是随机的,所以在开始音节过后,正式解析数据的时候会出现下面三种常见的开始解析的位置(解析窗口的一端刚好卡在两个音节之间的情况可以随机的分到这三种情况中):
可以看到当前解析窗口的数据中除了包含当前音节的数据以外,还包含上一个音节的数据和下一个音节的数据。
反过来思考就是,当前位置我们想要的字符的音节数据最可能的情况是,大部分在当前解析窗口中,但也会有一部分在上一个解析窗口中或者下一个解析窗口中。
所以这种方式下,我们从音频数据中选取频率的时候就不是选择出振幅最大的那么简单。
这是由 energy 公式生成的单频率的正弦波的频谱分析,因为是单频率的,所以只需要找到振幅最大的频率就可以。
但由于我们的解析窗口比音节长度要大,就没有办法保证目标频率是最高的,要选出目标频率,就需要定义很多的规则,比如振幅大于一定阈值的频率,我们认为它在解析窗口中存在,比如图1中的频谱图,只要是大于15DB的频率,我们都认为它存在。然后一个频率在上一个解析窗口中存在了,在这个解析窗口中又存在,那么它很可能是目标频率。
对应着这样的解析位置:
当然也有可能是这样:
显然这个选出目标频率的规则会是很复杂的,要跨3个音节判断,要根据不同的优先级设定权值,越是复杂的东西越容易出错。
当然规则定好了,还是可以识别的。那为什么会出现长度的限制问题了?
即使在我们开始解析的时候,解析窗口的起始点卡在了一个完美的位置:
但因为解析窗口比音节数据要大,向前滑动的时候这个偏移也会一直变大,长度够长的时候,就会出现这样的情况(必然会走入这样的情况):
解析窗口中的数据一半来自前一个音节,一半来自后一个音节。
这种情况下得到的傅里叶频谱是完全没有办法区分出哪个音节是前面那个,哪个音节是后面那个的,这个位置就是一个只能靠运气解析的位置。
同时这个解析窗口之前的状况是每个解析窗口都是前面音节的数据占比大,这个解析窗口之后就会是后面那个音节的数据占比大。所以在这个位置之后开始解析的时候还会漏掉一个音节。认为这个窗口里的频率是N,就会漏掉N+1,认为是N+1,就会漏掉N。
当然有的同学看到这里可能会说解析窗口定大一点,但是滑动的时候只滑动音节的大小不就可以解决这个问题了么?这样做虽然能够解决传输一定长度后出现的运气位置和漏掉音节的情况,但是还是不能解决我们开始解析的位置具有随机性的这个问题。如果我们开始解析的位置是这个样子了:
说了这么多,蠢作者想表达的就是解析窗口比一个音节长度大就会出现这样的一些比较难搞的问题,如果能搞定这几个问题,应该也会有不错的效果,不过分析到这里,蠢作者也就换了个思路,就是下面要说的对齐音节的方式。
解析窗口比音节数据要小,宝宝们类比上面的分析稍微分析一下就可以知道更加的不可行。
5.对齐音节:
回顾我上面举的例子,如果我们开始解析的位置能够是这样刚好卡在音节开始的地方,那解析的过程就会很容易了,每次都从窗口里找出振幅最高的频率就可以了。
那要怎么做到让解析位置卡在我们想要的地方了?
在正式的开始介绍对齐音节的方式之前,我们先看一个小实验。
实验的目的是分析解析窗口和音节错开到不同位置时,由快速傅里叶变换计算出的振幅大小变换规律。生成3个音节的 PCM 文件(44100Hz采样率,0.1s持续时间,每个音节4410个采样点),第一个音节是携带频率333Hz的正弦波,第二个音节是携带开始标志频率的正弦波,第3个音节还是携带频率333Hz的正弦波。以十分之一的偏差粒度分析从左到右扫描开始音节时开始标志频率振幅的变化。
看表说话,以下2个结论比较重要:
1.即使只包含了十分之一的开始音节信息,振幅也有0.544862,所以如果解析窗口和音节没有对齐的话,即使是十分之一的位置偏差,包含的相邻音节对当前音节也有很大的干扰;
2.在解析窗口左右偏移五分之一的时候计算出的振幅和完全对齐计算出的振幅没有差别。
这2个结论也是让我放弃解析窗口比音节长度大的原因,包含少量的其他音节数据也会产生比较大的干扰。
分析这个表还可以得出一个重要的结论:
解析窗口从左到右划过目标音节的时候,快速傅里叶变换计算出的目标音节的振幅是先变大后变小的,在保持最大振幅不变的几个位置的中间位置是最靠近完全对齐音节的位置的。
因此当我们的开始音节除了标志音频开始以外,又多了个重要的作用,我们识别到了开始音节的频率之后,可以通过来回扫描,找到开始音节振幅最大的位置,这个位置就应该在音节对齐的位置附近了。
具体的操作就是:
如果经过傅里叶频谱分析,当前传入的解析窗口中有开始音节频率,则把当前窗口的数据拷贝一份,和传入的下一个窗口的音频数据拼接起来,从左到右以一个解析窗口大小扫描,扫描的起始位置每次滑动 interval (90)个采样点。这样两个窗口在一趟扫描中需要解析50次。
PS:50 = unit_sample/interval(49)+1,(49*90=4410)。
将扫描到的开始频率的振幅值和上次扫描到的开始频率的振幅值比较,如果当前振幅值小于了上一次的振幅值,说明已经滑过了音节对齐的位置,对齐位置要从当前扫描位置回退(上个振幅出现的次数+1)/2*interval 个采样点,这样就找到了音节对齐的位置。
以表1中的情况为例,从左到右扫描到+3位置的时候,是第一次当前振幅小于上一次振幅的位置,说明滑过了对齐位置,而上一个振幅(也就是最大振幅0.808161)出现了5次,那么要回退(5+1)/2个interval(表1的 interval 是441),才是对齐后的开始音节位置。
好了,到这里开始音节对齐方式我们已经介绍的差不多了,剩下还要考虑一些意外的情况。
在实际中传入的音频数据是从环境中录制得到的,所以可能会有噪音干扰,导致还没有滑到对齐位置的时候,由于噪音导致的当前扫描位置比上一个扫描位置振幅要小,这种情况下结束扫描会导致对齐的效果太差,为了避免偏离太远,可以在结束对齐的时候多加一个判断条件,当今扫描位置要大于一定的阈值 HEADERTHRESHOLD ,比如0.65,0.7之类的。这样可以避免在离对齐位置比较远时出现的偶然的振幅值小于上一个振幅值的情况。
关于 HEADERTHRESHOLD 的选定,蠢作者想啰嗦两句,将这个值调大,可以更高程度的使得对齐位置准确,但是随着播放音频的设备离录音设备的距离变远,所有音频的振幅都会下降,导致解码端无法识别到开始音节。HEADERTHRESHOLD 越大,能识别的播放设备和录音设备物理距离就越小。所以这个值的设定需要我们在准确率和识别距离上做出相应的取舍。
6.顺序解析
开始音节对齐之后,后面就只需要顺序解析每个窗口的音频数据就可以啦。
在0~95个数据频率区中选出振幅最大的频率f,依次存入频率数组 frequency。
7.结束音节
就像需要一个频率标识音频开始播放一样,我们需要一个标志本次传输结束的频率。识别到结束频率后,意味着此次解码的第一步,从正弦波音频信号到对应的频率数组的解析就完成了。
阿弥陀佛~~~终于把这个部分写完了~~~
剩下的两个映射过程:
2.根据频率数组映射到数字数组;
3.根据码表查找对应的字符;
由于这篇文章已经很长了,我就不啰嗦了,就是查数组,查码表,找到事先约定的频率数组中各个频率对应的字符返回,就完成了整个解析的过程。
最后总结一下,整个解码的过程可以用下面这个很丑的状态机概况:
解码算法(状态机 START,ALIGNMENTHEADER,DECODE):
1.在 START 状态,对上层传入的设备录入的一个解析窗口的音频数据(解析窗口大小:采样率*音节持续时间*每个采样点字节数*声道数),通过快速傅里叶变换分析其中是否有开始标志频率,没有的话这部分数据直接丢弃;有的话,可能是音频开始的标志,将这个窗口数据拷贝一份保存,进入 ALIGNMENTHEADER 状态;
2.在 ALIGNMENTHEADER 状态,将传入的一个窗口的音频数据和保存的上一个窗口的音频数据拼接起来,从左到右以一个解析窗口大小扫描,扫描的起始位置每次右移 interval 个采样点,两个窗口总共扫描 unit_sample/interval+1 次,将扫描到的开始频率的振幅值和上次扫描到的开始频率的振幅值比较,如果当前振幅值大于了 HEADERTHRESHOLD,并且小于了上一次的振幅值,说明已经滑过了音节对齐的位置,对齐位置要从当前扫描位置回退(上个振幅出现的次数+1)/2*interval 个采样点,然后进入到 DECODE 状态;否则的话,识别开始音节失败,重新进入 START 状态。
3.在 DECODE 状态,从对齐后的开始音节的下一个窗口开始,依次读入窗口数据,通过快速傅里叶变换分析出每一个窗口中的频域信息,记录我们关心的98个频率的振幅,直到遇到传输结束标志,停止读入数据,计算并返回解析结果。
1)在0~95个数据频率区中选出振幅最大的数字a;
2)查询数字a对应的字符返回;
下一篇是声波传输系列的最终篇啦,关于一些继续优化的想法。