zoukankan      html  css  js  c++  java
  • 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

    如有不对之处还请评论指正

  • 相关阅读:
    Pandas基本命令
    python——内建模块instance的学习
    pyhton——logging日志模块的学习
    mongoDB集群的搭建
    goahead(web服务器)分析
    2019-9
    cmake
    mqtt+htttp+websocket
    u-boot中filesize环境变量【转载】
    cppcheck下载及使用
  • 原文地址:https://www.cnblogs.com/sskbskdrin/p/10647975.html
Copyright © 2011-2022 走看看