目录
WebRTC 作为一个开源的实时音视频通许方案,经过多年的发展基本上已经支持了所有的常用终端,比如 windows、mac、Android、iOS 等。我们都知道音视频通讯的前提是采集本地的音频和视频数据信息。今天,我们就来了解一下 WebRTC 在安卓端是如何采集音频信号的。
上一篇文章已经介绍了 WebRTC 如何在安卓系统上采集视频数据信号,相信小伙伴已经对视频采集流程有了一个基本的认识,那么我们不禁要问,那音频数据信号又是如何采集的呢?好的,我们今天就来了解一下这部分的内容。本文依然以安卓系统和 WebRTC M76 版本为例进行介绍。
WebRTC 中的音频采集逻辑和视频还不太一样,在不同的系统上采集视频时需要调用不同的系统 API 接口,不同平台的 C++ 代码实现逻辑也不一样。这方面就没有音频处理简单了,当然这里边有很多历史因素,因为音频数据的采集逻辑在各个平台上是同一套 C++ 代码。需要说明的是,上层进一步封装的语言可能会根据不同系统平台有所不同,比如安卓平台封装的是 Java 语言的 API 接口,iOS 苹果系统封装的是 Object-C 语言的 API 接口。
尽管,WebRTC 中声明了两种音频采集和播放接口,一种是基于文件的 MediaRecorder 和 MediaPlayer,一种是基于纯音频数据(PCM)的 AudioRecord 和 AudioTrack。但是,在实际应用场景中 WebRTC 仅使用了一种接口方式,使用了同步读写数据的 AudioRecord 和 AudioTrack 接口类。下面我们就来看一下具体的音频采集流程。
步骤一、获取麦克风权限
WebRTC 在进行进行音频采集之前,需要先申请安卓系统的麦克风权限。在 WebRTC 中已经提供了申请麦克风权限的方法——checkCallingOrSelfPermission(),直接使用就好。参考代码如下:
for (String permission : MANDATORY_PERMISSIONS) {
if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
logAndToast("Permission " + permission + " is not granted");
setResult(RESULT_CANCELED);
finish();
return;
}
}
其中,全局静态变量 MANDATORY_PERMISSIONS 已经包含了安卓系统音频相关的权限选项,具体内容如下:
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.RECORD_AUDIO",
"android.permission.INTERNET"
其中,三个选项的意思分别是修改系统音频设置选项、采集麦克风声音、使用网络的权限,只有在获取了安卓系统的麦克风权限才能进行下一步。
需要说明的是,这仅仅是代码层面的编码方式。在实际的项目中还要在 AndroidManifest.xml 清单文件中分别进行配置,对应上述三个选项的配置声明如下:
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
步骤二、音频采集模块初始化
在第一步中,如果我们已经成功获取了系统的麦克风权限,那么现在就可以初始化 WebRTC 音频采集的相关模块了。初始化音频采集模块时,需要指定音频的采样率和声道数,调用的方法是 initRecording()。该方法完成了音频数据内存大小的申请以及 AudioRecord 对象实例的创建,参考代码如下:
@CalledByNative
private int initRecording(int sampleRate, int channels) {
Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")");
if (audioRecord != null) {
reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording.");
return -1;
}
final int bytesPerFrame = channels * getBytesPerSample(audioFormat);
final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
if (!(byteBuffer.hasArray())) {
reportWebRtcAudioRecordInitError("ByteBuffer does not have backing array.");
return -1;
}
Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity());
emptyBytes = new byte[byteBuffer.capacity()];
nativeCacheDirectBufferAddress(nativeAudioRecord, byteBuffer);
final int channelConfig = channelCountToConfiguration(channels);
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
reportWebRtcAudioRecordInitError("AudioRecord.getMinBufferSize failed: " + minBufferSize);
return -1;
}
Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize);
int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes);
try {
audioRecord =
new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes);
} catch (IllegalArgumentException e) {
reportWebRtcAudioRecordInitError("AudioRecord ctor error: " + e.getMessage());
releaseAudioResources();
return -1;
}
if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
reportWebRtcAudioRecordInitError("Failed to create a new AudioRecord instance");
releaseAudioResources();
return -1;
}
effects.enable(audioRecord.getAudioSessionId());
logMainParameters();
logMainParametersExtended();
return framesPerBuffer;
}
需要说明的是,WebRTC 底层默认使用单声道,不仅输入是单声道,输出默认也是单声道。
上述代码中,byteBuffer 变量是单次读取音频数据的大小,单位是字节。它是由 bytesPerFrame 和 framesPerBuffer 相乘得到的,其中 bytesPerFrame 变量是每个音频帧的大小,每个音频帧是声道数和音频采样位决定的,WebRTC 通常使用 AudioFormat.ENCODING_PCM_16BIT 采样位枚举值,也就是 2 字节。如果是默认值单声道的话,每个音频帧的大小就是 1*2=2 字节。既然,byteBuffer 是单次读取音频数据的大小,那么,我们还需要知道每次读取多少个音频帧,再乘上每个音频帧的大小就可以了。因为 WebRTC 底层每 10 毫秒触发一次回调,每秒就会回调 100 次,此时,我们假设采样率是 48kHz,那么由计算可得每次会采集多少个音频帧,48000 除以 100 等于 480。那么 WebRTC 每次会读取的音频数据大小为 480 乘以 2 等于 960 个字节。如果是双声道而采样率不变化的话,每次读取的音频数据大小是 1920 字节。
另外,在创建 AudioRecord 对象实例时,参数 audioSource 指明了音频通讯的具体模式,WebRTC 一般默认是语音通话模式,这种模式会开启硬件的回声抑制效果。
步骤三、启动音频采集流程
音频采集模块初始化完成后,就可以正式启动音频采集流程了。WebRTC 中对应的采集方法是 startRecording(),该方法的主要任务是启动了声音采集,同时创建了 AudioRecordThread 对象实例线程并启动该线程。该线程不停的从内存中读取音频数据,然后同步给底层。参考代码如下:
@CalledByNative
private boolean startRecording() {
Logging.d(TAG, "startRecording");
assertTrue(audioRecord != null);
assertTrue(audioThread == null);
try {
audioRecord.startRecording();
} catch (IllegalStateException e) {
reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_EXCEPTION,
"AudioRecord.startRecording failed: " + e.getMessage());
return false;
}
if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_STATE_MISMATCH,
"AudioRecord.startRecording failed - incorrect state :"
+ audioRecord.getRecordingState());
return false;
}
audioThread = new AudioRecordThread("AudioRecordJavaThread");
audioThread.start();
return true;
}
步骤四、音频预处理
采集麦克风的音频数据后,WebRTC 会通知底层对音频数据进行预处理操作,比如混音和音频重采样的工作。主要是通过调用 JNI 方法 DataIsRecorded() 向底层进行信息传递。参考代码如下:
JNI_FUNCTION_ALIGN
void JNICALL AudioRecordJni::DataIsRecorded(JNIEnv* env,
jobject obj,
jint length,
jlong nativeAudioRecord) {
webrtc::AudioRecordJni* this_object =
reinterpret_cast<webrtc::AudioRecordJni*>(nativeAudioRecord);
this_object->OnDataIsRecorded(length);
}
void AudioRecordJni::OnDataIsRecorded(int length) {
RTC_DCHECK(thread_checker_java_.IsCurrent());
if (!audio_device_buffer_) {
RTC_LOG(LS_ERROR) << "AttachAudioBuffer has not been called";
return;
}
audio_device_buffer_->SetRecordedBuffer(direct_buffer_address_,
frames_per_buffer_);
audio_device_buffer_->SetVQEData(total_delay_in_milliseconds_, 0);
if (audio_device_buffer_->DeliverRecordedData() == -1) {
RTC_LOG(INFO) << "AudioDeviceBuffer::DeliverRecordedData failed";
}
}
完成音频数据的预处理后,会再进行音频编码,最后完成组包发送。当然,这些内容已经不是本文要讨论和介绍的内容了。至此,WebRTC 在安卓系统系统上采集麦克风声音的基本流程就介绍清楚了,但是,实际处理时还有很多细节内容,本文就不深入展开了,欢迎跟进后续内容。
通过本文的介绍,相信大家已经对 WebRTC 如何在安卓系统上采集本地麦克风的音频数据有了基本上的认识。但是,这同样仅仅是音频众多流程中一个小环节,后续还有耳返、编码、组包、传输、解包、解码、播放等过程。关于别的部分的内容,我们在后续章节再继续介绍。
文章来源:https://xie.infoq.cn/article/09c27c708330d2ca05454abfc
作者简介:😄大家好,我是 Data-Mining(liuzhen007),是一位典型的音视频技术爱好者,前后就职于传统广电巨头和音视频互联网公司,具有丰富的音视频直播和点播相关经验,对 WebRTC、FFmpeg 和 Electron 有非常深入的了解,😄公众号:玩转音视频。同时也是 CSDN 博客专家、华为云享专家(共创编辑)、InfoQ 签约作者,欢迎关注我分享更多干货!😄