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

社区征文RTC

目录

前言

正文

步骤一、获取麦克风权限

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

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

步骤四、音频预处理

结尾


前言

WebRTC 作为一个开源的实时音视频通许方案,经过多年的发展基本上已经支持了所有的常用终端,比如 windows、mac、Android、iOS 等。我们都知道音视频通讯的前提是采集本地的音频和视频数据信息。今天,我们就来了解一下 WebRTC 在安卓端是如何采集音频信号的。

正文

上一篇文章已经介绍了 WebRTC 如何在安卓系统上采集视频数据信号,相信小伙伴已经对视频采集流程有了一个基本的认识,那么我们不禁要问,那音频数据信号又是如何采集的呢?好的,我们今天就来了解一下这部分的内容。本文依然以安卓系统和 WebRTC M76 版本为例进行介绍。

image.png

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 签约作者,欢迎关注我分享更多干货!😄

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论