音訊開發之錄製播放pcm檔案

靚仔凌霄發表於2019-01-18

前幾篇的文章都是camera下采集視訊資料進行顯示,儲存下來的檔案也是h264格式的,並沒有包含音訊資料,所以多多少少有點單調的感覺。沒有聲音的視訊是沒有靈魂的,所以最近了解了一下音訊相關的開發,給視訊注入靈魂。

注入靈魂

1. 基礎知識

開始音訊學習之前,有必要先了解一下基礎知識,因為在音訊開發過程中,經常會涉及到這些。掌握了這些重要的概念,在學習中很多引數的配置會更容易理解。

  1. PCM編碼格式

    首先看看百度百科給出的解釋:PCM 脈衝編碼調製是Pulse Code Modulation的縮寫。脈衝編碼調製是數字通訊的編碼方式之一。主要過程是將話音、影象等模擬訊號每隔一定時間進行取樣,使其離散化,同時將抽樣值按分層單位四捨五入取整量化,同時將抽樣值按一組二進位制碼來表示抽樣脈衝的幅值。 這個解釋得非常抽象,反正我是沒看懂⊙﹏⊙。簡單的來說就是將聲音數字化,轉換為二進位制序列,這樣就可以把聲音儲存下來了,而儲存它的容器可以是mp3,wav等等容器。

  2. 音訊採集輸入源

    這個就相當於宣告孩子他媽是誰,也就是說聲音的源頭在哪兒。可選的型別以常量的形式定義在 MediaRecorder.AudioSource 類中 ,比較常用的是下面幾個

    1. MediaRecorder.AudioSource.CAMCORDER 設定錄音來源於同方向的相機麥克風相同,若相機無內建相機或無法識別,則使用預設的麥克風
    2. MediaRecorder.AudioSource.DEFAULT  預設音訊源
    3. MediaRecorder.AudioSource.MIC 設定錄音來源為主麥克風
    4. MediaRecorder.AudioSource.VOICE_CALL設定錄音來源為語音撥出的語音與對方說話的聲音
    5. MediaRecorder.AudioSource.VOICE_COMMUNICATION 攝像頭旁邊的麥克風
    6. MediaRecorder.AudioSource.VOICE_RECOGNITION 語音識別
  3. 取樣率

    我們把取樣到的一個個靜止畫面再以取樣率同樣的速度回放時,看到的就是連續的畫面。同樣的道理,把以44.1kHZ取樣率記錄的CD以同樣的速率播放時,就能聽到連續的聲音。顯然,這個取樣率越高,聽到的聲音和看到的影象就越連貫。當然,人的聽覺和視覺器官能分辨的取樣率是有限的,基本上高於44.1kHZ取樣的聲音,絕大部分人已經覺察不到其中的分別了。 而目前44100Hz是唯一可以保證相容所有Android手機的取樣率 。所以,如果不是特殊裝置和用途,這個值建議設定為44100。

  4. 通道數

    聲道數一般表示聲音錄製時的音源數量或回放時相應的揚聲器數量。常用的有:單通道和雙通道。 可選的值以常量的形式定義在 AudioFormat 類中,常用的是 CHANNEL_IN_MONO(單通道),CHANNEL_IN_STEREO(雙通道)

  5. 量化精度

    1. 聲音的位數就相當於畫面的顏色數,表示每個取樣的資料量,當然資料量越大,回放的聲音越準確,不至於把開水壺的叫聲和火車的鳴笛混淆。同樣的道理,對於畫面來說就是更清晰和準確,不至於把血和西紅柿醬混淆。不過受人的器官的機能限制,16位的聲音和24位的畫面基本已經是普通人類的極限了,電話就是3kHZ取樣的7位聲音,而CD是44.1kHZ取樣的16位聲音,所以CD就比電話更清楚。
    2. 對於一個取樣點,需要用二進位制數字來表示,這個二進位制的精度可以是:4bit、8bit、16bit、32bit。 位數越多,表示的聲音就越精細,聲音的質量就越好。不過資料量也會變大。
  6. 幀間隔

    音訊不像視訊那樣,有一幀一幀的概念。它是約定一個時間為單位,然後這個時間內的資料為一幀,這個時間被稱為取樣時間。這個時間沒有特別的標準,要看具體的編解碼器。

2. 音訊錄製

瞭解音訊開發相關基礎知識之後,我們就可以開始使用android提供的相關API實現音訊的錄製了,android提供了兩套音訊錄製的API:

  1. MediaRecorder:比較上層的 API,它可以直接把手機麥克風的音訊資料進行編碼然後儲存成檔案。使用簡單,但是支援的格式有限,並且不支援對音訊進行進一步的處理,例如變聲、混音等。
  2. AudioRecord:比較底層的一個 API,能夠得到原始的 PCM音訊資料。由於我們得到的是原始的 PCM 資料,我們可以對音訊進行進一步的處理,例如編碼、混音和變聲等。

這裡主要介紹的是使用AudioRecord進行錄製,MediaRecorder的錄製比較簡單,就不做過多介紹了。首先看看AudioRecord的建構函式:

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
複製程式碼

需要傳入5個引數,分別是輸入源,取樣率,通道數,量化精度,音訊緩衝區的大小 。其中最後一個也是最重要的一個引數,它代表音訊緩衝區的大小,該緩衝區的值不能低於一幀音訊幀的大小,

一幀音訊幀大小 = 取樣率 x 位寬 x 取樣時間 x 通道數 ,這個值不用我們自己計算,AudioRecord 類提供了一個幫助你確定這個值的函式 :

public static int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
複製程式碼

當建立好AudioRecord物件之後,就可以開始進行音訊資料的採集了,控制採集的開始和停止的方法是下面這兩個函式:

public void startRecording()
public void stop()
複製程式碼

開始採集之後,通過執行緒迴圈取走音訊資料:

public int read(byte[] audioData, int offsetInBytes, int sizeInBytes)
複製程式碼

最終錄製的程式碼如下,注意該方法需要在子執行緒中執行:

private File mAudioFile;
private FileOutputStream mAudioFileOutput;
private boolean isRecording = false;
private int sampleRate = 44100;//所有android系統都支援  取樣率
//單聲道輸入
private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM_16是所有android系統都支援的  16位的聲音就是人類能聽到的極限了,再高就聽不見了 位數越高聲音越清晰
private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int recordBufSize = 0; // 宣告recoordBufffer的大小欄位
private AudioRecord audioRecord = null; 
private boolean startAudioRecord(String fileName) {
        isRecording = true;
        mAudioFile = new File(mPath+fileName+System.currentTimeMillis()+".pcm");
        if (!mAudioFile.getParentFile().exists()){
            mAudioFile.getParentFile().mkdirs();
        }
        try {
            mAudioFile.createNewFile();
            //建立檔案輸出流
            mAudioFileOutput = new FileOutputStream(mAudioFile);
            //計算audioRecord能接受的最小的buffer大小
            recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
                    channelConfig,
                    autioFormat);
            Log.e(TAG, "最小的buffer大小: " + recordBufSize);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    sampleRate,
                    channelConfig,
                    autioFormat, recordBufSize);
            //初始化一個buffer 用於從AudioRecord中讀取聲音資料到檔案流
            byte data[] = new byte[recordBufSize];
            //開始錄音
            audioRecord.startRecording();

            while (isRecording){
                //只要還在錄音就一直讀取
                int read = audioRecord.read(data,0,recordBufSize);
                if (read > 0){
                    mAudioFileOutput.write(data,0,read);
                }
            }
            //stopRecorder();
        } catch (IOException e) {
            e.printStackTrace();
            stopAudioRecord();
            return false;
        }
        return true;
    }

public boolean stopAudioRecord(){
        isRecording = false;
        if (audioRecord != null){
            audioRecord.stop();
            audioRecord.release();
            audioRecord = null;
        }
        try {
            mAudioFileOutput.flush();
            mAudioFileOutput.close();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
複製程式碼

3. 音訊播放

播放pcm格式的音訊和錄製不同的地方有三個:

一個是建立AudioTrack物件,和AudioRecord的構造方法不同,第一個引數是音樂型別,AudioManager.STREAM_MUSIC表示用揚聲器播放,最後多出的一個引數是播放模式,一般使用AudioTrack.MODE_STREAM,適用於大多數的場景,將audio buffers從java層傳遞到native層即返回。 如果audio buffers佔用記憶體多,應該使用MODE_STREAM。 比如播放時間很長的聲音檔案, 比如音訊檔案使用高取樣率等:

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)
複製程式碼

另外兩個是把錄製的startRecording()和read()方法換成下面兩個:

public void play()
public int write(byte[] audioData, int offsetInBytes, int sizeInBytes)
複製程式碼

最終播放pcm的程式碼如下:

private FileInputStream mAudioPlayInputStream;
public void doPlay(File audioFile) {
    if (audioFile.isDirectory() || !audioFile.exists()) return;
            mIsPlaying = true;
            //配置播放器
            //音樂型別,揚聲器播放
            int streamType= AudioManager.STREAM_MUSIC;
            //錄音時採用的取樣頻率,所以播放時同樣的取樣頻率
            int sampleRate=44100;
            //單聲道,和錄音時設定的一樣
            int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
            //錄音時使用16bit,所以播放時同樣採用該方式
            int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
            //流模式
            int mode= AudioTrack.MODE_STREAM;

            //計算最小buffer大小
            int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);

            byte data[] = new byte[minBufferSize];
            //構造AudioTrack  不能小於AudioTrack的最低要求,也不能小於我們每次讀的大小
            mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
                    Math.max(minBufferSize,data.length),mode);

            //從檔案流讀資料
            try{
                //迴圈讀資料,寫到播放器去播放
                mAudioPlayInputStream = new FileInputStream(audioFile);

                //迴圈讀資料,寫到播放器去播放
                int read;
                //只要沒讀完,迴圈播放
                mAudioTrack.play();
                while (mIsPlaying){
                    int ret = 0;
                    read = mAudioPlayInputStream.read(data);
                    if (read > 0){
                        ret = mAudioTrack.write(data,0,read);
                    }
                    //mAudioFileOutput.write(data,0,read);
                    //檢查write的返回值,處理錯誤
                    switch (ret){
                        case AudioTrack.ERROR_INVALID_OPERATION:
                        case AudioTrack.ERROR_BAD_VALUE:
                        case AudioManager.ERROR_DEAD_OBJECT:
                            Log.d(TAG, "doPlay: 失敗,錯誤碼:"+ret);
                            return;
                        default:
                            break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                //讀取失敗
                Log.d(TAG, "doPlay: 失敗");
            }finally {
                stopPlay();
                Log.d(TAG, "結束播放");
            }
    }
public void stopPlay() {
        mIsPlaying = false;
        //播放器釋放
        if (mAudioTrack != null) {
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
        }
        //關閉檔案輸入流
        if (mAudioPlayInputStream != null) {
            try {
                mAudioPlayInputStream.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
複製程式碼

最後貼上所有的程式碼:

public class AudioUtil {
    private static final String TAG = "AudioUtil";
    private AudioRecord audioRecord = null;  // 宣告 AudioRecord 物件
    private int recordBufSize = 0; // 宣告recoordBufffer的大小欄位

    //所有android系統都支援  取樣率:取樣率越高,聽到的聲音和看到的影象就越連貫
    // 基本上高於44.1kHZ取樣的聲音,絕大部分人已經覺察不到其中的分別了
    private int sampleRate = 44100;
    //單聲道輸入
    private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
    //PCM_16是所有android系統都支援的  16位的聲音就是人類能聽到的極限了,再高就聽不見了 位數越高聲音越清晰
    private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
    private long mStartTimeStamp;
    private File mAudioFile;
    private String mPath = ContentValue.MAIN_PATH + "/AudioSimple/";
    private FileOutputStream mAudioFileOutput; //儲存錄音檔案
    private FileInputStream mAudioPlayInputStream; //播放錄音檔案
    private boolean isRecording = false;

    private static AudioUtil audioUtil;
    private boolean mIsPlaying;
    private AudioTrack mAudioTrack;
    private String mRecordFileName; //錄音儲存的檔案路徑
    private String mPlayFileName; //播放錄音的檔案路徑

    private Runnable mAudioRunnableTask = new Runnable() {
        @Override
        public void run() {
            boolean result = startAudioRecord(mRecordFileName);
            if (result){
                Log.e(TAG, "錄音結束");
            }else {
                Log.e(TAG, "錄音失敗");
            }
        }
    };

    private Runnable mAudioPlayRunnableTask = new Runnable() {
        @Override
        public void run() {
            File file = new File(mPlayFileName);
            doPlay(file);
        }
    };

    private AudioUtil(){}
    public static AudioUtil getInstance(){
        if (audioUtil == null){
            synchronized (AudioUtil.class){
                if (audioUtil == null){
                    audioUtil = new AudioUtil();
                }
            }
        }
        return audioUtil;
    }

    private AudioEncoder mAudioEncoder;
    private boolean startAudioRecord(String fileName) {
        isRecording = true;
        mStartTimeStamp = System.currentTimeMillis();
        mAudioFile = new File(mPath+fileName+mStartTimeStamp+".pcm");
        if (!mAudioFile.getParentFile().exists()){
            mAudioFile.getParentFile().mkdirs();
        }
        try {
            mAudioFile.createNewFile();
            //建立檔案輸出流
            mAudioFileOutput = new FileOutputStream(mAudioFile);

            //計算audioRecord能接受的最小的buffer大小
            recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
                    channelConfig,
                    autioFormat);
            Log.e(TAG, "最小的buffer大小: " + recordBufSize);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    sampleRate,
                    channelConfig,
                    autioFormat, recordBufSize);
            //初始化一個buffer 用於從AudioRecord中讀取聲音資料到檔案流
            byte data[] = new byte[recordBufSize];
            //開始錄音
            audioRecord.startRecording();

            while (isRecording){
                //只要還在錄音就一直讀取
                int read = audioRecord.read(data,0,recordBufSize);
                if (read > 0){
                    mAudioFileOutput.write(data,0,read);

                }
            }
            //stopRecorder();
        } catch (IOException e) {
            e.printStackTrace();
            stopAudioRecord();
            return false;
        }
        return true;
    }

    public void doPlay(File audioFile) {
        if(audioFile !=null){
            mIsPlaying = true;
            //配置播放器
            //音樂型別,揚聲器播放
            int streamType= AudioManager.STREAM_MUSIC;
            //錄音時採用的取樣頻率,所以播放時同樣的取樣頻率
            int sampleRate=44100;
            //單聲道,和錄音時設定的一樣
            int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
            //錄音時使用16bit,所以播放時同樣採用該方式
            int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
            //流模式
            int mode= AudioTrack.MODE_STREAM;

            //計算最小buffer大小
            int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);

            byte data[] = new byte[minBufferSize];
            //構造AudioTrack  不能小於AudioTrack的最低要求,也不能小於我們每次讀的大小
            mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
                    Math.max(minBufferSize,data.length),mode);

            //從檔案流讀資料
            try{
                //迴圈讀資料,寫到播放器去播放
                mAudioPlayInputStream = new FileInputStream(audioFile);

                //迴圈讀資料,寫到播放器去播放
                int read;
                //只要沒讀完,迴圈播放
                mAudioTrack.play();
                while (mIsPlaying){
                    int ret = 0;
                    read = mAudioPlayInputStream.read(data);
                    if (read > 0){
                        ret = mAudioTrack.write(data,0,read);
                    }
                    //mAudioFileOutput.write(data,0,read);
                    //檢查write的返回值,處理錯誤
                    switch (ret){
                        case AudioTrack.ERROR_INVALID_OPERATION:
                        case AudioTrack.ERROR_BAD_VALUE:
                        case AudioManager.ERROR_DEAD_OBJECT:
                            Log.d(TAG, "doPlay: 失敗,錯誤碼:"+ret);
                            return;
                        default:
                            break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                //讀取失敗
                Log.d(TAG, "doPlay: 失敗");
            }finally {
                stopPlay();
                Log.d(TAG, "結束播放");
            }
        }
    }

    public void startRecord(String fileName){
        this.mRecordFileName = fileName;
        new Thread(mAudioRunnableTask).start();
    }

    public void startPlay(String fileName){
        this.mPlayFileName = fileName;
        new Thread(mAudioPlayRunnableTask).start();
    }
    public boolean stopAudioRecord(){
        isRecording = false;
        if (audioRecord != null){
            audioRecord.stop();
            audioRecord.release();
            audioRecord = null;
        }
        if (mAudioEncoder != null){
            mAudioEncoder.stopEncodeAac();
        }
        try {
            mAudioFileOutput.flush();
            mAudioFileOutput.close();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    public boolean isRecording(){
        return isRecording;
    }
    public boolean isPlaying(){
        return mIsPlaying;
    }
    public void stopPlay(){
        mIsPlaying = false;
        //播放器釋放
        if(mAudioTrack != null){
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
        }
        //關閉檔案輸入流
        if(mAudioPlayInputStream !=null){
            try {
                mAudioPlayInputStream.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    private AudioListener mAudioListener;
    public void setAudioListener(AudioListener listener){
        this.mAudioListener = listener;
    }
    public interface AudioListener{
        void onRecordFinish();
        void onPlayFinish();
    }
}
複製程式碼

相關文章