一、簡介
現在有個需求,在局域網內實現實時語音,傳輸層協議使用UDP協議,如果直接使用AudioRecord進行錄制音頻流並發送到另一端進行播放,音質會非常差,而且斷斷續續,原因如下:
采樣頻率: fm = 44.1KHz
量化位數:16bit
聲道配置:2(雙聲道)
那么,碼率 V = 44.1K * 16 *2 = 1411.2 Kbps = 176.4KBps,即每秒傳輸速率大概176.4KB,
若音頻幀時間為20ms,每個音頻數據包大小為 size = 176.4KBps * 0.02s = 3.528KB,
一般情況下,我們每次讀取一個音頻幀的數據,可以取整為3600Byte,
所以 每秒大概發送 176.4/3.6=49 個數據包,每個數據包大小為3.6KB。
如果再考慮到數據報頭,實測每秒發送約45個數據包,每秒傳輸速率大概180KB。
由於一般都是使用手機連接Wifi,這就要求網絡質量和硬件設備必須很好,而且信道干擾較弱,並且鏈接的設備不能過多。只要稍微信號不好,就會導致丟包率特別高,而且延時十分大,根本無法滿足通信的需要。在這種情況下,我們就需要進行語音壓縮、降噪等處理。
二、局域網語音配置
如果傳輸的僅僅是語音信息,那么不需要很高的采樣頻率,可以使用8KHz進行采樣,單通道即可。
private int DEFAULT_SAMPLERATEINHZ = 8000; // 采樣頻率 private int DEFAULT_AUDIOFORMAT = AudioFormat.ENCODING_PCM_16BIT; // 數據格式 private int DEFAULT_STREAMTYPE = AudioManager.STREAM_MUSIC; // 音頻類型 private int DEFAULT_CHANNELCONFIG_OUT = AudioFormat.CHANNEL_OUT_MONO; // 聲道配置 private int DEFAULT_MODE = AudioTrack.MODE_STREAM; // 輸出模式 private int DEFAULT_CHANNELCONFIG_IN = AudioFormat.CHANNEL_IN_MONO; // 聲道配置 private int DEFAULT_AUDIOSOURCE = MediaRecorder.AudioSource.MIC; // 音頻來源
- 采樣頻率:8KHz,可以采集比較完整的語音信息。當然,對於高頻的信息無能為力;
- 數據格式:16bit,可以較為詳細的表示聲音的幅度;
- 聲道配置:單聲道輸入和輸出,能夠適配所有的機型,少部分手機不支持雙聲道(立體聲),如果設置立體聲會只出現左耳機有聲音的情況;
- 音頻類型:音樂流
- 輸出模式:音頻流
- 音頻來源:麥克風
三、Speex
3.1 簡介
Speex是一套主要針對語音的開源免費,無專利保護的音頻壓縮格式。Speex工程着力於通過提供一個可以替代高性能語音編解碼來降低語音應用輸入門檻 。另外,相對於其它編解碼器,Speex也很適合網絡應用,在網絡應用上有着自己獨特的優勢。同時,Speex還是GNU工程的一部分,在改版的BSD協議中得到了很好的支持。
3.2 技術特點
Speex是基於CELP並且專門為碼率在2-44kbps的語音壓縮而設計的。它的特點有:
- 窄帶(8kHz),寬帶(16kHz)和超寬帶(32kHz)壓縮於同一位流。
- 強化立體編碼
- 數據包丟失隱蔽
- 可變比特率(VBR)
- 語音捕捉(VAD)
- 非連續傳輸(DTX)
- 定點運算
- 感官回聲消除(AEC)
- 噪音屏蔽
3.3 開發-語音壓縮
由於語音壓縮的底層代碼都是用C/C++寫的,所以對於我們可憐的Android來說需要使用NDK進行JNI開發了,如果對這方面不了解的,可以參考一下小編整理的 JNI(一) - Android Studio簡單開發流程 ,然后需要再了解一下C語言的一些基礎知識,就可以進行簡單的JNI開發了,在本文的Demo中,小編也寫了大量的注解,希望能夠幫到大家。
將代碼集成到自己的項目中的步驟:
第一步:將Demo的整個jni目錄復制到自己的main目錄下;
第二步:修改復制過來的jni文件夾中的speex_jni.cpp中的方法名
方法名為Java _ 包名 _ 類名 _ 方法名(),中間使用單個下划線連接,詳見demo;
第三步:添加gradle配置
將紅色框圈住的地方復制到自己的項目中即可。
第四步:編譯項目生成.so文件
選擇Build->Make Project,然后找到.so庫復制到自己的libs下即可:
第五步:在程序中使用
將Speex這個編解碼工具類復制到自己的項目中,就可以正常使用了,具體使用方式詳見Demo。
private Speex speex; // Speex音頻編解碼器 speex = new Speex(); // 創建Speex編解碼實例 speex.open(4); speex.encode(recordData,0,encodedbytes,readNumber); // 語音壓縮-對音頻數據編碼 int decode = speex.decode(audioData, decodedShorts, audioData.length); // 對音頻數據解碼
3.4 相關計算
采樣頻率: fm = 8KHz
量化位數:16bit
聲道配置:1
那么,碼率 V = 8K * 16 * 1 = 128Kbps = 16KBps,即每秒傳輸速率大概16KB,
若音頻幀時間為20ms,每幀音頻數據大小為 size = 16KBps * 0.02s = 320KB,即160 Short,
設置壓縮質量為4,每幀音頻數據壓縮完后只有20Byte,壓縮比為 320:20,即 16:1,每秒發送1/0.02=50個數據包,即單單傳遞音頻數據占用的帶寬為 1 KBps,如果需要添加一些數據報頭,基本上也能維持在5KBps左右!
將176.4KBps變成16KBps,然后再壓縮成1KBps,好了,現在可以滿足基本的局域網內的語音傳輸需求了。如果帶上耳機的話,基本上能夠很完美的語音通話了。如果不帶耳機,隨着通話,麥克風或者環境的回聲會越來越強,將會嚴重降低通話質量,我們之后需要做的就是做回聲處理了。這個在之后的博客中專門介紹。
盡管Speex也十分的優秀,但是還是被新的技術給踢下了寶座,連它的官網上都寫明了該技術已經被Opus給替代,並且宣稱Opus的各項性能都將比Speex優秀的多。接下來小編將為大家初步介紹一下Opus的特點和API~
四、Opus - 音頻編解碼器中的瑞士軍刀
4.1 簡介
Opus是一個完全開放的、免費的、多功能的音頻編解碼器。 它在交互式的語音和音樂在互聯網中的傳輸方面有着無與倫比的優勢,但是同樣致力於存儲和流媒體應用程序。它是由互聯網工程任務組(IETF)制定的標准,標准格式為RFC 6716,由Skype的SILK編解碼器和Xiph.Org的CELT編解碼器合並發展而來,號稱音頻編解碼器中的瑞士軍刀(來自官方視頻)。
4.2 技術
Opus可以處理廣泛的音頻應用程序,包括IP電話、視頻會議、游戲內聊天、甚至遠程現場音樂表演。從低比特率窄帶語音到非常高質量的立體聲音樂,它都可以適用。技術特點:
- 6 kb /s到510 kb / s的比特率
- 采樣率從8 kHz(窄帶)到48 kHz(全頻)
- 幀大小從2.5毫秒到60毫秒
- 支持恆定比特率(CBR)和可變比特率(VBR)
- 從窄帶到全頻段的音頻帶寬
- 支持語音和音樂
- 支持單聲道和立體聲
- 支持多達255個頻道(多數據流的幀)
- 可動態調節比特率,音頻帶寬和幀大小
- 良好的魯棒性丟失率和數據包丟失隱藏(PLC)
- 浮點和定點實現
你可以在RFC 6716標准中閱讀完整的規范,包括參考實現。也可以在下載頁面獲得一個最新的Opus 標准。
Libopus是Opus編解碼器的參考實現,我們可以參考該代碼進行開發。
4.3 開發插件
為了能在Firefox中支持Opus,Mozilla提供了專門的Opus工具。Opus-tools提供命令行程序來進行編碼、檢查和解碼.opus文件。在HTML中語音相關的可以使用,原來不是Android的,需要用的自己去官方主頁下載即可。
小編翻譯到這里還沒有找到和Android相關的,藍瘦,香菇!
4.4 版本信息
盡管Opus現在是由IETF指定標准,但是Opus的實現也會一直持續改進。當然,所有未來版本仍將完全符合標准IETF規范。可以在開發界面查看最新的開發版本信息。
libopus 1.1.3(穩定發行版)
4.5 對比
小編曾說過,沒有對比,就沒有傷害。
- 質量和比特率
下圖說明了不同的編解碼器的質量和比特率直接的函數關系。
narrowband - 窄頻
wideband - 寬頻
super-wideband - 超寬頻
fullband - 全頻段
fullband stereo - 全頻段立體聲
從圖上可以看出,Opus的優勢十分明顯。尤其是較之前的Speex,具有更大的比特率范圍以及帶寬。
- 比特率/延遲比較
從圖中可以看出,Opus在任何比特率下都具有較少的延遲。
- 聽力測試
Opus進行了幾次測試,但是下面僅僅列出基於比特流的幾個測試結果。盡管在Opus發布的時候應該給出一個比較好的質量標准化的意見,但是我們希望更新的和更高級的編碼器將達到更好的質量。
4.6 模塊API文檔
截止到目前位置,最新的版本是1.13穩定發行版,所以在這里翻譯的都是該版本的API。
4.6.1 Opus Encoder
Opus 編碼器
類型定義
- typedef struct OpusEncoder OpusEncoder Opus編碼器,包含了編碼器的全部狀態。
- 1
- 2
- 1
- 2
方法
- int opus_encoder_get_size (int channels) 獲取一個OpusEncoder編碼器的大小。 - OpusEncoder * opus_encoder_create (opus_int32 Fs, int channels, int application, int *error) 分配和初始化一個編碼器狀態。 - int opus_encoder_init (OpusEncoder *st, opus_int32 Fs, int channels, int application) 初始化之前分配的編碼器指針st指向的內存中的編碼器,並且必須通過使用opus_encoder_get_size()方法來返回最小內存大小。 - opus_int32 opus_encode (OpusEncoder *st, const opus_int16 *pcm, int frame_size, unsigned char *data, opus_int32 max_data_bytes) 編碼。 - opus_int32 opus_encode_float (OpusEncoder *st, const float *pcm, int frame_size, unsigned char *data, opus_int32 max_data_bytes) 編碼音頻流。 - void opus_encoder_destroy (OpusEncoder *st) 釋放一個通過opus_encoder_create()分配的OpusEncoder。 - int opus_encoder_ctl (OpusEncoder *st, int request,...) 在Opus編碼器中執行CTL函數。
詳細描述
通過官方文檔解釋和C語言基礎,我們知道OpusEncoder *enc 表示一個Opus編碼器結構體的指針,指向該編碼器的內存,該結構體內部包含了該編碼器的全部狀態。C語言中是沒有類和對象的概念的,但是有結構體,可以用來模擬Java中的類,所以結構體的實例也就可以比作對象。從現在開始,我就成稱enc為該編碼器對象(的指針),這樣說比較習慣。
因為Opus是有狀態編碼,編碼過程始於創建一個編碼器狀態:
int error; OpusEncoder *enc; enc = opus_encoder_create(Fs, channels, application, &error); // 創建
從這一點開來,enc可以用來編碼一個音頻流。一個編碼器在同一時刻僅僅只能用於一個音頻流編碼,並且對於每一種音頻格式初始化的編碼器狀態,不可以再次初始化。
當執行 opus_encoder_create() 為編碼器分配了內存之后,就可以初始化這個預分配的內存:
int size; int error; OpusEncoder *enc; size = opus_encoder_get_size(channels); // 獲取需要的最小內存 enc = malloc(size); // 分配內存 error = opus_encoder_init(enc, Fs, channels, application); // 初始化內存
opus_encoder_get_size() 返回編碼器對象所需要的內存大小,注意,該代碼在未來的版本中可能會變成改變內存大小,所以不要根據這個代碼有任何假定,即不要根據獲取內存大小這個方法來有一些邏輯上的處理,因為之后的版本變動有可能會影響你的代碼。
編碼器的狀態會一直保存在內存中,並且只有淺復制才能有效的復制該狀態,例如:memcpy()。
使用 opus_encoder_ctl() 接口可以改變一些編碼器的設置。所有的設置已經被默認設置成推薦值,所以只有在必要的時候才去改變它們。最常見的想要改變的設置如下:
opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate));
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal_type));
- bitrate:比特率,b/s
- complexity:復雜度,1-10,1最低,10最高
- signal_type:信號類型,可以為OPUS_AUTO(默認)、OPUS_SIGNAL_VOICE、OPUS_SIGNAL_MUSIC
查看CTLS相關編碼來獲取完整的設置參數列表,大部分參數可以在一個音頻流的任何時候設置和改變。
為了對一幀數據編碼, opus_encode() 或者 opus_encode_float() 在調用的時候必須使用的是恰好的一幀(2.5,5,10,20,40,60毫秒)音頻數據。
len = opus_encode(enc, audio_frame, frame_size, packet, max_packet);
- 1
- 1
- audio_frame:opus_int16格式的音頻數據
- frame_size:抽樣的每一幀的時間大小(每個通道)
- packet:壓縮后的編碼寫入的字節數組
- max_packet:字節數組中可以寫入的最大字節數,推薦大小為4000字節,不要使用 max_packet 去控制可變比特率,而是使用 OPUS_SET_BITRATE 的CTL命令。
opus_encode() 和 opus_encode_float() 返回實際接入到packet中的編碼后的音頻數據的字節數。返回值也許是無效的,表明編碼錯誤。如果返回值小於2字節或者更小,那么這個packet不需要發送出去。
一旦這個編碼器對象不需要,可以銷毀掉:
opus_encoder_destroy(enc);
- 1
- 1
如果編碼器對象是使用opus_encoder_init() 而不是 opus_encoder_create() 方法創建的,那么除了可能需要釋放我們手動分配的內存之外,不需要其他的動作。
類型定義文檔
typedef struct OpusEncoder OpusEncoder 編碼器結構體,包含了一個編碼器所有的狀態。它是位置獨立的,可以被隨意的復制。
- 1
- 2
- 1
- 2
方法文檔
- opus_int32 opus_encode( OpusEncoder∗ st, const opus_int16∗ pcm, int frame_size, unsigned char∗ data, opus_int32 max_data_bytes)
- 1
- 1
參數:
參數 | 參數類型 | 入參或出參 | 解釋 |
---|---|---|---|
st | OpusEncoder∗ | in | 編碼器對象 |
pcm | const opus_int16* | in | 輸入信號(雙聲道則為交錯模式),長度為frame_size × channels × sizeof(opus_int16),即采樣個數 × 聲道數 × 16。 |
frame_size | int | in | 輸入的音頻信號的每個聲道的采樣數量,這一定是一個Opus框架編碼器采樣率的大小。例如,當采樣率為48KHz的時候,采樣數量允許的數值為120、240、480、960、1920和2880。傳遞一個持續時間少於10 ms的音頻數據(480個樣本48 kHz),編碼器將不會使用LPC或混合模式。 |
data | unsigned char∗ | out | 輸出編碼結果,至少包含max_data_bytes個字節數。 |
max_data_bytes | opus_int32 | in | 為了輸出編碼結果分配的內存,它可能用於控制一個即時比特率的上線,但是不應該作為唯一的比特率控制。 |
關於采樣率和采樣數量的關系,上文已經提到過,opus_encode() 或者 opus_encode_float() 在調用的時候必須使用的是恰好的一幀(2.5,5,10,20,40,60毫秒)音頻數據,如果采樣頻率為48KHz,那么:
∵ 采樣頻率 Fm = 48KHz
∴ 采樣間隔 T = 1/Fm = 1/48000 s = 1/48 ms
∴ 當T0 = 2.5 ms 時,N = T0/T = 2.5/(1/48) = 120,
當T0 = 5.0 ms 時,N = T0/T = 2.5/(1/48) = 240,
當T0 = 10 ms 時,N = T0/T = 2.5/(1/48) = 480,
當T0 = 20 ms 時,N = T0/T = 2.5/(1/48) = 960,
當T0 = 40 ms 時,N = T0/T = 2.5/(1/48) = 1920,
當T0 = 60 ms 時,N = T0/T = 2.5/(1/48) = 2880,
即,當Fm = 48KHz時:
采樣時間(ms) | 2.5 | 5 | 10 | 20 | 40 | 60 |
---|---|---|---|---|---|---|
采樣個數 | 120 | 240 | 480 | 960 | 1920 | 2880 |
- opus_int32 opus_encode_float( OpusEncoder∗ st, const float∗ pcm, int frame_size, unsigned char∗ data, opus_int32 max_data_bytes)
- 1
- 1
4.6.2 Opus Decoder
實在是翻譯不動了。。。
其實有了上面的介紹,看官們應該能有個大概的認知,下面貼出一個中文版的翻譯文檔,大家自己瀏覽,因為目前我也差不多看了一般,所以小編對該文檔的后面章節正確性與否並不負責~
五、小結
如果對比一下Speex和Opus,會發現從集成和使用的角度來看,十分的相似,API的時候方式也差不多,只不過Opus的功能更加的強大,API也更加豐富,但是也變相的增加了我們的開發難度,需要對C語言有相當好的功底,才可以定制實現自己的需求。
編程之路,任重而道遠。先是為了生活而編程,后是為了超越而編程!