引言
音頻和視頻是多媒體應用程式向使用者提供資訊的主要方式,這些音頻、視頻資料一般都具有較高的採樣率,經過壓縮的未經處理資料才具有實用價值,否則不僅要佔用大量儲存空間而且在播放或進行網路傳輸時效率也是非常低下的,所以音頻、視頻數字壓縮編碼在多媒體應用中有著廣泛而又重要的用途。本文主要對音訊編碼壓縮作了闡述。
音訊編碼壓縮方式有許多種,如基於ITU-T G.728語音編碼協議的LD-CELP 低時延碼激勵線性預測性編碼、基於ITU-T G.711語音編碼協議的PCM(Pulse Code Modulation ,脈衝編碼調製)編碼以及我們非常熟悉的GSM數字蜂窩行動電話的語音編碼通訊協定等等。這些不同的壓縮方式有著不同的資料壓縮比和還原音質,具體的編碼格式和演算法更是大相徑庭。多數協議都比較複雜,普通程式難以實現其加、解壓演算法,而為多媒體提供了較強支援的Windows 98作業系統引入了ACM和VCM技術,用來管理系統中存在的所有的音頻和視頻編、解碼器(Coder-Decoder,即CODECs,用來實現音頻、視頻資料編解碼的驅動程式)。可以通過它們提供的編程介面調用系統中存在的現成的轉碼器來實現音頻資料的加、解壓。Windows 98系統內建的音頻CODECs 支援一些早期的音頻資料壓縮標準,如ADPCM (Adaptive Differential Pulse Code Modulation,自適應差分脈衝編碼調製)編碼等,而Internet Explorer 5.0 等應用程式套件組合含的音頻CODECs支援一些較新 的壓縮標準, 如MPEG Layer 3等。本文所要介 紹的就是ACM音頻壓縮介面的編程方法,所使用的編程工具為Microsoft Visual C++ 6.0。
實現思路
儘管一個CODEC在理論上能夠用於壓縮、解壓縮任一種資料流,但還是設計有各種各樣的CODECs 以實現更高的壓縮比、更高的逼真度或即時壓縮效能來壓縮某種特定的資料類型。例如,把擷取很高的視頻壓縮資料壓縮率的最好方法應用到音頻資料時未必就能得到相同的效果。
壓縮音頻資料的主要原理是降低儲存某一聲音序列所需的資料量。少的資料量就意味著聲音所佔有的空間更少,就能夠以更快的速度通過MODEM在網路上傳遞。如果資料以Windows系統所支援的某種通用格式壓縮的話,就可不經手工解壓縮而直接播放--系 統將使用它自己的CODECs解壓縮資料並播放。Windows 98本身附帶有幾種標準的CODECs,如DSP Group,Inc. TrueSpeech CODEC等。因此我們寫的任何應用於 Windows 98下的程式都可應用這些CODEC,具體系統中都存在有哪些CODECs可以在控制面版的"多媒體"選項的"裝置"標籤頁中查到。
CODEC 支援從源音頻格式到目標格式的轉換,而在實際應用中, 可能某種CODEC 不支援直接將源音頻格式轉換成目標格式,比如我們通過麥克風向多媒體電腦錄入了一些頻率為11025Hz、8位元據、單聲道的PCM資料,如果選用系統的TrueSpeech CODEC進行處理,就會引起失敗,因為這種CODEC只能處理頻率為8KHz,16位單聲道的資料。所以轉換時要採取兩步轉換法,即先將源格式轉換成一種中間格式,再將此中間格式轉換成目標格式,因為線性PCM 編碼 最為簡單,且為絕大多數CODEC 所支援,所以一般中間格式都選為線性PCM 格式的一種。比如就可以先將未經處理資料轉換成TrueSpeech CODEC所支援的中間PCM格式,然後再將其通過TrueSpeech CODEC轉換成最終的壓縮格式。
程式的設計實現
有關ACM的API函數定義在標頭檔msacm.h中, 除了在工程中加入對此標頭檔的引用之外, 對ACM編程還必須包含標頭檔mmsystem.h和mmreg.h,這兩個標頭檔定義了多媒體編程中最基本 的常量和資料結構。為了避免有些高 版 本ACM才提供的函數和功能在較低版本的ACM中上不可用,程式中應調用acmGetVersion函 數查詢使用者機器中ACM 的版本資訊。
雖然可以根據控制面版手工得到關於某種音頻CODECs的資訊,但在應用程式中也常常需要知道某種音頻CODECs是否存在,並擷取其編解碼參數等資訊,可以通過回呼函數find_format_enum來枚舉系統中的音頻壓縮格式:
BOOL CALLBACK find_format_enum(HACMDRIVERID hadid, LPACMFORMATDETAILS pafd, DWORD dwInstance, DWORD fdwSupport) { FIND_DRIVER_INFO* pdi = (FIND_DRIVER_INFO*) dwInstance; if (pafd->dwFormatTag == (DWORD)pdi->wFormatTag) { pdi->hadid = hadid; return FALSE; //停止枚舉 } return TRUE; //繼續枚舉 } |
在該回呼函數中用到的FIND_DRIVER_INFO是自訂的資料結構,其兩個成員變數分別用來儲存ACM磁碟機代號的控制代碼和要轉換的資料格式:
typedef struct { HACMDRIVERID hadid; WORD wFormatTag; } FIND_DRIVER_INFO; |
現在可以枚舉出系統中當前所有的驅動程式。我們在程式中所調用的枚舉函數使用回呼函數來彙報每個裝置的資料,這在Windows編程是一種很普遍的方法。要獲得有關某一驅動程式能力更多的詳細資料,必須裝載驅動程式並開啟它,可通過調用 acmOpenDriver實現。一旦驅動程式開啟,可請求枚舉它所支援的wave資料格式。但這就存在一個問題:所有wave格式描述結構都基於WAVEFORAMTEX,許多格式使用此結構的擴充形式來儲存其特定的資訊。如果我們想枚舉所有格式,需要知道為此結構分配多少供驅動 程式填寫詳細資料的空間。可以通過向acmMetrics函數傳遞ACM_METRIC_MAX_SIZE_FORMAT標 志得到所需的最大的結構的尺寸。開啟驅動程式後要通過acmMetrics函數枚舉到所支援的格式,該函數可以擷取到許多ACM對象的有用資訊。實現該過程的主要代碼如下:
BOOL CALLBACK find_driver_enum (HACMDRIVERID hadid, DWORD dwInstance, DWORD fdwSupport) { …… MMRESULT mmr = acmDriverOpen(&had, hadid, 0); //枚舉所支援的格式 …… mmr = acmMetrics((HACMOBJ)had, ACM_METRIC_MAX_SIZE_FORMAT, &dwSize); if (dwSize < sizeof(WAVEFORMATEX)) dwSize = sizeof(WAVEFORMATEX); WAVEFORMATEX* pwf = (WAVEFORMATEX*) malloc(dwSize); …… pwf->cbSize = LOWORD(dwSize) - sizeof(WAVEFORMATEX); pwf->wFormatTag = pdi->wFormatTag; ACMFORMATDETAILS fd; …… fd.cbStruct = sizeof(fd); fd.pwfx = pwf; fd.cbwfx = dwSize; fd.dwFormatTag = pdi->wFormatTag; mmr=acmFormatEnum(had, &fd, find_format_enum, (DWORD)(VOID*)pdi, 0); //枚舉格式 …… acmDriverClose(had, 0); //關閉磁碟機 …… } |
根據指定的格式要找到其所對應的ACM磁碟機代號可以用枚舉所有音頻CODECs的ACM API函數acmDriverEnum來實現,在acmDriverEnum() 的參數中指定了在前面描述過的回呼函數find_driver_enum,可以 進 一 步查詢每個CODEC的資訊,最終可以擷取到ACM磁碟機代號的控制代碼。實現此功能的回呼函數名為find_driver,本文後面將會用到。
在把原始Wave音頻資料轉換到中間PCM格式資料之前,需要做些前期準備工作,填充一些相關的結構資訊,具體有:WAVEFORMATEX結構描述源格式、中間PCM格式、以及最終的壓縮格式等。下面先填充一個用來描述來源資料格式的WAVEFORMATEX結構:
WAVEFORMATEX wfSrc; memset(&wfSrc, 0, sizeof(wfSrc)); wfSrc.cbSize = 0; wfSrc.wFormatTag = WAVE_FORMAT_PCM; //PCM脈衝編碼調製 wfSrc.nChannels = 1; //單聲道 wfSrc.nSamplesPerSec = 11025; //11.025kHz wfSrc.wBitsPerSample = 8; //8 bit wfSrc.nBlockAlign = wfSrc.nChannels * wfSrc.wBitsPerSample / 8; wfSrc.nAvgBytesPerSec = wfSrc.nSamplesPerSec * wfSrc.nBlockAlign; |
然後通過前面提到的回呼函數find_driver來擷取由wFormatTag指定的中間資料格式所對應的驅動程式的ACM磁碟機代號,在此設定的是由WAVE_FORMAT_DSPGROUP_TRUESPEECH指定的有Windows 98系統內建的TrueSpeech CODEC:
WORD wFormatTag = WAVE_FORMAT_DSPGROUP_TRUESPEECH; HACMDRIVERID hadid = find_driver(wFormatTag); |
選定了驅動程式,現在要為最終驅動程式將產生的壓縮資料格式建立一個WAVEFORMATEX結構,並為驅動程式用於輸入的中間PCM格式產生一個WAVEFORMATEX結構:
| WAVEFORMATEX* pwfDrv = get_driver_format(hadid, wFormatTag); // 獲得格式的詳情 |
在結構pwfDrv的成員變數wBitsPerSample裡存放著驅動格式的位元,在nSamplesPerSec裡存放著驅動格式的採樣率。然後可以用非常類似的方法擷取驅動程式所支援的PCM格式標籤:
| WAVEFORMATEX* pwfPCM = get_driver_format(hadid, WAVE_FORMAT_PCM); |
當以上所需資訊都以擷取到後就可以開始轉換資料了。轉換由被ACM稱作流的對象來實現。我們可以開啟流,將源格式、目標格式傳遞給它,要求它進行轉換。先將其轉換成中間PCM格式。
將
Wave
音頻轉換為
CODEC
所支援的
PCM
格式
通過CODEC將源Wave音頻轉換成CODEC所支援的PCM格式,可以使用任何可以做PCM間轉換的磁碟機。另外還有一點很重要:我們開啟轉換流時,要指明ACM_STREAMOPENF_NONREALTIME標誌。若省略此標誌,那麼一些驅動程式(例如TrueSpeech CODEC)將會報告發生第512號"不可能發生的"錯誤。該錯誤指明所要求的轉換不能即時進行,如果在試圖播放資料的同時轉換大量資料,就必須注意這點。下面是該步轉換過程的簡要描述:
mmr = acmStreamOpen(&hstr, NULL, //任意磁碟機 &wfSrc, //源格式 pwfPCM, //目標格式 NULL, //無過濾 NULL, //無回調 0, //初始資料 ACM_STREAMOPENF_NONREALTIME); |
根據以位元組計的平均速率計算出輸出緩衝區的大小,並加上一機動位(bit)如果沒有此額外的空間IMA_ADPCM驅動程式將不能轉換。中間的轉換結果將存放在pDst1Data中:
DWORD dwSrcBytes = dwSrcSamples * wfSrc.wBitsPerSample / 8; DWORD dwDst1Samples = dwSrcSamples * pwfPCM->nSamplesPerSec / wfSrc.nSamplesPerSec; DWORD dwDst1Bytes = dwDst1Samples * pwfPCM->wBitsPerSample / 8; unsigned char * pDst1Data = new unsigned char[dwDst1Bytes]; …… ACMSTREAMHEADER strhdr; //填滿轉換資訊 memset(&strhdr, 0, sizeof(strhdr)); strhdr.cbStruct = sizeof(strhdr); strhdr.pbSrc = cpBuf; //指定要轉換的源Wave音頻資料為cpBuf中的資料 strhdr.cbSrcLength = dwSrcBytes; strhdr.pbDst = pDst1Data; strhdr.cbDstLength = dwDst1Bytes; mmr = acmStreamPrepareHeader(hstr, &strhdr, 0); mmr = acmStreamConvert(hstr, &strhdr, 0); //轉換資料 …… acmStreamClose(hstr, 0); |
當流開啟時,第二個參數為NULL,表示接受任何驅動程式進行轉換。複雜的只是計算需要給輸出資料分配多大的緩衝區。PCM格式間的轉換不牽扯壓縮和解壓縮,緩衝區大小直接就計算出來了。至於調用acmStreamPrepareHeader這個ACM API函數,是由於它可以為驅動程式安排好一切並允許驅動程式在轉換前鎖定記憶體。
產生最終的壓縮格式
向最終壓縮格式的轉換過程與前面的PCM格式間轉換非常相似,只不過此次轉換我們提供了開啟流時想要使用的驅動程式的控制代碼。實際上,此處仍可使用NULL,因為已預知此驅動程式存在,但提供控制代碼避免了系統浪費時間為我們尋找此驅動程式:
mmr = acmStreamOpen(&hstr, had, //磁碟機控制代碼 pwfPCM, //源格式 pwfDrv, //目標格式 NULL, //無過濾 NULL, //無回調 0, //執行個體化資料 ACM_STREAMOPENF_NONREALTIME); |
另外,計算用於壓縮資料的緩衝區的尺寸有點難辦,需要憑猜測。WAVEFORMATEX結構的nAvgBytesPerSec 域表示回放期間讀取位元組的平均速率。我們可使用它來估計儲存壓縮的wave需要多大空間。一 些驅動程式給出的資料確實是平均的,而不是最差場合下的值,因此我選擇多增加50%的空間。 此方法在實踐中雖然有些浪費但很有效:
DWORD dwDst2Bytes = pwfDrv->nAvgBytesPerSec * dwDst1Samples / pwfPCM->nSamplesPerSec; dwDst2Bytes = dwDst2Bytes * 3 / 2; unsigned char * pDst2Data = new unsigned char [dwDst2Bytes]; |
其中,無符號字元型指標pDst2Data用於儲存壓縮的最終Wave音頻資料,其大小經上式估算後存到dwDst2Bytes中。一旦轉換完成,ACMSTREAMHEADER的結構的cbDstLengthUsed 域指出緩衝區實際用了多少位元組。可以通過它來計算出壓縮比:
| double result= (double) dwSrcBytes / (double) strhdr2.cbDstLengthUsed; |
當源訊號為8K採樣、16bits PCM編碼、單聲道、長度為1秒的Wave音頻訊號, 驅動程式採用Windows 98內建的TrueSpeech 音頻CODEC,它能實現大約10:1的壓縮,這樣高的壓縮率還是比較另人滿意的。
小結:
本文以TrueSpeech CODEC為例對使用ACM音頻壓縮編程介面實現Wave音頻壓縮的過程作了介紹。如果有自已的壓縮格式,也可建立並安裝自已的CODEC,實現的方法與之基本類似。在理解了上述編程思想的前提下,對代碼稍加改動就可編寫出相 應的解壓程式。本程式在Windows 2000 Professional下,由Microsoft Visual C++ 6.0編譯通過