1. 程式人生 > >Android 呼叫系統api錄音的兩種方式(MediaRecorder、AudioRecord)

Android 呼叫系統api錄音的兩種方式(MediaRecorder、AudioRecord)

廢話

許可權、許可權、許可權,必須要先獲取了錄音許可權,其他的事情晚點再說。

另外,新版本的Android 10系統會對錄音有調整,引入了一個錄音焦點的概念,也就是說以前的麥克風只能一個APP使用,必須要等它斷開了別人才能用,現在換成可以搶的形式,也就是如果沒有音焦,程式碼有可能不會報錯,但是是錄不進聲音的。

Android系統API提供的錄音方式就兩種:MediaRecorder、AudioRecord

MediaRecorder:簡易模式,呼叫簡單,只有開始、結束,錄音之後的檔案也是指定編碼格式,系統播放器可以直接播放。

AudioRecord:原始模式,可以暫停、繼續,可以實時獲取到錄音錄製的資料,然後進行一些騷操作,然後錄出來的東西是最原始的pcm資料,系統播放器不能直接播放。

MediaRecorder

話不多說,直接上程式碼,具體用法,直接將需要儲存檔案的路徑通過構造方法傳進去,然後呼叫開始和結束方法即可:


import android.media.MediaRecorder;
import android.os.Handler;

import java.io.File;
import java.io.IOException;

/**
 * 錄音功能
 */

public class MediaRecordingUtils {

    //檔案路徑
    private String filePath;

    private MediaRecorder mMediaRecorder;
    private final String TAG = "fan";
    public static final int MAX_LENGTH = 1000 * 60 * 200;// 最大錄音時長,單位毫秒,1000*60*10;

    private OnAudioStatusUpdateListener audioStatusUpdateListener;

    /**
     * 檔案儲存預設sdcard/record
     */
    public MediaRecordingUtils() {
    }

    public MediaRecordingUtils(String filePath) {
        this.filePath=filePath;
//        File path = new File(filePath);
//        if (!path.exists())
//            path.mkdirs();
//        this.FolderPath = filePath;
    }

    private long startTime;
    private long endTime;


    /**
     * 開始錄音 使用aac格式
     * 錄音檔案
     *
     * @return
     */
    public void startRecord() {
        // 開始錄音
        /* ①Initial:例項化MediaRecorder物件 */
        if (mMediaRecorder == null)
            mMediaRecorder = new MediaRecorder();
        try {
            /* ②setAudioSource/setVedioSource */
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 設定麥克風
            /* ②設定音訊檔案的編碼:AAC/AMR_NB/AMR_MB/Default 聲音的(波形)的取樣 */
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
            /*
             * ②設定輸出檔案的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
             * ,H263視訊/ARM音訊編碼)、MPEG-4、RAW_AMR(只支援音訊且音訊編碼要求為AMR_NB)
             */
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

//            filePath = FolderPath + DateUtil.getTimeForLong() + ".aac";
            /* ③準備 */
            mMediaRecorder.setOutputFile(filePath);
            mMediaRecorder.setMaxDuration(MAX_LENGTH);
            mMediaRecorder.prepare();
            /* ④開始 */
            mMediaRecorder.start();
            // AudioRecord audioRecord.
            /* 獲取開始時間* */
            startTime = System.currentTimeMillis();
            updateMicStatus();
            ALog.e("fan", "startTime" + startTime);
        } catch (IllegalStateException e) {
            ALog.e(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
        } catch (IOException e) {
            ALog.e(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
        }
    }

    /**
     * 停止錄音
     */
    public long stopRecord() {
        if (mMediaRecorder == null)
            return 0L;
        endTime = System.currentTimeMillis();

        //有一些網友反應在5.0以上在呼叫stop的時候會報錯,翻閱了一下谷歌文件發現上面確實寫的有可能會報錯的情況,捕獲異常清理一下就行了,感謝大家反饋!
        try {
            mMediaRecorder.stop();
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;

            audioStatusUpdateListener.onStop(filePath);
            filePath = "";

        } catch (RuntimeException e) {
            try {
                mMediaRecorder.reset();
                mMediaRecorder.release();
                mMediaRecorder = null;

                File file = new File(filePath);
                if (file.exists())
                    file.delete();

                filePath = "";
            } catch (Exception e1) {

            }

        }
        return endTime - startTime;
    }

    /**
     * 取消錄音
     */
    public void cancelRecord() {

        try {

            mMediaRecorder.stop();
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;

        } catch (RuntimeException e) {
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;
        }
        File file = new File(filePath);
        if (file.exists())
            file.delete();

        filePath = "";

    }

    private final Handler mHandler = new Handler();
    private Runnable mUpdateMicStatusTimer = new Runnable() {
        public void run() {
            updateMicStatus();
        }
    };


    private int BASE = 1;
    private int SPACE = 100;// 間隔取樣時間

    public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
        this.audioStatusUpdateListener = audioStatusUpdateListener;
    }

    /**
     * 更新麥克狀態
     */
    private void updateMicStatus() {

        if (mMediaRecorder != null) {
            double ratio = (double) mMediaRecorder.getMaxAmplitude() / BASE;
            double db = 0;// 分貝
            if (ratio > 1) {
                db = 20 * Math.log10(ratio);
                if (null != audioStatusUpdateListener) {
                    audioStatusUpdateListener.onUpdate(db, System.currentTimeMillis() - startTime);
                }
            }
            mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
        }
    }

    public String getFilePath() {
        return filePath;
    }

    public interface OnAudioStatusUpdateListener {
        /**
         * 錄音中...
         *
         * @param db   當前聲音分貝
         * @param time 錄音時長
         */
        public void onUpdate(double db, long time);

        /**
         * 停止錄音
         *
         * @param filePath 儲存路徑
         */
        public void onStop(String filePath);
    }

}

AudioRecord


/**
 * 錄音
 * 用法:1-init,filePath檔案的字尾為.pcm 2-start  3-stop
 * stop之後,所有的音訊資料會以pcm的格式寫入到filePath這個檔案內,並且是末尾新增的方式,而非覆蓋(以達到暫停錄音繼續錄音的效果),需要轉換為其他格式才能讓系統播放器直接播放
 */
public class AudioRecordingUtils {


    //指定音訊源 這個和MediaRecorder是相同的 MediaRecorder.AudioSource.MIC指的是麥克風
    private static final int mAudioSource = MediaRecorder.AudioSource.MIC;
    //指定取樣率 (MediaRecoder 的取樣率通常是8000Hz AAC的通常是44100Hz。 設定取樣率為44100,目前為常用的取樣率,官方文件表示這個值可以相容所有的設定)
    private static final int mSampleRateInHz = 44100;
    //指定捕獲音訊的聲道數目。在AudioFormat類中指定用於此的常量
    private static final int mChannelConfig = AudioFormat.CHANNEL_IN_STEREO; //立體聲
    //指定音訊量化位數 ,在AudioFormaat類中指定了以下各種可能的常量。通常我們選擇ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脈衝編碼調製,它實際上是原始音訊樣本。
    //因此可以設定每個樣本的解析度為16位或者8位,16位將佔用更多的空間和處理能力,表示的音訊也更加接近真實。
    private static final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
    //指定緩衝區大小。呼叫AudioRecord類的getMinBufferSize方法可以獲得。


    private AudioRecord audioRecord = null;  // 宣告 AudioRecord 物件
    private int recordBufSize = 0; // 宣告recoordBufffer的大小欄位

    private boolean isRecording = false;

    private String saveFilePath;
    //    private FileOutputStream os = null;
    private File mRecordingFile;

    private OnAudioRecordingListener onAudioRecordingListener;

    public void init(String filePath, OnAudioRecordingListener onAudioRecordingListener) {
        this.onAudioRecordingListener = onAudioRecordingListener;
        saveFilePath = filePath;
        recordBufSize = AudioRecord.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);//計算最小緩衝區
        audioRecord = new AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig,
                mAudioFormat, recordBufSize);//建立AudioRecorder物件

        //建立一個流,存放從AudioRecord讀取的資料
        mRecordingFile = new File(saveFilePath);
        if (mRecordingFile.exists()) {//音訊檔案儲存過了刪除
            mRecordingFile.delete();
        }
        try {
            mRecordingFile.createNewFile();//建立新檔案
        } catch (IOException e) {
            e.printStackTrace();
            ALog.e("lu", "建立儲存音訊檔案出錯");
        }

    }
    public static double bytes2Double(byte[] arr) {
        long value = 0;
        for (int i = 0; i < 8; i++) {
            value |= ((long) (arr[i] & 0xff)) << (8 * i);
        }
        return Double.longBitsToDouble(value);
    }
    public void startRecording() {
        //判斷AudioRecord的狀態是否初始化完畢
        //在AudioRecord物件構造完畢之後,就處於AudioRecord.STATE_INITIALIZED狀態了。
        if (audioRecord == null || audioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
            ALog.e("尚未初始化完成");
            return;
        }

        XyObservable.addTask(new XyCallBack() {//開一個子執行緒的意思
            private double volume = 0;

            @Override
            public void run() {
                //標記為開始採集狀態
                isRecording = true;
                try {
                    //獲取到檔案的資料流
                    DataOutputStream mDataOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mRecordingFile, true)));
                    byte[] buffer = new byte[recordBufSize];
                    audioRecord.startRecording();//開始錄音
                    //getRecordingState獲取當前AudioReroding是否正在採集資料的狀態
                    while (isRecording && audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                        int bufferReadResult = audioRecord.read(buffer, 0, recordBufSize);
                        for (int i = 0; i < bufferReadResult; i++) {
                            mDataOutputStream.write(buffer[i]);
                        }
                        setFinish();//這裡會調到下面的finish()方法,finish()方法處於UI執行緒中
                    }
                    mDataOutputStream.close();
                } catch (Throwable t) {
                    ALog.e("lu", "Recording Failed");
                    stopRecording();
                }
            }

            @Override
            public void finish() {
                if (onAudioRecordingListener != null) {
                    onAudioRecordingListener.onChange(volume);
                }
            }
        });

    }

    /**
     * 暫停錄音
     */
    public void pauseRecording() {
        isRecording = false;
        if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
            audioRecord.stop();
        }
    }

    //停止錄音
    public void stopRecording() {
        isRecording = false;
        //停止錄音,回收AudioRecord物件,釋放記憶體
        if (audioRecord != null) {
            if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                audioRecord.stop();
            }
            if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
                audioRecord.release();
            }
        }
    }

    public interface OnAudioRecordingListener {
        public void onChange(double volume);
    }
}

然後再附帶一個將原始pcm轉換為wav格式的方法:


public class Pcm2WavUtils {

    /**
     * PCM檔案轉WAV檔案
     *
     * @param inPcmFilePath  輸入PCM檔案路徑
     * @param outWavFilePath 輸出WAV檔案路徑
     * @param sampleRate     取樣率,例如44100
     * @param channels       聲道數 單聲道:1或雙聲道:2
     * @param bitNum         取樣位數,8或16
     */
    public void convertPcm2Wav(String inPcmFilePath, String outWavFilePath, int sampleRate,
                                      int channels, int bitNum) {

        FileInputStream in = null;
        FileOutputStream out = null;
        byte[] data = new byte[1024];

        try {
            //取樣位元組byte率
            long byteRate = sampleRate * channels * bitNum / 8;

            in = new FileInputStream(inPcmFilePath);
            out = new FileOutputStream(outWavFilePath);

            //PCM檔案大小
            long totalAudioLen = in.getChannel().size();

            //總大小,由於不包括RIFF和WAV,所以是44 - 8 = 36,在加上PCM檔案大小
            long totalDataLen = totalAudioLen + 36;

            writeWaveFileHeader(out, totalAudioLen, totalDataLen, sampleRate, channels, byteRate);

            int length = 0;
            while ((length = in.read(data)) > 0) {
                out.write(data, 0, length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 輸出WAV檔案
     *
     * @param out           WAV輸出檔案流
     * @param totalAudioLen 整個音訊PCM資料大小
     * @param totalDataLen  整個資料大小
     * @param sampleRate    取樣率
     * @param channels      聲道數
     * @param byteRate      取樣位元組byte率
     * @throws IOException
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                            long totalDataLen, int sampleRate, int channels, long byteRate) throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);//資料大小
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//過渡位元組
        //資料大小
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //編碼方式 10H為PCM編碼格式
        header[20] = 1; // format = 1
        header[21] = 0;
        //通道數
        header[22] = (byte) channels;
        header[23] = 0;
        //取樣率,每個通道的播放速度
        header[24] = (byte) (sampleRate & 0xff);
        header[25] = (byte) ((sampleRate >> 8) & 0xff);
        header[26] = (byte) ((sampleRate >> 16) & 0xff);
        header[27] = (byte) ((sampleRate >> 24) & 0xff);
        //音訊資料傳送速率,取樣率*通道數*取樣深度/8
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // 確定系統一次要處理多少個這樣位元組的資料,確定緩衝區,通道數*取樣位數
        header[32] = (byte) (channels * 16 / 8);
        header[33] = 0;
        //每個樣本的資料位數
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }
}