Android音頻開發(2):如何采集一幀音頻

bnkx0378 8年前發布 | 16K 次閱讀 安卓開發 Android開發 移動開發

本文重點關注如何在Android平臺上采集一幀音頻數據。閱讀本文之前,建議先讀一下我的上一篇文章 《Android音頻開發(1):基礎知識》 ,因為音頻開發過程中,經常要涉及到這些基礎知識,掌握了這些重要的概念后,開發過程中的很多參數和流程就會更加容易理解。

Android SDK 提供了兩套音頻采集的API,分別是:MediaRecorder 和 AudioRecord,前者是一個更加上層一點的API,它可以直接把手機麥克風錄入的音頻數據進行編碼壓縮(如AMR、MP3等)并存成文件,而后者則更接近底層,能夠更加自由靈活地控制,可以得到原始的一幀幀PCM音頻數據。

如果想簡單地做一個錄音機,錄制成音頻文件,則推薦使用 MediaRecorder,而如果需要對音頻做進一步的算法處理、或者采用第三方的編碼庫進行壓縮、以及網絡傳輸等應用,則建議使用 AudioRecord,其實 MediaRecorder 底層也是調用了 AudioRecord 與 Android Framework 層的 AudioFlinger 進行交互的。

音頻的開發,更廣泛地應用不僅僅局限于本地錄音,因此,我們需要重點掌握如何利用更加底層的 AudioRecord API 來采集音頻數據(注意,使用它采集到的音頻數據是原始的PCM格式,想壓縮為mp3,aac等格式的話,還需要專門調用編碼器進行編碼)。

1. AudioRecord 的工作流程

首先,我們了解一下 AudioRecord 的工作流程:

(1) 配置參數,初始化內部的音頻緩沖區

(2) 開始采集

(3) 需要一個線程,不斷地從 AudioRecord 的緩沖區將音頻數據“讀”出來,注意,這個過程一定要及時,否則就會出現“overrun”的錯誤,該錯誤在音頻開發中比較常見,意味著應用層沒有及時地“取走”音頻數據,導致內部的音頻緩沖區溢出。

(4) 停止采集,釋放資源

2. AudioRecord 的參數配置

上面是 AudioRecord 的構造函數,我們可以發現,它主要是靠構造函數來配置采集參數的,下面我們來一一解釋這些參數的含義(建議對照著我的上一篇文章來理解):

(1) audioSource

該參數指的是音頻采集的輸入源,可選的值以常量的形式定義在 MediaRecorder.AudioSource 類中,常用的值包括:DEFAULT(默認),VOICE_RECOGNITION(用于語音識別,等同于DEFAULT),MIC(由手機麥克風輸入),VOICE_COMMUNICATION(用于VoIP應用)等等。

(2) 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 size = 采樣率 x 位寬 x 采樣時間 x 通道數

采樣時間一般取 2.5ms~120ms 之間,由廠商或者具體的應用決定,我們其實可以推斷,每一幀的采樣時間取得越短,產生的延時就應該會越小,當然,碎片化的數據也就會越多。

在Android開發中,AudioRecord 類提供了一個幫助你確定這個 bufferSizeInBytes 的函數,原型如下:

int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);

不同的廠商的底層實現是不一樣的,但無外乎就是根據上面的計算公式得到一幀的大小,音頻緩沖區的大小則必須是一幀大小的2~N倍,有興趣的朋友可以繼續深入源碼探究探究。

實際開發中,強烈建議由該函數計算出需要傳入的 bufferSizeInBytes,而不是自己手動計算。

3. 音頻的采集線程

當創建好了 AudioRecord 對象之后,就可以開始進行音頻數據的采集了,通過下面兩個函數控制采集的開始/停止:

AudioRecord.startRecording();

AudioRecord.stop();

一旦開始采集,必須通過線程循環盡快取走音頻,否則系統會出現 overrun,調用的讀取數據的接口是:

AudioRecord.read(byte[] audioData, int offsetInBytes, int sizeInBytes);

4. 示例代碼

我將 AudioRecord 類的接口簡單封裝了一下,提供了一個 AudioCapturer 類,可以到我的Github下載: https://github.com/Jhuster/Android/blob/master/Audio/AudioCapturer.java

這里也貼出來一份:

/*
 *  COPYRIGHT NOTICE  
 *  Copyright (C) 2016, Jhuster <lujun.hust@gmail.com>
 *  https://github.com/Jhuster/Android
 *   
 *  @license under the Apache License, Version 2.0 
 *
 *  @file    AudioCapturer.java
 *  
 *  @version 1.0     
 *  @author  Jhuster
 *  @date    2016/03/10    
 */
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;

public class AudioCapturer {

    private static final String TAG = "AudioCapturer";

    private static final int DEFAULT_SOURCE = MediaRecorder.AudioSource.MIC;
    private static final int DEFAULT_SAMPLE_RATE = 44100;
    private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    private static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;

    private AudioRecord mAudioRecord;
    private int mMinBufferSize = 0;

    private Thread mCaptureThread;  
    private boolean mIsCaptureStarted = false;
    private volatile boolean mIsLoopExit = false;

    private OnAudioFrameCapturedListener mAudioFrameCapturedListener;

    public interface OnAudioFrameCapturedListener {
        public void onAudioFrameCaptured(byte[] audioData);
    }   

    public boolean isCaptureStarted() {     
        return mIsCaptureStarted;
    }

    public void setOnAudioFrameCapturedListener(OnAudioFrameCapturedListener listener) {
        mAudioFrameCapturedListener = listener;
    }

    public boolean startCapture() {
        return startCapture(DEFAULT_SOURCE, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_CONFIG,
            DEFAULT_AUDIO_FORMAT);
    }

    public boolean startCapture(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {

        if (mIsCaptureStarted) {
            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 = new AudioRecord(audioSource,sampleRateInHz,channelConfig,audioFormat,mMinBufferSize);                
        if (mAudioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
            Log.e(TAG, "AudioRecord initialize fail !");
        return false;
        }       

        mAudioRecord.startRecording();

        mIsLoopExit = false;
        mCaptureThread = new Thread(new AudioCaptureRunnable());
        mCaptureThread.start();

        mIsCaptureStarted = true;

        Log.d(TAG, "Start audio capture success !");

        return true;
    }

    public void stopCapture() {

        if (!mIsCaptureStarted) {
            return;
        }

        mIsLoopExit = false;        
        try {
            mCaptureThread.interrupt();
            mCaptureThread.join(1000);
        } 
        catch (InterruptedException e) {        
            e.printStackTrace();
        }

        if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
            mAudioRecord.stop();                        
        }

        mAudioRecord.release();     

        mIsCaptureStarted = false;
        mAudioFrameCapturedListener = null;

        Log.d(TAG, "Stop audio capture success !");
    }

    private class AudioCaptureRunnable implements Runnable {        

        @Override
        public void run() {

            while (!mIsLoopExit) {

                byte[] buffer = new byte[mMinBufferSize];

                int 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 (ret == AudioRecord.ERROR_INVALID_OPERATION) {
                    Log.e(TAG , "Error ERROR_INVALID_OPERATION");
                }
                else { 
                    if (mAudioFrameCapturedListener != null) {
                        mAudioFrameCapturedListener.onAudioFrameCaptured(buffer);
                    }   
                    Log.d(TAG , "OK, Captured "+ret+" bytes !");
                }                                                       
            }       
        }    
    }
}

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

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

5. 小結

音頻開發的知識點其實挺多的,一篇文章也無法詳細地展開敘述,因此,不夠全面和詳盡的地方,請大家搜索專業的資料進行深入了解。文章中有不清楚的地方歡迎留言或者來信 lujun.hust@gmail.com 交流,或者關注我的新浪微博@盧_俊 或者 微信公眾號 @Jhuster 獲取最新的文章和資訊。

來自: http://ticktick.blog.51cto.com/823160/1749719

 本文由用戶 bnkx0378 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!