AudioRecord音频录制

一、前言

  Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。如果想简单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder,而如果需要对音频做进一步的算法处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。直播中实时采集音频自然是要用AudioRecord了。

二、实现流程

1.获取权限
2.初始化获取每一帧流的Size
3.初始化音频录制AudioRecord
4.开始录制与保存录制音频文件
5.停止录制
6.给音频文件添加头部信息,并且转换格式成wav

1.获取权限

<!--音频录制权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--读取和写入存储权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

备注:如果是Android5.0以上系统,以上3个权限需要动态授权。

2.初始化获取每一帧流的Size

// 声明recordBufffer的大小字段
private Integer mRecordBufferSize;
private void initMinBufferSize(){
    // 获取每一帧的字节流大小
    mRecordBufferSize = AudioRecord.getMinBufferSize(44100 
        , AudioFormat.CHANNEL_IN_MONO
        , AudioFormat.ENCODING_PCM_16BIT);
}

第一个参数sampleRateInHz:采样率(赫兹)

采样率就是采样频率,每秒钟记录多少个样本。现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。

第二个参数channelConfig:声道配置,描述音频声道的配置,例如左声道/右声道/前声道/后声道。

public static final int CHANNEL_IN_LEFT = 0x4;//左声道
public static final int CHANNEL_IN_RIGHT = 0x8;//右声道
public static final int CHANNEL_IN_FRONT = 0x10;//前声道
public static final int CHANNEL_IN_BACK = 0x20;//后声道
public static final int CHANNEL_IN_LEFT_PROCESSED = 0x40;
public static final int CHANNEL_IN_RIGHT_PROCESSED = 0x80;
public static final int CHANNEL_IN_FRONT_PROCESSED = 0x100;
public static final int CHANNEL_IN_BACK_PROCESSED = 0x200;
public static final int CHANNEL_IN_PRESSURE = 0x400;
public static final int CHANNEL_IN_X_AXIS = 0x800;
public static final int CHANNEL_IN_Y_AXIS = 0x1000;
public static final int CHANNEL_IN_Z_AXIS = 0x2000;
public static final int CHANNEL_IN_VOICE_UPLINK = 0x4000;
public static final int CHANNEL_IN_VOICE_DNLINK = 0x8000;
public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;//单声道
public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);//立体声道(左右声道)

第三个参数audioFormat:音频格式,表示音频数据的格式。

public static final int ENCODING_PCM_16BIT = 2; //16位PCM编码
public static final int ENCODING_PCM_8BIT = 3; //8位PCM编码
public static final int ENCODING_PCM_FLOAT = 4; //4位PCM编码
public static final int ENCODING_AC3 = 5;
public static final int ENCODING_E_AC3 = 6;
public static final int ENCODING_DTS = 7;
public static final int ENCODING_DTS_HD = 8;
public static final int ENCODING_MP3 = 9; //MP3编码 此格式可能会因为设备不支持报错
public static final int ENCODING_AAC_LC = 10;
public static final int ENCODING_AAC_HE_V1 = 11;
public static final int ENCODING_AAC_HE_V2 = 12;

注意:一般的手机设备可能只支持 16位PCM编码,如果其他的都会报错为坏值。

3.初始化音频录制AudioRecord

// 声明AudioRecord对象
private AudioRecord mAudioRecord;
private void initAudioRecord(){
    mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC
        , 44100
        , AudioFormat.CHANNEL_IN_MONO
        , AudioFormat.ENCODING_PCM_16BIT
        , mRecordBufferSize);
}

第一个参数audioSource 音频源——这里选择使用麦克风:MediaRecorder.AudioSource.MIC
第二个参数sampleRateInHz 采样率(赫兹)——与前面初始化获取每一帧流的Size方法getMinBufferSize的参数保持一致
第三个参数channelConfig 声道配置 描述音频声道的配置,例如左声道/右声道/前声道/后声道。——与前面初始化获取每一帧流的Size方法getMinBufferSize的参数保持一致
第四个参数audioFormat 音频格式  表示音频数据的格式。——与前面初始化获取每一帧流的Size方法getMinBufferSize的参数保持一致
第五个参数缓存区大小,就是上面我们配置的AudioRecord.getMinBufferSize的值

4.开始录制与保存录制音频文件

public void startRecord() {
    // mRecordBufferSize为getMinBufferSize值
    final byte data[] = new byte[mRecordBufferSize];
    final File file = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm");
    if (!file.mkdirs()) {
        Log.e(TAG, "Directory not created");
    }
    if (file.exists()) {
        file.delete();
    }
    // 开始录音
    mAudioRecord.startRecording();
    // 全局布尔变量,用来开始/停止获取录音数据
    isRecording = true;
    // ps: pcm数据无法直接播放,需要转换为WAV格式。
    new Thread(new Runnable() {
        @Override
        public void run() {
            FileOutputStream os = null;
            try {
                os = new FileOutputStream(file);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            if (os != null) {
                while (isRecording) {
                    int read = mAudioRecord.read(data, 0, mRecordBufferSize);
                    // 如果读取音频数据没有出现错误,就将数据写入到文件
                    if (AudioRecord.ERROR_INVALID_OPERATION != read) {
                        try {
                            os.write(data);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
                try {
                    Log.i(TAG, "run: close file output stream !");
                    os.flush();
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                addHeadData(); // 添加音频头部信息并且转成wav格式
            }
        }
    }).start();
}

这里说明一下为什么用布尔值来关闭录制。有些小伙伴会发现AudioRecord是可以获取到录制状态的,那么肯定有人会用状态来判断while是否还需要处理流,这种是错误的做法。因为MIC属于硬件层任何硬件的东西都是异步的而且会有很大的延时,所以回调的状态也是有延时的,有时候流没了,但是状态还是显示为正在录制。简单来说就是为了能实时对应状态,减少延时带来的影响。

5.停止录制

就是调用mAudioRecord.stop();方法来停止录制,但是因为我在上面的保存流后做了调用停止视频录制,所以我这里只需要切换布尔值就可以关闭音频录制。

public void stopRecord() {
    isRecording = false;
    // 释放资源
    if (mAudioRecord != null) {
        mAudioRecord.stop();
        mAudioRecord.release();
        mAudioRecord = null;
    }
}

6.给音频文件添加头部信息,并且转换格式成wav

音频录制完成后,这个时候去存储目录找到音频文件部分,会提示无法播放文件。其实是因为没有加入音频头部信息。一般通过麦克风采集的录音数据都是PCM格式的,即不包含头部信息,播放器无法知道音频采样率、位宽等参数,导致无法播放,显然是非常不方便的。pcm转换成wav,我们只需要在pcm的文件起始位置加上至少44个字节的WAV头信息即可。

偏移地址命名内容
00-03ChunkId"RIFF"
04-07ChunkSize下一个地址开始到文件尾的总字节数(此Chunk的数据大小)
08-11fccType"WAVE"
12-15SubChunkId1"fmt",最后一位空格
16-19SubChunkSize1一般为16,表示fmt Chunk的数据块大小为16字节
20-21FormatTag1:表示PCM编码
22-23Channels声道数,单声道为1,双声道为2
24-27SamplesPerSec采样率
28-31BytesPerSec码率 :采样率 * 采样位数 * 声道个数,bytePerSecond = sampleRate * (bitsPerSample / 8) * channels
32-33BlockAlign每次采样的大小:位宽*声道数/8
34-35BitsPerSample位宽
36-39SubChunkId2"data"
40-43SubChunkSize2音频数据的长度
44-.............data音频数据
// 增加头部信息
private void addHeadData(){
    PcmToWavUtil pcmToWavUtil = new PcmToWavUtil(44100, AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    File pcmFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test" +
                        ".pcm");
    File wavFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test" +
                        ".wav");
    if (!wavFile.mkdirs()) {
        Log.e(TAG, "wavFile Directory not created");
    }
    if (wavFile.exists()) {
        wavFile.delete();
    }
    pcmToWavUtil.pcmToWav(pcmFile.getAbsolutePath(), wavFile.getAbsolutePath());
}

三、写入头部信息的工具类

注意输入File和输出File不能同一个,因为没有做缓存。

/**
 * 将pcm音频文件转换为wav音频文件
 */
public class PcmToWavUtil {

    /**
     * 缓存的音频大小
     */
    private int mBufferSize;
    /**
     * 采样率
     */
    private int mSampleRate;
    /**
     * 声道数
     */
    private int mChannel;

    /**
     * @param sampleRate sample rate、采样率
     * @param channel channel、声道
     * @param encoding Audio data format、音频格式
     */
    public PcmToWavUtil(int sampleRate, int channel, int encoding) {
        this.mSampleRate = sampleRate;
        this.mChannel = channel;
        this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding);
    }

    /**
     * pcm文件转wav文件
     *
     * @param inFilename 源文件路径
     * @param outFilename 目标文件路径
     */
    public void pcmToWav(String inFilename, String outFilename) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen; // 总录音长度
        long totalDataLen; // 总数据长度
        long longSampleRate = mSampleRate;
        int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
        long byteRate = 16 * mSampleRate * channels / 8;
        byte[] data = new byte[mBufferSize];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;
            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 加入wav文件头
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        // RIFF/WAVE header
        header[0] = 'R';
        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);
        //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;
        // format = 1
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        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) (2 * 16 / 8);
        header[33] = 0;
        // bits per sample
        header[34] = 16;
        header[35] = 0;
        //data
        header[36] = 'd';
        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);
    }
}

四、最后介绍下其他API

1.获取AudioRecord初始化状态

/**
 * 注意:这里是初始化状态,不是录制状态,它只会返回2个状态
 * AudioRecord#STATE_INITIALIZED // 已经初始化
 * AudioRecord#STATE_UNINITIALIZED // 没有初始化
 */
public int getState() {
    return mState;
}

2.获取AudioRecord录制状态

/**
 * 返回录制状态,它只返回2个状态
 * AudioRecord#RECORDSTATE_STOPPED //停止录制
 * AudioRecord#RECORDSTATE_RECORDING //正在录制
 */
public int getRecordingState() {
    synchronized (mRecordingStateLock) {
        return mRecordingState;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值