在之前的EMIPLIB库分析三文章中分析过MIPWAVInput类。之前那篇文章因为只是探寻EMIPLIB的执行框架,所以对一些细节未做过多地分析。因此,遗留下了很多不了解的实现代码。MIPWAVInput类内就有很多这样的代码。
首先,初始化MIPWAVInput时调用open方法使用了最后两个参数的缺省值。最后一个参数的缺省值指示代码为MIPWAVInput申请一块空间,此空间为一数组存储浮点数,并生成一个MIPRawFloatAudioMessage消息。同时,因为open函数最后一个参数使用了缺省值的影响,MIPWAVInput的push函数内会使用MIPWAVReader的浮点数版本的readFrames:bool MIPWAVReader::readFrames(float *buffer, int numFrames, int *numFramesRead)。疑问是,为何确定读取语音数据时强制进行浮点数转换?确定使用浮点数的源头来自MIPWAVInput的init函数。也就是说这项设置来自客户端代码。看到示例代码显示的就是使用浮点数。如果由客户端代码决定是否使用浮点数,那么该依据什么来做出这项决定呢?还是说,其实是否用浮点数还是无符号16位整型无所谓。测试结果表明,open函数的最后一个参数是true或者false对同一个wav文件而言没有区别,在对端都可以听到正确的语音。是不是因为浮点数占用空间更多,可以提供对原始语音更精确地回放?代码中关于这个参数的相关说明或注释基本没有,所以也无从考证。
第二处疑问是每个采样数据取出后的转换过程。浮点数版本的readFrames函数内调用fread API从wav文件内读取特定个数字节后,会对每个字节进行浮点数转换操作。这里为了简化起见,只考虑wav文件内单个采样字节数是2的情形。 即,下面代码中else部分代码。
<p>for (int i = 0 ; i < num ; i++) { for (int j = 0 ; j < m_channels ; j++) { if (m_bytesPerSample == 1) { ... } else { uint32_t x = 0; if ((m_pFrameBuffer[byteBufPos + m_bytesPerSample - 1] & 0x80) == 0x80) x = m_negStartVal; int shiftNum = 0; for (int k = 0 ; k < m_bytesPerSample ; k++, shiftNum += 8, byteBufPos++) x |= ((uint32_t)(m_pFrameBuffer[byteBufPos])) << shiftNum;</p><p> int32_t y = *((int32_t *)(&x));</p><p> buffer[floatBufPos] = ((float)y)*m_scale; } floatBufPos++; } }</p>
上述代码显示,一个采样数据中的低八位放置在一个无符号32位整型数据中的最低八位,一个采样数据中的高八位放置在一个无符号32位整型数据中的倒数第二个低八位,无符号32位整型数据中的高十六位都置成1,得到的32位无符号整型数据再乘以m_scale。m_scale值是在解析wav文件头时依据wav文件内的参数信息计算得到的。从计算公式可以看出,m_scale的值主要依据每采样位数指标。
m_scale = (float)(2.0/((float)(((uint64_t)1) << bitsPerSample)));
现在用一个具体的数据代入这个公式看看能得到什么值。之前为了简化分析单个采样是二个字节的情形,也就是说单个采样十六位。针对这个公式而言,bitsPerSample就是16。uint64_t类型其实就是unsigned __int64,无符号64位整型。将无符号64位整型的1左移16位。也就是最低十六位都是零,原来的最低位移到了第十七位,现在第十六位是1。左移操作其实就是乘,左移十六位相当于乘以整型数字16。再用这个浮点数16作为分母,浮点数2.0作为分子,最终的结果就是m_scale的值。即,现在m_scale的值是浮点数0.125。
上述计算m_scale的公式是第三处疑问。一,为何使用无符号64位整型的1,而不是无符号16或者32位整型的1。二、为何用左移操作而不是乘法操作符。三、为何分子的值是2.0,而不是其他值。
暂且将m_scale计算公式中的疑问放一旁,先回到采样数据转换那个过程。现在知道了,m_scale的值是0.125。即,最后的结果要乘以0.125然后再赋值给readFrames的输入参数之一buffer数组的某个位置。乘以0.125其实就是除以8。赋给buffer数组的float值,为何要除以8。这样的计算公式完全没头绪,不知为何如此。
这三处疑问应该都与语音数据格式有关。这促使我想知道语音源头的格式是什么。之前做分析时,没考虑这些,现在也没任何关于这类信息的相关代码。因此我又看了一遍MIPWAVReader的open函数。果然在解析wav文件头时有相关的代码。
if (!(fmtData[0] == 1 && fmtData[1] == 0)) { fclose(f); setErrorString(MIPWAVREADER_ERRSTR_NOTPCMDATA); return false; }
代码显示,emiplib库只支持源数据格式是PCM的wav文件。也就是说,后续的转换和处理都是基于PCM数据作为基础数据。
依据示例中建立处理链的代码,知道下一个MIPComponent是MIPSamplingRateConverter。以下是这个类的注释说明。
/** Converts sampling rate and number of channels of raw audio messages. * This component accepts incoming floating point or 16 bit signed integer raw audio * messages and produces * similar messages with a specific sampling rate and number of channels set during * initialization. */
MIPSamplingRateConverter可以接收浮点数或者16位有符号整型数据表示的原始语音数据。这段文字也许说明了采用浮点数或者16位整型无本质的区别,可能还是因为处理后数据的精度原因。但还是无法解释为何要将原始的16位PCM数据除以8。
由于这也是格式转换,让人容易想到另外两个开源软件:sox和audacity。这两个程序都是格式转换的利器,只不过运行方式不同。sox是控制台操作方式,audacity是个窗口程序。sox的关于自身实现方式的文档中有这么一句:
SoX’s formats and effects operate with an internal sample format of signed 32-bit integer.
sox在内部的处理都是基于无符号32位整型数据表示的采样数据。这句话隐含的意思是,无论提供给sox的原始采样数据以何种方式表示,sox都会将其转换成无符号32位整型。似乎与我之前的一个假设相呼应:采用浮点数或者16位整型无本质的区别。在任何处理引擎的内部,sox、audacity或者emiplib,都会选择一种或者两种数据类型来表示采样数据。在引擎内部,这种类型是个中间类型,引入了这个中间类型让引擎可以针对某种格式的语音只实现中间类型至目标类型的转换器,简化了引擎的实现。这个结论来自于自身对这个问题的思考,并没有任何其他的文字可以佐证我的想法。