EMIPLIB庫分析二

前一篇中詳細分析了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,說明是PCM形式的聲音數據。即,EMIPLIB只支持PCM類型的WAV文件。第三和第四個字節組合起來是通道數。通道數仍然需要通過移位和按位或操作才能得到。這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完整地再實現一次應該非常有趣。

 

 


 

 

發佈了82 篇原創文章 · 獲贊 2 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章