做音频开发的同学一般会和byte数组打交道比较多,因为PCM原始数据一般都是byte数组来表示,如果音频的位深是16bit,那就会是连续两个byte元素表示一个音频幅值。如果需要对幅值进行计算,那就要先将两个byte还原回short值(16位数)再进行计算。
我的byte数组是小端存储,于是我想当然的认为short值计算的Java代码应该是:
//2个byte转化为1个short //将高字节填充到short的高8位,低字节填充到short的低8位 short data = bytes[0] + (bytes[1] << 8); 复制代码
运行结果发现,在两个byte都是正数的情况下无问题,任一个是负数或者两个负数的情况下,会出现高8位数据减去了1的情况。比如:
0xb1 + (0x04 << 8) 会得到值 0x03b1,而不是 0x04b1 复制代码
学计算机组成原理的时候,相信大家都对“补码”一词有概念。这个问题正是补码引起的。
CPU里只有加法器(ALU),没有减法器,因为可以用补码将减法变为加法。
原码和补码的关系:
正数: 补码和原码一致<br> 负数: 原码的符号位不变,其他位取反加1就是补码 如: -1的原码(8bit) : 1000 0001 -1的补码(8bit) : 1111 1111 (即0xff) 所以,做个最简答的减法 1 - 1 得:1 - 1 = 1 + (-1) = 0x01 + 0xff = 0 可见,利用补码和溢出的方式,可以很巧妙的将减法转化为加法 复制代码
强转会发生什么,运行如下代码就会知道。
byte b = -1; //8位的-1:0xff short s = (short) b; //16位的-1: 0xffff 复制代码
所以,负数的byte强转为short时,高八位会全补上1,以保证强转后数据的值不发生变化,但是如果遇上两个byte来表示short的情况,就引发了背景中出现的问题
0xb1 + (0x04 << 8) = 0xffb1 + 0x0400 = 0x03b1 复制代码
相信大家也见过很多博客中这样子的代码
byte b = -1; short s = (short) b & 0x00ff; 复制代码
这样可以保证补的高8位一定是0,而不是1。我们再用这个方法来算一下:
(0xb1 & 0x00ff) + (0x04 << 8) = 0x00b1 + 0x0400 = 0x04b1 复制代码
这样就完美将值进行了还原