音视频开发【3】-android-AudioRecord录制PCM音频

先说下题外话哈,最近做了个领取电商平台优惠券的小程序,扫码支持下哈~


image

android平台上录制音频主要有两种方式,MediaRecorder、AudioRecord。

  • MediaRecorder 封装的层次比较高,可以直接将手机麦克风录入的音频数据进行编码压缩并存储,生成如AMR、MP3等音频文件。
  • AudioRecord 接近底层,录制的数据是PCM格式的原始音频裸数据,可以对其做进一步的算法处理、编码压缩等应用,能够自由进行开发控制。

音频开发应用场景很多,仅仅录制保存满足不了大部分实际需求,所以,掌握 AudioRecord 的使用是必须的,本文针对AudioRecord的使用进行总结。

AudioRecord 的使用主要有以下几个步骤:

  1. 初始化AudioRecord
  2. 初始化音频数据buffer
  3. 开启采集
  4. 启动新线程,从音频数据buffer中读取音频数据
  5. 停止采集、释放资源

初始化AudioRecord


    public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
  • audioSource:音频源,在类 MediaRecorder.AudioSource 中以常量的形式定义,常用的有DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),VOICE_COMMUNICATION(用于VoIP应用)等。
  • sampleRateInHz:音频采样率,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。
  • channelConfig:音频通道数,在类AudioFormat中以常量的形式定义,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道),注意,前者能够兼容所有android手机。
  • audioFormat:音频数据位宽(量化精度),在类AudioFormat中以常量的形式定义,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保证兼容所有Android手机。
  • bufferSizeInBytes:AudioRecord内部音频缓冲区大小,不能低于一帧音频的大小,而一帧音频的大小(int size = 采样率 x 位宽 x 采样时间 x 通道数)与音频采样时间有关,一般取 2.5ms~120ms 之间,不同厂商取值不同,所以AudioRecord类提供了 int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) 函数来获取改值,实际开发中,强烈建议由该函数计算出需要传入的 bufferSizeInBytes,而不是自己手动计算。虽然不同的厂商的底层实现是不一样的,但无外乎就是根据上面的计算公式得到一帧的大小,音频缓冲区的大小则必须是一帧大小的2~N倍。

初始化音频数据buffer

通过前面获取的 bufferSizeInBytes ,初始化读取音频数据的buffer。

final byte[] data = new byte[minBufferSize];

开启采集

调用 AudioRecord 实例的 startRecording() 方法,开始采集音频

特别注意:开始采集后,要尽快通过音频读取数据取走音频,一旦缓冲区中的数据大于前面设置的bufferSizeInBytes,会发生 over-running

启动新线程,从音频数据buffer中读取音频数据

开启新线程,调用 AudioRecord 实例的 public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) 方法,获取音频数据。

停止采集、释放资源

调用 AudioRecord 实例的 stop()release() 方法,将改实例置为 null。

示例代码

别忘了配录音权限

public class MainActivity extends AppCompatActivity {
    //采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
    public static final int SAMPLE_RATE_INHZ = 44100;
    //声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
    public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    //返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
    public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;

    Button btnBegin;
    Button btnEnd;

    private AudioRecord audioRecord;
    private boolean isRecording;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnBegin = (Button) findViewById(R.id.btnBegin);
        btnEnd = (Button) findViewById(R.id.btnEnd);

        btnBegin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                beginRecord();
            }
        });

        btnEnd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                endRecorde();
            }
        });
    }

    private void beginRecord() {
        final int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT);
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT, minBufferSize);
        final byte[] data = new byte[minBufferSize];
        audioRecord.startRecording();
        isRecording = true;

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (isRecording) {
                    int read = audioRecord.read(data, 0, minBufferSize);
                    if (read != AudioRecord.ERROR_INVALID_OPERATION) {
                        //处理音频数据 data
                    }
                }
            }
        }).start();
    }

    private void endRecorde() {
        isRecording = false;
        if (audioRecord != null) {
            audioRecord.stop();
            audioRecord.release();
            audioRecord = null;
        }
    }
}

封装

参考网上相关资源,这里给出一个简单的AudioRecord录音的封装类:AudioRecorder。

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;

/**
 * Created by wangyt on 2018/10/26
 */
public class AudioRecorder {

    private static final String TAG = "AudioRecorder";

    public interface OnAudioDataArrivedListener {
        void onAudioDataArrived(byte[] audioData);
    }

    //声源
    private static final int DEFFAULT_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    //采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
    private static final int DEFAULT_SAMPLE_RATE_INHZ = 44100;
    //声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
    private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    //返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
    private static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    //内部缓冲区大小
    private int minBufferSize = 0;
    //是否已启动录音
    private boolean isStarted = false;
    //是否可以从缓冲区中读取数据
    private boolean canReadDataFromBuffer = true;
    //从缓冲区中读取数据的回调方法
    private OnAudioDataArrivedListener onAudioDataArrivedListener;

    private AudioRecord audioRecord;


    public boolean startRecord() {
        return startRecord(DEFFAULT_AUDIO_SOURCE,
                DEFAULT_SAMPLE_RATE_INHZ,
                DEFAULT_CHANNEL_CONFIG,
                DEFAULT_AUDIO_FORMAT);
    }

    public boolean startRecord(int audioSource, int sampleRate, int channel, int audioFormat) {
        if (isStarted) {
            Log.e(TAG, "startRecord: AudioRecorder has been already started");
            return false;
        }

        //获取内部缓冲区最小size
        minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, audioFormat);
        if (minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
            Log.e(TAG, "startRecord: minBufferSize is error_bad_value");
            return false;
        }
        Log.d(TAG, "startRecord: minBufferSize = " + minBufferSize + "bytes");

        //初始化 audioRecord
        audioRecord = new AudioRecord(audioSource, sampleRate, channel, audioFormat, minBufferSize);
        if (audioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
            Log.e(TAG, "startRecord: audioRecord is uninitialized");
            return false;
        }

        //启动录制
        audioRecord.startRecording();

        //可以从内部缓冲区中读取数据
        canReadDataFromBuffer = true;

        //启动子线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (canReadDataFromBuffer){
                    //初始化缓冲区数据接收数组
                    byte[] data = new byte[minBufferSize];

                    //读取内部缓冲区中读取数据
                    int result = audioRecord.read(data, 0, minBufferSize);

                    if (result == AudioRecord.ERROR_BAD_VALUE){
                        Log.e(TAG, "run: audioRecord.read result is ERROR_BAD_VALUE");
                    }else if (result == AudioRecord.ERROR_INVALID_OPERATION){
                        Log.e(TAG, "run: audioRecord.read result is ERROR_INVALID_OPERATION");
                    }else {
                        if (onAudioDataArrivedListener != null){
                            //调用读取数据回调方法
                            onAudioDataArrivedListener.onAudioDataArrived(data);
                        }
                        Log.d(TAG, "run: audioRecord read " + result + "bytes");
                    }
                }
            }
        }).start();

        //设置录音已启动
        isStarted = true;
        Log.d(TAG, "startRecord: audioRecorder has been already started");
        return true;
    }

    public void stopRecord(){
        //如果录音尚未启动,直接返回
        if (!isStarted) return;
        //设置内部缓冲区数据不可读取
        canReadDataFromBuffer = false;
        //停止录音
        if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING){
            audioRecord.stop();
        }
        //释放资源
        audioRecord.release();
        //设置录音未启动
        isStarted = false;
        //回调置为空
        onAudioDataArrivedListener = null;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容