对 AudioRecord 封装,内部管理录音状态,并读取数据到 ByteBuffer 通知外层获取录音数据。
AudioRecord 封装代码如下:
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import com.alan.audioio.audio.common.IDataAvailableListener;
import com.alan.audioio.audio.common.IOStatus;
import com.alan.audioio.audio.exception.AudioException;
import java.nio.ByteBuffer;
/**
* Author: AlanWang4523.
* Date: 2019-06-14 22:59.
* Mail: alanwang4523@gmail.com
*/
public class AudioRecorder {
private final static String TAG = AudioRecorder.class.getSimpleName();
private IDataAvailableListener mDataAvailableListener;
private volatile @IOStatus int mStatus;
private final Object mLock = new Object();
private Thread mWorkThread;
private AudioRecord mAudioRecord;
private ByteBuffer mDataBuffer;
private int mBufferSizePerFrame;
public AudioRecorder() {
mStatus = IOStatus.UNINITIATED;
}
/**
* 设置 IDataAvailableListener
* @param dataAvailableListener 用于处理采集到的音频数据
*/
public void setDataAvailableListener(IDataAvailableListener dataAvailableListener) {
this.mDataAvailableListener = dataAvailableListener;
}
/**
* 初始化
* @param sampleRateInHz 采样率
* @param channelCount 通道数
* @param bufferSize bufferSize
* @throws AudioException AudioException
*/
public void init(int sampleRateInHz, int channelCount, int bufferSize) throws AudioException {
try {
if (bufferSize <= 0) {
throw new AudioException("The buffer size must be greater than 0!", null);
}
int channelConfig = channelCount == 2 ?
AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 默认采样 short 型格式
int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRateInHz, channelConfig, audioFormat, minBufferSize);
mBufferSizePerFrame = bufferSize;
mDataBuffer = ByteBuffer.allocateDirect(mBufferSizePerFrame);
mStatus = IOStatus.INITIATED;
} catch (Exception e) {
throw new AudioException("Init AudioRecorder Failed!", e);
}
}
/**
* 开始录音
*/
public void start() {
synchronized (mLock) {
if (mStatus == IOStatus.INITIATED) {
mWorkThread = new Thread(null, new WorkRunnable(),
TAG + "-" + System.currentTimeMillis());
mStatus = IOStatus.START;
mWorkThread.start();
} else if (mStatus == IOStatus.PAUSE) {
mAudioRecord.startRecording();
mStatus = IOStatus.RESUME;
mLock.notify();
} else if (mStatus == IOStatus.RESUME) {
return;
} else {
throw new IllegalStateException();
}
}
}
/**
* 暂停录音
*/
public void pause() {
synchronized (mLock) {
if (mStatus == IOStatus.START || mStatus == IOStatus.RESUME) {
mAudioRecord.stop();
mStatus = IOStatus.PAUSE;
}
}
}
/**
* 恢复录音
*/
public void resume() {
synchronized (mLock) {
if (mStatus == IOStatus.PAUSE) {
mAudioRecord.startRecording();
mStatus = IOStatus.RESUME;
mLock.notify();
}
}
}
/**
* 停止录音
*/
public void stop() {
synchronized (mLock) {
if (mStatus == IOStatus.START || mStatus == IOStatus.RESUME) {
mStatus = IOStatus.STOP;
// 需要调用 notify,避免在 pause 状态调用 stop 时,work thread 还在 wait
mLock.notify();
}
}
}
/**
* 释放资源
*/
public void release() {
synchronized (mLock) {
// 如果初始化后还没开始录制则释放资源
if (mStatus == IOStatus.INITIATED) {
mAudioRecord.release();
} else {
mStatus = IOStatus.STOP;
// 需要调用 notify,避免在 pause 状态调用 release 时,work thread 还在 wait
mLock.notify();
}
}
// 等待工作线程结束
if (mWorkThread != null) {
try {
mWorkThread.join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.mDataAvailableListener = null;
}
private class WorkRunnable implements Runnable {
@Override
public void run() {
int readLen;
int totalReadLen;
int needReadLen;
mAudioRecord.startRecording();
while (true) {
// 状态处理
synchronized (mLock) {
while (mStatus == IOStatus.PAUSE) {
try {
mLock.wait();
} catch (InterruptedException e) {
// do nothing
}
}
if (mStatus == IOStatus.STOP || mStatus == IOStatus.UNINITIATED) {
break;
}
}
ByteBuffer dataBuffer = mDataBuffer;
// 从 AudioRecord 中读取指定数量(mBufferSizePerFrame)的音频数据
totalReadLen = 0;
needReadLen = mBufferSizePerFrame;
dataBuffer.position(totalReadLen);
do {
readLen = mAudioRecord.read(dataBuffer.array(),
dataBuffer.arrayOffset() + totalReadLen, needReadLen);
if (readLen > 0) {
needReadLen -= readLen;
totalReadLen += readLen;
}
} while ((needReadLen > 0) && (readLen >= 0));
dataBuffer.limit(totalReadLen);
dataBuffer.rewind();
if (totalReadLen >= 0 && mDataAvailableListener != null) {
// 通知外层可以取录音数据
mDataAvailableListener.onDataAvailable(dataBuffer);
}
}
try {
mAudioRecord.stop();
} catch (IllegalStateException e) {
e.printStackTrace();
}
try {
mAudioRecord.release();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
}
IOStatus 状态定义:
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Author: AlanWang4523.
* Date: 2020/11/10 21:19.
* Mail: alanwang4523@gmail.com
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
IOStatus.UNINITIATED, IOStatus.INITIATED,
IOStatus.START, IOStatus.PAUSE,
IOStatus.RESUME, IOStatus.STOP})
public @interface IOStatus {
int UNINITIATED = -1;
int INITIATED = 0;
int START = 1;
int PAUSE = 2;
int RESUME = 3;
int STOP = 4;
}
录音数据回调:
import java.nio.ByteBuffer;
/**
* Author: AlanWang4523.
* Date: 2020/11/10 21:40.
* Mail: alanwang4523@gmail.com
*/
public interface IDataAvailableListener {
/**
* 有数据到来
* @param byteBuffer 具体数据
* 如果是来自采集端可以从 byteBuffer 读取数据,有效数据长度为 byteBuffer.limit()
* 如果是来自播放端可以往 byteBuffer 写入数据,写完后调用 byteBuffer.limit(count) 设置有效数据长度
*/
void onDataAvailable(ByteBuffer byteBuffer);
}
测试代码:
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.alan.audioio.R;
import com.alan.audioio.audio.AudioRecorder;
import com.alan.audioio.audio.WavFile;
import com.alan.audioio.audio.common.IDataAvailableListener;
import com.alan.audioio.audio.exception.AudioException;
import com.alan.audioio.utils.ALog;
import java.io.IOException;
import java.nio.ByteBuffer;
import androidx.appcompat.app.AppCompatActivity;
public class TestRecordToWavActivity extends AppCompatActivity implements View.OnClickListener {
public static void launchMe(Context context) {
context.startActivity(new Intent(context, TestRecordToWavActivity.class));
}
private String mVocalSavePath = "/sdcard/Alan/audio/record_wrapper.wav";
private AudioRecorder mAudioRecorder;
private WavFile mWavFile;
private TextView mBtnCommonTest;
private boolean mIsStartTest = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_common_test);
mBtnCommonTest = findViewById(R.id.btnCommonTest);
mBtnCommonTest.setOnClickListener(this);
mBtnCommonTest.setText("StarTest");
}
@Override
protected void onDestroy() {
if (mIsStartTest) {
mIsStartTest = false;
}
releaseRecorder();
super.onDestroy();
}
@Override
public void onClick(View view) {
if (view.getId() == R.id.btnCommonTest) {
if (!mIsStartTest) {
mIsStartTest = true;
mBtnCommonTest.setText("Stop");
ALog.e("onClick::startTest--------------------->>");
initRecorder();
mAudioRecorder.start();
} else {
mIsStartTest = false;
mBtnCommonTest.setText("StarTest");
mAudioRecorder.stop();
}
}
}
private void initRecorder() {
int sampleRate = 44100;
int channelCount = 1;
int bufferSize = 1024;
WavFile.HeadInfo headInfo = WavFile.HeadInfo.build()
.setSampleRate(sampleRate)
.setChannelCount(channelCount)
.setBytePerSample(2);
try {
mWavFile = new WavFile(mVocalSavePath, headInfo);
mAudioRecorder = new AudioRecorder();
mAudioRecorder.init(sampleRate, channelCount, bufferSize);
mAudioRecorder.setDataAvailableListener(new IDataAvailableListener() {
@Override
public void onDataAvailable(ByteBuffer byteBuffer) {
try {
mWavFile.write(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit());
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (AudioException | IOException e) {
e.printStackTrace();
Toast.makeText(this, "init failed.", Toast.LENGTH_SHORT).show();
finish();
}
}
public void releaseRecorder() {
if (mAudioRecorder != null) {
mAudioRecorder.release();
}
if (mWavFile != null) {
try {
mWavFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
WavFile 封装见:Android Audio Wav 文件读写操作的封装
完整代码见:AudioIO