写点什么

WebRTC 如何在安卓系统上采集音频数据 | 社区征文

作者:liuzhen007
  • 2022 年 2 月 13 日
  • 本文字数:4504 字

    阅读完需:约 15 分钟

WebRTC 如何在安卓系统上采集音频数据 | 社区征文

 目录

前言

正文

步骤一、获取麦克风权限

步骤二、音频采集模块初始化

步骤三、启动音频采集流程

步骤四、音频预处理

结尾


前言

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_ALIGNvoid 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 如何在安卓系统上采集本地麦克风的音频数据有了基本上的认识。但是,这同样仅仅是音频众多流程中一个小环节,后续还有耳返、编码、组包、传输、解包、解码、播放等过程。关于别的部分的内容,我们在后续章节再继续介绍。


作者简介:😄大家好,我是 Data-Mining(liuzhen007),是一位典型的音视频技术爱好者,前后就职于传统广电巨头和音视频互联网公司,具有丰富的音视频直播和点播相关经验,对 WebRTC、FFmpeg 和 Electron 有非常深入的了解,😄公众号:玩转音视频。同时也是 CSDN 博客专家、华为云享专家(共创编辑)、InfoQ 签约作者,欢迎关注我分享更多干货!😄


发布于: 刚刚阅读数: 2
用户头像

liuzhen007

关注

敲代码,搞开发。 2021.05.01 加入

本人深耕音视频技术,走全栈路线,前后端通吃,兼顾各端与流媒体服务器。 博客主页地址:https://liuzhen.blog.csdn.net 微信公众号:玩转音视频 欢迎交流学习!

评论

发布
暂无评论
WebRTC 如何在安卓系统上采集音频数据 | 社区征文