Android 开发 AudioRecord音频录制_android audiohal in_read 录音-程序员宅基地

技术标签: Android  

前言

Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。

实现流程

获取权限
初始化获取每一帧流的Size
初始化音频录制AudioRecord
开始录制与保存录制音频文件
停止录制
给音频文件添加头部信息,并且转换格式成wav
释放AudioRecord,录制流程完毕

获取权限

<!--音频录制权限 -->
<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个权限需要动态授权

初始化获取每一帧流的Size

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

第一个参数sampleRateInHz 采样率(赫兹),方法注释里有说明

只能在4000到192000的范围内取值

在AudioFormat类里
public static final int SAMPLE_RATE_HZ_MIN = 4000; 最小4000
public static final int SAMPLE_RATE_HZ_MAX = 192000; 最大192000

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

在AudioFormat类录
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 音频格式 表示音频数据的格式。

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

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;
初始化音频录制AudioRecord

private AudioRecord mAudioRecord;
private void initAudioRecord(){
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC
                , 8000
                , AudioFormat.CHANNEL_IN_MONO
                , AudioFormat.ENCODING_PCM_16BIT
                , mRecordBufferSize);
    }

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

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

private boolean mWhetherRecord;
private File pcmFile;
private void startRecord(){
        pcmFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord.pcm");
        mWhetherRecord = true;
        new Thread(new Runnable() {
            @Override
            public void run() {
                mAudioRecord.startRecording();//开始录制
                FileOutputStream fileOutputStream = null;
                try {
                    fileOutputStream = new FileOutputStream(pcmFile);
                    byte[] bytes = new byte[mRecordBufferSize];
                    while (mWhetherRecord){
                        mAudioRecord.read(bytes, 0, bytes.length);//读取流
                        fileOutputStream.write(bytes);
                        fileOutputStream.flush();

                    }
                    Log.e(TAG, "run: 暂停录制" );
                    mAudioRecord.stop();//停止录制
                    fileOutputStream.flush();
                    fileOutputStream.close();
                   addHeadData();//添加音频头部信息并且转成wav格式
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                    mAudioRecord.stop();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

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

停止录制

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

private void stopRecord(){
        mWhetherRecord = false;
    }

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

音频录制完成后,这个时候去存储目录找到音频文件部分,会提示无法播放文件.其实是因为没有加入音频头部信息.一般通过麦克风采集的录音数据都是PCM格式的,即不包含头部信息,播放器无法知道音频采样率、位宽等参数,导致无法播放,显然是非常不方便的。pcm转换成wav,我们只需要在pcm的文件起始位置加上至少44个字节的WAV头信息即可。
偏移地址   命名       内容
00-03   ChunkId       “RIFF”
04-07   ChunkSize      下个地址开始到文件尾的总字节数(此Chunk的数据大小)
08-11   fccType       “WAVE”
12-15   SubChunkId1     "fmt ",最后一位空格。
16-19   SubChunkSize1    一般为16,表示fmt Chunk的数据块大小为16字节
20-21   FormatTag      1:表示是PCM 编码
22-23   Channels       声道数,单声道为1,双声道为2
24-27   SamplesPerSec    采样率
28-31   BytesPerSec     码率 :采样率 * 采样位数 * 声道个数,bytePerSecond = sampleRate * (bitsPerSample / 8) * channels
32-33   BlockAlign       每次采样的大小:位宽*声道数/8
34-35   BitsPerSample     位宽
36-39   SubChunkId2     “data”
40-43   SubChunkSize2    音频数据的长度
44-…   data         音频数据

private void addHeadData(){
        pcmFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord.pcm");
        handlerWavFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord_handler.wav");
        PcmToWavUtil pcmToWavUtil = new PcmToWavUtil(8000,AudioFormat.CHANNEL_IN_MONO,AudioFormat.ENCODING_PCM_16BIT);
        pcmToWavUtil.pcmToWav(pcmFile.toString(),handlerWavFile.toString());
    }

写入头部信息的工具类

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

public class PcmToWavUtil {
    private static final String TAG = "PcmToWavUtil";

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


    /**
     * @param sampleRate sample rate、采样率
     * @param channel channel、声道
     * @param encoding Audio data format、音频格式
     */
    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);
                out.flush();

            }
            Log.e(TAG, "pcmToWav: 停止处理");
            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);
    }
}

释放AudioRecord,录制流程完毕

调用release()方法释放资源

mAudioRecord.release();

最后你就可以在指定目录下找到音频文件播放了

最后介绍下其他API
获取AudioRecord初始化状态

public int getState() {
    return mState;
}

注意!这里是初始化状态,不是录制状态,它只会返回2个状态

AudioRecord#STATE_INITIALIZED    //已经初始化
AudioRecord#STATE_UNINITIALIZED  //没有初始化

获取AudioRecord录制状态

public int getRecordingState() {
        synchronized (mRecordingStateLock) {
            return mRecordingState;
        }
    }

返回录制状态,它只返回2个状态

AudioRecord#RECORDSTATE_STOPPED    //停止录制
AudioRecord#RECORDSTATE_RECORDING    //正在录制
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/casual_clover/article/details/101066507

智能推荐

探索iptables BPF模块的悲惨历程_bpf iptables-程序员宅基地

文章浏览阅读1.3w次。前面一篇文章提到了两个Netfilter上的新玩意儿,其中之一就是bpf match模块,另一个nftables的ball还没有放出来,即使放出来了,它的吸引力也没有bpf match大,毕竟伯克利的东西不可小觑啊!虽然nftables也是借用了BPF的思想,但是能看得出的仅在代码层面上,反之,bpf match模块却是iptables质的飞跃,综合状态机,JIT(及时编译技术),它可以在很短的时_bpf iptables

第一行代码学习笔记第三章——UI开发的点点滴滴_通常调用view 的( )方法来获取到界面控件imageview 和 textview 的实例-程序员宅基地

文章浏览阅读431次,点赞3次,收藏3次。知识点目录3.1 如何编写程序界面3.2 常用控件的使用方法* 3.2.1 TextView* 3.2.2 Button* 3.2.3 EditText* 3.2.4 ImageView* 3.2.5 ProgressBar* 3.2.6 AlertDialog* 3.2.7 ProgressDialog3.3 详解4中基本布局* 3.3.1 线性布局*..._通常调用view 的( )方法来获取到界面控件imageview 和 textview 的实例

ci mysql空闲连接回收_数据库连接池的配置问题-空闲线程的监控和回收-程序员宅基地

文章浏览阅读550次。我们系统中目前设置的是jdbc.maxIdleTime=14400000 ,minIdle=2014400000/3600/1000=4小时我们的数据库目前设置为15分钟即断开。故客户端连接池设置时间短一点:10分钟,会回收连接直到20.剩下的20个连接即使一直空闲也不回收,符合连接池的意义。但是数据库会把这20个连接关闭。故依旧无法解决抛错或者超时的问题。只能查看druid的监控,查看峰值活..._mysql连接断开多久回收

微信小程序获取用户手机号码教程(前端+后端)_微信小程序获取手机号-程序员宅基地

文章浏览阅读3.3w次,点赞31次,收藏175次。微信小程序获取用户手机号码的教程,包括前端开发和后端开发,代码亲测可用,以及包括如何获取用户信息的最新方法_微信小程序获取手机号

python炫酷烟花表白源代码,html代码烟花特效python-程序员宅基地

文章浏览阅读163次,点赞3次,收藏3次。然后,定义烟花的形状,采用 turtle.Shape 的方式来定义,包括圆形和尾迹。接着,注册烟花的形状,采用 turtle.register_shape 的方式来注册。然后,定义烟花类 Firework,包括初始化位置、颜色、速度等属性,以及更新位置的方法 update。接着,创建烟花,将其添加到 fireworks 列表中。最后,进入动画循环,不断更新烟花的位置,直到所有烟花都绽放完毕。大家好,小编为大家解答python炫酷烟花表白源代码的问题。首先,设置窗口大小和标题,定义烟花的颜色和数量。

python算法常用技巧与内置库_python自带运算库-程序员宅基地

文章浏览阅读848次,点赞5次,收藏28次。python算法常用技巧与内置库近些年随着python的越来越火,python也渐渐成为了很多程序员的喜爱。许多程序员已经开始使用python作为第一语言来刷题。最近我在用python刷题的时候想去找点python的刷题常用库api和刷题技巧来看看。类似于C++的STL库文档一样,但是很可惜并没有找到,于是决定结合自己的刷题经验和上网搜索做一份文档出来,供自己和大家观看查阅。1.输入输出:1.1 第一行给定两个值n,m,用空格分割,第一个n决定接下来有n行的输入,m决定每一行有多少个数字,m个数字均_python自带运算库

随便推点

vulnhub靶场,JANGOW: 1.0.1_jangow: 1.0.1靶场-程序员宅基地

文章浏览阅读1.7k次,点赞2次,收藏5次。vulnhub靶场,JANGOW: 1.0.1环境准备靶机下载地址:https://www.vulnhub.com/entry/jangow-101,754/攻击机:kali(192.168.109.128)靶机:JANGOW: 1.0.1(192.168.109.198)下载好靶机之后直接使用VMware Workstation Pro虚拟机导入环境,启动即可,将网段设置为NAT模式目标:提升为root权限获取root目录下的flag信息收集使用arp-scan确定目标靶机确定目标靶机_jangow: 1.0.1靶场

AES加密解密原理_电信座机aes-1解密-程序员宅基地

文章浏览阅读696次。https://blog.csdn.net/qq_28205153/article/details/55798628https://blog.csdn.net/Simple_Man_Just/article/details/69258923?utm_source=blogxgwz1 _电信座机aes-1解密

DNS域名解析详细说明_本地域名服务器域名解析次数-程序员宅基地

文章浏览阅读2k次。一、DNS系统(一)、DNS概述在日常生活中人们习惯使用域名访问服务器,但机器间互相只认IP地址,域名与IP地址之间是多对一的关系,一个Ip地址不一定只对应一个域名,且一个域名只可以对应一个Ip地址,它们之间的转换工作称为域名解析,域名解析需要由专门的域名解析服务器来完成,整个过程是自动进行的。(二)、DNS的定义DNS是”域名系统(Domain Name System)"的英文缩写。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS服务使用TCP和UDP的_本地域名服务器域名解析次数

利用MySQL的Binlog实现数据同步与订阅(上):基础篇_mysql数据同步可以通过读取binlog某个位置开始的数据往后走么-程序员宅基地

文章浏览阅读4.4k次,点赞11次,收藏16次。终于等到了周末,在经历了一周的忙碌后,终于可以利用空闲写篇博客。其实,博主有一点困惑,困惑于这个世界早已“堆积”起人类难以想象的“大”数据,而我们又好像执着于去“造”一个又一个“差不多”的“内容管理系统”,从前我们说互联网的精神是开放和分享,可不知从什么时候起,我们亲手打造了一个又一个的“信息孤岛”。而为了打通这些“关节”,就不得不去造一张巨大无比的蜘蛛网,你说这就是互联网的本质,对此我表示无法反驳。我更关心的是这其中最脆弱的部分,即:一条数据怎么从A系统流转到B系统。可能你会想到API或者ETL这样的关键_mysql数据同步可以通过读取binlog某个位置开始的数据往后走么

线性代数在数据科学中的十个强大应用(一)-程序员宅基地

文章浏览阅读3.1k次,点赞9次,收藏55次。介绍线性代数与数据科学的关系就像罗宾与蝙蝠侠。这位数据科学忠实的伙伴经常会被大家所忽视,但实际上,它是数据科学主要领域--包括计算机视觉(CV)与自然语言处理(NLP)等热门领域的强力支撑。数据开发者往往会因为数学太难而尝试避开这个主题。因为有很多现成的数据处理库可以帮助他们避开线性代数这个烦恼。这是极其错误的想法。线性代数是我们所熟知的所用强大机器学习算法的背后核心,同样是数据科学家..._线性代数在大数据技术的应用

MT7688 wifi 调试__wifi_updown-程序员宅基地

文章浏览阅读1.9k次,点赞2次,收藏6次。最近公司要用到MT7688的内置wifi,查了许多资料终于调通,过程也比较坎坷,在这里整理一下。1.先看官方文档:坑1:git://git.openwrt.org/15.05/openwrt.git 失效root@localhost:~# git clone git://git.openwrt.org/15.05/openwrt.git正克隆到 'openwrt'...fat...__wifi_updown