这一节主要介绍如何采集一帧音频数据,如果你对音频的基础概念比较陌生,建议看我的上一篇Android 音频开发(一) 基础入门篇。因为音频开发过程中,经常要涉及到这些基础知识,掌握了这些重要的基础知识后,开发过程中的很多参数和流程就会更加容易理解。

1:Android SDK 常用的2种音频采集API

Android SDK 提供了两套音频采集的API,分别如下:

  1. MediaRecorder

    MediaRecorder是更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件

  2. AudioRecord

    AudioRecord更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。

2:MediaRecorder和AudioRecord区别和使用场景

如果想简单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder;而如果需要对音频做进一步的算法处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。

音频的开发,更广泛地应用不仅仅局限于本地录音,因此,我们需要重点掌握如何利用更加底层的 AudioRecord API 来采集音频数据(注意,使用它采集到的音频数据是原始的PCM格式,想压缩为mp3,aac等格式的话,还需要专门调用编码器进行编码)。下面就着重介绍AudioRecord的使用。

3:AudioRecord 的工作流程

先看看AudioRecord 的构造函数,以及对应参数,官方代码如下:

    public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes)throws IllegalArgumentException {this((new AudioAttributes.Builder()).setInternalCapturePreset(audioSource).build(),(new AudioFormat.Builder()).setChannelMask(getChannelMaskFromLegacyConfig(channelConfig,true/*allow legacy configurations*/)).setEncoding(audioFormat).setSampleRate(sampleRateInHz).build(),bufferSizeInBytes,AudioManager.AUDIO_SESSION_ID_GENERATE);}

看构造方法你会发现有五个重要参数,它主要是靠构造函数来配置采集参数的,下面我们来一一解释这些参数的含义:

  1. audioSource

    audioSource是音频采集的输入源,可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),VOICE_COMMUNICATION(用于VoIP应用)等等。

  2. sampleRateInHz

    sampleRateInHz表示采样率,注意,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。

  3. channelConfig

    通道数的配置,可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道)

  4. audioFormat

    这个参数是用来配置“数据位宽”的,可选的值也是以常量的形式定义在 AudioFormat 类中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保证兼容所有Android手机的。

  5. bufferSizeInBytes

    这个是最难理解又最重要的一个参数,它配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小,而前一篇文章介绍过,一帧音频帧的大小计算如下:

    int bufferSizeInBytesSize= 采样率 x 位宽 x 采样时间 x 通道数

    采样时间一般取 2.5ms~120ms 之间,由厂商或者具体的应用决定,我们其实可以推断,每一帧的采样时间取得越短,产生的延时就应该会越小,当然,碎片化的数据也就会越多。

    由于Android的定制化比较严重,不建议采用以上的计算公式计算,幸好AudioRecord 类提供了一个帮助你确定这个 bufferSizeInBytes 的函数,源码如下:

/*** Returns 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, and higher values* should be chosen according to the expected frequency at which the AudioRecord instance* will be polled for new data.* See {@link #AudioRecord(int, int, int, int, int)} for more information on valid* configuration values.* @param sampleRateInHz the sample rate expressed in Hertz.*   {@link AudioFormat#SAMPLE_RATE_UNSPECIFIED} is not permitted.* @param channelConfig describes the configuration of the audio channels.*   See {@link AudioFormat#CHANNEL_IN_MONO} and*   {@link AudioFormat#CHANNEL_IN_STEREO}* @param audioFormat the format in which the audio data is represented.*   See {@link AudioFormat#ENCODING_PCM_16BIT}.* @return {@link #ERROR_BAD_VALUE} if the recording parameters are not supported by the*  hardware, or an invalid parameter was passed,*  or {@link #ERROR} if the implementation was unable to query the hardware for its*  input properties,*   or the minimum buffer size expressed in bytes.* @see #AudioRecord(int, int, int, int, int)*/static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) {int channelCount = 0;switch (channelConfig) {case AudioFormat.CHANNEL_IN_DEFAULT: // AudioFormat.CHANNEL_CONFIGURATION_DEFAULTcase AudioFormat.CHANNEL_IN_MONO:case AudioFormat.CHANNEL_CONFIGURATION_MONO:channelCount = 1;break;case AudioFormat.CHANNEL_IN_STEREO:case AudioFormat.CHANNEL_CONFIGURATION_STEREO:case (AudioFormat.CHANNEL_IN_FRONT | AudioFormat.CHANNEL_IN_BACK):channelCount = 2;break;case AudioFormat.CHANNEL_INVALID:default:loge("getMinBufferSize(): Invalid channel configuration.");return ERROR_BAD_VALUE;}int size = native_get_min_buff_size(sampleRateInHz, channelCount, audioFormat);if (size == 0) {return ERROR_BAD_VALUE;}else if (size == -1) {return ERROR;}else {return size;}}

4:AudioRecord 的工作流程

  1. 配置参数,初始化内部的音频缓冲区

    配置初始化参数,初始化参数大概有五个,具体的参数和说明见下面代码:

    /*** 伴生对象:用来定义初始化的一些配置参数*/companion object {private const val TAG = "AudioCapturer"//设置audioSource音频采集的输入源(可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),VOICE_COMMUNICATION(用于VoIP应用))private const val DEFAULT_SOURCE = MediaRecorder.AudioSource.MIC//设置sampleRateInHz采样率(注意,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。)private const val DEFAULT_SAMPLE_RATE = 44100//设置channelConfig通道数,可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道)private const val DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO//设置audioFormat数据位宽,可选的值也是以常量的形式定义在 AudioFormat 类中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保证兼容所有Android手机的。private const val DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT//注意还有第五个最重要参数bufferSizeInBytes,它配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小,一帧音频帧的大小计算如下//int size = 采样率 x 位宽 x 采样时间 x 通道数(由于厂商的定制化,强烈建议通过AudioRecord类的getMinBufferSize方法确定bufferSizeInBytes的大小,getMinBufferSize方法:int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);)}
  1. 开始采集
    当配置好了初始化参数后,就可以通过构造函数创建好AudioRecord,创建好AudioRecord象之后,就可以开始进行音频数据的采集,通过AudioRecord.startRecording()函数控制采集。
 /*** 开始采集*/@JvmOverloadsfun startCapture(audioSource: Int = DEFAULT_SOURCE, sampleRateInHz: Int = DEFAULT_SAMPLE_RATE, channelConfig: Int = DEFAULT_CHANNEL_CONFIG, audioFormat: Int = DEFAULT_AUDIO_FORMAT): Boolean {if (isCaptureStarted) {Log.e(TAG, "Capture already started !")return false}mMinBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)if (mMinBufferSize == AudioRecord.ERROR_BAD_VALUE) {Log.e(TAG, "Invalid parameter !")return false}Log.d(TAG, "getMinBufferSize = $mMinBufferSize bytes !")mAudioRecord = AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mMinBufferSize)if (mAudioRecord!!.state == AudioRecord.STATE_UNINITIALIZED) {Log.e(TAG, "AudioRecord initialize fail !")return false}mAudioRecord!!.startRecording()mIsLoopExit = falsemCaptureThread = Thread(AudioCaptureRunnable())mCaptureThread!!.start()isCaptureStarted = trueLog.d(TAG, "Start audio capture success !")return true}
  1. 开启线程,实时读取音频缓冲区

    在读取缓冲区的时候我们会遇到过这样的问题,就是一直报**“overrun”**的错误,这是为什么了,原来是因为没有及时从AudioRecord 的缓冲区将音频数据“读”出来。所以我们要注意,在开启开启采集数据的时候,我们需要开线程实时的读取AudioRecord 的缓冲区的数据,读的过程一定要及时,否则就会出现“overrun”的错误,该错误在音频开发中比较常见,意味着应用层没有及时地“取走”音频数据,导致内部的音频缓冲区溢出。

 /*** 定义采集线程*/private inner class AudioCaptureRunnable : Runnable {override fun run() {while (!mIsLoopExit) {val buffer = ByteArray(mMinBufferSize)val ret = mAudioRecord!!.read(buffer, 0, mMinBufferSize)if (ret == AudioRecord.ERROR_INVALID_OPERATION) {Log.e(TAG, "Error ERROR_INVALID_OPERATION")} else if (ret == AudioRecord.ERROR_BAD_VALUE) {Log.e(TAG, "Error ERROR_BAD_VALUE")} else {if (mAudioFrameCapturedListener != null) {mAudioFrameCapturedListener!!.onAudioFrameCaptured(buffer)}Log.d(TAG, "OK, Captured $ret bytes !")}}}}
  1. 停止采集,释放资源
    因为读取是用到了io流的技术,老生常谈的问题就是在停止采集的时候要关闭流,及时的释放资源。
  /*** 停止采集,释放资源*/fun stopCapture() {if (!isCaptureStarted) {return}mIsLoopExit = truetry {mCaptureThread!!.interrupt()mCaptureThread!!.join(1000)} catch (e: InterruptedException) {e.printStackTrace()}if (mAudioRecord!!.recordingState == AudioRecord.RECORDSTATE_RECORDING) {mAudioRecord!!.stop()}mAudioRecord!!.release()isCaptureStarted = falsemAudioFrameCapturedListener = nullLog.d(TAG, "Stop audio capture success !")}

下面列出简单的完整封装列子如下:

5:完整实例代码

package com.bnd.myaudioandvideo.utilsimport android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.util.Log/**** AudioRecord简单封装*/
class AudioCapturer {private var mAudioRecord: AudioRecord? = nullprivate var mMinBufferSize = 0private var mCaptureThread: Thread? = nullvar isCaptureStarted = falseprivate set@Volatileprivate var mIsLoopExit = falseprivate var mAudioFrameCapturedListener: OnAudioFrameCapturedListener? = nullinterface OnAudioFrameCapturedListener {fun onAudioFrameCaptured(audioData: ByteArray?)}fun setOnAudioFrameCapturedListener(listener: OnAudioFrameCapturedListener?) {mAudioFrameCapturedListener = listener}/*** 开始采集*/@JvmOverloadsfun startCapture(audioSource: Int = DEFAULT_SOURCE, sampleRateInHz: Int = DEFAULT_SAMPLE_RATE, channelConfig: Int = DEFAULT_CHANNEL_CONFIG, audioFormat: Int = DEFAULT_AUDIO_FORMAT): Boolean {if (isCaptureStarted) {Log.e(TAG, "Capture already started !")return false}mMinBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)if (mMinBufferSize == AudioRecord.ERROR_BAD_VALUE) {Log.e(TAG, "Invalid parameter !")return false}Log.d(TAG, "getMinBufferSize = $mMinBufferSize bytes !")mAudioRecord = AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mMinBufferSize)if (mAudioRecord!!.state == AudioRecord.STATE_UNINITIALIZED) {Log.e(TAG, "AudioRecord initialize fail !")return false}mAudioRecord!!.startRecording()mIsLoopExit = falsemCaptureThread = Thread(AudioCaptureRunnable())mCaptureThread!!.start()isCaptureStarted = trueLog.d(TAG, "Start audio capture success !")return true}/*** 停止采集,释放资源*/fun stopCapture() {if (!isCaptureStarted) {return}mIsLoopExit = truetry {mCaptureThread!!.interrupt()mCaptureThread!!.join(1000)} catch (e: InterruptedException) {e.printStackTrace()}if (mAudioRecord!!.recordingState == AudioRecord.RECORDSTATE_RECORDING) {mAudioRecord!!.stop()}mAudioRecord!!.release()isCaptureStarted = falsemAudioFrameCapturedListener = nullLog.d(TAG, "Stop audio capture success !")}/*** 定义采集线程*/private inner class AudioCaptureRunnable : Runnable {override fun run() {while (!mIsLoopExit) {val buffer = ByteArray(mMinBufferSize)val ret = mAudioRecord!!.read(buffer, 0, mMinBufferSize)if (ret == AudioRecord.ERROR_INVALID_OPERATION) {Log.e(TAG, "Error ERROR_INVALID_OPERATION")} else if (ret == AudioRecord.ERROR_BAD_VALUE) {Log.e(TAG, "Error ERROR_BAD_VALUE")} else {if (mAudioFrameCapturedListener != null) {mAudioFrameCapturedListener!!.onAudioFrameCaptured(buffer)}Log.d(TAG, "OK, Captured $ret bytes !")}}}}/*** 伴生对象:用来定义初始化的一些配置参数*/companion object {private const val TAG = "AudioCapturer"//设置audioSource音频采集的输入源(可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),VOICE_COMMUNICATION(用于VoIP应用))private const val DEFAULT_SOURCE = MediaRecorder.AudioSource.MIC//设置sampleRateInHz采样率(注意,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。)private const val DEFAULT_SAMPLE_RATE = 44100//设置channelConfig通道数,可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道)private const val DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO//设置audioFormat数据位宽,可选的值也是以常量的形式定义在 AudioFormat 类中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保证兼容所有Android手机的。private const val DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT//注意还有第五个最重要参数bufferSizeInBytes,它配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小,一帧音频帧的大小计算如下//int size = 采样率 x 位宽 x 采样时间 x 通道数(由于厂商的定制化,强烈建议通过AudioRecord类的getMinBufferSize方法确定bufferSizeInBytes的大小,getMinBufferSize方法:int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);)}
}

使用前要注意,添加如下权限:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

6:总结

音频开发的知识点还是很多的,学习音频开发需要大家有足够的耐心,一步一个脚印的积累,只有这样才能把音频开发学好。如果你对基础知识比较模糊,建议先看我的上一篇博客《Android 音频开发(一) 基础入门篇》。下面推荐几个比较好的博主,希望对大家有所帮助。

  1. csdn博主:《雷神雷霄骅》
  2. 51CTO博客:《Jhuster的专栏》

Android 音频开发(二) 采集一帧音频数据相关推荐

  1. Android音频开发(2):如何采集一帧音频

    本文重点关注如何在Android平台上采集一帧音频数据.阅读本文之前,建议先读一下我的上一篇文章<Android音频开发(1):基础知识>,因为音频开发过程中,经常要涉及到这些基础知识,掌 ...

  2. Android音频开发(1):音频相关知识

    Android 音频开发 目录 Android音频开发(1):音频相关知识 Android音频开发(2):使用AudioRecord录制pcm格式音频 Android音频开发(3):使用AudioRe ...

  3. 乐鑫Esp32学习之旅 23 安信可 esp32-a1s 音频开发板移植最新 esp-adf 音频框架,小试牛刀如何实现在线文字转语音播放。

    本系列博客学习由非官方人员 半颗心脏 潜心所力所写,仅仅做个人技术交流分享,不做任何商业用途.如有不对之处,请留言,本人及时更改. 1. 爬坑学习新旅程,虚拟机搭建esp32开发环境,打印 " ...

  4. 【Android游戏开发二十七】讲解游戏开发与项目下的hdpi 、mdpi与ldpi资源文件夹以及游戏高清版本的设置...

    今天一个开发者问到我为什么游戏开发要删除项目下的hdpi.mdpi和ldpi文件夹:下面详细给大家解答一下: 首先童鞋们如果看过我写的<[Android游戏开发二十一]Android os设备谎 ...

  5. Android画板开发(二) 橡皮擦实现

    Android画板开发(一) 基本画笔的实现 Android画板开发(二) 橡皮擦实现 Android画板开发(三) 撤销反撤销功能实现 Android画板开发(四) 添加背景和保存画板内容为图片 A ...

  6. Android 蓝牙开发(二) --手机与蓝牙音箱配对,并播放音频

    Android 蓝牙开发(一) – 传统蓝牙聊天室 Android 蓝牙开发(三) – 低功耗蓝牙开发 项目工程BluetoothDemo 上一章中,我们已经学习了传统蓝牙的开发,这一章,我们来学习如 ...

  7. 【Android应用开发技术:媒体开发】音频

    作者:郭孝星 微博:郭孝星的新浪微博 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells Github:https://github.co ...

  8. Android 蓝牙开发(七)hfp音频连接

    转载请注明出处:http://blog.csdn.net/vnanyesheshou/article/details/71374935 本文已授权微信公众号 fanfan程序媛 独家发布 扫一扫文章底 ...

  9. Android App开发动画特效中帧动画和电影淡入淡出动画的讲解及实战(附源码和演示视频 简单易懂)

    需要图片集和源码请点赞关注收藏后评论区留言~~~ 一.帧动画 Android的动画分为三类,帧动画,补间动画和属性动画.其中帧动画是实现原理最简单的一种,跟现实生活中的电影胶卷类似,都是在短时间内连续 ...

最新文章

  1. POJ 3376 Finding Palindromes(扩展kmp+trie)
  2. python简单编程例子-Python入门 —— 用pycharm写一个简单的小程序3
  3. 全排列两种实现方式(java)—poj2718
  4. 经典论文复现 | 基于深度学习的图像超分辨率重建
  5. oracle之单行函数之多表查询值之课后练习
  6. 【转】C# 网络连接中异常断线的处理:ReceiveTimeout, SendTimeout 及 KeepAliveValues(设置心跳)
  7. Python编程从入门到实践~操作列表
  8. SO_REUSEADDR SO_REUSEPORT 解析
  9. 用DevExpress做界面开发:ASP.NET界面开发框架
  10. JDBC的数据库的基础事务管理
  11. G1手机上的VOIP之旅 - SIP Server + SipDroid
  12. 书生浏览器不能打开这个文件或者url_这些浏览器工作原理你都吃透了吗?
  13. Vue导出Excel表格信息
  14. 华为中兴FPGA面试题总结
  15. android真机调试工具,ADB 安卓真机调试工具
  16. Java8中list转map方法
  17. 数据仓库模型设计与工具
  18. debian8.4安装sqliteman总结
  19. 电子标签有哪些封装方式
  20. java 斜率求角度_计算两条线之间的角度而不必计算斜率? (Java)

热门文章

  1. Windows 10下 jupyter notebook 安装,打开,使用,关闭方法
  2. 《Cortex-M0权威指南》之体系结构---存储器系统
  3. 新东方预计6个月亏损超8亿美元
  4. 菜鸟:春节保障300城照常收货 3亿补贴直接发给一线员工
  5. 彻底凉凉!两头部网红女主播账号被封,逃税被罚近亿元 还被曝不给员工交社保...
  6. 神秘操作系统Ocean惊艳曝光引众说纷纭 UI同质化局面或被打破
  7. 工信部:主要互联网企业开屏信息“关不掉”基本解决
  8. 试驾Marvel R :传统车企认真起来,就没新势力什么事了
  9. 华为发布MetaAAU 能耗降低30% 性能节能双提升
  10. iPhone 13系列要上全新配色:全系存储容量调整