android技术分享|androidwebrtc对audiorecord的使用(代码片段)

anyRTC anyRTC     2022-11-30     281

关键词:

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

android技术分享|超简单!给androidwebrtc增加美颜滤镜功能(代码片段)

视频采集渲染流程分析在增加滤镜功能之前,需要对WebRTC视频采集的流程有一定了解。WebRTC中定义了VideoCapture接口类,其中定义了相机的初始化,预览,停止预览销毁等操作。实现类是CameraCapture,并且封装了C... 查看详情

androidwebrtc入门教程--使用相机(代码片段)

...116e6c8博客来整理,然后加一些自己的理解。权当记录AndroidWebRTC入门教程(一)–使用相机AndroidWebRTC入门教程 查看详情

androidwebrtc入门教程--使用相机(代码片段)

前言,最近在搞网页投屏,发现WebRTC的Android版本较少,这里的话,参考了一些优秀的博客,主要是这个大佬的https://www.jianshu.com/p/eb5fd116e6c8博客来整理,然后加一些自己的理解。权当记录源码工程:http... 查看详情

androidwebrtc中rtcstatusreport参数含义

参考技术A每一个发送或接受的音频或视频Track都有一个对应的ssrc报告,其中包含有:包含有关视频宽带相关信息,一般在分析视频质量不好的时候,可以先查看这个报告中的发送和接收的可用宽带信息主要描述本地Candidate和远端C... 查看详情

android webRTC语音通话

】androidwebRTC语音通话【英文标题】:androidwebRTCvoicecall【发布时间】:2021-11-2101:20:15【问题描述】:我想为安卓开发一个语音通话应用。由于原生androidwebRTC项目没有得到维护并且缺乏良好的文档,我最终使用了带有webView的peerjs... 查看详情

android动画技术总结分享(代码片段)

前言动画的使用是 Android 开发中常用的知识本次分享探讨 Android动画,包括动画的种类、使用、原理等,以及支持跨平台动画库--Lottie 目录 1.动画类型Android动画主要分为分为两大类(三种):视图动画࿱... 查看详情

使用 libjingle 在 android WebRTC 中自定义音频设备

】使用libjingle在androidWebRTC中自定义音频设备【英文标题】:CustomaudiodeviceinandroidWebRTCusinglibjingle【发布时间】:2019-02-2014:37:39【问题描述】:我正在开发本机androidWebRTC客户端,该客户端支持从自定义设备流式传输音频(我通过蓝... 查看详情

技术分享|app自动化测试(android)–高级定位技巧

原文链接XPath高级定位技巧XPath简介XPath的英文全称为:XMLPathLanguage,意旨对XML中的元素进行路径定位的一种语言,它可适用XML标记语言,Html标记语言,appDom结构。XPath是自动化工具的定位基础,可适用于Selenium工具 查看详情

技术分享|app自动化测试(android)–触屏操作自动化

原文链接导入TouchActionPython版本fromappium.webdriver.common.touch_actionimportTouchActionJava版本importio.appium.java_client.TouchAction;常用的手势操作pr 查看详情

android技术分享|视频通话开发流程(代码片段)

多人呼叫多人呼叫与点对点呼叫区别在于多人呼叫是一次呼叫1个以上的人,中途也可以再呼叫邀请别人加入通话。整个呼叫的流程跟点对点呼叫类似,但也有些区别,需要添加额外的API逻辑来实现功能。下面我们分... 查看详情

android技术分享|视频通话开发流程(代码片段)

多人呼叫多人呼叫与点对点呼叫区别在于多人呼叫是一次呼叫1个以上的人,中途也可以再呼叫邀请别人加入通话。整个呼叫的流程跟点对点呼叫类似,但也有些区别,需要添加额外的API逻辑来实现功能。下面我们分... 查看详情

android技术分享|实现视频连麦直播(代码片段)

视频连麦产品端核心步骤分析游客申请连麦/取消申请主播同意/拒绝申请音视频发布取消支持很多观众观看支持多人连麦低延时IM弹幕视频连麦技术端调研emmm,大致可以分为视频采集、编码,传输,解码,渲染%¥#&... 查看详情

致android开发工程师:过了笔试,千万别栽在自己最擅长得技术面试上——70道android技术面试题分享.

...xff0c;对自己的辛苦付出不值。那么今天笔者就给大家分享Android70道技术面试题,帮你绕过那些技术面的坑!为了能够在面试回答中优雅而不失体面回答面试考点,本文借 查看详情

android技术分享|activity过渡动画—让切换更加炫酷(代码片段)

介绍在android5.0以上版本中,google为我们提供了几种activity切换的过渡动画,目的是为了让activity切换转场更加美观,而在android5.0之前的activity切换显得生硬。虽然可以自定义给activity增添动画效果,但是效果也不尽如意。而androi5.... 查看详情

阿里android架构师进阶成长笔记,100g资源限时分享

移动架构师的成长路线是什么样的?首先,身为程序员,安身立命的根本还是技术,架构师要能够解决当前业务中的技术问题,同时培养自己的技术前瞻性,为业务的未来储备技术;其次,架构师还有很... 查看详情

android技术分享|一行代码实现安卓屏幕采集编码(代码片段)

...要共享手机屏幕给他人观看,特别是在线教育行业。Android从5.0开始支持了MediaProjection,利用MediaProjection,可以实现截屏录屏功能。本库对屏幕采集编码进行了封装,简单的调用即可实现MediaProjection权限申请,H... 查看详情

android技术分享|自定义layoutmanager(代码片段)

效果预览注:文本只是简单的Demo,功能单一,主要讲解流程与步骤,请根据特定的需求修改。各属性图:因为item随着滑动会有不同的缩放,所以实际normalViewGap会被缩放计算。我们在自定义ViewGroup中,... 查看详情

androidwebrtc多人网状p2p视频聊天

参考技术A什么是网状P2P?假设3个人视频会议,每个人都要同时接收2路流,上传两路流。每个客户端要创建多个(N-1个)PeerConnection,同时和多个人建立P2P连接。多人P2P跟两人P2P通信的唯一区别就是要创建多个PeerConnection,也很... 查看详情