Android音視頻之AudioRecord錄音(一)


在音視頻開發中,錄音當然是必不可少的。首先我們要學會單獨的錄音功能,當然這里說的錄音是指用AudioRecord來錄音,讀取錄音原始數據,讀到的就是所謂的PCM數據。對於錄音來說,最重要的幾個參數要搞明白:

1、simpleRate采樣率,采樣率就是采樣頻率,每秒鍾記錄多少個樣本。

2、channelConfig通道配置,其實就是所謂的單通道,雙通道之類的,AudioFormat.CHANNEL_IN_MONO單通道,AudioFormat.CHANNEL_IN_STEREO雙通道,這里只列了這兩種,還有其它的,可自行查閱。

3、audioFormat音頻格式,其實就是采樣的精度,每個樣本的位數,AudioFormat.ENCODING_PCM_8BIT每個樣本占8位,AudioFormat.ENCODING_PCM_16BIT每個樣本占16位,這里也只用了這兩個,別的沒研究。

在學習過程中會用到的一些參數,我這里封裝了一個類,如下

public class AudioParams {

    enum Format {
        SINGLE_8_BIT, DOUBLE_8_BIT, SINGLE_16_BIT, DOUBLE_16_BIT
    }

    private Format format;
    int simpleRate;

    AudioParams(int simpleRate, Format f) {
        this.simpleRate = simpleRate;
        this.format = f;
    }

    AudioParams(int simpleRate, int channelCount, int bits) {
        this.simpleRate = simpleRate;
        set(channelCount, bits);
    }

    int getBits() {
        return (format == Format.SINGLE_8_BIT || format == Format.DOUBLE_8_BIT) ? 8 : 16;
    }

    int getEncodingFormat() {
        return (format == Format.SINGLE_8_BIT || format == Format.DOUBLE_8_BIT) ? AudioFormat.ENCODING_PCM_8BIT :
            AudioFormat.ENCODING_PCM_16BIT;
    }

    int getChannelCount() {return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ? 1 : 2;}

    int getChannelConfig() {
        return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ? AudioFormat.CHANNEL_IN_MONO :
            AudioFormat.CHANNEL_IN_STEREO;
    }

    int getOutChannelConfig() {
        return (format == Format.SINGLE_8_BIT || format == Format.SINGLE_16_BIT) ? AudioFormat.CHANNEL_OUT_MONO :
            AudioFormat.CHANNEL_OUT_STEREO;
    }

    void set(int channelCount, int bits) {
        if ((channelCount != 1 && channelCount != 2) || (bits != 8 && bits != 16)) {
            throw new IllegalArgumentException("不支持其它格式 channelCount=$channelCount bits=$bits");
        }
        if (channelCount == 1) {
            if (bits == 8) {
                format = Format.SINGLE_8_BIT;
            } else {
                format = Format.SINGLE_16_BIT;
            }
        } else {
            if (bits == 8) {
                format = Format.DOUBLE_8_BIT;
            } else {
                format = Format.DOUBLE_16_BIT;
            }
        }
    }
}

這里固定使用了單通道8位,雙通道8位,單通道16位,雙通道16位,所以用了枚舉來限制。

為了方便把錄音數據拿出來顯示、存儲,這里寫了一個回調方法如下

    public interface RecordCallback {
        /**
         * 數據回調
         *
         * @param bytes 數據
         * @param len   數據有效長度,-1時表示數據結束
         */
        void onRecord(byte[] bytes, int len);
    }

有了這些參數,現在就可以錄音了,先看一下樣例

    public void startRecord(AudioParams params, RecordCallback callback) {
        int simpleRate = params.simpleRate;
        int channelConfig = params.getChannelConfig();
        int audioFormat = params.getEncodingFormat();
        // 根據AudioRecord提供的api拿到最小緩存大小
        int bufferSize = AudioRecord.getMinBufferSize(simpleRate, channelConfig, audioFormat);
        //創建Record對象
        record = new AudioRecord(MediaRecorder.AudioSource.MIC, simpleRate, channelConfig, audioFormat, bufferSize);
        recordThread = new Thread(() -> {
            byte[] buffer = new byte[bufferSize];
            record.startRecording();
            recording = true;
            while (recording) {
                int read = record.read(buffer, 0, bufferSize);
                // 將數據回調到外部
                if (read > 0 && callback != null) {
                    callback.onRecord(buffer, read);
                }
            }
            if (callback != null) {
                // len 為-1時表示結束
                callback.onRecord(buffer, -1);
                recording = false;
            }
            //釋放資源
            release();
        });
        recordThread.start();
    }

這個方法就是簡單的采集音頻數據,這個數據就是最原始的pcm數據。

拿到pcm數據以后,如果直接保存到文件是無法直接播放的,因為這只是一堆數據,沒有任何格式說明,如果想讓普通播放器可以播放,需要在文件中加入文件頭,來告訴播放器這個數據的格式,這里是直接保存成wav格式的數據。下面就是加入wav格式文件頭的方法

    private static byte[] getWaveFileHeader(int totalDataLen, int sampleRate, int channelCount, int bits) {
        byte[] header = new byte[44];
        // RIFF/WAVE header
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';

        int fileLength = totalDataLen + 36;
        header[4] = (byte) (fileLength & 0xff);
        header[5] = (byte) (fileLength >> 8 & 0xff);
        header[6] = (byte) (fileLength >> 16 & 0xff);
        header[7] = (byte) (fileLength >> 24 & 0xff);
        //WAVE
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        // 'fmt ' chunk
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        // 4 bytes: size of 'fmt ' chunk
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;

        // pcm format = 1
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channelCount;
        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);

        int byteRate = sampleRate * bits * channelCount / 8;
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) (byteRate >> 8 & 0xff);
        header[30] = (byte) (byteRate >> 16 & 0xff);
        header[31] = (byte) (byteRate >> 24 & 0xff);
        // block align
        header[32] = (byte) (channelCount * bits / 8);
        header[33] = 0;
        // bits per sample
        header[34] = (byte) bits;
        header[35] = 0;
        //data
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalDataLen & 0xff);
        header[41] = (byte) (totalDataLen >> 8 & 0xff);
        header[42] = (byte) (totalDataLen >> 16 & 0xff);
        header[43] = (byte) (totalDataLen >> 24 & 0xff);
        return header;
    }

根據幾個參數設置一下文件頭,然后直接寫入錄音采集到的pcm數據,就可被正常播放了。wav文件頭格式定義,可點擊這里查看或自行百度。

如果想要通過AudioRecord錄音直接保存到文件,可參考下面方法

    public void startRecord(String filePath, AudioParams params, RecordCallback callback) {
        int channelCount = params.getChannelCount();
        int bits = params.getBits();

        final boolean storeFile = filePath != null && !filePath.isEmpty();

        startRecord(params, (bytes, len) -> {
            if (storeFile) {
                if (file == null) {
                    File f = new File(filePath);
                    if (f.exists()) {
                        f.delete();
                    }
                    try {
                        file = new RandomAccessFile(f, "rw");
                        file.write(getWaveFileHeader(0, params.simpleRate, channelCount, bits));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (len > 0) {
                    try {
                        file.write(bytes, 0, len);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    try {
                        // 因為在前面已經寫入頭信息,所以這里要減去頭信息才是數據的長度
                        int length = (int) file.length() - 44;
                        file.seek(0);
                        file.write(getWaveFileHeader(length, params.simpleRate, channelCount, bits));
                        file.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (callback != null) {
                callback.onRecord(bytes, len);
            }
        });
    }

先通過RandomAccessFile創建文件,先寫入文件頭,由於暫時我們不知道會錄多長,有多少pcm數據,長度先用0表示,等錄音結束后,通過seek(int)方法重新寫入文件頭信息,也可以先把pcm數據保存到臨時文件,然后再寫入到一個新的文件中,這里就不舉例說明了。

最后放入完整類的代碼

package cn.sskbskdrin.record.audio;

import android.media.AudioRecord;
import android.media.MediaRecorder;

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

/**
 * @author sskbskdrin
 * @date 2019/April/3
 */
public class AudioRecordManager {

    private AudioParams DEFAULT_FORMAT = new AudioParams(8000, 1, 16);

    private AudioRecord record;

    private Thread recordThread;

    private boolean recording = false;

    private RandomAccessFile file;

    public void startRecord(String filePath, RecordCallback callback) {
        startRecord(filePath, DEFAULT_FORMAT, callback);
    }

    public void startRecord(String filePath, AudioParams params, RecordCallback callback) {
        int channelCount = params.getChannelCount();
        int bits = params.getBits();

        final boolean storeFile = filePath != null && !filePath.isEmpty();

        startRecord(params, (bytes, len) -> {
            if (storeFile) {
                if (file == null) {
                    File f = new File(filePath);
                    if (f.exists()) {
                        f.delete();
                    }
                    try {
                        file = new RandomAccessFile(f, "rw");
                        file.write(getWaveFileHeader(0, params.simpleRate, channelCount, bits));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (len > 0) {
                    try {
                        file.write(bytes, 0, len);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    try {
                        // 因為在前面已經寫入頭信息,所以這里要減去頭信息才是數據的長度
                        int length = (int) file.length() - 44;
                        file.seek(0);
                        file.write(getWaveFileHeader(length, params.simpleRate, channelCount, bits));
                        file.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (callback != null) {
                callback.onRecord(bytes, len);
            }
        });
    }

    public void startRecord(AudioParams params, RecordCallback callback) {
        int simpleRate = params.simpleRate;
        int channelConfig = params.getChannelConfig();
        int audioFormat = params.getEncodingFormat();
        // 根據AudioRecord提供的api拿到最小緩存大小
        int bufferSize = AudioRecord.getMinBufferSize(simpleRate, channelConfig, audioFormat);
        //創建Record對象
        record = new AudioRecord(MediaRecorder.AudioSource.MIC, simpleRate, channelConfig, audioFormat, bufferSize);
        recordThread = new Thread(() -> {
            byte[] buffer = new byte[bufferSize];
            record.startRecording();
            recording = true;
            while (recording) {
                int read = record.read(buffer, 0, bufferSize);
                // 將數據回調到外部
                if (read > 0 && callback != null) {
                    callback.onRecord(buffer, read);
                }
            }
            if (callback != null) {
                // len 為-1時表示結束
                callback.onRecord(buffer, -1);
                recording = false;
            }
            //釋放資源
            release();
        });
        recordThread.start();
    }

    public void stop() {
        recording = false;
    }

    public void release() {
        recording = false;
        if (record != null) {
            record.stop();
            record.release();
        }
        record = null;
        file = null;
        recordThread = null;
    }

    private static byte[] getWaveFileHeader(int totalDataLen, int sampleRate, int channelCount, int bits) {
        byte[] header = new byte[44];
        // RIFF/WAVE header
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';

        int fileLength = totalDataLen + 36;
        header[4] = (byte) (fileLength & 0xff);
        header[5] = (byte) (fileLength >> 8 & 0xff);
        header[6] = (byte) (fileLength >> 16 & 0xff);
        header[7] = (byte) (fileLength >> 24 & 0xff);
        //WAVE
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        // 'fmt ' chunk
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        // 4 bytes: size of 'fmt ' chunk
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;

        // pcm format = 1
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channelCount;
        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);

        int byteRate = sampleRate * bits * channelCount / 8;
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) (byteRate >> 8 & 0xff);
        header[30] = (byte) (byteRate >> 16 & 0xff);
        header[31] = (byte) (byteRate >> 24 & 0xff);
        // block align
        header[32] = (byte) (channelCount * bits / 8);
        header[33] = 0;
        // bits per sample
        header[34] = (byte) bits;
        header[35] = 0;
        //data
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalDataLen & 0xff);
        header[41] = (byte) (totalDataLen >> 8 & 0xff);
        header[42] = (byte) (totalDataLen >> 16 & 0xff);
        header[43] = (byte) (totalDataLen >> 24 & 0xff);
        return header;
    }

    public interface RecordCallback {
        /**
         * 數據回調
         *
         * @param bytes 數據
         * @param len   數據有效長度,-1時表示數據結束
         */
        void onRecord(byte[] bytes, int len);
    }
}
View Code

 

如有不對之處還請評論指正


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM