Android 錄音實現(AudioRecord)

上一篇文章介紹了使用 MediaRecorder 實現錄音功能 Android錄音實現(MediaRecorder) ,下面我們繼續看看使用 AudioRecord 實現錄音功能。

AudioRecord

首先看看Android幫助文檔中對該類的簡單概述: AndioRecord 類的主要功能是讓各種 Java 應用能夠管理音頻資源,以便它們通過此類能夠錄制平臺的聲音輸入硬件所收集的聲音。此功能的實現就是通過 "pulling 同步"(reading讀取)AudioRecord 對象的聲音數據來完成的。在錄音過程中,應用所需要做的就是通過后面三個類方法中的一個去及時地獲取 AudioRecord 對象的錄音數據。 AudioRecord 類提供的三個獲取聲音數據的方法分別是 read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int)。無論選擇使用那一個方法都必須事先設定方便用戶的聲音數據的存儲格式。

開始錄音的時候,一個 AudioRecord 需要初始化一個相關聯的聲音buffer,這個 buffer 主要是用來保存新的聲音數據。這個 buffer 的大小,我們可以在對象構造期間去指定。它表明一個 AudioRecord 對象還沒有被讀取(同步)聲音數據前能錄多長的音(即一次可以錄制的聲音容量)。聲音數據從音頻硬件中被讀出,數據大小不超過整個錄音數據的大小(可以分多次讀出),即每次讀取初始化 buffer 容量的數據。

采集工作很簡單,我們只需要構造一個AudioRecord對象,然后傳入各種不同配置的參數即可。一般情況下錄音實現的簡單流程如下:

  1. 音頻源:我們可以使用麥克風作為采集音頻的數據源。

  2. 采樣率:一秒鐘對聲音數據的采樣次數,采樣率越高,音質越好。

  3. 音頻通道:單聲道,雙聲道等,

  4. 音頻格式:一般選用PCM格式,即原始的音頻樣本。

  5. 緩沖區大小:音頻數據寫入緩沖區的總數,可以通過AudioRecord.getMinBufferSize獲取最小的緩沖區。(將音頻采集到緩沖區中然后再從緩沖區中讀取)。

代碼實現如下:

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 實現錄音
 *
 * @author chenmy0709
 * @version V001R001C01B001
 */
public class AudioRecorder {
    //音頻輸入-麥克風
    private final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC;
    //采用頻率
    //44100是目前的標準,但是某些設備仍然支持22050,16000,11025
    //采樣頻率一般共分為22.05KHz、44.1KHz、48KHz三個等級
    private final static int AUDIO_SAMPLE_RATE = 16000;
    //聲道 單聲道
    private final static int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_MONO;
    //編碼
    private final static int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
    // 緩沖區字節大小
    private int bufferSizeInBytes = 0;

    //錄音對象
    private AudioRecord audioRecord;

    //錄音狀態
    private Status status = Status.STATUS_NO_READY;

    //文件名
    private String fileName;

    //錄音文件
    private List<String> filesName = new ArrayList<>();


    /**
     * 類級的內部類,也就是靜態類的成員式內部類,該內部類的實例與外部類的實例
     * 沒有綁定關系,而且只有被調用時才會裝載,從而實現了延遲加載
     */
    private static class AudioRecorderHolder {
        /**
         * 靜態初始化器,由JVM來保證線程安全
         */
        private static AudioRecorder instance = new AudioRecorder();
    }

    private AudioRecorder() {
    }

    public static AudioRecorder getInstance() {
        return AudioRecorderHolder.instance;
    }

    /**
     * 創建錄音對象
     */
    public void createAudio(String fileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
        // 獲得緩沖區字節大小
        bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
                channelConfig, channelConfig);
        audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
        this.fileName = fileName;
    }

    /**
     * 創建默認的錄音對象
     *
     * @param fileName 文件名
     */
    public void createDefaultAudio(String fileName) {
        // 獲得緩沖區字節大小
        bufferSizeInBytes = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
                AUDIO_CHANNEL, AUDIO_ENCODING);
        audioRecord = new AudioRecord(AUDIO_INPUT, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, bufferSizeInBytes);
        this.fileName = fileName;
        status = Status.STATUS_READY;
    }


    /**
     * 開始錄音
     *
     * @param listener 音頻流的監聽
     */
    public void startRecord(final RecordStreamListener listener) {

        if (status == Status.STATUS_NO_READY || TextUtils.isEmpty(fileName)) {
            throw new IllegalStateException("錄音尚未初始化,請檢查是否禁止了錄音權限~");
        }
        if (status == Status.STATUS_START) {
            throw new IllegalStateException("正在錄音");
        }
        Log.d("AudioRecorder", "===startRecord===" + audioRecord.getState());
        audioRecord.startRecording();

        new Thread(new Runnable() {
            @Override
            public void run() {
                writeDataTOFile(listener);
            }
        }).start();
    }

    /**
     * 暫停錄音
     */
    public void pauseRecord() {
        Log.d("AudioRecorder", "===pauseRecord===");
        if (status != Status.STATUS_START) {
            throw new IllegalStateException("沒有在錄音");
        } else {
            audioRecord.stop();
            status = Status.STATUS_PAUSE;
        }
    }

    /**
     * 停止錄音
     */
    public void stopRecord() {
        Log.d("AudioRecorder", "===stopRecord===");
        if (status == Status.STATUS_NO_READY || status == Status.STATUS_READY) {
            throw new IllegalStateException("錄音尚未開始");
        } else {
            audioRecord.stop();
            status = Status.STATUS_STOP;
            release();
        }
    }

    /**
     * 釋放資源
     */
    public void release() {
        Log.d("AudioRecorder", "===release===");
        //假如有暫停錄音
        try {
            if (filesName.size() > 0) {
                List<String> filePaths = new ArrayList<>();
                for (String fileName : filesName) {
                    filePaths.add(FileUtil.getPcmFileAbsolutePath(fileName));
                }
                //清除
                filesName.clear();
                //將多個pcm文件轉化為wav文件
                mergePCMFilesToWAVFile(filePaths);

            } else {
                //這里由于只要錄音過filesName.size都會大于0,沒錄音時fileName為null
                //會報空指針 NullPointerException
                // 將單個pcm文件轉化為wav文件
                //Log.d("AudioRecorder", "=====makePCMFileToWAVFile======");
                //makePCMFileToWAVFile();
            }
        } catch (IllegalStateException e) {
            throw new IllegalStateException(e.getMessage());
        }

        if (audioRecord != null) {
            audioRecord.release();
            audioRecord = null;
        }

        status = Status.STATUS_NO_READY;
    }

    /**
     * 取消錄音
     */
    public void canel() {
        filesName.clear();
        fileName = null;
        if (audioRecord != null) {
            audioRecord.release();
            audioRecord = null;
        }

        status = Status.STATUS_NO_READY;
    }


    /**
     * 將音頻信息寫入文件
     *
     * @param listener 音頻流的監聽
     */
    private void writeDataTOFile(RecordStreamListener listener) {
        // new一個byte數組用來存一些字節數據,大小為緩沖區大小
        byte[] audiodata = new byte[bufferSizeInBytes];

        FileOutputStream fos = null;
        int readsize = 0;
        try {
            String currentFileName = fileName;
            if (status == Status.STATUS_PAUSE) {
                //假如是暫停錄音 將文件名后面加個數字,防止重名文件內容被覆蓋
                currentFileName += filesName.size();

            }
            filesName.add(currentFileName);
            File file = new File(FileUtil.getPcmFileAbsolutePath(currentFileName));
            if (file.exists()) {
                file.delete();
            }
            fos = new FileOutputStream(file);// 建立一個可存取字節的文件
        } catch (IllegalStateException e) {
            Log.e("AudioRecorder", e.getMessage());
            throw new IllegalStateException(e.getMessage());
        } catch (FileNotFoundException e) {
            Log.e("AudioRecorder", e.getMessage());

        }
        //將錄音狀態設置成正在錄音狀態
        status = Status.STATUS_START;
        while (status == Status.STATUS_START) {
            readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
            if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos != null) {
                try {
                    fos.write(audiodata);
                    if (listener != null) {
                        //用于拓展業務
                        listener.recordOfByte(audiodata, 0, audiodata.length);
                    }
                } catch (IOException e) {
                    Log.e("AudioRecorder", e.getMessage());
                }
            }
        }
        try {
            if (fos != null) {
                fos.close();// 關閉寫入流
            }
        } catch (IOException e) {
            Log.e("AudioRecorder", e.getMessage());
        }
    }

    /**
     * 將pcm合并成wav
     *
     * @param filePaths
     */
    private void mergePCMFilesToWAVFile(final List<String> filePaths) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (PcmToWav.mergePCMFilesToWAVFile(filePaths, FileUtil.getWavFileAbsolutePath(fileName))) {
                    //操作成功
                } else {
                    //操作失敗
                    Log.e("AudioRecorder", "mergePCMFilesToWAVFile fail");
                    throw new IllegalStateException("mergePCMFilesToWAVFile fail");
                }
                fileName = null;
            }
        }).start();
    }

    /**
     * 將單個pcm文件轉化為wav文件
     */
    private void makePCMFileToWAVFile() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (PcmToWav.makePCMFileToWAVFile(FileUtil.getPcmFileAbsolutePath(fileName), FileUtil.getWavFileAbsolutePath(fileName), true)) {
                    //操作成功
                } else {
                    //操作失敗
                    Log.e("AudioRecorder", "makePCMFileToWAVFile fail");
                    throw new IllegalStateException("makePCMFileToWAVFile fail");
                }
                fileName = null;
            }
        }).start();
    }

    /**
     * 獲取錄音對象的狀態
     *
     * @return
     */
    public Status getStatus() {
        return status;
    }

    /**
     * 獲取本次錄音文件的個數
     *
     * @return
     */
    public int getPcmFilesCount() {
        return filesName.size();
    }

    /**
     * 錄音對象的狀態
     */
    public enum Status {
        //未開始
        STATUS_NO_READY,
        //預備
        STATUS_READY,
        //錄音
        STATUS_START,
        //暫停
        STATUS_PAUSE,
        //停止
        STATUS_STOP
    }

}

AudioRecorder 錄音聲音數據從音頻硬件中被讀出,編碼格式為 PCM格式,但 PCM語音數據,如果保存成音頻文件,是不能夠被播放器播放的,所以必須先寫代碼實現數據編碼以及壓縮。下面實現 PCM 語音數據轉為 WAV 文件。

/**
 * 將一個pcm文件轉化為wav文件
 * @param pcmPath         pcm文件路徑
 * @param destinationPath 目標文件路徑(wav)
 * @param deletePcmFile   是否刪除源文件
 * @return
 */
public static boolean makePCMFileToWAVFile(String pcmPath, String destinationPath, boolean deletePcmFile) {
    byte buffer[] = null;
    int TOTAL_SIZE = 0;
    File file = new File(pcmPath);
    if (!file.exists()) {
        return false;
    }
    TOTAL_SIZE = (int) file.length();
    // 填入參數,比特率等等。這里用的是16位單聲道 8000 hz
    WaveHeader header = new WaveHeader();
    // 長度字段 = 內容的大小(TOTAL_SIZE) +
    // 頭部字段的大小(不包括前面4字節的標識符RIFF以及fileLength本身的4字節)
    header.fileLength = TOTAL_SIZE + (44 - 8);
    header.FmtHdrLeth = 16;
    header.BitsPerSample = 16;
    header.Channels = 2;
    header.FormatTag = 0x0001;
    header.SamplesPerSec = 8000;
    header.BlockAlign = (short) (header.Channels * header.BitsPerSample / 8);
    header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
    header.DataHdrLeth = TOTAL_SIZE;

    byte[] h = null;
    try {
        h = header.getHeader();
    } catch (IOException e1) {
        Log.e("PcmToWav", e1.getMessage());
        return false;
    }

    if (h.length != 44) // WAV標準,頭部應該是44字節,如果不是44個字節則不進行轉換文件
        return false;

    // 先刪除目標文件
    File destfile = new File(destinationPath);
    if (destfile.exists())
        destfile.delete();

    // 合成的pcm文件的數據,寫到目標文件
    try {
        buffer = new byte[1024 * 4]; // Length of All Files, Total Size
        InputStream inStream = null;
        OutputStream ouStream = null;

        ouStream = new BufferedOutputStream(new FileOutputStream(
                destinationPath));
        ouStream.write(h, 0, h.length);
        inStream = new BufferedInputStream(new FileInputStream(file));
        int size = inStream.read(buffer);
        while (size != -1) {
            ouStream.write(buffer);
            size = inStream.read(buffer);
        }
        inStream.close();
        ouStream.close();
    } catch (FileNotFoundException e) {
        Log.e("PcmToWav", e.getMessage());
        return false;
    } catch (IOException ioe) {
        Log.e("PcmToWav", ioe.getMessage());
        return false;
    }
    if (deletePcmFile) {
        file.delete();
    }
    Log.i("PcmToWav", "makePCMFileToWAVFile  success!" + new SimpleDateFormat("yyyy-MM-dd hh:mm").format(new Date()));
    return true;
}

Demo源碼

總結:AudioRecorder 錄音相比較 MediaRecorder 使用起來會麻煩一些,但優點也是顯而易見的,AudioRecorder 錄音時直接操縱硬件獲取音頻流數據,該過程是實時處理,可以用代碼實現各種音頻的封裝,同時也可實現暫停功能,關于實現暫停錄音功能今天在這里就不贅述了,推薦大家閱讀 imhxl 博主的分享 http://blog.csdn.net/imhxl/article/details/52190451

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容