前1篇中詳細分析了MIPComponentChain類。了解了履行框架的運作情況。還有必要知曉框架的實現細節,以便于真正掌握庫的設計意圖。有1點有些模糊,就是MIPComponnet間傳遞數據這1部份。現在只是有個大致的了解。pull component生成消息,push component接收這些消息。依然以feedbackexample例程為研究對象。
feedbackexample例程中,在啟動處理進程前,會生成很多MIPComponent,然后根據順序放入MIPComponentChain中。如果只從發送RTP這個角度看,順次被放入的類是這樣的順序。
這樣1個順序正是將wav文件處理后在網絡上以RTP包發送的順序。這些組件中MIPAverageTime類已分析過了。它履行的操作就是休眠規定時長,是在push函數中實現的。MIPAverageTime類的pull函數會返回1個MIPSystemMessage類實例。現在就依照這個順序,順次分析每一個類的pull和push函數,再參照MIPComponentChain類Thread函數的處理進程,看看到底傳遞了哪些消息和如何傳遞的。
再1次貼出Thread函數內第2階段代碼。
第1對pull MIPComponent和push MIPComponent
現在以實際的MIPComponent組件順序為例來解釋這第2階段。第1個MIPConnection的pull component是MIPAverageTime,push component是MIPWAVInput。然后調用pPullComp的pull函數,即調用MIPAverageTime的pull函數。
pull函數的作用就是將內部的MIPSystemMessage成員變量返回給調用方。但只能返回1次,下次再調用pull函數時返回1個空指針。然后調用pPushComp的push函數,傳入剛才取得的MIPSystemMessage。也就是調用了MIPWAVInput的push函數。現在再溫習1下MIPWAVInput類的初始化進程。代碼顯示初始化進程是調用open函數,傳入了wav文件名,和1個MIPTime類實例。open函數的注釋詳細說明了各個參數的作用。
open函數內真正讀取文件的類是MIPWAVReader。如果讀取成功,獲得這個文件的采樣率和通道數。代碼以下:
接著是創建106位整型的緩沖區或浮點數緩沖區。緩沖區大小由輸入參數interval,和采樣率和通道數決定。再相應地創建MIPRaw16bitAudioMessage或MIPRawFloatAudioMessage類實例。創建MIPRaw16bitAudioMessage類實例也需要采樣率、通道數、計算出的幀數和之前創建的緩沖區地址。這就是MIPWAVInput類open函數內容。既然提到了MIPWAVReader類,無妨再仔細看看。
MIPWAVReader的open函數有點長,看模樣有點內容。必須得看看。打開文件這步就略過。首先確保文件頭部的前4個字節1定是“RIFF”。這應當是wav文件格式的要求。
然后再讀取4個字節,這4個字節應當指明了實際數據的大小。這里使用了移位操作和按位或操作。從計算進程可以看出,讀出來的4個字節中第1個字節是整型中的最低8位,第2個字節是倒數第2個低8位,順次類推。最后將這4個字節轉換成32位再按位或得到終究的數據大小值。
取出了前8個字節后,再取出4個字節。確保這4個字節組成的字串是“WAVE”。同時,將數據大小值減去4。
經過上述兩次讀取,現在已肯定這是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內。
((uint64_t)1) << bitsPerSample,左移1位,也就是乘以2。份子又是2.0,會抵消掉。m_scale也就是bitsPerSample的倒數。“fmt ”類型處理中最后1步是檢查“fmt ”類型中讀取的數據塊大小值與全部wav文件頭部讀取的塊數據大小值是不是公道。
“data”類型。首先判斷dataChunkSize值是不是大于等于零。在進入while循環前此值被賦值為負1。接著調用ftell,獲得當前文件流的位置,并賦給m_dataStartPos。
由于此時是“data”類型,也就是說是實際數據塊。m_dataStartPos存儲的也就是實際數據的起始位置。同理,“data”類型字段后的4個字節就是實際數據塊的大小。
處理完兩種類型后,得到了采樣率、通道數和實際數據塊的起始地址等信息。最后要檢查1下這些信息是不是合法。再根據這些值計算后續處理需要的其他值。幀大小值由通道數和每一個采樣多少個字節決定。還必須確保實際數據塊大小這個值是幀大小值的整數倍。這個整數倍存儲在m_totalFrames內。再根據幀大小申請1塊存儲空間。最后是計算m_negStartVal值。后面應當會用到它,現在不清楚為什么這么計算。
好,現在應當算是掌握了90%的MIPWAVReader類代碼。這個類的主要職責是判定文件是個合法的wav文件,并從文件頭部讀取相應的信息,并為將來處理這個文件申請正確大小的緩沖區。再回過頭去看MIPWAVInput類的open函數,在調用完MIPWAVReader類的open函數后,會立即再調用MIPWAVReader的getSamplingRate和getNumbersOfChannels兩個函數。經過上述代碼分析,可以知道此時可以取到這個wav文件采樣率和通道數兩個信息。
再根據采樣率和傳入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類時也有幀大小和每一個采樣多少字節這樣的數據。無妨現在再去看看。
每一個采樣多少個位是從文件中取出來的,再除以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循環頭部的num內存儲的是每次實際讀取的數據個數。buffer是MIPWAVInput調用MIPWAVReader時傳入的內存空間地址。m_pFrameBuffer是MIPWAVReader申請的內存空間。byteBufPos在for循環前被賦值零,它指向m_pFrameBuffer數組位置。intBufPos在while循環前也被賦值零,它指向buffer數組位置。for循環內又嵌套了另外一個for循環。由于num是讀取的數據個數,但每一個數據是由所有通道的數據組成的。所之內部for循環針對單個數據而言,每次遍歷1個通道的數據。單個通道單個采樣數據的處理分兩種情形。1是每一個采樣數據是1個字節,另外一種是每一個采樣數據不是1個字節。先分析單個采樣數據1個字節的情形。
將單字節擴大成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類實例也要初始化后才能使用。
有些奇怪的是,采樣率和通道數居然采取硬編碼方式。剛看到init函數時,第1反應就是采樣率和通道數由MIPWAVInput提供。在分析MIPSamplingRateConverter的init函數前先了解下這個類的用處。下面這段摘自MIPSamplingRateConverter類的頭文件。內容很好理解。這個類接收浮點數或16位有符號整型表示的原始語音數據,并根據初始化階段提供的采樣率和通道數生成相似的語音數據。所以提供給init函數的采樣率和通道數是硬編碼方式。這是期望的語音格式。
進入到init函數內部。函數很簡單,就是將輸入參數賦值給相應的成員變量。如果之前已使用過這個MIPSamplingRateConverter實例,再次調用init函數會先清算之前的處理。調用init時未提供最后1個實參。也就是使用了參數的缺省值,floatSamples缺省值是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類。
在創建MIPRawFloatAudioMessage類前,先將計算要申請多少內存空間。frameTime由pAudioMsg的幀數和采樣率決定。應當還記得,pAudioMsg的采樣率值取自wav文件,幀數由采樣率和MIPWAVInput初始化函數open的輸入參數MIPTime決定。初始化MIPWAVInput用的MIPTime值是20毫秒。此處計算frameTime類似于MIPWAVInput類內計算幀數的逆進程,根據幀數計算每次采樣的間隔。計算轉換后的幀數時用到的采樣率來自MIPSamplingRateConverter初始化時用到的輸入參數。實際數據是8000。突然想到這個類的名稱是MIPSamplingRateConverter,翻譯過來就是采樣率轉換器。這么理解的話,MIPSamplingRateConverter初始化時輸入的采樣率是希望得到的采樣率。也就是說希望將wav文件內的語音數據轉換成采樣率為8000,通道數是1的語音數據。轉換前和轉換后唯1相同的是每次采樣的間隔時長。真實的轉換進程是這句:
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。計算公式以下:
其實就是計算每次迭代的i值和i+1的值乘以numInputFrames/numOutputFrames。由于numInputFrames大于numOutputFrames,所以這個值肯定是大于1。即,i和i+1乘以1個大于1的數值。而且還要計算兩個乘積的差值。差值應當就是1個numInputFrames/numOutputFrames,反正就是1個大于1的數值。然后再以這個差值為迭代對象處理下轉換前的數據。
針對這個for循環處理和之前計算num的進程。我認為這個進程可以這么理解。這個num值是為了計算出轉換前的幀數是轉換后的幀數的多少個整數倍。如果是兩倍,num的值就是2,那末將轉換前的幀數緊縮成1半。即,將兩個幀合并成1個。如果num是2,將循環兩次,也就是每一個inputSum數組元素將累加兩個轉換前的數據。累加的兩個數據都是同1個通道的。如果num的值是3,那末將累加3個同通道的數據。依此類推。然后再除以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類實例也要初始化后才能使用。
這個init函數只是初始化內部變量。
MIPSamplingRateConverter的pull函數會每次取出1個MIPMessage。這個MIPMessage實際上是MIPRawFloatAudioMessage,它繼承自MIPAudioMessage,MIPAudioMessage是MIPMediaMessage的子類,MIPMediaMessage是MIPMessage的子類。接下來MIPSampleEncoder的push函數會接收這個MIPRawFloatAudioMessage。
上面這段是源碼中MIPSampleEncoder類的說明文字。之前的MIPSamplingRateConverter的作用是轉換采樣率和通道數。現在的MIPSampleEncoder的作用是轉換采樣編碼格式。這個進程就是在MIPSampleEncoder的push函數內完成。
push函數內的第1步是申請1個內存空間。首先是根據轉換前語音數據的通道數和幀數得到總幀數。然后再根據目的采樣編碼格式決定申請何種類型的數據。實際情況是目的采樣編碼格式是MIPRAWAUDIOMESSAGE_TYPE_S16。根據push函數內的代碼可知會履行這句:
申請1個無符號16位整型數組。numIn是原語音數據的通道數和幀數的乘積。接著是得到原語音數據的緩沖區。根據原語音數據的類型,pSamplesFloatIn指向了這個緩沖區。接著是對每一個數據進行處理,處理過后的數據都放入pSamples16指向的緩沖區內。最后再用這些轉換后的數據生成1個MIPRaw16bitAudioMessage對象并放入MIPSampleEncoder的內部隊列中。
應當還記得,每對MIPConnection都不會只調用1次pull和push函數,那是1個有著退出機制的do-while循環。退出標志就是pull函數取不出MIPMessage對象了。所以現在看看MIPSamplingRateConverter的pull函數會在甚么情況下取不出MIPMessage。
代碼顯示每次調用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類實例也要初始化后才能使用。
init函數只是初始化內部變量而已。
MIPSampleEncoder的pull函數會每次取出1個MIPMessage。這個MIPMessage實際上是MIPRaw16bitAudioMessage,它繼承自MIPAudioMessage,MIPAudioMessage是MIPMediaMessage的子類,MIPMediaMessage是MIPMessage的子類。接下來MIPULawEncoder的push函數會接收這個MIPRawFloatAudioMessage。
上面這段是源碼中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函數只是初始化內部變量而已。
MIPULawEncoder的pull函數會每次取出1個MIPMessage。這個MIPMessage實際上是MIPEncodedAudioMessage,它繼承自MIPAudioMessage,MIPAudioMessage是MIPMediaMessage的子類,MIPMediaMessage是MIPMessage的子類。接下來MIPRTPULawEncoder的push函數會接收這個MIPRawFloatAudioMessage。
上面這段是源碼中MIPRTPULawEncoder類的說明文字。意思很清楚,這個類的作用是為已編碼為u律的語音數據生成RTP包。它生成的消息是MIPRTPSendMessage。MIPRTPULawEncoder類的push函數代碼顯示,這個類只接收采樣率為8000,通道數不為1的語音數據。這和類的說明內容1致。生成MIPRTPSendMessage消息的進程不復雜,由于數據已處理過了,只是拷貝而已。唯1要注意的是這句:
調用MIPEncodedAudioMessage消息的getTime函數,并將返回值提供給MIPRTPSendMessage的setSamplingInstant函數。看看getTime取出了甚么數據。getTime函數是在父類MIPMediaMessage實現的函數,只是返回內部成員變量m_time,它的類型是MIPTime。m_time在消息類被創建時被初始化為0。如果m_time的值非常重要,那就會在消息生成后的其他時間被賦值。只能回溯了,先檢查MIPULawEncoder類。MIPULawEncoder類的push函數內有這么1句:
copyMediaInfoFrom函數也是MIPMediaMessage類實現的函數。次函數的作用就是從另外一個MIPMediaMessage消息里拷貝來m_sourceID和m_time。由于只要是MIPMediaMessage消息,都會有這兩個成員變量。繼續回溯,回到MIPSampleEncoder類的push函數。
copyAudioInfoFrom函數內又調用了copyMediaInfoFrom函數,所以這里依然不是m_time生成的源頭。接著看MIPSamplingRateConverter的push函數,又再次看到了以下的語句:
然后是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的初始化進程較復雜。
MIPRTPComponent的init函數所需的參數是個RTPSession。這個類是emiplib庫依賴的底層庫之1jrtplib提供的類。RTPSession類定義了傳輸RTP數據時需使用的各項參數。包括對端地址、對端端口號等。RTPUDPv4TransmissionParams也是jrtplib提供的類。這里調用了RTPUDPv4TransmissionParams的3個設置函數。前兩個通過函數名稱可以立即了解到它們的用處,最后1個的用處不清楚。SetOwnTimestampUnit函數的用處應當就是取每次發送多長時間間隔的數據。這里采樣率是8000,時間間隔就是125毫秒。init函數內部只是保存下傳入的RTPSession變量地址。init函數還有個參數可以有缺省值,示例代碼使用了這個缺省參數值。init函數的注釋很清楚地解釋了這個參數的作用:與靜音有關。
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次應當非常有趣。