WebRTC Native 原始碼導讀(十三):音訊裝置模組 ADM
我真正接觸 WebRTC 的 ADM 是在做 iOS 混音的時候,iOS 的音訊採集、播放之前沒有做過,所以想著從 WebRTC 的音訊採集播放程式碼裡借鑑一下 AudioUnit 的使用,結果折騰了半天愣是沒搞定,後來索性直接使用了 ADM,為混音專案草草地畫上了句號。
今天,就讓我們仔細看看 WebRTC 的 ADM。
功能介紹
ADM 被 WebRtcVoiceEngine 所使用, 它在 WebRTC 中的地位如下圖所示 :
縱觀 ADM 的介面,我們可以總結出它有如下功能:選擇採集/播放音訊裝置、採集/播放啟停控制、採集/播放音量控制、採集/播放靜音、雙聲道採集/播放、獲取播放延遲。
在 Android 和 iOS 平臺,選擇裝置、靜音、雙聲道都沒有實現。Android 只支援設定播放音量,iOS 採集播放都不支援設定音量。播放延遲也並沒有實際實現,返回的都是固定值,不過這個播放延遲也沒有什麼實際的作用,只是供查詢用。
不過,Android 的 ADM 物件是在 Java 程式碼裡建立的,而且 Java 類提供了採集/播放靜音的介面,因此倒也能比較方便的使用這兩個介面。其內部實現原理是,採集若靜音,則傳送靜音資料(全零),播放若靜音則播放靜音資料(全零)。
下面就讓我們來看看啟停控制、資料傳遞在兩個平臺的實現。
Android ADM
Android 的 ADM 實現類在 sdk/android/src/jni/audio_device/audio_device_module.cc
中,其主要介面都是委託給 AudioInput
和 AudioOutput
,而它們分別由 AudioRecordJni
和 AudioTrackJni
實現,而 AudioRecordJni
和 AudioTrackJni
的介面則是呼叫 Java 層的 WebRtcAudioRecord
和 WebRtcAudioTrack
類。
Java 層的 ADM 類只封裝了建立 native 層 ADM 物件的介面,以及提供能能夠設定採集、播放靜音的介面,和我們上面介紹的 ADM 類關係不大。
音訊採集
native 層的程式碼,最終都會呼叫到 org.webrtc.audio.WebRtcAudioRecord
這個 Java 類裡,注意 WebRTC 專案裡有兩個 WebRtcAudioRecord 類,一個在 audio 包下,是新的實現,另一個在 voiceengine 包下,是老的實現,我們只關注新的實現。
WebRTC 的音訊採集利用 Android 系統的 AudioRecord 類實現,AudioRecord 和我是老相識了,網上介紹的文章也很多,這裡我就只強調幾個有意思的要點:
- 由於採集到的資料要交給 native 程式碼使用,為了避免降低資料傳遞的開銷,在初始化採集時(
initRecording
),會把 direct byte buffer 的 native 地址,快取在 native 層;但在 native 程式碼裡,仍會有資料拷貝,不是直接使用這個 direct byte buffer 的地址; - 構造 AudioRecord 物件時,捕獲
IllegalArgumentException
,構造完成後,驗證其狀態為STATE_INITIALIZED
; -
startRecording
函式中,呼叫audioRecord.startRecording
時,捕獲IllegalStateException
,並在之後驗證其狀態為RECORDSTATE_RECORDING
; - 原來的實現裡,若
audioRecord.startRecording
後狀態不對,會 sleep 200ms,再重試,一共重試三次,後來這一邏輯被去掉了; - 從 AudioRecord 讀資料是在一個單獨的執行緒裡,這個執行緒會呼叫
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
以提高執行緒優先順序; - 音訊資料採集到之後,送往 native 層之後,會把資料拷貝一份,交給一個可選的資料回撥;這個邏輯有兩個小問題,一是修改資料的需求無法滿足,二是每次都新建陣列,會引發記憶體抖動;
- 如果
audioRecord.read
返回的讀得資料大小不等於欲讀資料大小,那就不會使用讀到的資料,若返回值為ERROR_INVALID_OPERATION
,那就會停止採集、報告錯誤;
資料傳遞:
- 在採集執行緒讀取到資料後,呼叫 native 介面執行到
AudioRecordJni::DataIsRecorded
; - 呼叫
AudioDeviceBuffer::SetRecordedBuffer
,其中會把採集到的資料拷貝到rec_buffer_
中; - 呼叫
AudioDeviceBuffer::DeliverRecordedData
,接下來就是對資料的編碼、傳送了:
注:WebRTC Android JNI 介面的 C 層函式定義,都不是手寫的,而是用 Python 指令碼生成的,生成的程式碼在 out/debug/gen/sdk/android/generated_xxx_jni/jni
目錄中,其內則是呼叫 sdk/android/src/jni
目錄下的 XXXJni
類。
音訊播放
和音訊採集類似,native 的程式碼都會呼叫到 Java 的 WebRtcAudioTrack 類,而且這個類也有新老兩個版本,我們只關注 audio 包下的新版本。
WebRTC 的音訊播放利用 Android 系統的 AudioTrack 類實現,這裡我也只強調有意思的要點:
- WebRTC 全域性只會使用一個 AudioTrack 物件,無論是一個 PC 多路流,還是多個 PC,多路流的音訊資料會在 AudioMixer 裡混好音,然後用這個 AudioTrack 物件進行播放;
- 和採集類似,播放端也會在
initPlayout
裡快取 direct byte buffer 的 native 地址; - 構造 AudioTrack 物件時,也會捕獲
IllegalArgumentException
,構造完畢後,也會驗證其處於STATE_INITIALIZED
狀態; - 在
startPlayout
裡呼叫audioTrack.play
時也會捕獲IllegalStateException
,之後也會驗證其處於PLAYSTATE_PLAYING
狀態; - 向 AudioTrack 寫資料是在一個單獨的執行緒裡,它也會呼叫
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
提高執行緒優先順序; - 如果
audioTrack.write
返回的寫入資料大小不等於欲寫資料大小,那也不會重試寫未被寫入的資料,但若返回了錯誤,那就會停止播放、報告錯誤;
資料傳遞:
AudioTrackJni::GetPlayoutData AudioDeviceBuffer::RequestPlayoutData
AudioDeviceBuffer::GetPlayoutData
AEC
如果系統硬體支援 AEC,則 WebRTC 會禁用軟體實現的 AEC,這一邏輯實現在 WebRtcVoiceEngine::ApplyOptions
中。
查詢邏輯實現在 WebRtcAudioEffects#isAcousticEchoCancelerSupported
裡,通過對比 AudioEffect.queryEffects
返回的 Descriptor
實現:型別為 AudioEffect.EFFECT_TYPE_AEC
,但 uuid 不是 AOSP 軟體實現的 uuid。
啟用硬體 AEC 時,會呼叫 ADM 的介面,進而呼叫到 WebRtcAudioRecord#enableBuiltInAEC
,但其中只是記下一個標記,實際啟用則是在 WebRtcAudioRecord#initRecording
中觸發,構造好 AudioRecord 後,拿著它的 session id,去啟用 AEC(和下面要講的 NS)。
NS
NS 和 AEC 類似,如果系統硬體支援 NS,則禁用 WebRTC 軟體實現的 NS。
iOS ADM
iOS 的 ADM 實現類在 sdk/objc/native/src/audio/audio_device_module_ios.mm
中,而它則把所有的介面呼叫都轉發給了同目錄下的 audio_device_ios.mm
中,所以幹活的都是 AudioDeviceIOS 類。
而 AudioDeviceIOS 則是把工作分派給了 VoiceProcessingAudioUnit 和 FineAudioBuffer,前者是對 AudioUnit 的封裝,後者則是對資料長度處理邏輯的封裝,因為 iOS 系統採集和播放的資料單位長度和 WebRTC 內部處理的長度可能不一致,所以需要對資料做緩衝處理。
音訊採集
WebRTC iOS 的音訊採集通過 AudioUnit 實現,而對 AudioUnit 的使用,則封裝在 VoiceProcessingAudioUnit 類中,具體程式碼這裡就不貼了,另外我對 AudioUnit 也不是很熟悉,也就不像安卓那樣做「看點分析」了 :)
只有一點比較特殊的是,WebRTC 禁用了 AudioUnit 為採集資料的 buffer 分配,而是自己管理 buffer,在收到 AudioUnit 的回撥之後,再手動呼叫 AudioUnitRender 把採集到的資料取出來。
這裡我們看看音訊採集的資料傳遞過程:
資料到達 AudioDeviceBuffer 類之後,就和安卓殊途同歸了。
音訊播放
音訊播放這邊我們也只是看一下資料傳遞流程:
和採集一樣,一直到 AudioDeviceBuffer 類,Android 和 iOS 的邏輯都是一樣的,只不過 iOS 是系統自己有單獨的 IO 執行緒,無需像 Android 那樣自己維護單獨的執行緒。
AEC
WebRTC iOS 並未實現開關 AEC 的邏輯,但是提供了一個 ios_force_software_aec_HACK
選項,用以在硬體 AEC 不生效的機器上強制開啟軟體 AEC 實現,但這種做法的具體表現如何,WebRTC 也是持觀望態度。
整體上來講,iOS 的 AEC 效果比 Android 還是好很多的,它的開啟是通過 設定 AudioUnit 的 componentSubType
為 kAudioUnitSubType_VoiceProcessingIO
實現的 ,設定這個 sub type 後,會啟用系統的 AEC, AGC, NS 等。
NS
iOS 的 NS 完全依賴系統的實現,沒有實現任何開關、查詢的邏輯。
聲音路由
WebRTC Android 和 iOS 聲音路由相關的邏輯沒有封裝到 SDK 裡,而是在 demo 專案中,聲音路由主要是指控制聲音從哪裡播放出來(聽筒/揚聲器)、音量調節是否生效。
Android
Android 的聲音路由可以通過呼叫 AudioManager
的介面實現,WebRTC 在 demo 的程式碼裡對其進行了一點封裝,包括監聽音訊裝置的變化、聲音路由的切換與恢復等,在 AppRTCAudioManager
類裡。
WebRTC 對 AudioManager
的主要呼叫程式碼如下:
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); audioManager.setSpeakerphoneOn(on);
首先 AudioRecord 使用 VOICE_COMMUNICATION
、AudioTrack 使用 STREAM_VOICE_CALL
,才能實現 AEC 等效果,而此時只有調節通話音量才能正確調節 AudioTrack 的音量大小,AudioManager 設定為 MODE_IN_COMMUNICATION
模式就是為了讓按音量調節鍵能調節通話音量。setSpeakerphoneOn 則是控制聲音是從揚聲器播放,還是從聽筒播放。
iOS
iOS 可以在 ADM Init(建立 PC factory)之前通過 RTCAudioSessionConfiguration setWebRTCConfiguration
設定音訊相關配置:
- category: AVFoundation 定義的 category,包括
AVAudioSessionCategoryPlayback
,AVAudioSessionCategoryRecord
,AVAudioSessionCategoryPlayAndRecord
,AVAudioSessionCategoryMultiRoute
等; - categoryOptions: AVFoundation 定義的 AVAudioSessionCategoryOptions,包括
AVAudioSessionCategoryOptionMixWithOthers
,AVAudioSessionCategoryOptionDuckOthers
,AVAudioSessionCategoryOptionDefaultToSpeaker
等; - mode: AVFoundation 定義的 mode,包括
AVAudioSessionModeVoiceChat
,AVAudioSessionModeVideoRecording
,AVAudioSessionModeMoviePlayback
,AVAudioSessionModeVideoChat
等; - sampleRate: 取樣率;
- ioBufferDuration: 音訊 IO 的單位資料長度;
- inputNumberOfChannels: 採集聲道數;
- outputNumberOfChannels: 播放聲道數;
通話期間則可以通過如下程式碼實現配置的變更:
RTCAudioSessionConfiguration* configuration = [RTCAudioSessionConfiguration webRTCConfiguration]; // change config RTCAudioSession* session = [RTCAudioSession sharedInstance]; [session lockForConfiguration]; NSError* error = nil; BOOL hasSucceeded = [session setConfiguration:configuration active:YES error:&error]; if (!hasSucceeded) { // error } [session unlockForConfiguration];
另外,如需切換聽筒(receiver, earpiece)與揚聲器(speaker),可以在上述程式碼之後呼叫:
AVAudioSession* sysSession = [AVAudioSession sharedInstance]; if (speakerOn) { [sysSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]; } else { [sysSession overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error]; }
因為 WebRTC 預設使用的 category 是 AVAudioSessionCategoryPlayAndRecord
,此 category 預設是把聲音從聽筒播放的,因此 AVAudioSessionPortOverrideSpeaker
就可以使聲音從揚聲器播放, AVAudioSessionPortOverrideNone
就可以使聲音恢復從聽筒播放。
總結
好了,WebRTC ADM 的分析我們就進行到這裡,這部分程式碼其實也是相對獨立的,稍作裁剪就能摘出來直接使用,至於如何摘出 WebRTC 相關的 C++ 程式碼檔案,可以看看 C#webrtc-src-extractor" rel="nofollow,noindex" target="_blank">我寫的一個小指令碼 。
再會 :)