程序员人生 网站导航

EMIPLIB库分析二

栏目:互联网时间:2016-06-12 15:52:07

前1篇中详细分析了MIPComponentChain类。了解了履行框架的运作情况。还有必要知晓框架的实现细节,以便于真正掌握库的设计意图。有1点有些模糊,就是MIPComponnet间传递数据这1部份。现在只是有个大致的了解。pull component生成消息,push component接收这些消息。依然以feedbackexample例程为研究对象。

feedbackexample例程中,在启动处理进程前,会生成很多MIPComponent,然后根据顺序放入MIPComponentChain中。如果只从发送RTP这个角度看,顺次被放入的类是这样的顺序。

MIPAverageTimer,这是起始节点。 MIPWAVInput,读入wav文件。 MIPSamplingRateConverter,采样率转换? MIPSampleEncoder,采样数据编码? MIPULawEncoder,u率编码。 MIPRTPULawEncoder,RTP u率编码? MIPRTPComponent,RTP组件。

这样1个顺序正是将wav文件处理后在网络上以RTP包发送的顺序。这些组件中MIPAverageTime类已分析过了。它履行的操作就是休眠规定时长,是在push函数中实现的。MIPAverageTime类的pull函数会返回1个MIPSystemMessage类实例。现在就依照这个顺序,顺次分析每一个类的pull和push函数,再参照MIPComponentChain类Thread函数的处理进程,看看到底传递了哪些消息和如何传递的。

再1次贴出Thread函数内第2阶段代码。

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(); }

 

第1对pull MIPComponent和push MIPComponent

现在以实际的MIPComponent组件顺序为例来解释这第2阶段。第1个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成员变量返回给调用方。但只能返回1次,下次再调用pull函数时返回1个空指针。然后调用pPushComp的push函数,传入刚才取得的MIPSystemMessage。也就是调用了MIPWAVInput的push函数。现在再温习1下MIPWAVInput类的初始化进程。代码显示初始化进程是调用open函数,传入了wav文件名,和1个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使用106位的整型,此值为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();

接着是创建106位整型的缓冲区或浮点数缓冲区。缓冲区大小由输入参数interval,和采样率和通道数决定。再相应地创建MIPRaw16bitAudioMessage或MIPRawFloatAudioMessage类实例。创建MIPRaw16bitAudioMessage类实例也需要采样率、通道数、计算出的帧数和之前创建的缓冲区地址。这就是MIPWAVInput类open函数内容。既然提到了MIPWAVReader类,无妨再仔细看看。
MIPWAVReader的open函数有点长,看模样有点内容。必须得看看。打开文件这步就略过。首先确保文件头部的前4个字节1定是“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; }

然后再读取4个字节,这4个字节应当指明了实际数据的大小。这里使用了移位操作和按位或操作。从计算进程可以看出,读出来的4个字节中第1个字节是整型中的最低8位,第2个字节是倒数第2个低8位,顺次类推。最后将这4个字节转换成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;}

取出了前8个字节后,再取出4个字节。确保这4个字节组成的字串是“WAVE”。同时,将数据大小值减去4。

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;}

经过上述两次读取,现在已肯定这是1个合法的wave文件。接着进入1个while循环,每次迭代又将先分两次读取8个字节,同时将块数据大小值减去8。退出while循环的条件是块数据大小值等于零。头4个字节是类型。存在两种类型。1种是“data”,另外一种是“fmt ”。“fmt ”可以理解为格式,或称之为参数。“data”就是实际数据。第2次读取的4字节依然是1个整型值,只不过要将其转换才能使用。转换进程同之条件到的块数据大小值。如果还存在其他类型,则立即退出处理进程。下面分别看看针对这两种类型将会做哪些处理。

“fmt ”类型。读取16个字节。这16个字节中,第1个字节必须是1,第2个字节必须是0。第3和第4个字节组合起来是通道数。通道数依然需要通过移位和按位或操作才能得到。这16个字节中的第8个字节不能是0。这多是标准中规定的。第5、6和7个字节组合成采样率值。采样率值是个整型,因此依然通过移位和按位或操作。第15和16个字节组合成每一个采样率多少个位的数值。这个数值只能是8、16、24和32这4个数值中的其中之1。再根据这个数值计算出每一个采样多少个字节这个数值,除以8便可。接着还计算了另外一个值存在m_scale内。

m_scale = (float)(2.0/((float)(((uint64_t)1) << bitsPerSample)));

((uint64_t)1) << bitsPerSample,左移1位,也就是乘以2。份子又是2.0,会抵消掉。m_scale也就是bitsPerSample的倒数。“fmt ”类型处理中最后1步是检查“fmt ”类型中读取的数据块大小值与全部wav文件头部读取的块数据大小值是不是公道。

“data”类型。首先判断dataChunkSize值是不是大于等于零。在进入while循环前此值被赋值为负1。接着调用ftell,获得当前文件流的位置,并赋给m_dataStartPos。

m_dataStartPos = ftell(f);

由于此时是“data”类型,也就是说是实际数据块。m_dataStartPos存储的也就是实际数据的起始位置。同理,“data”类型字段后的4个字节就是实际数据块的大小。

处理完两种类型后,得到了采样率、通道数和实际数据块的起始地址等信息。最后要检查1下这些信息是不是合法。再根据这些值计算后续处理需要的其他值。帧大小值由通道数和每一个采样多少个字节决定。还必须确保实际数据块大小这个值是帧大小值的整数倍。这个整数倍存储在m_totalFrames内。再根据帧大小申请1块存储空间。最后是计算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),注释说明采取210毫秒间隔。采样率1般是1个类似于8000这样的数值,或更大。将这个值加1个0.5对终究结果影响不大。再乘以0.02。我们知道采样率是指每秒钟采样的频率。如果是8000,说明每秒钟采样8000次。210毫秒的间隔意味着每210毫秒将要发送多少个采样,这么算下来8000/50=160(我疏忽了那个加上的0.5,由于这对结果影响不大)。从中可以看出这里将每一个间隔处理的采样数称之为1个帧。接着根据open函数的最后1个参数决定创建怎样的缓冲区。要末是无符号16位的数组,要末是浮点数数组。数组大小由之前计算出的帧大小和通道数决定。再相应创建各自相干的MIPMessage子类,MIPRaw16bitAudioMessage或MIPRawFloatAudioMessage。由于feedbackexmaple代码实际调用open函数时未提供intSamples实参,也就是使用了参数的缺省值。intSamples缺省值是false。也就是说open函数内创建了1个MIPRawFloatAudioMessage类实例。这里说明了1个事实,1个采样数据既可以存储在1个无符号的16位整型数据中,也能够存储在1个32位浮点数中。我们分析的例程采取的是浮点数数据存储1个采样数据。是否是大部份都采样浮点数而不是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的帧大小值不1致。MIPWAVInput的帧大小值是规定的,要末是无符号16位要末是浮点数大小。而MIPWAVReader的帧大小是从wav文件的头部格式段取出的。2者为什么存在差异?总之,现在还没法看出2者为什么有差异,先留着这个疑问。至此,MIPWAVInput的open函数也分析完了。再回想下,为什么要分析MIPWAVInput的open函数,由于这是MIPWAVInput初始化的1步。

 

在分析MIPWAVInput的open函数前,是停在“调用pPushComp的push函数,传入刚才取得的MIPSystemMessage”。略微再温习1下,第1个节点是MIPAverageTimer。和MIPAverageTimer组成第1个MIPConnection的push component是MIPWAVInput。也就是说,此时调用MIPWAVInput的push,传给push函数的是MIPSystemMessage消息。MIPWAVInput的push函数前部是判断传入的MIPMessage消息类型是不是合法,MIPSystemMessage确切符合要求。第2个判断是文件是不是已成功打开。现在得记住1件事,MIPWAVInput已申请了1段内存空间,这个空间只能存下1定间隔时间内的采样数据。这个值存在m_numFrames内。继续看push函数的处理。是1个判断,只要没到文件结束处就能够继续处理。应当可以想象,MIPWAVInput的push函数不会只被调用1次。应当是按顺序读取全部文件,从头至尾。所以得有个标志标记是不是已全部处理终了。进入处理进程内部,则是立即调用MIPWAVReader的readFrames。传入参数则是MIPWAVInput的open函数被调用时申请的内容空间,和此内存空间大小。这个内存空间大小,现在再重申1遍它的值是:存下1定间隔时间内的采样数据个数。如果依照采样率8000,210毫秒为间隔,采样数据个数是:8000/50=160。现在又得切换到MIPWAVReader的readFrames函数内。从MIPWAVInput的角度看MIPWAVReader的readFrames函数是读出特定个数的采样数据。此时我们得再记住MIPWAVReader的1些数据。MIPWAVReader的帧数,即全部wav文件的MIPWAVReader帧数。单个MIPWAVReader帧大小。单个MIPWAVReader帧大小由通道数和每一个采样数据的字节数决定,是2者的乘积。MIPWAVReader帧数由实际数据字节数和单个MIPWAVReader帧大小,这两个数据决定。由前者除以后者得到。基本判断结束后,即进入1个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])⑴28)<<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循环针对单个数据而言,每次遍历1个通道的数据。单个通道单个采样数据的处理分两种情形。1是每一个采样数据是1个字节,另外一种是每一个采样数据不是1个字节。先分析单个采样数据1个字节的情形。

buffer[intBufPos] = (((int16_t)(m_pFrameBuffer[byteBufPos])⑴28)<<8);

将单字节扩大成short类型整数,再减去128,最后左移8位。具体目的不详。

记者分析1个采样多个字节的情形。第1步判断单个采样数据中最后1个字节的最高位是不是为1,来决定向x变量赋何值。如果最高位是1,那末向x赋m_negStartVal值,否则x的值为0。m_negStartVal的值在MIPWAVReader的open函数内计算出。此值根据1个采样多少字节而定。接下来的for循环式会将每一个字节放置在1个32位无符号整型中的肯定位置。规则是,在原始数据数组中序号最小的字节将放置在32位无符号整型中的最低8位,倒数第2小的字节将放置在32位无符号整型中的倒数低8位,其它依此类推。由于终究的结果是1个32位无符号整型,但1个采样有可能小于4个字节。所以32位无符号整型数据的高位有多是无效的。这些无效的位都将通过按位或操作被置为1。例如,每一个采样数据有两个字节,m_negStartVal的值是0xffff0000,最高16位都为1。m_negStartVal的值赋给x。x会与终究结果进行按位或操作。由于最高16位都是1,所以终究结果数据的最高16位都是1。但上述无效位设置成1的处理是在单个采样数据中最后1个采样字节的最高位为1的情形下才产生。其他情形无效位都是0。由于x被赋值为0,0x00000000,所有位都是0。最后是将结果放入buffer数组中。这是个16位有符号整型数组。之前我们计算出的是个32位无符号整型。从位数上看,整整多了16位。肯定得处理过后才能放入buffer内。如果1个采样两个字节则直接取这32位无符号整型中的最低16位。如果1个采样3个字节则右移8位,再取最低的16位放入buffer内。其实也就是如果1个采样3个字节,则抛弃处理后得到的32位无符号整型的最低8位,只保存高16位。如果是其他情形(应当就是每一个采样4个字节),也是只保存高16位。针对原始语音数据的处理就是这样。为何会这么做还真不知道。

总结1下,经过上述分析。现在知道,不管原始数据中1个采样由多少个字节组成,MIPWAVReader均将其转换成1个16位有符号整型交给MIPWAVInput。现在再回到MIPWAVInput的open函数内调用MIPWAVReader的readFrames处继续分析。接下来是读取文件结尾处数据的处理。包括重置缓冲区,置文件已读取终了标志或将MIPWAVReader置成下次读取时再从头开始。MIPWAVInput的push函数的作用就是从MIPWAVReader中读取特定数量的原始语音数据放置在MIPRaw16bitAudioMessage或MIPRawFloatAudioMessage类中。这两个类都继承自MIPMessage。同时,push函数的实现也表明,1次push函数操作不会取出1个wav文件的所有数据,只是1部份。

现在再次回到MIPComponentChain类的Thread函数内部第2阶段代码。文章最前面已列出了那部份代码,可以回到那再看1遍。我们将MIPAverageTimer和MIPWAVInput两个类再次放入这个处理进程中看看到底产生了甚么。MIPAverageTimer是起始节点。MIPWAVInput与MIPAverageTimer组成了第1个MIPConnection。所以首先调用MIPAverageTimer的pull函数,取出了1个MIPSystemMessage类,然后交给MIPWAVInput。MIPWAVInput的push函数可以接收这个MIPSystemMessage类,因此履行了1次读取wav文件数据的操作,并将数据放置在了MIPRawFloatAudioMessage类内。我们看到MIPAverageTimer的pull函数和MIPWAVInput的push函数,是在1个do while循环内。也就是说,如果没出现毛病而且也能取到1个MIPMessage类变量,那末将继续再调用1次MIPAverageTimer的pull函数和MIPWAVInput的push函数。第2次MIPAverageTimer的pull函数被调用,但这次被调用不会再返回MIPSystemMessage类了,由于上次pull函数操作已将标志m_gotMsg置成true了。由于第2次的MIPAverageTimer的pull函数没取到MIPMessage消息,因此也不会第2次再调用MIPWAVInput的push函数。因此do while结束,继续处理第2个MIPConnection。

第2对pull MIPComponent和push MIPComponent
参照feedbackexample源码第2个MIPConnection由MIPWAVInput和MIPSamplingRateConverter组成。同理,MIPSamplingRateConverter类实例也要初始化后才能使用。

returnValue = chain.addConnection(&sndFileInput, &sampConv);
int samplingRate = 8000; int numChannels = 1; returnValue = sampConv.init(samplingRate, numChannels);

有些奇怪的是,采样率和通道数居然采取硬编码方式。刚看到init函数时,第1反应就是采样率和通道数由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时未提供最后1个实参。也就是使用了参数的缺省值,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 = ⑴; m_init = true; return true; }

初始化分析过了,来看看实际处理的进程。先调用第2个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的语音数据。转换前和转换后唯1相同的是每次采样的间隔时长。真实的转换进程是这句:

MIPResample<float,float>(oldFrames, numInFrames, numInChannels, newFrames, numNewFrames, m_outChannels)

6个输入参数1目了然。前3个是转换前的数据格式,后3个是希望得到的数据格式。看模样得分析MIPResample类了。很明显这是个模板类。找到头文件后发现这是个模板函数,不是模板类。第1个float指明输入及输出数据采取哪一种类型,第2个float说明内部计算使用哪一种类型。首先检查输入及输出通道数,确保满足要求。简单的说就是要做到,如果输入数据的通道数大于1,但输出数据的通道数与输入的不同但又不等于1就认为有错。即,多个通道可以转换成通道数相同的多个通道,多个通道也能够转换成1个通道,但多个通道不能转换成通道数不1致的多个通道。接着分3种情况转换数据:转换前和转换后的帧数1致、转换前的帧数大于转换后的帧数和转换前的帧数小于转换后的帧数。

先看帧数1致的情形。又分成3个小条件分支。经过之前的分析现在已知道不存在转换前后通道数都大于1但不1致的情形。所以只可能有3种情形:转换前通道数是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个大于1的数值。而且还要计算两个乘积的差值。差值应当就是1个numInputFrames/numOutputFrames,反正就是1个大于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,那末将转换前的帧数紧缩成1半。即,将两个帧合并成1个。如果num是2,将循环两次,也就是每一个inputSum数组元素将累加两个转换前的数据。累加的两个数据都是同1个通道的。如果num的值是3,那末将累加3个同通道的数据。依此类推。然后再除以num,求平均值。

for (int j = 0 ; j < numInputChannels ; j++) inputSum[j] /= (Tcalc)num;

这么处理的目的也很明显。由于转换前后的帧数不1致。如此处理睬丢失1些数据。例如,转换前帧数100,转换后帧数80,前者比后者多20。100除80值是1。由因而1,那末inputSum内的每一个通道数据不是累加数据。又由于外部迭代是以转换后的帧数为计数对象,所以转换前的最后20个帧将不会被处理。以上是这个情性下如何处理帧数不1致。接着是与帧数1致情形相同,一样存在3个1模1样的条件分支,各个分支处理也相同。

最后来看转换前帧数小于转换后帧数情形。这个情形下的处理以1个for循环为主,以转换前的帧数为计数基准。每次迭代startValues内存储的是转换前单个帧的各个通道数据。迭代开始处,与之前1样根据转换前后帧数的差异计算3个值。最后得出的num值的含义也1样。然后是除最后1次迭代外(i<numInputFrames - 1,最后1次迭代i的值是numInputFrames - 1),其他迭代必须再计算stepValues数组的值。stepValues内存储的是下1个帧同1个通道的数据减去当前帧同1个通道的数据。也就是说,每次迭代时startValues内存储了当前帧各个通道的数据,stepValues内存储了下1个帧与当前帧同1通道的差值。接着是在处理转换前单个帧时另外一个for循环,以计算得到的num值为迭代计数对象。然后再以转换前通道数为计数对象计算interpolation数组。这个数组值得计算公式是下述两个值的和:当前帧通道数据,与下1帧同1通道的差值除以num再乘以通道索引。接着是与帧数1致情形相同,一样存在3个1模1样的针对通道数的条件分支,各个分支处理也相同。此时interpolation数组值作为转换前的数据赋给转换后的缓冲区。我认为这1情形转换后的缓冲区有1小段时空白的。例如,转换前是帧数是80,转换后是100。我认为可以转换完全的80帧数据,但依此处理进程,没法填充转换后后20帧的缓冲区。但如果转换前后两个帧数数据的关系是整数倍,顺次处理进程是可以填充满转换后缓冲区。

履行完MIPResample函数后,就做完了转换操作。接着用转换后得到的帧数和缓冲区创建MIPRawFloatAudioMessage类实例。接着调用MIPRawFloatAudioMessage的copyMediaInfoFrom,从push函数的输入参数MIPMessage中拷贝sourceid和time信息。创建完MIPRawFloatAudioMessage实例后,立即判断输入参数迭代值是不是是1个新的。如果是1个新的迭代值,那末就删除之前那次迭代创建的所有MIPRawFloatAudioMessage实例。push函数的参数iteration是指1次完全遍历MIPConneciton的进程。最后则是将此MIPRawFloatAudioMessage实例放入内部队列m_messages内。

至此,分析完了MIPSamplingRateConverter的push函数。经过两个MIPConnection的分析,现在知道每次处理1个MIPConnection时,pull函数的作用就是从MIPComponent中取出MIPMessage,push函数的作用就是接收1个MIPMessage再在此基础上生成1个MIPMessage。 第1个MIPConnection的pull component-MIPAverageTimer的pull函数取出了MIPSystemMessage,交给push componnet-MIPWAVInput的push函数,push函数内再生成MIPRawFloatAudioMessage。MIPRawFloatAudioMessage函数内保存了1部份wav文件内的语音数据。第2个MIPConnection的pull component-MIPWAVInput的pull函数取出了MIPRawFloatAudioMessage,交给push component-MIPSamplingRateConverter的push函数,push函数内再生成MIPRawFloatAudioMessage。全部链条应当就会依照这类击鼓传花方式传递MIPMessage,处理完后再生成1个新的MIPMessage传递给下1个MIPComponent,直到链条的终止MIPComponent。

这只是处理完1遍第2个MIPConnection 。处理每一个MIPConnection都有1个小的do while循环。在这个小的do while循环内会第2次调用MIPWAVInput的pull函数。pull函数内的代码交代地很清楚,在不调用push函数重置m_gotMessage为false的情况下调用pull函数将不会返回MIPMessage。即,履行完第2次MIPWAVInput的pull函数后由于取不到MIPMessage,do-while结束。接下来处理第3个MIPConnection。

第3对pull MIPComponent和push MIPComponent

参照feedbackexample源码第3个MIPConnection由MIPSamplingRateConverter和MIPSampleEncoder组成。同理,MIPSampleEncoder类实例也要初始化后才能使用。 

sampEnc.init(MIPRAWAUDIOMESSAGE_TYPE_S16);

这个init函数只是初始化内部变量。

MIPSamplingRateConverter的pull函数会每次取出1个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函数内的第1步是申请1个内存空间。首先是根据转换前语音数据的通道数和帧数得到总帧数。然后再根据目的采样编码格式决定申请何种类型的数据。实际情况是目的采样编码格式是MIPRAWAUDIOMESSAGE_TYPE_S16。根据push函数内的代码可知会履行这句:

pSamples16 = new uint16_t [numIn];

申请1个无符号16位整型数组。numIn是原语音数据的通道数和帧数的乘积。接着是得到原语音数据的缓冲区。根据原语音数据的类型,pSamplesFloatIn指向了这个缓冲区。接着是对每一个数据进行处理,处理过后的数据都放入pSamples16指向的缓冲区内。最后再用这些转换后的数据生成1个MIPRaw16bitAudioMessage对象并放入MIPSampleEncoder的内部队列中。

应当还记得,每对MIPConnection都不会只调用1次pull和push函数,那是1个有着退出机制的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队列内取出1个MIPMessage,如果队列为空那末就取不出MIPMessage了。即,处理这第3对MIPConnection时,如果MIPSamplingRateConverter内的队列为空则结束do-while循环,继续下1对MIPConnection的处理。此时,MIPSampleEncoder内的队列内存储了已处理过的MIPMessage对象。

第4对pull MIPComponent和push MIPComponent

参照feedbackexample源码第4个MIPConnection由MIPSampleEncoder和MIPULawEncoder组成。同理,MIPULawEncoder类实例也要初始化后才能使用。 

returnValue = uLawEnc.init();

init函数只是初始化内部变量而已。
MIPSampleEncoder的pull函数会每次取出1个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函数类似。取出帧数、通道数和采样率等信息,计算需要的缓冲区大小。然后逐一字节进行转换。转换进程与之前1样,虽然代码能够看懂但为什么是这样的转换进程实在是弄不明白。
MIPULawEncoder的pull函数与MIPSampleEncoder的pull函数1样。

第5对pull MIPComponent和push MIPComponent 

参照feedbackexample源码第4个MIPConnection由MIPULawEncoder和MIPRTPULawEncoder组成。同理,MIPRTPULawEncoder类实例也要初始化后才能使用。 init函数只是初始化内部变量而已。 

returnValue = rtpEnc.init();

MIPULawEncoder的pull函数会每次取出1个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的语音数据。这和类的说明内容1致。生成MIPRTPSendMessage消息的进程不复杂,由于数据已处理过了,只是拷贝而已。唯1要注意的是这句:

pNewMsg->setSamplingInstant(pEncMsg->getTime());

调用MIPEncodedAudioMessage消息的getTime函数,并将返回值提供给MIPRTPSendMessage的setSamplingInstant函数。看看getTime取出了甚么数据。getTime函数是在父类MIPMediaMessage实现的函数,只是返回内部成员变量m_time,它的类型是MIPTime。m_time在消息类被创建时被初始化为0。如果m_time的值非常重要,那就会在消息生成后的其他时间被赋值。只能回溯了,先检查MIPULawEncoder类。MIPULawEncoder类的push函数内有这么1句:

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函数1样。
第6对pull MIPComponent和push MIPComponent

参照feedbackexample源码第4个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库依赖的底层库之1jrtplib提供的类。RTPSession类定义了传输RTP数据时需使用的各项参数。包括对端地址、对端端口号等。RTPUDPv4TransmissionParams也是jrtplib提供的类。这里调用了RTPUDPv4TransmissionParams的3个设置函数。前两个通过函数名称可以立即了解到它们的用处,最后1个的用处不清楚。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函数会每次取出1个MIPMessage。这个MIPMessage实际上是MIPRTPSendMessage,它继承自MIPMessage。接下来MIPRTPComponent的push函数会接收这个MIPRTPSendMessage。

push函数内首先检查传入的MIPMessage的类型是不是满足要求。接着有1个静音相干的处理,由于这不是重点暂且略过。然后是调用传入消息变量的getSamplingInstant方法。应当还记得,在分析第5对MIPConnection的最后时调用了MIPRTPSendMessage的setSamplingInstant。这两个方法是相互呼应的。但在那时,我们分析的结果是提供给setSamplingInstant方法的值永久都是0。再调用MIPTime的getCurrentTime方法。最后是计算2者的差值。得到的这个数据依然是为了设置RTPSession变量。最后1步就是调用RTPSession的SendPacket方法向网络对端发送RTP数据。

 

经过分析这6对MIPConnection的处理,现在大致了解了emiplib库的底层运行机制。emiplib会在后台启动1个线程来履行这个运行框架。框架的搭建在线程创建之前,且必须由开发人员显示指定这样1个履行顺序框架。履行顺序框架由众多的MIPCompnent组成,每一个履行特定功能的模块均继承自MIPComponent。功能链条上前后顺序相邻的两个MIPComponent组成1个MIPConnection。每一个MIPConnection内,次序在前的MIPComponent称为pull component,次序在后的称为push component。运行框架在后台线程内运作,顺次处理每一个MIPConnection:先调用pull component的pull函数取出1个MIPMessage,然后调用push component的push函数向其提供这个MIPMessage。现在可以清晰地感觉到从wav文件中取出1段语音数据后如何经过这个运行框架终究发送到特定网络地址的进程。

 

emiplib的履行框架现在已清楚了,但在这分析进程中又发现了很多其他的知识盲点。特别是在很多的编码格式转换进程中遇到的转换算法。代码能看明白,但不明白这些代码后面所体现出的算法本质。其他不熟习的地方还有最后使用的发送RTP数据的RTPSession类。这个类很多相干设置的意图不清楚。

分析过后的另外一个想法就是,想将这个履行框架用libuv库重新再实现1遍。emiplib库使用多线程方式实现了这个履行框架。最近在研究和使用node.js提供的libuv库。这个库提供了1套非常棒的异步履行框架。如果能用libuv完全地再实现1次应当非常有趣。

 

 


 

 

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐