音频数据采集-AudioRecord

一. AudioRecord 和 MediaRecorder

Android 提供了两个 API 用于录音,AudioRecord 和 MediaRecorder

  • AudioRecord:能够获取原始的 PCM 数据,实现音频数据的实时处理,PCM 音频不能直接播放,需要通过 AudioTrack 播放。
  • MediaRecorder:封装了编码器,内部集成了录音,编码等功能,但是支持的格式较少,且不能实时处理音频数据。

因此在需要处理音频数据的基础上,通常选择用 AudioRecord 来采集音频数据。

二. AudioRecord 的使用

1. 构造

构造 AudioRecord 需要几个参数:

  • int audioSource 音频源,通常使用麦克风

MediaRecorder.AudioSource.MIC

  • int sampleRateInHz 采样率,一秒钟对数据的采样次数,采样率越高,音质越好

16_000 或者 44_100 等

  • int channelConfig 音频通道,单声道或者双声道

AudioFormat.CHANNEL_IN_MONO
AudioFormat.CHANNEL_IN_STEREO

  • int audioFormat 音频格式

AudioFormat.ENCODING_PCM_16BIT
AudioFormat.ENCODING_PCM_8BIT

  • int bufferSizeInBytes 音频数据写入缓冲区的总数,可以通过AudioRecord.getMinBufferSize获取最小的缓冲区,将音频采集到缓冲区中然后再从缓冲区中读取
2. 开始录音
mAudioRecord.startRecording();
3. 读取数据
mAudioRecord.read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes);
>>audioData 写入的数组
>>offsetInBytes 偏移
>>sizeInBytes 读取的数据量

mAudioRecord.read(@NonNull short[] audioData, int offsetInShorts, int sizeInShorts);
4. 停止录音
mAudioRecord.stop();
5. 释放资源
mAudioRecord.release();

三. 封装的 DAudioRecord

package com.example.dplayer.audio;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.Log;

import androidx.annotation.IntDef;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class DAudioRecord implements IAudioRecord {
    private static final String TAG = "DAudioRecord";

    public final static int DEFAULT_INPUT = MediaRecorder.AudioSource.MIC;
    public final static int DEFAULT_SAMPLE_RATE_IN_HZ = 16_000;
    public final static int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    public final static int DEFAULT_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
    public final static int DEFAULT_BUFFER_SIZE_IN_BYTES = 2048;

    @IntDef({Status.NO_READY, Status.READY, Status.RECORDING, Status.PAUSE, Status.STOP, Status.DESTROY})
    @Retention(RetentionPolicy.SOURCE)
    @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD, ElementType.LOCAL_VARIABLE})
    public @interface Status {
        int NO_READY = 0;
        int READY = 1;
        int RECORDING = 2;
        int PAUSE = 3;
        int STOP = 4;
        int DESTROY = 5;
    }

    private final static int MSG_START = 1;
    private final static int MSG_RESUME = 2;
    private final static int MSG_PAUSE = 3;
    private final static int MSG_STOP = 4;
    private final static int MSG_DESTROY = 5;

    @Status
    private int mStatus;
    private int mAudioSource;
    private int mSampleRateInHz;
    private int mChannelConfig;
    private int mAudioFormat;
    private int mBufferSizeInBytes;
    private byte[] mBuffer;
    private AudioRecord mAudioRecord;
    private RecordCallback mRecordCallback;
    private Handler mHandler;
    private HandlerThread mHandlerThread;
    private boolean mStopReading = false;

    public interface RecordCallback {
        void onSuccess(byte[] data);

        void onError(int code, Exception e);
    }

    public interface OnStateListener {
        void onStart();
        void onPause();
        void onResume();
        void onStop();
    }

    private OnStateListener mOnStateListener;

    public void setOnStateListener(OnStateListener onStateListener) {
        mOnStateListener = onStateListener;
    }

    public void setRecordCallback(RecordCallback recordCallback) {
        mRecordCallback = recordCallback;
    }

    protected DAudioRecord(
            int audioSource,
            int sampleRateInHz,
            int channelConfig,
            int audioFormat,
            int bufferSizeInBytes,
            byte[] buffer) {
        mAudioSource = audioSource;
        mSampleRateInHz = sampleRateInHz;
        mChannelConfig = channelConfig;
        mAudioFormat = audioFormat;
        mBufferSizeInBytes = bufferSizeInBytes;
        mBuffer = buffer;
        mStatus = Status.NO_READY;
        mBufferSizeInBytes = Math.max(mBufferSizeInBytes, AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat));
        createAudioRecord();
        createThread();
    }

    private void createAudioRecord() {
        if (mStatus != Status.NO_READY) {
            Log.e(TAG, "status is not no ready!");
            return;
        }
        mAudioRecord = new AudioRecord(
                mAudioSource,
                mSampleRateInHz,
                mChannelConfig,
                mAudioFormat,
                mBufferSizeInBytes);
        mStatus = Status.READY;
        Log.i(TAG, "createAudioRecord, audio record ready");
    }

    private void createThread() {
        mHandlerThread = new HandlerThread("audio-thread");
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_START:
                        onStart();
                        break;
                    case MSG_RESUME:
                        onResume();
                        break;
                    case MSG_PAUSE:
                        onPause();
                        break;
                    case MSG_STOP:
                        onStop();
                        break;
                    case MSG_DESTROY:
                        onDestroy();
                        break;
                }
                return true;
            }
        });
    }


    @Override
    public void start() {
        mStopReading = false;
        mHandler.sendEmptyMessage(MSG_START);
    }

    private void onStart() {
        if (mStatus != Status.STOP && mStatus != Status.READY) {
            return;
        }
        mStatus = Status.RECORDING;
        while (mAudioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
            Log.w(TAG,"STATE_UNINITIALIZED");
        }
        mAudioRecord.startRecording();
        Log.i(TAG, "start");
        if (mOnStateListener != null) {
            mOnStateListener.onStart();
        }
        doRecording();
    }

    @Override
    public void resume() {
        mStopReading = false;
        mHandler.sendEmptyMessage(MSG_RESUME);
    }

    private void onResume() {
        if (mStatus != Status.PAUSE) {
            return;
        }
        mStatus = Status.RECORDING;
        mAudioRecord.startRecording();
        Log.d(TAG, "resume");
        doRecording();
    }

    @Override
    public void pause() {
        mStopReading = true;
        mHandler.sendEmptyMessage(MSG_PAUSE);
    }

    private void onPause() {
        if (mStatus != Status.RECORDING) {
            return;
        }
        mStatus = Status.PAUSE;
        mAudioRecord.stop();
        Log.d(TAG, "pause");
    }

    @Override
    public void stop() {
        mStopReading = true;
        mHandler.sendEmptyMessage(MSG_STOP);
    }

    private void onStop() {
        if (mStatus != Status.RECORDING && mStatus != Status.PAUSE) {
            return;
        }
        mStatus = Status.STOP;
        mAudioRecord.stop();
        Log.i(TAG, "stop");
    }

    @Override
    public void destroy() {
        mStopReading = true;
        mHandler.sendEmptyMessage(MSG_DESTROY);
    }

    private void onDestroy() {
        if (mAudioRecord != null) {
            mAudioRecord.release();
            mAudioRecord = null;
        }
        mStatus = Status.DESTROY;
        Log.i(TAG, "destroy");
    }

    private void doRecording() {
        try {
            onReading();
        } catch (Exception e) {
            handleError(AudioRecord.ERROR, e);
        }
    }

    private void onReading() {
        int ret;
        while (mAudioRecord != null && !mStopReading && mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING
                && mStatus == Status.RECORDING) {
            ret = mAudioRecord.read(mBuffer, 0, mBufferSizeInBytes);
            if (ret >= 0) {
                handleSuccess(mBuffer);
                continue;
            }
            handleErrorRead(ret);
        }
        Log.i(TAG,"stop reading");
    }

    private void handleErrorRead(int ret) {
        switch (ret) {
            case AudioRecord.ERROR_INVALID_OPERATION:
                handleError(ret, new Exception("the object isn't properly initialized"));
                break;
            case AudioRecord.ERROR_BAD_VALUE:
                handleError(ret, new Exception("the parameters don't resolve to valid data and indexes"));
                break;
            case AudioRecord.ERROR_DEAD_OBJECT:
                handleError(ret, new Exception("the object is not valid anymore and" +
                        "needs to be recreated. The dead object error code is not returned if some data was" +
                        "successfully transferred. In this case, the error is returned at the next read"));
                break;
            case AudioRecord.ERROR:
                handleError(ret, new Exception("unknown error"));
                break;
        }
    }

    private void handleSuccess(byte[] buffer) {
        if (mRecordCallback != null) {
            mRecordCallback.onSuccess(buffer);
        }
    }

    private void handleError(int code, Exception e) {
        if (mRecordCallback != null) {
            mRecordCallback.onError(code, e);
        }
    }

    public static final class Builder {
        private int mAudioSource;
        private int mSampleRateInHz;
        private int mChannelConfig;
        private int mAudioFormat;
        private int mBufferSizeInBytes;
        private byte[] mBuffer;

        public Builder() {
            this(DEFAULT_INPUT,
                    DEFAULT_SAMPLE_RATE_IN_HZ,
                    DEFAULT_CHANNEL_CONFIG,
                    DEFAULT_ENCODING,
                    DEFAULT_BUFFER_SIZE_IN_BYTES,
                    new byte[DEFAULT_BUFFER_SIZE_IN_BYTES]);
        }

        public Builder(
                int audioSource,
                int sampleRateInHz,
                int channelConfig,
                int audioFormat,
                int bufferSizeInBytes,
                byte[] buffer) {
            mAudioSource = audioSource;
            mSampleRateInHz = sampleRateInHz;
            mChannelConfig = channelConfig;
            mAudioFormat = audioFormat;
            mBufferSizeInBytes = bufferSizeInBytes;
            mBuffer = buffer;
        }

        public Builder setAudioSource(int audioSource) {
            mAudioSource = audioSource;
            return this;
        }

        public Builder setSampleRateInHz(int sampleRateInHz) {
            mSampleRateInHz = sampleRateInHz;
            return this;
        }

        public Builder setChannelConfig(int channelConfig) {
            mChannelConfig = channelConfig;
            return this;
        }

        public Builder setAudioFormat(int audioFormat) {
            mAudioFormat = audioFormat;
            return this;
        }

        public Builder setBufferSizeInBytes(int bufferSizeInBytes) {
            mBufferSizeInBytes = bufferSizeInBytes;
            return this;
        }

        public DAudioRecord build() {
            return new DAudioRecord(
                    mAudioSource,
                    mSampleRateInHz,
                    mChannelConfig,
                    mAudioFormat,
                    mBufferSizeInBytes,
                    mBuffer);
        }
    }
}

github Demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,273评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,349评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,709评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,520评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,515评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,158评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,755评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,660评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,203评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,287评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,427评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,122评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,801评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,272评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,393评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,808评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,440评论 2 359