您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

Android WebRTC 对 AudioRecord 的使用

时间:2022-01-13 09:31:49  来源:  作者:anyRTC云平台

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 层的大致流程。

 

参考《WebRTC 开发实战》

https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/audio/WebRtcAudioRecord.java



Tags:Android   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
AudioRecord 是 Android 基于原始PCM音频数据录制的类,WebRCT 对其封装的代码位置位于 org/webrtc/audio/WebRtcAudioRecord.java,接下来我们学习一下 AudioRecord 是如何创建...【详细内容】
2022-01-13  Tags: Android  点击:(0)  评论:(0)  加入收藏
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  Tags: Android  点击:(16)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  Tags: Android  点击:(16)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  Tags: Android  点击:(20)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  Tags: Android  点击:(26)  评论:(0)  加入收藏
新型Android恶意木马程序伪装成数十款街机、射击和策略游戏,通过华为应用市场AppGallery进行分发,从而窃取设备信息和用户的手机号码,全球目前至少有930万台Android设备被该恶...【详细内容】
2021-12-01  Tags: Android  点击:(29)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  Tags: Android  点击:(19)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  Tags: Android  点击:(44)  评论:(0)  加入收藏
如果你是一名忠实的Android玩家,那么可能会知道,今年的Android 12系统在版本规划上与“往届”相比可以说是很有些特殊。具体来说,除了前段时间刚刚推出正式版的Android 12外,谷...【详细内容】
2021-11-10  Tags: Android  点击:(26)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id 'com.android.library' id 'kotlin-android' id 'k...【详细内容】
2021-11-05  Tags: Android  点击:(44)  评论:(0)  加入收藏
▌简易百科推荐
AudioRecord 是 Android 基于原始PCM音频数据录制的类,WebRCT 对其封装的代码位置位于 org/webrtc/audio/WebRtcAudioRecord.java,接下来我们学习一下 AudioRecord 是如何创建...【详细内容】
2022-01-13  anyRTC云平台    Tags:Android   点击:(0)  评论:(0)  加入收藏
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  像程序那样思考    Tags:Android开发   点击:(16)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  YuCoding    Tags:Android   点击:(16)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  网易Leo    Tags:Android开发   点击:(20)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  网易Leo    Tags:Android开发   点击:(26)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  像程序那样思考    Tags:Android开发   点击:(19)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  永沧    Tags:Android   点击:(44)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id 'com.android.library' id 'kotlin-android' id 'k...【详细内容】
2021-11-05  羊城小阳    Tags:Android   点击:(44)  评论:(0)  加入收藏
谷歌离推出Play Store应用程序的新数据隐私部分又近了一步。应用程序开发人员现在可以通过谷歌在Play控制台的新 "数据安全表 "填写相关细节。该公司表示,所需信息将从2022年...【详细内容】
2021-10-20    中关村在线  Tags:安卓   点击:(58)  评论:(0)  加入收藏
架构究竟是什么?如何更好的理解架构?我们知道一个APP通常是由class组成,而这些class之间如何组合,相互之间又如何产生作用,就是影响这个APP的关键点。细分的话我们可以将其分为类...【详细内容】
2021-09-17  像程序那样思考    Tags:Android架构   点击:(53)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条