文档章节

Android音视频之AudioRecord录音(一)

o
 osc_w9s1w4o0
发布于 2019/04/03 15:34
字数 1886
阅读 3
收藏 0

精选30+云产品,助力企业轻松上云!>>>

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

 

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

o
粉丝 0
博文 500
码字总数 0
作品 0
私信 提问
加载中
请先登录后再评论。
Android音视频开发之使用AudioRecord采集音频

AudioRecord 是 Android 系统提供的用于实现录音功能的 API,官方文档是这么解释的: AndioRecord类的主要功能是让各种Java应用能够管理音频资源,以便它们通过此类能够录制声音相关的硬件所...

落英坠露
2018/10/30
0
0
Android音视频之使用MediaCodec编解码AAC

现在的短视频、音视频通话都离不开编码和解码,今天就来聊一下音频的编解码。 1. 音频的基本概念 在音频开发中,有些基本概念是需要了解的。 采样率(SampleRate):每秒采集声音的数量,它用...

落英坠露
2019/05/03
0
0
Android 音视频去回声、降噪(Android音频采集及回音消除)(转)

好久没写文章了,其实是想记录一下自己在Android开发的成长历程。谈到音视频这块,对于新手来说刚接触到这一块,那是非常非常的恶心~我自己弄这一块也是从头开始,在 网上也翻阅了无数的资料...

osc_un9pvkc0
2018/03/15
30
0
音视频编解码——LAME

一、LAME简介 LAME是目前非常优秀的一种MP3编码引擎,在业界,转码成Mp3格式的音频文件时,最常用的就是LAME库。当达到320Kbit/s时,LAME编码出来的音频质量几乎可以和CD的音质相媲美,并且还...

osc_ojx9hm4t
2018/06/15
1
0
Android 音视频深入 七 学习之路的总结和资料分享

说个实话一开始我对基于Android如何开发音视频很迷茫,甚至对音视频开发都不是很明白,我看了Android 音视频开发入门指南 https://blog.51cto.com/ticktick/1956269这篇博客,我觉得我也许不...

qq5a70561b29238
2018/02/06
0
0

没有更多内容

加载失败,请刷新页面

加载更多

面试必问之mysql基础

mysql存储引擎 如何选择mysql存储引擎 先得了解下各个存储引擎区别 功能 MylSAM MEMORY InnoDB Archive 功能 MylSAM MEMORY InnoDB Archive 存储限制 256TB RAM 64TB None 支持事务 No No Ye...

lipengxs
36分钟前
16
0
错误:将标头发送到客户端后无法设置标头 - Error: Can't set headers after they are sent to the client

问题: I'm fairly new to Node.js and I am having some issues. 我对Node.js相当陌生,遇到了一些问题。 I am using Node.js 4.10 and Express 2.4.3. 我正在使用Node.js 4.10和Express 2......

法国红酒甜
48分钟前
18
0
Spring中事务不生效的几种情况

数据库引擎不支持事务。 没有被Spring管理。 方法不是public的。 自身调用问题。 数据源没有配置事务管理器。 不支持事务。 异常被吃了。 异常类型错误。 事务失效类型: 数据库引擎不支持事...

九分石人
今天
31
0
从Linux内核理解JAVA的NIO

前言 IO 可以简单分为磁盘 IO 和 网络 IO ,磁盘 IO 相对于网络 IO 速度会快一点,本文主要介绍 磁盘 IO ,网络 IO 下周写。 JAVA 对 NIO 抽象为 Channel , Channel 又可以分为 FileChannel ...

万古云霄
今天
22
0
Material Design用在c#的wpf app中

官网:http://materialdesigninxaml.net/ 样式丰富 做网站和手机不粗 个人觉得不适合用在.net framework中,在.net core中应该大有作为。...

齐勇cn
今天
28
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部