资讯专栏INFORMATION COLUMN

Android音视频之AudioRecord录音(一)

番茄西红柿 / 386人阅读

摘要:在音视频开发中,录音当然是必不可少的。对于录音来说,最重要的几个参数要搞明白采样率,采样率就是采样频率,每秒钟记录多少个样本。通道配置,其实就是所谓的单通道,双通道之类的,单通道,双通道,这里只列了这两种,还有其它的,可自行查阅。

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

 

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/2392.html

相关文章

  • 视频通信基础知识采集

    摘要:音频则通过麦克风进行采集,不同产品的麦克风对音频采样率的支持不同。采集源图像的采集源有摄像头屏幕录制等,视频通信的采集源主要是摄像头,通过摄像头获取图像信息。而端视频采集,相比安卓更加便利,使用框架提供的一系列的即可实现。 在之前的文章里,我们说了不少关于音视频相关的内容,但是一直没有系统的来介绍视频通信,接下来我们将出一个系列关于视频通信的文章。帮助大家对视频通信有一个更全面的认识。...

    kel 评论0 收藏0
  • android有趣 文章合集- 收藏集 - 掘金

    摘要:使用起来非常开发利器介绍掘金本文翻译自著名博客,原作者是,点击此处可查看原文。这时你的心里肯定有轮播图控件的实现详解附开源链接掘金轮播图在开发中是非常常见的控件,一般的首页广告和电商类的商品详情图片都会用轮播图来实现。 Android性能优化(六)之卡顿那些事 - 掘金1、 Introduction 对普通用户而言,类如内存占用高、耗流量、耗电量等性能问题可能不会轻易发现,但是卡顿问题...

    FingerLiu 评论0 收藏0
  • Android 视频开发核心知识点笔记整合

    摘要:这里给大家推荐一套学习路线,并附有相关音视频开发核心知识点笔记,相信可以给大家提供一些帮助,有需要的朋友们也可以下载下来随时查漏补缺。 很多开发者都知道Androi...

    lily_wang 评论0 收藏0
  • 企业级Android视频开发学习路线+项目实战+源码解析(WebRTC Native 源码、X26

    摘要:因此,对音视频人才的需求也从小众变成了大众,这更多的是大家对未来市场的预期导致的结果。做个勤奋向上的人,加紧学习,抓住中心,宁精勿杂,宁专勿多。 前言 如今音视频的...

    tomato 评论0 收藏0
  • 微信开发录音上传、下载、转码

    摘要:具体请看我在提问里的回答下载七牛云文件间歇性失败总结至此,在微信开发中关于录音这一块儿的功能,就已经介绍完毕。 showImg(https://segmentfault.com/img/remote/1460000013595733?w=454&h=339); 原文是在我自己博客中,小伙伴也可以点阅读原文进行跳转查看,还有好听的背景音乐噢~     一年的时间里,前前后后都在搞微信开发...

    余学文 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<