最近在调试一个基于十年前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入门级水平来讲,这个坑踩得不冤。学语言还是要学精通。另外,就是要有追求完美的心境,不放过细节。只要功夫深,问题终可解!
1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
我今天看到了一个ruby代码片段。[1,2,3,4,5,6,7].inject(:+)=>28[1,2,3,4,5,6,7].inject(:*)=>5040这里的注入(inject)和之前看到的完全不一样,比如[1,2,3,4,5,6,7].inject{|sum,x|sum+x}请解释一下它是如何工作的? 最佳答案 没有魔法,符号(方法)只是可能的参数之一。这是来自文档:#enum.inject(initial,sym)=>obj#enum.inject(sym)=>obj#enum.inject(initial){|mem
我真的只是不确定这意味着什么或我应该做什么才能让网页在我的本地主机上运行。现在它只是显示一个错误,上面写着“我们很抱歉,但出了点问题。”当我运行railsserver并在chrome中打开localhost:3000时。这是控制台输出:StartedGET"/users/sign_in"for127.0.0.1at2013-07-0512:07:07-0400ProcessingbyDevise::SessionsController#newasHTMLCompleted500InternalServerErrorin55msNoMethodError(undefinedmethod`
好的,所以我有了我正在使用的应用程序的这种方法,它可以在生产中使用。我的问题为什么这行得通?这是新的Ruby语法吗?defeditload_elements(current_user)unlesscurrent_user.role?(:admin)respond_todo|format|format.json{render:json=>@user}format.xml{render:xml=>@user}format.htmlendrescueActiveRecord::RecordNotFoundrespond_to_not_found(:json,:xml,:html)end
你能解释一下吗?我想评估来自两个不同来源的值和计算。一个消息来源为我提供了以下信息(以编程方式):'a=2'第二个来源给了我这个表达式来评估:'a+3'这个有效:a=2eval'a+3'这也有效:eval'a=2;a+3'但我真正需要的是这个,但它不起作用:eval'a=2'eval'a+3'我想了解其中的区别,以及如何使最后一个选项起作用。感谢您的帮助。 最佳答案 您可以创建一个Binding,并将相同的绑定(bind)与每个eval相关联调用:1.9.3p194:008>b=binding=>#1.9.3p194:009>eva
我一直在寻找一种以编程方式或通过命令行将mp3转换为aac的方法,但没有成功。理想情况下,我有一段代码可以从我的Rails应用程序中调用,将mp3转换为aac。我安装了ffmpeg和libfaac,并能够使用以下命令创建aac文件:ffmpeg-itest.mp3-acodeclibfaac-ab163840dest.aac当我将输出文件的名称更改为dest.m4a时,它无法在iTunes中播放。谢谢! 最佳答案 FFmpeg提供AAC编码功能(如果您已编译它们)。如果您使用的是Windows,则可以从here获取完整的二进制文件。
(跟进我之前的问题,Ruby:howcanIcopyavariablewithoutpointingtothesameobject?)我正在编写一个简单的Ruby程序来在.svg文件中进行一些替换。第一步是从文件中提取信息并将其放入数组中。为了避免每次调用此函数时都从磁盘读取文件,我尝试使用memoize设计模式-在第一次调用后的每次调用中都使用缓存结果。为此,我使用了一个在函数之前定义的全局变量。但是,即使我在返回局部变量之前将该变量.dup为局部变量,调用该变量的函数仍在修改全局变量。这是我的实际代码:#memoizetokeepfromhavingtoreadoriginalfi
当我刚刚运行middleman时服务,all.css编译得很好,只包含对+box-shadow(none)的调用:/*line1,/home/yang/asdf/source/stylesheets/content.css.sass*/div{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}但是当我构建网站时,我得到了这个Sass/Compass错误:$middlemanbuildSlim::EmbeddedEngineisdeprecated,itiscalledSlim::EmbeddedinSlim2.0
按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我来自C、php和bash背景,很容易学习,因为它们都有相同的C结构,我可以将其与我已经知道的联系起来。然后2年前我学了Python并且学得很好,Python对我来说比Ruby更容易学。然后从去年开始,我一直在尝试学习Ruby,然后是Rails,我承认,直到现在我还是学不会,讽刺的是那些打着简单易学的烙印,但是对于我这样一个老练的程序员来说,我只是无法将它