Android技术分享| Android WebRTC 对 AudioRecord 的使用

前言

在这里插入图片描述

AudioRecord 是 Android 基于原始PCM音频数据录制的类,WebRCT 对其封装的代码位置位于org/webrtc/audio/WebRtcAudioRecord.java,接下来我们学习一下 AudioRecord 是如何创建启动,读取音频采集数据以及销毁等功能的。

创建和初始化
  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 * (BITS_PER_SAMPLE / 8);
    final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
    byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
    Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity());
    emptyBytes = new byte[byteBuffer.capacity()];
    // Rather than passing the ByteBuffer with every callback (requiring
    // the potentially expensive GetDirectBufferAddress) we simply have the
    // the native class cache the address to the memory once.
    nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord);

    // Get the minimum buffer size required for the successful creation of
    // an AudioRecord object, in byte units.
    // Note that this size doesn't guarantee a smooth recording under load.
    final int channelConfig = channelCountToConfiguration(channels);
    int minBufferSize =
        AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
    if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
      reportWebRtcAudioRecordInitError("AudioRecord.getMinBufferSize failed: " + minBufferSize);
      return -1;
    }
    Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize);

    // Use a larger buffer size than the minimum required when creating the
    // AudioRecord instance to ensure smooth recording under load. It has been
    // verified that it does not increase the actual recording latency.
    int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
    Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes);
    try {
      audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig,
          AudioFormat.ENCODING_PCM_16BIT, 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;
    }
    if (effects != null) {
      effects.enable(audioRecord.getAudioSessionId());
    }
    logMainParameters();
    logMainParametersExtended();
    return framesPerBuffer;
  }

在初始化的方法中,主要做了两件事。

  • 创建缓冲区

    1. 由于实际使用数据的代码在native层,因此这里创建了一个Java的direct buffer,而且AudioRecord也有通过ByteBuffer读数据的接口,并且实际把数据复制到ByteBuffer的代码也在native层,所以这里使用direct buffer效率会更高。
    2. ByteBuffer的容量为单次读取数据的大小。Android的数据格式是打包格式(packed),在多个声道时,同一个样点的不同声道连续存储在一起,接着存储下一个样点的不同声道;一帧就是一个样点的所有声道数据的合集,一次读取的帧数是10ms的样点数(采样率除以100,样点个数等于采样率时对应于1s的数据,所以除以100就是10ms的数据);ByteBuffer的容量为帧数乘声道数乘每个样点的字节数(PCM 16 bit表示每个样点为两个字节)。
    3. 这里调用的nativeCacheDirectBufferAddress JNI函数会在native层把ByteBuffer的访问地址提前保存下来,避免每次读到音频数据后,还需要调用接口获取访问地址。
  • 创建 AudioRecord对象,构造函数有很多参数,分析如下

    1. audioSource

      指的是音频采集模式,默认是 VOICE_COMMUNICATION,该模式会使用硬件AEC(回声抑制)

    2. sampleRate

      采样率

    3. channelConfig

      声道数

    4. audioFormat

      音频数据格式,这里实际使用的是 AudioFormat.ENCODING_PCM_16BIT,即PCM 16 bit的数据格式。

    5. bufferSize

      系统创建AudioRecord时使用的缓冲区大小,这里使用了两个数值的较大者:通过AudioRecord.getMinBufferSize接口获取的最小缓冲区大小的两倍,读取数据的ByteBuffer的容量。通过注释我们可以了解到,考虑最小缓冲区的两倍是为了确保系统负载较高的情况下音频采集仍能平稳运行,而且这里设置更大的缓冲区并不会增加音频采集的延迟。

启动
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;
  }

在该方法中,首先启动了 audioRecord,接着判断了读取线程事都正在录制中。

读数据
 private class AudioRecordThread extends Thread {
    private volatile boolean keepAlive = true;

    public AudioRecordThread(String name) {
      super(name);
    }

    // TODO(titovartem) make correct fix during webrtc:9175
    @SuppressWarnings("ByteBufferBackingArray")
    @Override
    public void run() {
      Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
      Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo());
      assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING);

      long lastTime = System.nanoTime();
      while (keepAlive) {
        int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
        if (bytesRead == byteBuffer.capacity()) {
          if (microphoneMute) {
            byteBuffer.clear();
            byteBuffer.put(emptyBytes);
          }
          // It's possible we've been shut down during the read, and stopRecording() tried and
          // failed to join this thread. To be a bit safer, try to avoid calling any native methods
          // in case they've been unregistered after stopRecording() returned.
          if (keepAlive) {
            nativeDataIsRecorded(bytesRead, nativeAudioRecord);
          }
          if (audioSamplesReadyCallback != null) {
            // Copy the entire byte buffer array.  Assume that the start of the byteBuffer is
            // at index 0.
            byte[] data = Arrays.copyOf(byteBuffer.array(), byteBuffer.capacity());
            audioSamplesReadyCallback.onWebRtcAudioRecordSamplesReady(
                new AudioSamples(audioRecord, data));
          }
        } else {
          String errorMessage = "AudioRecord.read failed: " + bytesRead;
          Logging.e(TAG, errorMessage);
          if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
            keepAlive = false;
            reportWebRtcAudioRecordError(errorMessage);
          }
        }
        if (DEBUG) {
          long nowTime = System.nanoTime();
          long durationInMs = TimeUnit.NANOSECONDS.toMillis((nowTime - lastTime));
          lastTime = nowTime;
          Logging.d(TAG, "bytesRead[" + durationInMs + "] " + bytesRead);
        }
      }

      try {
        if (audioRecord != null) {
          audioRecord.stop();
        }
      } catch (IllegalStateException e) {
        Logging.e(TAG, "AudioRecord.stop failed: " + e.getMessage());
      }
    }

    // Stops the inner thread loop and also calls AudioRecord.stop().
    // Does not block the calling thread.
    public void stopThread() {
      Logging.d(TAG, "stopThread");
      keepAlive = false;
    }
  }

从 AudioRecord去数据的逻辑在 AudioRecordThread 线程的 Run函数中。

  1. 在线程启动的地方,先设置线程的优先级为URGENT_AUDIO,这里调用的是Process.setThreadPriority。
  2. 在一个循环中不停地调用audioRecord.read读取数据,把采集到的数据读到ByteBuffer中,然后调用nativeDataIsRecorded JNI函数通知native层数据已经读到,进行下一步处理。
停止和销毁
  private boolean stopRecording() {
    Logging.d(TAG, "stopRecording");
    assertTrue(audioThread != null);
    audioThread.stopThread();
    if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS)) {
      Logging.e(TAG, "Join of AudioRecordJavaThread timed out");
      WebRtcAudioUtils.logAudioState(TAG);
    }
    audioThread = null;
    if (effects != null) {
      effects.release();
    }
    releaseAudioResources();
    return true;
  }

可以看到,这里首先把AudioRecordThread读数据循环的keepAlive条件置为false,接着调用ThreadUtils.joinUninterruptibly等待AudioRecordThread线程退出。

这里有一点值得一提,keepAlive变量加了volatile关键字进行修饰,这是因为修改和读取这个变量的操作可能发生在不同的线程,使用volatile关键字进行修饰,可以保证修改之后能被立即读取到。

AudioRecordThread线程退出循环后,会调用audioRecord.stop()停止采集;线程退出之后,会调用audioRecord.release()释放AudioRecord对象。

以上,就是 Android WebRTC 音频采集 Java 层的大致流程。

最后

更多Android音视频开发学习资料可以扫码免费领取!

《Android音视频精编源码解析》

第一章 WebRTC Native 源码导读

​ ● 安卓相机采集实现分析

​ ● 安卓预览实现分析

​ ● 安卓视频硬编码实现分析

​ ● VideoCRE 与内存抖动优化

​ ● 安卓 P2P 连接过程和 DataChannel 使用

​ ● 视频数据 native 层之旅

​ ● 混音

​ ● P2P 连接过程完全解析

​ ● API 概览

​ ● RTP H.264 封包与解包

img

第二章 X264源码解读

​ ● 概述

​ ● x264命令行工具

​ ● 编码器主干部分-2

​ ● x264_slice_write()

​ ● 滤波(Filter)部分

​ ● 宏块分析(Analysis)部分-帧内宏块(Intra)

img

第三章 FFmpeg

​ ● FFmpeg 编译和集成

​ ● FFmpeg + ANativeWindow 实现视频解码播放

​ ● FFmpeg + OpenSLES 实现音频解码播放

​ ● FFmpeg + OpenGLES 实现音频可视化播放

​ ● FFmpeg + OpenGLES 实现视频解码播放和视频滤镜

​ ● FFmpeg 播放器实现音视频同步的三种方式

​ ● FFmpeg + OpenGLES 实现 3D 全景播放器

​ ● FFmpeg 播放器视频渲染优化

​ ● FFmpeg、x264以及fdk-aac 编译整合

​ ● FFmpeg 视频录制 - 视频添加滤镜和编码

​ ● FFmpeg + Android AudioRecorder 音频录制编码

​ ● Android FFmpeg 实现带滤镜的微信小视频录制功能

img

第四章 ijkplayer 源码分析系列

​ ● 整体结构总结

​ ● read_thread流程

​ ● 解码流程

​ ● 渲染流程

img

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值