工業(yè)設(shè)計相關(guān)網(wǎng)站公司官網(wǎng)搭建
Speex 是一個開源的, 適合語音編解碼的算法, 常應(yīng)用于網(wǎng)絡(luò)電話中.
在下面的的介紹中, 我們將使用 SpeexSharp 對 Speex 編碼在 .NET 中的使用做介紹
SpeexSharp 可以在 nuget 中直接安裝, 并且已經(jīng)封裝了編解碼器的類供使用. 如果你不希望了解 Speex 的具體編解碼過程, 可以忽略下面的 ‘編碼’ 和 ‘解碼’ 部分, 只看 Speex 的介紹, 然后直接使用這些類進行編解碼.
采樣
Speex 的編解碼是基于采樣的, 傳入數(shù)據(jù)的時候, 我們需要給定采樣, 傳出的時候, Speex 也是解碼為采樣.
Speex 支持的采樣格式有兩種, 浮點數(shù)和有符號 16 位整數(shù).
模式和質(zhì)量
Speex 目前有三種模式, 窄帶, 寬帶, 超寬帶. 這三種模式中, 對音頻數(shù)據(jù)編碼后的數(shù)據(jù)大小是不一樣的. 如其名, 在窄帶模式下, 音頻編碼后最小, 質(zhì)量也最低, 反之, 超寬帶是編碼后最大, 質(zhì)量最好的模式.
選擇好模式之后, 你還可以對編碼質(zhì)量進行微調(diào). 質(zhì)量的等級是一個從 0 到 10 的值(包含 0 和 10), 設(shè)置編碼器的質(zhì)量之后, 編碼的結(jié)果大小和質(zhì)量也會變更.
存儲流
要進行 Speex 編碼, 我們需要一個存儲需要編碼的數(shù)據(jù)的緩沖區(qū), Speex 已經(jīng)為我們準備好了這個類型, 并且 Speex 會自動管理這個緩沖區(qū). 它叫做 SpeexBits
.
初始化一個 SpeexBits
, 我們需要聲明一個 SpeexBits
類型的變量, 然后調(diào)用 Speex 的初始化函數(shù).
SpeexBits bits;
Speex.BitsInit(&bits);
無論是編碼還是解碼, 都是需要 SpeexBits
作為存儲的.
編碼時, 用戶將采樣數(shù)據(jù)的指針傳給編碼函數(shù), 函數(shù)內(nèi)部對幀進行編碼, 最后將編碼后的結(jié)果放入 SpeexBits
中.
解碼時則是用戶將需要解碼的數(shù)據(jù)放入 SpeexBits
, 將輸出數(shù)據(jù)的緩沖區(qū)傳給解碼函數(shù), 解碼函數(shù)從中讀取, 解碼數(shù)據(jù), 然后將解碼后的數(shù)據(jù)寫入用戶指定的緩沖區(qū)中.
另外, SpeexBits 之所以叫 Bits, 是因為它其中存儲的數(shù)據(jù), 基本單位是比特. 在編碼時, 你可能會得到 69.5 個字節(jié). 也就是 556 個比特. 在這種情況下, 我們要存儲它的數(shù)據(jù)時, 肯定是要向上舍入, 也就是存儲它內(nèi)部的 70 個字節(jié).
幀大小與采樣率
Speex 的編解碼是對于 “幀” 而言的. 每一次編碼, 都必須是一個完整的幀, 即一定數(shù)量的采樣數(shù). 而幀的大小取決于上面提到的 Speex 編碼模式.
而且, 在編碼的時候, 傳入采樣的采樣率也應(yīng)該與編碼器設(shè)定的采樣率一致. 這樣才能獲得最好的編碼效果.
數(shù)據(jù) \ 模式 | 窄帶 | 寬帶 | 超寬帶 |
---|---|---|---|
幀大小 | 160 | 320 | 640 |
默認采樣率 | 8000Hz | 16000Hz | 32000Hz |
注意, 幀大小是針對 ‘采樣數(shù)量’ 的, 例如, 如果你要以寬帶模式編碼浮點數(shù)采樣, 那么你需要 320 個浮點采樣, 每個 4 字節(jié), 總共需要 1280 個字節(jié). 如果是帶符號 16 位整數(shù)則是需要 640 個字節(jié).
如果你希望將使用 Speex 編碼的語音存儲到文件, 你可能需要做一些處理. 因為 Speex 的編解碼是針對于幀的, 所以你的文件中至少需要有標識幀的地方. 在解碼時, 讀取一幀, 然后調(diào)用解碼方法, 得到原始采樣.
最簡單的方式就是在每一幀的前面加一字節(jié)的頭, 這個字節(jié)用來標識后面多少字節(jié)是一幀.
編碼過程
要進行完整的編碼, 需要進行以下大概步驟
- 準備用于存儲的 SpeexBits
- 初始化一個編碼器
- 調(diào)用編碼方法
- 從 SpeexBits 中讀取編碼結(jié)果
下面是寬帶模式編碼的示例代碼:
// 要進行編碼的采樣數(shù)據(jù)
float[] samples;// 取 320 個采樣, 也就是寬帶模式下的一幀
float[] frame = samples.Take(320).ToArray();// 定義并初始化用于存儲的 SpeexBits
SpeexBits bits;
Speex.BitsInit(&bits);// 獲取表示寬帶模式的指針, 0, 1, 2 分別是窄帶, 寬帶, 超寬帶
SpeexMode* mode = Speex.LibGetMode(1);// 初始化編碼器, 得到表示編碼器狀態(tài)的指針
void* encoderState = Speex.EncoderInit(mode);// 將數(shù)組轉(zhuǎn)為指針
fixed (float* framePtr = frame)
{// 重置 SpeexBits 內(nèi)容Speex.BitsReset(&bits);// 調(diào)用編碼方法Speex.Encode(encoderState, framePtr, &bits);
}// 獲取編碼后的數(shù)量 (也就是 bits 中存儲的字節(jié)數(shù))
int bitCount = bits.BitCount;
int byteCount = (bits.BitCount + 7) >> 3 // 向上舍入// 聲明一個緩沖區(qū)用于存儲編碼后結(jié)果
byte[] buffer = new byte[byteCount];// 固定緩沖區(qū), 轉(zhuǎn)為指針
fixed (byte* bufferPtr = buffer)
{// 將 Bits 內(nèi)存儲的編碼結(jié)果寫入到我們自己的緩沖區(qū)中Speex.BitsWrite(&bits, bufferPtr, buffer.Length);
}// 做其他處理.
需要注意的是, 每一次編碼之后, 你都應(yīng)該重置一下 SpeexBits
因為編碼方法的結(jié)果再往 SpeexBits 存入時, 如果沒有抹除舊的數(shù)據(jù), SpeexBits 中就會同時存儲著舊的數(shù)據(jù)和新的數(shù)據(jù), 如果你沒有手動往 SpeexBits 里面寫入一些東西做標識, 那么你就無法區(qū)分不同的幀了.
最簡單的方式就是, 每一次編碼后, 讀取編碼結(jié)果, 然后清空 SpeexBits.
如果你需要將所有采樣都編碼了, 很簡單, 只需要用 for 進行循環(huán)就好了. 但在這之前, 你還需要對原始的采樣做填充處理, 確保它的大小是幀大小的整數(shù)倍, 這樣你在做編碼的時候, 就不會出現(xiàn)訪問沖突的問題了.
float[] samples;int padLength = samples.Length % 360;
if (padLength != 0)padLength = 360 - padLength;float[] paddedSamples = new [samples.Length + padLength];
Array.Copy(samples, 0, paddedSamples, 0, samples.Length);fixed (float* paddedSamplesPtr = paddedSamples)
{for (int i = 0; i < paddedSamples.Length; i += 360){Speex.BitsReset(&bits);Speex.Encode(encoderState, paddedSamplesPtr + i, &bits);}
}
解碼過程
解碼同樣很簡單, 只需要我們將已經(jīng)編碼的一幀以及輸出緩沖區(qū)傳入到 Decode
函數(shù)中, Speex 就會將解碼后的一幀存入到緩沖區(qū)中.
- 準備用于存儲的 SpeexBits
- 初始化一個解碼器
- 將需要解碼的幀存入到 SpeexBits 中
- 調(diào)用解碼方法
同樣的, 解碼的時候也是逐幀解碼的, 你傳入的緩沖區(qū)至少能容納一幀的音頻才可以.
下面是寬帶模式解碼的示例代碼:
// 要進行解碼的 Speex 數(shù)據(jù)
byte[] speex;// 聲明用于存儲解碼結(jié)果的緩沖區(qū)
float[] buffer = new float[320];// 定義并初始化用于存儲的 SpeexBits
SpeexBits bits;
Speex.BitsInit(&bits);// 獲取表示寬帶模式的指針
SpeexMode* mode = Speex.LibGetMode(1);// 初始化解碼器
void* decoderState = Speex.DecoderInit(mode);// 固定 Speex 數(shù)據(jù)
fixed (byte* speexPtr = speex)
{// 將其讀入到 SpeexBits 中Speex.BitsReadFrom(&bits, speexPtr, speex.Length);
}// 固定緩沖區(qū)
fixed (float* bufferPtr = buffer)
{// 進行解碼Speex.Decode(decoderState, &bits, bufferPtr);
}// 做其他處理
需要注意的是, 在解碼時, 讀入 SpeexBits 的數(shù)據(jù)只能是一幀, 如果你存入兩幀或者更多的話, 那么解碼會出問題.
如果你存入了半個幀或者數(shù)據(jù)損壞的一個幀, 解碼仍然能成功, 只不過輸出結(jié)果的質(zhì)量會下降.
托管調(diào)用
以上, 我們已經(jīng)了解了 Speex 編解碼的具體過程, 但 SpeexSharp 還提供了編解碼器的類封裝, 內(nèi)部會自動初始化 SpeexBits, 存取編解碼數(shù)據(jù)等.
如果要使用這些封裝好的類來進行編解碼, 只需要新建 SpeexEncoder 或 SpeexDecoder 的示例即可.