最近在调试一个基于十年前Android版本的多媒体应用软件时,遇到了音频播放的问题,这里记录问题的发现、分析和处理过程。
有人可能会好奇,十年前的Android版本是什么版本?大家可以去Google网站上查查,就是目前Android网站上可以看到的最老的Android版本:
没错,就是Android4.x。再之前比较经典和流行的Android版本是2.x系列。可能很多开发者都没有见过这些古董级的机型。
当时开发采用的技术是Java+Native的方式。Native层完成音视频的传输和编解码。Java层根据官方的API,完成音频采集和播放动作。Java和Native之间通过JNI完成接口的互调。开发IDE是Eclipse,AndroidSDK最高支持到24,NDK版本为r8e。就这样一个组合,开发完成的Android应用,最近在一个Android9设备上仍然可以运行,说明Android API的兼容性还是可以的。
软件基本可以运行,但是测试发现,播放音频的时候,有掺杂的比较规律的哒哒哒哒音。因为这个设备是定制的、且是户外使用,喇叭的声音比较大,就是说即使将设备音量调的比较低,播放出来的声音也很大,所以怀疑是不是因为声音较大,导致喇叭震动产生了哒哒哒哒的杂音。
验证这个猜疑很简单,找一个音频文件,拷贝到设备里,使用原生的播放器进行播放,听是否有上述杂音。经过实际测试,发现一切正常。这就说明问题来源于应用软件本身,而跟系统、喇叭和硬件等都没有关系。
如果问题出自于应用,那么可能的原因有两个方面:1是数据本身带有杂音;2是数据的播放节奏控制的不好。其实基本可以确定,问题应该主要在数据本身。如果是节奏的问题,整个音频播放听着其实是不够流畅的,而现在的问题更像是在数据中掺杂了异常音频数据。
如何确定问题产生的具体位置呢?最直接的办法是在可能的数据流程节点上将数据录制下来,单独播放,看是否存在问题。
先在Java层录制了通过JNI接口获取的音频播放数据,使用CoolEdit单独播放,发现存在杂音。这说明播放的数据本身就有问题。因为底层的音频流有多路,存在混音操作,怀疑是不是在这个点出现了问题。录制底层混音后的数据,单独播放测试,发现一切正常。那问题就出在混音后数据到JNI获取这一段。
在这条路径上,添加了多个录音点,测试发现都是好的。甚至在Native层录制被填充音频数据的缓冲区进行测试仍然是正常的。这个缓冲区由JNI传递过来。问题查到此时,就感觉有点奇怪了,难道是Natvie和Java层互传数据有问题?决定在最靠近JNI的上下两层进行数据的保存,然后比较看看数据差异在哪里。
使用Beyond的十六进制比较工具:
发现,有不少数据被修改了:
仔细检查数据,发现基本都是很多个位置的4个字节被改为了0x00 0x00 0x00 0x00。分析这些被修改数据的间隔,发现间距基本都是缓冲区的大小。也就是说,每一包数据的头或者尾的4个字节被修改了。离找到问题根源点越来越接近了。
那这里被修改的到底是头还是尾呢?在Java层将每一包的开头4个字节和结尾4个字节打印出来,跟录制的数据中被修改的点进行比对。
这里的数据看着有点乱。这也是中间踩的一个坑。Java中将字节打印出来,采用了类似下面的接口。开始四个字节还可以,最后四个字节,代码一执行,就崩溃。
" Firt 4 bytes data is " + Integer.toHexString(mBuffer.getChar(0)) + " " + Integer.toHexString(mBuffer.getChar(1)) + " " + Integer.toHexString(mBuffer.getChar(2)) + " " + Integer.toHexString(mBuffer.getChar(3))
看了下说明,是按两个字节来处理的,所以索引的最大值是limit - 2。(是本人落伍了。Java里的char是占用两个字节的。这些内容早还给老师了。C/C++“荼毒”太深!)
通过比对打印的4个字节跟录制的差异数据,发现每一包的末尾4个字节会被替换为0x00 0x00 0x00 0x00。这是什么原因?难道是接口所用的Java API :java.nio.ByteBuffer有问题?
网上找到一个资料,说是ByteBuffer的array()方法会删除前4个字节的数据,建议使用get和put方法。决定按该方法来修改测试。也就是将下面的方式:
ByteBuffer mBuffer;
mBuffer.array()
换为:
byte[] bytesArray = new byte[mBufferSize];
mBuffer.get(bytesArray);
修改后,测试,噪音消失了。
再次录音,比较数据,发现JNI上下层数据也一致了。
如此,基本确定了array()方法是存在问题的。具体是为什么呢?API本身的实现问题还是版本差异的问题?决定再找找根本原因。
写个简单的程序测试,发现,确实开头四个字节会被清零。这也就意味着,连续的包,后四个字节会被清零,跟之前的现象就可以匹配上了。
mBuffer = ByteBuffer.allocateDirect(128);for (byte i=0; i<100; i++) {mBuffer.put(i);}for (char i=0; i<100; i++) {Log.i(TAG, "Index " + Integer.toHexString(i) + " byte data is " + Integer.toHexString( (mBuffer.get(i)) ) );}final byte[] buffer = mBuffer.array();for (char i=0; i<100; i++) {Log.i(TAG, "Index " + Integer.toHexString(i) + " byte data is " + Integer.toHexString(buffer[i]));}
为啥会将开始的4个字节清零呢?或者说为啥有4个字节的偏移?
网上搜索到了相关的一篇博文,质量比较高,基本上说清楚问题了。大家可以参考:
https://blog.csdn.net/lanlangaogao/article/details/120342954
后来,我又验证了下,实际分配的长度确实是加了7
如此,如果每次分配的地址在0、4、8对齐的位置的话,offset就是0、4、0。那出现4字节的偏移导致丢失4子节尾巴的数据,就不奇怪了。
这个问题暂时就追到这里吧。对于Java入门级水平来讲,这个坑踩得不冤。学语言还是要学精通。另外,就是要有追求完美的心境,不放过细节。只要功夫深,问题终可解!