Android 音频开发(二) 采集一帧音频数据

这一节主要介绍如何采集一帧音频数据,如果你对音频的基础概念比较陌生,建议看我的上一篇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_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 的工作流程

  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()函数控制采集。
 /**
     * 开始采集
     */
    @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
    }
  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 = 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 音频开发(一) 基础入门篇》。下面推荐几个比较好的博主,希望对大家有所帮助。

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

猜你喜欢

转载自blog.csdn.net/ljx1400052550/article/details/114186501