欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android音视频之AudioRecord录音(一)

程序员文章站 2022-05-03 12:01:02
在音视频开发中,录音当然是必不可少的。首先我们要学会单独的录音功能,当然这里说的录音是指用AudioRecord来录音,读取录音原始数据,读到的就是所谓的PCM数据。对于录音来说,最重要的几个参数要搞明白: 1、simpleRate采样率,采样率就是采样频率,每秒钟记录多少个样本。 2、channe ......

在音视频开发中,录音当然是必不可少的。首先我们要学会单独的录音功能,当然这里说的录音是指用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);
    }
}

 

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