音頻開發之錄製播放pcm文件

前幾篇的文章都是camera下采集視頻數據進行顯示,保存下來的文件也是h264格式的,並無包含音頻數據,因此多多少少有點單調的感受。沒有聲音的視頻是沒有靈魂的,因此最近了解了一下音頻相關的開發,給視頻注入靈魂。java

注入靈魂

1. 基礎知識

開始音頻學習以前,有必要先了解一下基礎知識,由於在音頻開發過程當中,常常會涉及到這些。掌握了這些重要的概念,在學習中不少參數的配置會更容易理解。android

  1. PCM編碼格式ide

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

  2. 音頻採集輸入源學習

    這個就至關於聲明孩子他媽是誰,也就是說聲音的源頭在哪兒。可選的類型以常量的形式定義在 MediaRecorder.AudioSource 類中 ,比較經常使用的是下面幾個this

    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。spa

  4. 通道數線程

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

  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();
    }
}
複製代碼
相關文章
相關標籤/搜索