前一篇中详细分析了MIPComponentChain类。了解了执行框架的运作情况。还有必要知晓框架的实现细节,以便于真正掌握库的设计意图。有一点有些模糊,就是MIPComponnet间传递数据这一部分。现在只是有个大致的了解。pull component生成消息,push component接收这些消息。仍然以feedbackexample例程为研究对象。
feedbackexample例程中,在启动处理过程前,会生成很多MIPComponent,然后依据顺序放入MIPComponentChain中。如果只从发送RTP这个角度看,依次被放入的类是这样的顺序。
MIPAverageTimer,这是起始节点。 MIPWAVInput,读入wav文件。 MIPSamplingRateConverter,采样率转换? MIPSampleEncoder,采样数据编码? MIPULawEncoder,u率编码。 MIPRTPULawEncoder,RTP u率编码? MIPRTPComponent,RTP组件。
这样一个顺序正是将wav文件处理后在网络上以RTP包发送的顺序。这些组件中MIPAverageTime类已经分析过了。它执行的操作就是休眠规定时长,是在push函数中实现的。MIPAverageTime类的pull函数会返回一个MIPSystemMessage类实例。现在就依照这个顺序,依次分析每个类的pull和push函数,再参照MIPComponentChain类Thread函数的处理过程,看看到底传递了哪些消息以及如何传递的。
再一次贴出Thread函数内第二阶段代码。
for (it = m_orderedConnections.begin() ; !error && it != m_orderedConnections.end() ; it++) { MIPComponent *pPullComp = (*it).getPullComponent(); MIPComponent *pPushComp = (*it).getPushComponent(); uint32_t mask1 = (*it).getMask1(); uint32_t mask2 = (*it).getMask2(); pPullComp->lock(); pPushComp->lock(); MIPMessage *msg = 0; do { if (!pPullComp->pull(*this, iteration, &msg)) {error = true;errorComponent = pPullComp->getComponentName();errorString = pPullComp->getErrorString();} else { if ( msg ) { uint32_t msgType = msg->getMessageType();uint32_t msgSubtype = msg->getMessageSubtype(); if ( ( msgType&mask1 ) && ( msgSubtype&mask2 ) ) { if ( !pPushComp->push(*this, iteration, msg) ) {error = true;errorComponent = pPushComp->getComponentName();errorString = pPushComp->getErrorString();} } } } } while (!error && msg); pPullComp->unlock(); if (pPushComp->getComponentPointer() != pPullComp->getComponentPointer()) pPushComp->unlock(); }
第一对pull MIPComponent和push MIPComponent
现在以实际的MIPComponent组件顺序为例来解释这第二阶段。第一个MIPConnection的pull component是MIPAverageTime,push component是MIPWAVInput。然后调用pPullComp的pull函数,即调用MIPAverageTime的pull函数。
if (!m_gotMsg) { *pMsg = &m_timeMsg; m_gotMsg = true; } else { *pMsg = 0; m_gotMsg = false; }
pull函数的作用就是将内部的MIPSystemMessage成员变量返回给调用方。但只能返回一次,下次再调用pull函数时返回一个空指针。然后调用pPushComp的push函数,传入刚才获得的MIPSystemMessage。也就是调用了MIPWAVInput的push函数。现在再复习一下MIPWAVInput类的初始化过程。代码显示初始化过程是调用open函数,传入了wav文件名,以及一个MIPTime类实例。open函数的注释详细说明了各个参数的作用。
/** Opens a sound file. * With this function, a sound file can be opened for reading. * \param fname The name of the sound file文件名 * \param interval During each iteration, a number of frames corresponding to the time interval described * by this parameter are read.依据这个参数计算出每次迭代读取的帧数。 * \param loop Flag indicating if the sound file should be played over and over again or just once.是否循环播放的标志。 * \param intSamples If \c true, 16 bit integer samples will be used. If \c false, floating point samples will be used.此值为true使用十六位的整型,此值为false使用浮点数。 */ 实际调用时只给了两个实际参数,后两个使用函数的缺省值。也就是缺省是循环播放和使用浮点数。
bool open(const std::string &fname, MIPTime interval, bool loop = true, bool intSamples = false);
open函数内真正读取文件的类是MIPWAVReader。如果读取成功,取得这个文件的采样率和通道数。代码如下:
m_pSndFile = new MIPWAVReader(); if (!m_pSndFile->open(fname)) { setErrorString(std::string(MIPWAVINPUT_ERRSTR_CANTOPENFILE) + m_pSndFile->getErrorString()); delete m_pSndFile; m_pSndFile = 0; return false; } m_sampRate = m_pSndFile->getSamplingRate(); m_numChannels = m_pSndFile->getNumberOfChannels();
接着是创建十六位整型的缓冲区或者浮点数缓冲区。缓冲区大小由输入参数interval,以及采样率和通道数决定。再相应地创建MIPRaw16bitAudioMessage或者MIPRawFloatAudioMessage类实例。创建MIPRaw16bitAudioMessage类实例也需要采样率、通道数、计算出的帧数和之前创建的缓冲区地址。这就是MIPWAVInput类open函数内容。既然提到了MIPWAVReader类,不妨再仔细看看。
MIPWAVReader的open函数有点长,看样子有点内容。必须得看看。打开文件这步就略过。首先确保文件头部的前四个字节一定是“RIFF”。这应该是wav文件格式的要求。
uint8_t riffID[4]; if (fread(riffID, 1, 4, f) != 4) { fclose(f); setErrorString(MIPWAVREADER_ERRSTR_CANTREADRIFFID); return false; } if (!(riffID[0] == 'R' && riffID[1] == 'I' && riffID[2] == 'F' && riffID[3] == 'F')) { fclose(f); setErrorString(MIPWAVREADER_ERRSTR_BADRIFFID); return false; }
然后再读取四个字节,这四个字节应该指明了实际数据的大小。这里使用了移位操作以及按位或操作。从计算过程可以看出,读出来的四个字节中第一个字节是整型中的最低八位,第二个字节是倒数第二个低八位,依次类推。最后将这四个字节转换成32位再按位或得到最终的数据大小值。
uint8_t riffChunkSizeBytes[4]; int64_t riffChunkSize; if (fread(riffChunkSizeBytes, 1, 4, f) != 4) {fclose(f); setErrorString(MIPWAVREADER_ERRSTR_CANTREADRIFFCHUNKSIZE); return false;}
riffChunkSize = (int64_t)((uint32_t)riffChunkSizeBytes[0] | (((uint32_t)riffChunkSizeBytes[1]) << 8) | (((uint32_t)riffChunkSizeBytes[2]) << 16) | (((uint32_t)riffChunkSizeBytes[3]) << 24));
if (riffChunkSize < 4) {fclose(f); setErrorString(MIPWAVREADER_ERRSTR_RIFFCHUNKSIZETOOSMALL); return false;}
取出了前八个字节后,再取出四个字节。确保这四个字节组成的字串是“WAVE”。同时,将数据大小值减去四。
uint8_t waveID[4]; if (fread(waveID, 1, 4, f) != 4) {fclose(f); setErrorString(MIPWAVREADER_ERRSTR_CANTREADWAVEID); return false;} riffChunkSize -= 4; if (!(waveID[0] == 'W' && waveID[1] == 'A' && waveID[2] == 'V' && waveID[3] == 'E')) {fclose(f); setErrorString(MIPWAVREADER_ERRSTR_BADWAVEID); return false;}
经过上述两次读取,现在已确定这是一个合法的wave文件。接着进入一个while循环,每次迭代又将先分两次读取八个字节,同时将块数据大小值减去8。退出while循环的条件是块数据大小值等于零。头四个字节是类型。存在两种类型。一种是“data”,另一种是“fmt ”。“fmt ”可以理解为格式,或称之为参数。“data”就是实际数据。第二次读取的四字节仍然是一个整型值,只不过要将其转换才能使用。转换过程同之前提到的块数据大小值。如果还存在其他类型,则立即退出处理过程。下面分别看看针对这两种类型将会做哪些处理。
“fmt ”类型。读取16个字节。这16个字节中,第一个字节必须是1,第二个字节必须是0。第三和第四个字节组合起来是通道数。通道数仍然需要通过移位和按位或操作才能得到。这16个字节中的第八个字节不能是0。这可能是标准中规定的。第5、6和7个字节组合成采样率值。采样率值是个整型,因此仍然通过移位和按位或操作。第15和16个字节组合成每个采样率多少个位的数值。这个数值只能是8、16、24和32这四个数值中的其中之一。再依据这个数值计算出每个采样多少个字节这个数值,除以8即可。接着还计算了另一个值存在m_scale内。
m_scale = (float)(2.0/((float)(((uint64_t)1) << bitsPerSample)));
((uint64_t)1) << bitsPerSample,左移一位,也就是乘以2。分子又是2.0,会抵消掉。m_scale也就是bitsPerSample的倒数。“fmt ”类型处理中最后一步是检查“fmt ”类型中读取的数据块大小值与整个wav文件头部读取的块数据大小值是否合理。
“data”类型。首先判断dataChunkSize值是否大于等于零。在进入while循环前此值被赋值为负一。接着调用ftell,取得当前文件流的位置,并赋给m_dataStartPos。
m_dataStartPos = ftell(f);
因为此时是“data”类型,也就是说是实际数据块。m_dataStartPos存储的也就是实际数据的起始位置。同理,“data”类型字段后的四个字节就是实际数据块的大小。
处理完两种类型后,得到了采样率、通道数以及实际数据块的起始地址等信息。最后要检查一下这些信息是否合法。再依据这些值计算后续处理需要的其他值。帧大小值由通道数和每个采样多少个字节决定。还必须确保实际数据块大小这个值是帧大小值的整数倍。这个整数倍存储在m_totalFrames内。再依据帧大小申请一块存储空间。最后是计算m_negStartVal值。后面应该会用到它,现在不清楚为何这么计算。
if (m_bytesPerSample == 4) m_negStartVal = 0x00000000; else if (m_bytesPerSample == 3) m_negStartVal = 0xff000000; else if (m_bytesPerSample == 2) m_negStartVal = 0xffff0000; else m_negStartVal = 0xffffff00;
好,现在应该算是掌握了90%的MIPWAVReader类代码。这个类的主要职责是判定文件是个合法的wav文件,并从文件头部读取相应的信息,并为将来处理这个文件申请正确大小的缓冲区。再回过头去看MIPWAVInput类的open函数,在调用完MIPWAVReader类的open函数后,会立即再调用MIPWAVReader的getSamplingRate和getNumbersOfChannels两个函数。经过上述代码分析,可以知道此时可以取到这个wav文件采样率以及通道数两个信息。
bool MIPWAVInput::open(const std::string &fname, MIPTime interval, bool loop, bool intSamples) { ......... m_sampRate = m_pSndFile->getSamplingRate(); m_numChannels = m_pSndFile->getNumberOfChannels(); int frames = (int)(interval.getValue()*((real_t)m_sampRate)+0.5); m_numFrames = frames; m_loop = loop; if (intSamples) { m_pFramesInt = new uint16_t[m_numFrames*m_numChannels]; m_pMsg = new MIPRaw16bitAudioMessage(m_sampRate, m_numChannels, m_numFrames, true, MIPRaw16bitAudioMessage::Native, m_pFramesInt, false); } else { m_pFramesFloat = new float[m_numFrames*m_numChannels]; m_pMsg = new MIPRawFloatAudioMessage(m_sampRate, m_numChannels, m_numFrames, m_pFramesFloat, false); } m_eof = false; m_gotMessage = false; m_intSamples = intSamples; m_sourceID = 0; return true; }
再依据采样率和传入open函数的interval参数计算出frames。实际feedbackexample历程代码中传入的MIPTime是interval(0.020),注释说明采用二十毫秒间隔。采样率一般是一个类似于8000这样的数值,或者更大。将这个值加一个0.5对最终结果影响不大。再乘以0.02。我们知道采样率是指每秒钟采样的频率。如果是8000,说明每秒钟采样8000次。二十毫秒的间隔意味着每二十毫秒将要发送多少个采样,这么算下来8000/50=160(我忽略了那个加上的0.5,因为这对结果影响不大)。从中可以看出这里将每个间隔处理的采样数称之为一个帧。接着依据open函数的最后一个参数决定创建怎样的缓冲区。要么是无符号16位的数组,要么是浮点数数组。数组大小由之前计算出的帧大小和通道数决定。再相应创建各自相关的MIPMessage子类,MIPRaw16bitAudioMessage或者MIPRawFloatAudioMessage。由于feedbackexmaple代码实际调用open函数时未提供intSamples实参,也就是使用了参数的缺省值。intSamples缺省值是false。也就是说open函数内创建了一个MIPRawFloatAudioMessage类实例。这里说明了一个事实,一个采样数据既可以存储在一个无符号的16位整型数据中,也可以存储在一个32位浮点数中。我们分析的例程采用的是浮点数数据存储一个采样数据。是不是大部分都采样浮点数而不是16位无符号整型,什么情况下会采用16位无符号整型?这些信息估计得查询其他资料才能知晓。我记得之前在分析MIPWAVReader类时也有帧大小和每个采样多少字节这样的数据。不妨现在再去看看。
int bitsPerSample = (int)(((uint16_t)fmtData[14]) | (((uint16_t)fmtData[15]) << 8)); m_bytesPerSample = bitsPerSample/8;
m_frameSize = m_bytesPerSample*m_channels
每个采样多少个位是从文件中取出来的,再除以8就得到了每个采样多少个字节这个数据。帧大小是通道数乘以每个采样字节数得到的。此时似乎可以得出MIPWAVReader的帧大小值与MIPWAVInput的帧大小值不一致。MIPWAVInput的帧大小值是规定的,要么是无符号16位要么是浮点数大小。而MIPWAVReader的帧大小是从wav文件的头部格式段取出的。二者为何存在差异?总之,现在还没法看出二者为何有差异,先留着这个疑问。至此,MIPWAVInput的open函数也分析完了。再回想下,为何要分析MIPWAVInput的open函数,因为这是MIPWAVInput初始化的一步。
在分析MIPWAVInput的open函数前,是停在“调用pPushComp的push函数,传入刚才获得的MIPSystemMessage”。稍微再复习一下,第一个节点是MIPAverageTimer。和MIPAverageTimer组成第一个MIPConnection的push component是MIPWAVInput。也就是说,此时调用MIPWAVInput的push,传给push函数的是MIPSystemMessage消息。MIPWAVInput的push函数前部是判断传入的MIPMessage消息类型是否合法,MIPSystemMessage确实符合要求。第二个判断是文件是否已成功打开。现在得记住一件事,MIPWAVInput已经申请了一段内存空间,这个空间只能存下一定间隔时间内的采样数据。这个值存在m_numFrames内。继续看push函数的处理。是一个判断,只要没到文件结束处就可以继续处理。应该可以想象,MIPWAVInput的push函数不会只被调用一次。应该是按顺序读取整个文件,从头至尾。所以得有个标志标记是否已全部处理完毕。进入处理过程内部,则是立即调用MIPWAVReader的readFrames。传入参数则是MIPWAVInput的open函数被调用时申请的内容空间,以及此内存空间大小。这个内存空间大小,现在再重申一遍它的值是:存下一定间隔时间内的采样数据个数。如果按照采样率8000,二十毫秒为间隔,采样数据个数是:8000/50=160。现在又得切换到MIPWAVReader的readFrames函数内。从MIPWAVInput的角度看MIPWAVReader的readFrames函数是读出特定个数的采样数据。此时我们得再记住MIPWAVReader的一些数据。MIPWAVReader的帧数,即整个wav文件的MIPWAVReader帧数。单个MIPWAVReader帧大小。单个MIPWAVReader帧大小由通道数和每个采样数据的字节数决定,是二者的乘积。MIPWAVReader帧数由实际数据字节数和单个MIPWAVReader帧大小,这两个数据决定。由前者除以后者得到。基本判断结束后,即进入一个while循环。这个while循环会确保读取了MIPWAVInput要求的个数。每次实际读取操作前都将确保每次读取的数据个数不会大小4096,否则只读取4096个数据。接着是实际读取操作。读取的单个数据大小是单个MIPWAVReader帧大小。还记得吗,这个值是通道数乘以每个采样数据的字节数。读取的数据个数确保不会超过4096,因为MIPWAVReader的open函数内只申请了至多4096个数据的空间。这个确保读取出足够个数的数据框架容易理解。这个readFrames函数最主要的部分是在while循环内的for循环内。这是每次实际读取操作后的处理。
for (int i = 0 ; i < num ; i++) { for (int j = 0 ; j < m_channels ; j++) { if (m_bytesPerSample == 1) { buffer[intBufPos] = (((int16_t)(m_pFrameBuffer[byteBufPos])-128)<<8); byteBufPos++; } 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; int32_t y = *((int32_t *)(&x)); if (m_bytesPerSample == 2) buffer[intBufPos] = (int16_t)y; else if (m_bytesPerSample == 3) buffer[intBufPos] = (int16_t)(y >> 8); else buffer[intBufPos] = (int16_t)(y >> 16); } intBufPos++; } }
for循环头部的num内存储的是每次实际读取的数据个数。buffer是MIPWAVInput调用MIPWAVReader时传入的内存空间地址。m_pFrameBuffer是MIPWAVReader申请的内存空间。byteBufPos在for循环前被赋值零,它指向m_pFrameBuffer数组位置。intBufPos在while循环前也被赋值零,它指向buffer数组位置。for循环内又嵌套了另一个for循环。因为num是读取的数据个数,但每个数据是由所有通道的数据组成的。所以内部for循环针对单个数据而言,每次遍历一个通道的数据。单个通道单个采样数据的处理分两种情形。一是每个采样数据是一个字节,另一种是每个采样数据不是一个字节。先分析单个采样数据一个字节的情形。
buffer[intBufPos] = (((int16_t)(m_pFrameBuffer[byteBufPos])-128)<<8);
将单字节扩展成short类型整数,再减去128,最后左移八位。具体目的不详。
记者分析一个采样多个字节的情形。第一步判断单个采样数据中最后一个字节的最高位是否为1,来决定向x变量赋何值。如果最高位是1,那么向x赋m_negStartVal值,否则x的值为0。m_negStartVal的值在MIPWAVReader的open函数内计算出。此值依据一个采样多少字节而定。接下来的for循环式会将每个字节放置在一个32位无符号整型中的确定位置。规则是,在原始数据数组中序号最小的字节将放置在32位无符号整型中的最低八位,倒数第二小的字节将放置在32位无符号整型中的倒数低八位,其它依此类推。由于最终的结果是一个32位无符号整型,但一个采样有可能小于四个字节。所以32位无符号整型数据的高位有可能是无效的。这些无效的位都将通过按位或操作被置为1。例如,每个采样数据有两个字节,m_negStartVal的值是0xffff0000,最高16位都为1。m_negStartVal的值赋给x。x会与最终结果进行按位或操作。由于最高16位都是1,所以最终结果数据的最高16位都是1。但上述无效位设置成1的处理是在单个采样数据中最后一个采样字节的最高位为1的情形下才发生。其他情形无效位都是0。因为x被赋值为0,0x00000000,所有位都是0。最后是将结果放入buffer数组中。这是个16位有符号整型数组。之前我们计算出的是个32位无符号整型。从位数上看,整整多了16位。肯定得处理过后才能放入buffer内。如果一个采样两个字节则直接取这32位无符号整型中的最低16位。如果一个采样三个字节则右移八位,再取最低的16位放入buffer内。其实也就是如果一个采样三个字节,则丢弃处理后得到的32位无符号整型的最低八位,只保留高16位。如果是其他情形(应该就是每个采样四个字节),也是只保留高16位。针对原始语音数据的处理就是这样。为什么会这么做还真不知道。
总结一下,经过上述分析。现在知道,无论原始数据中一个采样由多少个字节组成,MIPWAVReader均将其转换成一个16位有符号整型交给MIPWAVInput。现在再回到MIPWAVInput的open函数内调用MIPWAVReader的readFrames处继续分析。接下来是读取文件结尾处数据的处理。包括重置缓冲区,置文件已读取完毕标志或者将MIPWAVReader置成下次读取时再从头开始。MIPWAVInput的push函数的作用就是从MIPWAVReader中读取特定数量的原始语音数据放置在MIPRaw16bitAudioMessage或者MIPRawFloatAudioMessage类中。这两个类都继承自MIPMessage。同时,push函数的实现也表明,一次push函数操作不会取出一个wav文件的所有数据,只是一部分。
现在再次回到MIPComponentChain类的Thread函数内部第二阶段代码。文章最前面已经列出了那部分代码,可以回到那再看一遍。我们将MIPAverageTimer和MIPWAVInput两个类再次放入这个处理过程中看看到底发生了什么。MIPAverageTimer是起始节点。MIPWAVInput与MIPAverageTimer组成了第一个MIPConnection。所以首先调用MIPAverageTimer的pull函数,取出了一个MIPSystemMessage类,然后交给MIPWAVInput。MIPWAVInput的push函数可以接收这个MIPSystemMessage类,因此执行了一次读取wav文件数据的操作,并将数据放置在了MIPRawFloatAudioMessage类内。我们看到MIPAverageTimer的pull函数和MIPWAVInput的push函数,是在一个do
while循环内。也就是说,如果没出现错误而且也能取到一个MIPMessage类变量,那么将继续再调用一次MIPAverageTimer的pull函数和MIPWAVInput的push函数。第二次MIPAverageTimer的pull函数被调用,但这次被调用不会再返回MIPSystemMessage类了,因为上次pull函数操作已经将标志m_gotMsg置成true了。由于第二次的MIPAverageTimer的pull函数没取到MIPMessage消息,因此也不会第二次再调用MIPWAVInput的push函数。因此do
while结束,继续处理第二个MIPConnection。
第二对pull MIPComponent和push MIPComponent
参照feedbackexample源码第二个MIPConnection由MIPWAVInput和MIPSamplingRateConverter组成。同理,MIPSamplingRateConverter类实例也要初始化后才能使用。
returnValue = chain.addConnection(&sndFileInput, &sampConv);
int samplingRate = 8000; int numChannels = 1; returnValue = sampConv.init(samplingRate, numChannels);
有些奇怪的是,采样率和通道数竟然采用硬编码方式。刚看到init函数时,第一反应就是采样率和通道数由MIPWAVInput提供。在分析MIPSamplingRateConverter的init函数前先了解下这个类的用途。下面这段摘自MIPSamplingRateConverter类的头文件。内容很好理解。这个类接收浮点数或者16位有符号整型表示的原始语音数据,并依据初始化阶段提供的采样率和通道数生成相似的语音数据。所以提供给init函数的采样率和通道数是硬编码方式。这是期望的语音格式。
/** 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. */
进入到init函数内部。函数很简单,就是将输入参数赋值给相应的成员变量。如果之前已使用过这个MIPSamplingRateConverter实例,再次调用init函数会先清理之前的处理。调用init时未提供最后一个实参。也就是使用了参数的缺省值,floatSamples缺省值是true。
bool MIPSamplingRateConverter::init(int outRate, int outChannels, bool floatSamples) { if (m_init) cleanUp(); m_outRate = outRate; m_outChannels = outChannels; m_floatSamples = floatSamples; m_prevIteration = -1; m_init = true; return true; }
初始化分析过了,来看看实际处理的过程。先调用第二个MIPConnection的pull component的pull函数。即,MIPWAVInput的pull函数。MIPWAVInput的pull函数就是取出MIPRaw16bitAudioMessage类。此类由MIPWAVInput的push函数的处理得到,由wav文件的部分语音数据组成。再调用push component的push函数。向push函数提供之前取到的MIPRaw16bitAudioMessage变量。进入到MIPSamplingRateConverter的push函数内部。开始部分常规性的检测消息类型是否正确。接着从MIPRaw16bitAudioMessage变量中取出采样率、帧数以及通道数等信息。再依据m_floatSamples变量决定是创建MIPRawFloatAudioMessage还是MIPRaw16bitAudioMessage。之前分析init函数时知道m_floatSamples的值是true。那么此时就将创建MIPRawFloatAudioMessage类。
int numInChannels = pAudioMsg->getNumberOfChannels(); int numInFrames = pAudioMsg->getNumberOfFrames(); real_t frameTime = (((real_t)numInFrames)/((real_t)pAudioMsg->getSamplingRate())); int numNewFrames = (int)((frameTime * ((real_t)m_outRate))+0.5); int numNewSamples = numNewFrames * m_outChannels; MIPAudioMessage *pNewMsg = 0; ......... MIPRawFloatAudioMessage *pFloatAudioMsg = (MIPRawFloatAudioMessage *)pMsg; const float *oldFrames = pFloatAudioMsg->getFrames(); float *newFrames = new float [numNewSamples]; if (!MIPResample<float,float>(oldFrames, numInFrames, numInChannels, newFrames, numNewFrames, m_outChannels)) {setErrorString(MIPSAMPLINGRATECONVERTER_ERRSTR_CANTRESAMPLE); return false;} pNewMsg = new MIPRawFloatAudioMessage(m_outRate, m_outChannels, numNewFrames, newFrames, true); pNewMsg->copyMediaInfoFrom(*pAudioMsg); // copy time info and source ID
在创建MIPRawFloatAudioMessage类前,先将计算要申请多少内存空间。frameTime由pAudioMsg的帧数和采样率决定。应该还记得,pAudioMsg的采样率值取自wav文件,帧数由采样率和MIPWAVInput初始化函数open的输入参数MIPTime决定。初始化MIPWAVInput用的MIPTime值是20毫秒。此处计算frameTime类似于MIPWAVInput类内计算帧数的逆过程,依据帧数计算每次采样的间隔。计算转换后的帧数时用到的采样率来自MIPSamplingRateConverter初始化时用到的输入参数。实际数据是8000。突然想到这个类的名称是MIPSamplingRateConverter,翻译过来就是采样率转换器。这么理解的话,MIPSamplingRateConverter初始化时输入的采样率是希望得到的采样率。也就是说希望将wav文件内的语音数据转换成采样率为8000,通道数是1的语音数据。转换前和转换后唯一相同的是每次采样的间隔时长。真正的转换过程是这句:
MIPResample<float,float>(oldFrames, numInFrames, numInChannels, newFrames, numNewFrames, m_outChannels)
六个输入参数一目了然。前三个是转换前的数据格式,后三个是希望得到的数据格式。看样子得分析MIPResample类了。很明显这是个模板类。找到头文件后发现这是个模板函数,不是模板类。第一个float指明输入及输出数据采用哪种类型,第二个float说明内部计算使用哪种类型。首先检查输入及输出通道数,确保满足要求。简单的说就是要做到,如果输入数据的通道数大于1,但输出数据的通道数与输入的不同但又不等于1就认为有错。即,多个通道可以转换成通道数相同的多个通道,多个通道也可以转换成一个通道,但多个通道不能转换成通道数不一致的多个通道。接着分三种情况转换数据:转换前和转换后的帧数一致、转换前的帧数大于转换后的帧数和转换前的帧数小于转换后的帧数。
先看帧数一致的情形。又分成三个小条件分支。经过之前的分析现在已经知道不存在转换前后通道数都大于1但不一致的情形。所以只可能有三种情形:转换前通道数是1转换后通道数大于1,转换前通道数大于1转换后通道数是1,转换前后通道数一致。这里的处理可以认为是直接赋值。转换前通道数是1转换后通道数大于1,将转换前单个通道的采样数据重复放入转换后的各个通道内。转换前通道数大于1转换后通道数是1,将转换前各个通道的采样数据累加再除以转换前通道数得到的值赋给转换后的单个通道。转换前后通道数一致,执行一一对应赋值。
再看转换前帧数大于转换后帧数情形。处理以转换后帧数为迭代计数对象。每次迭代都需计算两个数值:startFrame和stopFrame。计算公式如下:
int startFrame = (i*numInputFrames)/numOutputFrames; int stopFrame = ((i+1)*numInputFrames)/numOutputFrames; int num = stopFrame-startFrame;
其实就是计算每次迭代的i值和i+1的值乘以numInputFrames/numOutputFrames。由于numInputFrames大于numOutputFrames,所以这个值肯定是大于1。即,i和i+1乘以一个大于1的数值。而且还要计算两个乘积的差值。差值应该就是一个numInputFrames/numOutputFrames,反正就是一个大于1的数值。然后再以这个差值为迭代对象处理下转换前的数据。
for (int j = 0 ; j < num ; j++, pIn += numInputChannels){ for (int k = 0 ; k < numInputChannels ; k++) inputSum[k] += (Tcalc)pIn[k]; }
针对这个for循环处理以及之前计算num的过程。我认为这个过程可以这么理解。这个num值是为了计算出转换前的帧数是转换后的帧数的多少个整数倍。如果是两倍,num的值就是2,那么将转换前的帧数压缩成一半。即,将两个帧合并成一个。如果num是2,将循环两次,也就是每个inputSum数组元素将累加两个转换前的数据。累加的两个数据都是同一个通道的。如果num的值是3,那么将累加三个同通道的数据。依此类推。然后再除以num,求平均值。
for (int j = 0 ; j < numInputChannels ; j++) inputSum[j] /= (Tcalc)num;
这么处理的目的也很明显。因为转换前后的帧数不一致。如此处理会丢失一些数据。例如,转换前帧数100,转换后帧数80,前者比后者多20。100除80值是1。由于是1,那么inputSum内的每个通道数据不是累加数据。又由于外部迭代是以转换后的帧数为计数对象,所以转换前的最后20个帧将不会被处理。以上是这个情性下如何处理帧数不一致。接着是与帧数一致情形相同,同样存在三个一模一样的条件分支,各个分支处理也相同。
最后来看转换前帧数小于转换后帧数情形。这个情形下的处理以一个for循环为主,以转换前的帧数为计数基准。每次迭代startValues内存储的是转换前单个帧的各个通道数据。迭代开始处,与之前一样依据转换前后帧数的差异计算三个值。最后得出的num值的含义也一样。然后是除最后一次迭代外(i<numInputFrames - 1,最后一次迭代i的值是numInputFrames - 1),其他迭代必须再计算stepValues数组的值。stepValues内存储的是下一个帧同一个通道的数据减去当前帧同一个通道的数据。也就是说,每次迭代时startValues内存储了当前帧各个通道的数据,stepValues内存储了下一个帧与当前帧同一通道的差值。接着是在处理转换前单个帧时另一个for循环,以计算得到的num值为迭代计数对象。然后再以转换前通道数为计数对象计算interpolation数组。这个数组值得计算公式是下述两个值的和:当前帧通道数据,与下一帧同一通道的差值除以num再乘以通道索引。接着是与帧数一致情形相同,同样存在三个一模一样的针对通道数的条件分支,各个分支处理也相同。此时interpolation数组值作为转换前的数据赋给转换后的缓冲区。我认为这一情形转换后的缓冲区有一小段时空白的。例如,转换前是帧数是80,转换后是100。我认为可以转换完整的80帧数据,但依此处理过程,无法填充转换后后20帧的缓冲区。但如果转换前后两个帧数数据的关系是整数倍,依次处理过程是可以填充满转换后缓冲区。
执行完MIPResample函数后,就做完了转换操作。接着用转换后得到的帧数和缓冲区创建MIPRawFloatAudioMessage类实例。接着调用MIPRawFloatAudioMessage的copyMediaInfoFrom,从push函数的输入参数MIPMessage中拷贝sourceid和time信息。创建完MIPRawFloatAudioMessage实例后,立即判断输入参数迭代值是否是一个新的。如果是一个新的迭代值,那么就删除之前那次迭代创建的所有MIPRawFloatAudioMessage实例。push函数的参数iteration是指一次完整遍历MIPConneciton的过程。最后则是将此MIPRawFloatAudioMessage实例放入内部队列m_messages内。
至此,分析完了MIPSamplingRateConverter的push函数。经过两个MIPConnection的分析,现在知道每次处理一个MIPConnection时,pull函数的作用就是从MIPComponent中取出MIPMessage,push函数的作用就是接收一个MIPMessage再在此基础上生成一个MIPMessage。 第一个MIPConnection的pull component-MIPAverageTimer的pull函数取出了MIPSystemMessage,交给push componnet-MIPWAVInput的push函数,push函数内再生成MIPRawFloatAudioMessage。MIPRawFloatAudioMessage函数内保留了一部分wav文件内的语音数据。第二个MIPConnection的pull
component-MIPWAVInput的pull函数取出了MIPRawFloatAudioMessage,交给push component-MIPSamplingRateConverter的push函数,push函数内再生成MIPRawFloatAudioMessage。整个链条应该就会按照这种击鼓传花方式传递MIPMessage,处理完后再生成一个新的MIPMessage传递给下一个MIPComponent,直到链条的终止MIPComponent。
这只是处理完一遍第二个MIPConnection 。处理每个MIPConnection都有一个小的do while循环。在这个小的do while循环内会第二次调用MIPWAVInput的pull函数。pull函数内的代码交待地很清楚,在不调用push函数重置m_gotMessage为false的情况下调用pull函数将不会返回MIPMessage。即,执行完第二次MIPWAVInput的pull函数后由于取不到MIPMessage,do-while结束。接下来处理第三个MIPConnection。
第三对pull MIPComponent和push MIPComponent
参照feedbackexample源码第三个MIPConnection由MIPSamplingRateConverter和MIPSampleEncoder组成。同理,MIPSampleEncoder类实例也要初始化后才能使用。
sampEnc.init(MIPRAWAUDIOMESSAGE_TYPE_S16);
这个init函数只是初始化内部变量。
MIPSamplingRateConverter的pull函数会每次取出一个MIPMessage。这个MIPMessage其实是MIPRawFloatAudioMessage,它继承自MIPAudioMessage,MIPAudioMessage是MIPMediaMessage的子类,MIPMediaMessage是MIPMessage的子类。接下来MIPSampleEncoder的push函数会接收这个MIPRawFloatAudioMessage。
/** Changes the sample encoding of raw audio messages. * This component can be used to change the sample encoding of raw audio messages. * It accepts all raw audio messages and produces similar raw audio messages, using * a predefined encoding type. */
上面这段是源码中MIPSampleEncoder类的说明文字。之前的MIPSamplingRateConverter的作用是转换采样率和通道数。现在的MIPSampleEncoder的作用是转换采样编码格式。这个过程就是在MIPSampleEncoder的push函数内完成。
push函数内的第一步是申请一个内存空间。首先是依据转换前语音数据的通道数和帧数得到总帧数。然后再依据目的采样编码格式决定申请何种类型的数据。实际情况是目的采样编码格式是MIPRAWAUDIOMESSAGE_TYPE_S16。依据push函数内的代码可知会执行这句:
pSamples16 = new uint16_t [numIn];
申请一个无符号16位整型数组。numIn是原语音数据的通道数和帧数的乘积。接着是得到原语音数据的缓冲区。依据原语音数据的类型,pSamplesFloatIn指向了这个缓冲区。接着是对每个数据进行处理,处理过后的数据都放入pSamples16指向的缓冲区内。最后再用这些转换后的数据生成一个MIPRaw16bitAudioMessage对象并放入MIPSampleEncoder的内部队列中。
应该还记得,每对MIPConnection都不会只调用一次pull和push函数,那是一个有着退出机制的do-while循环。退出标志就是pull函数取不出MIPMessage对象了。所以现在看看MIPSamplingRateConverter的pull函数会在什么情况下取不出MIPMessage。
if (m_msgIt == m_messages.end()) { *pMsg = 0; m_msgIt = m_messages.begin(); } else { *pMsg = *m_msgIt; m_msgIt++; }
代码显示每次调用pull时,都会从m_messages队列内取出一个MIPMessage,如果队列为空那么就取不出MIPMessage了。即,处理这第三对MIPConnection时,如果MIPSamplingRateConverter内的队列为空则结束do-while循环,继续下一对MIPConnection的处理。此时,MIPSampleEncoder内的队列内存储了已经处理过的MIPMessage对象。
第四对pull MIPComponent和push MIPComponent
参照feedbackexample源码第四个MIPConnection由MIPSampleEncoder和MIPULawEncoder组成。同理,MIPULawEncoder类实例也要初始化后才能使用。
returnValue = uLawEnc.init();
init函数只是初始化内部变量而已。
MIPSampleEncoder的pull函数会每次取出一个MIPMessage。这个MIPMessage其实是MIPRaw16bitAudioMessage,它继承自MIPAudioMessage,MIPAudioMessage是MIPMediaMessage的子类,MIPMediaMessage是MIPMessage的子类。接下来MIPULawEncoder的push函数会接收这个MIPRawFloatAudioMessage。
/** An u-law encoder. * This component accepts raw audio messages using 16 bit signed native endian * encoding. The samples are converted to u-law encoded samples and a message * with type MIPMESSAGE_TYPE_AUDIO_ENCODED and subtype MIPENCODEDAUDIOMESSAGE_TYPE_ULAW * is produced. */
上面这段是源码中MIPULawEncoder类的说明文字。之前的MIPSampleEncoder的作用是转换采样编码格式。现在的MIPULawEncoder的作用是转换成u律采样格式。这个过程就是在MIPULawEncoder的push函数内完成。
MIPULawEncoder的push函数与之前两个MIPComponent的push函数类似。取出帧数、通道数和采样率等信息,计算需要的缓冲区大小。然后逐个字节进行转换。转换过程与之前一样,虽然代码能够看懂但为何是这样的转换过程实在是搞不明白。
MIPULawEncoder的pull函数与MIPSampleEncoder的pull函数一样。
第五对pull MIPComponent和push MIPComponent
参照feedbackexample源码第四个MIPConnection由MIPULawEncoder和MIPRTPULawEncoder组成。同理,MIPRTPULawEncoder类实例也要初始化后才能使用。 init函数只是初始化内部变量而已。
returnValue = rtpEnc.init();
MIPULawEncoder的pull函数会每次取出一个MIPMessage。这个MIPMessage其实是MIPEncodedAudioMessage,它继承自MIPAudioMessage,MIPAudioMessage是MIPMediaMessage的子类,MIPMediaMessage是MIPMessage的子类。接下来MIPRTPULawEncoder的push函数会接收这个MIPRawFloatAudioMessage。
/** Creates RTP packets for U-law encoded audio packets. * This component accepts incoming U-law encoded 8000Hz mono audio packets and generates * MIPRTPSendMessage objects which can then be transferred to a MIPRTPComponent instance. */
上面这段是源码中MIPRTPULawEncoder类的说明文字。意思很清楚,这个类的作用是为已经编码为u律的语音数据生成RTP包。它生成的消息是MIPRTPSendMessage。MIPRTPULawEncoder类的push函数代码显示,这个类只接收采样率为8000,通道数不为1的语音数据。这和类的说明内容一致。生成MIPRTPSendMessage消息的过程不复杂,因为数据已经处理过了,只是拷贝而已。唯一要注意的是这句:
pNewMsg->setSamplingInstant(pEncMsg->getTime());
调用MIPEncodedAudioMessage消息的getTime函数,并将返回值提供给MIPRTPSendMessage的setSamplingInstant函数。看看getTime取出了什么数据。getTime函数是在父类MIPMediaMessage实现的函数,只是返回内部成员变量m_time,它的类型是MIPTime。m_time在消息类被创建时被初始化为0。如果m_time的值非常重要,那就会在消息生成后的其他时间被赋值。只能回溯了,先检查MIPULawEncoder类。MIPULawEncoder类的push函数内有这么一句:
pNewMsg->copyMediaInfoFrom(*pAudioMsg); // copy time and sourceID
copyMediaInfoFrom函数也是MIPMediaMessage类实现的函数。次函数的作用就是从另一个MIPMediaMessage消息里拷贝来m_sourceID和m_time。因为只要是MIPMediaMessage消息,都会有这两个成员变量。继续回溯,回到MIPSampleEncoder类的push函数。
pNewMsg->copyAudioInfoFrom(*pAudioMsg);
copyAudioInfoFrom函数内又调用了copyMediaInfoFrom函数,所以这里仍然不是m_time生成的源头。接着看MIPSamplingRateConverter的push函数,又再次看到了如下的语句:
pNewMsg->copyMediaInfoFrom(*pAudioMsg); // copy time info and source ID
然后是MIPWAVInput的push函数,函数内没有针对m_time的任何代码。MIPWAVInput的open函数内也没有任何关于m_time的代码。依据示例open函数内应该是创建MIPRawFloatAudioMessage消息,但此消息的构造函数内也没有相关的代码。不明白了,m_time在任何时候都是0,那还有什么作用。MIPRTPSendMessage的setSamplingInstant函数是将输入参数赋给MIPRTPSendMessage的m_samplingInstant成员变量。
MIPRTPULawEncoder的pull函数与MIPULawEncoder的pull函数一样。
第六对pull MIPComponent和push MIPComponent
参照feedbackexample源码第四个MIPConnection由MIPRTPULawEncoder和MIPRTPComponent组成。同理,MIPRTPComponent类实例也要初始化后才能使用。 MIPRTPComponent的初始化过程较复杂。
<p>RTPSession rtpSession; ... int samplingRate = 8000; ... RTPUDPv4TransmissionParams transmissionParams; RTPSessionParams sessionParams; int portBase = 27888; int status;</p><p>transmissionParams.SetPortbase(portBase); sessionParams.SetOwnTimestampUnit(1.0/((double)samplingRate)); sessionParams.SetMaximumPacketSize(64000); sessionParams.SetAcceptOwnPackets(true); status = rtpSession.Create(sessionParams,&transmissionParams); checkError(status);</p><p>// Instruct the RTP session to send data to ourselves. status = rtpSession.AddDestination(RTPIPv4Address(ntohl(inet_addr("192.168.77.51")),portBase)); checkError(status);</p><p>// Tell the RTP component to use this RTPSession object. returnValue = rtpComp.init(&rtpSession);</p>
MIPRTPComponent的init函数所需的参数是个RTPSession。这个类是emiplib库依赖的底层库之一jrtplib提供的类。RTPSession类定义了传输RTP数据时需使用的各项参数。包括对端地址、对端端口号等。RTPUDPv4TransmissionParams也是jrtplib提供的类。这里调用了RTPUDPv4TransmissionParams的三个设置函数。前两个通过函数名称可以立即了解到它们的用途,最后一个的用途不清楚。SetOwnTimestampUnit函数的用途应该就是取每次发送多长时间间隔的数据。这里采样率是8000,时间间隔就是125毫秒。init函数内部只是保存下传入的RTPSession变量地址。init函数还有个参数可以有缺省值,示例代码使用了这个缺省参数值。init函数的注释很清楚地解释了这个参数的作用:与静音有关。
/** Initializes the component. * With this function the component can be initialized. * \param pSess The JRTPLIB RTPSession object which will be used to receive and transmit * RTP packets. * \param silentTimestampIncrement When using some kind of silence suppression or push-to-talk * system, it is possible that during certain intervals no * messages will reach this component. For these 'skipped' * intervals, the RTP timestamp will be increased by this amount. */
MIPRTPULawEncoder的pull函数会每次取出一个MIPMessage。这个MIPMessage其实是MIPRTPSendMessage,它继承自MIPMessage。接下来MIPRTPComponent的push函数会接收这个MIPRTPSendMessage。
push函数内首先检查传入的MIPMessage的类型是否满足要求。接着有一个静音相关的处理,由于这不是重点暂且略过。然后是调用传入消息变量的getSamplingInstant方法。应该还记得,在分析第五对MIPConnection的最后时调用了MIPRTPSendMessage的setSamplingInstant。这两个方法是相互呼应的。但在那时,我们分析的结果是提供给setSamplingInstant方法的值永远都是0。再调用MIPTime的getCurrentTime方法。最后是计算二者的差值。得到的这个数据仍然是为了设置RTPSession变量。最后一步就是调用RTPSession的SendPacket方法向网络对端发送RTP数据。
经过分析这六对MIPConnection的处理,现在大致了解了emiplib库的底层运行机制。emiplib会在后台启动一个线程来执行这个运行框架。框架的搭建在线程创建之前,且必须由开发人员显示指定这样一个执行顺序框架。执行顺序框架由众多的MIPCompnent组成,每个执行特定功能的模块均继承自MIPComponent。功能链条上前后顺序相邻的两个MIPComponent组成一个MIPConnection。每个MIPConnection内,次序在前的MIPComponent称为pull component,次序在后的称为push
component。运行框架在后台线程内运作,依次处理每个MIPConnection:先调用pull component的pull函数取出一个MIPMessage,然后调用push component的push函数向其提供这个MIPMessage。现在可以清晰地感觉到从wav文件中取出一段语音数据后如何经由这个运行框架最终发送到特定网络地址的过程。
emiplib的执行框架现在已经清楚了,但在这分析过程中又发现了很多其他的知识盲点。尤其是在很多的编码格式转换过程中遇到的转换算法。代码能看明白,但不明白这些代码后面所体现出的算法本质。其他不熟悉的地方还有最后使用的发送RTP数据的RTPSession类。这个类很多相关设置的意图不清楚。
分析过后的另一个想法就是,想将这个执行框架用libuv库重新再实现一遍。emiplib库使用多线程方式实现了这个执行框架。最近在研究和使用node.js提供的libuv库。这个库提供了一套非常棒的异步执行框架。如果能用libuv完整地再实现一次应该非常有趣。