Android 音频开发(二) 采集一帧音频数据
作者:互联网
这一节主要介绍如何采集一帧音频数据,如果你对音频的基础概念比较陌生,建议看我的上一篇Android 音频开发(一) 基础入门篇。因为音频开发过程中,经常要涉及到这些基础知识,掌握了这些重要的基础知识后,开发过程中的很多参数和流程就会更加容易理解。
1:Android SDK 常用的2种音频采集API
Android SDK 提供了两套音频采集的API,分别如下:
-
MediaRecorder
MediaRecorder是更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件
-
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);
}
看构造方法你会发现有五个重要参数,它主要是靠构造函数来配置采集参数的,下面我们来一一解释这些参数的含义:
-
audioSource
audioSource是音频采集的输入源,可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),VOICE_COMMUNICATION(用于VoIP应用)等等。
-
sampleRateInHz
sampleRateInHz表示采样率,注意,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。
-
channelConfig
通道数的配置,可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道)
-
audioFormat
这个参数是用来配置“数据位宽”的,可选的值也是以常量的形式定义在 AudioFormat 类中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保证兼容所有Android手机的。
-
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_DEFAULT
case 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 的工作流程
-
配置参数,初始化内部的音频缓冲区
配置初始化参数,初始化参数大概有五个,具体的参数和说明见下面代码:
/**
* 伴生对象:用来定义初始化的一些配置参数
*/
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);)
}
- 开始采集
当配置好了初始化参数后,就可以通过构造函数创建好AudioRecord,创建好AudioRecord象之后,就可以开始进行音频数据的采集,通过AudioRecord.startRecording()函数控制采集。
/**
* 开始采集
*/
@JvmOverloads
fun 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 = false
mCaptureThread = Thread(AudioCaptureRunnable())
mCaptureThread!!.start()
isCaptureStarted = true
Log.d(TAG, "Start audio capture success !")
return true
}
-
开启线程,实时读取音频缓冲区
在读取缓冲区的时候我们会遇到过这样的问题,就是一直报**“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 !")
}
}
}
}
- 停止采集,释放资源
因为读取是用到了io流的技术,老生常谈的问题就是在停止采集的时候要关闭流,及时的释放资源。
/**
* 停止采集,释放资源
*/
fun stopCapture() {
if (!isCaptureStarted) {
return
}
mIsLoopExit = true
try {
mCaptureThread!!.interrupt()
mCaptureThread!!.join(1000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
if (mAudioRecord!!.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
mAudioRecord!!.stop()
}
mAudioRecord!!.release()
isCaptureStarted = false
mAudioFrameCapturedListener = null
Log.d(TAG, "Stop audio capture success !")
}
下面列出简单的完整封装列子如下:
5:完整实例代码
package com.bnd.myaudioandvideo.utils
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.util.Log
/***
* AudioRecord简单封装
*/
class AudioCapturer {
private var mAudioRecord: AudioRecord? = null
private var mMinBufferSize = 0
private var mCaptureThread: Thread? = null
var isCaptureStarted = false
private set
@Volatile
private var mIsLoopExit = false
private var mAudioFrameCapturedListener: OnAudioFrameCapturedListener? = null
interface OnAudioFrameCapturedListener {
fun onAudioFrameCaptured(audioData: ByteArray?)
}
fun setOnAudioFrameCapturedListener(listener: OnAudioFrameCapturedListener?) {
mAudioFrameCapturedListener = listener
}
/**
* 开始采集
*/
@JvmOverloads
fun 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 = false
mCaptureThread = Thread(AudioCaptureRunnable())
mCaptureThread!!.start()
isCaptureStarted = true
Log.d(TAG, "Start audio capture success !")
return true
}
/**
* 停止采集,释放资源
*/
fun stopCapture() {
if (!isCaptureStarted) {
return
}
mIsLoopExit = true
try {
mCaptureThread!!.interrupt()
mCaptureThread!!.join(1000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
if (mAudioRecord!!.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
mAudioRecord!!.stop()
}
mAudioRecord!!.release()
isCaptureStarted = false
mAudioFrameCapturedListener = null
Log.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 音频开发(一) 基础入门篇》。下面推荐几个比较好的博主,希望对大家有所帮助。
- csdn博主:《雷神雷霄骅》
- 51CTO博客:《Jhuster的专栏》
标签:一帧,int,音频,AudioRecord,DEFAULT,AudioFormat,Android,CHANNEL 来源: https://blog.csdn.net/ljx1400052550/article/details/114186501