安卓录音有两种实现方式,分别用MediaRecorder和AudioRecord实现,遗憾的是两种实现方式都没有暂停的api。
1.MediaRecorder实现暂停的思路是每次点击暂停都生成一个文件,用数组保存该文件的路径,在结束的时候把数组里所有的路径对应文件拼接起来生成一个完成的录音文件,但是尝试发现MediaRecorder录制出来的每个文件都带有头文件,不知道头文件的格式合并起来的语音无法正常播放。
最终选择了AudioRecord的方式实现,AudioRecord支持实时文件流处理,并且录制出来的原始PCM格式文件是不含任务头文件的裸文件,合并数据之后就简单了。
2.主要的思路是在recoding正在录音的时候不断的读取语音文件流,不断地往文件中追加内容,当按了暂停之后停止向文件追加内容,再次点击开始的时候继续往getRecordSdcardPCMFile路径中追加内容,直至结束录音为止
while (status == recoding) {
audioRecord.read(b, 0, b.length);
//向文件中追加内容
mRandomAccessFile.seek(mRandomAccessFile.length());
mRandomAccessFile.write(b, 0, b.length);
//LogUtils.e("buff大小:" + b.length);
}
以下为完成的实现代码
public class AudioRecoderTool {
public static int recodEnd =0;//结束或者未开始
public static int recoding =1;//正在录音
public static int recodPasue =2;//暂停中
//音频输入-麦克风
private final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC;
//采用频率
//44100是目前的标准,但是某些设备仍然支持22050,16000,11025
//采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级
private final static int AUDIO_SAMPLE_RATE =16000;
//声道 单声道 // 设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道
private final static int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_STEREO;
//编码
private final static int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
// 缓冲区字节大小
private int bufferSizeInBytes =0;
//录音对象
private AudioRecord audioRecord;
//文件名
private String fileName;
//录音状态
private int status = recodEnd;
//线程池
private ExecutorService mExecutorService;
private OnAudioStatusUpdateListener audioStatusUpdateListener;
private long startTime;
public AudioRecoderTool() {
mExecutorService = Executors.newCachedThreadPool();
}
private void initAudio()
{
// 获得缓冲区字节大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL, AUDIO_ENCODING);
audioRecord =new AudioRecord(AUDIO_INPUT, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, bufferSizeInBytes);
}
private final Handler mHandler =new Handler();
private Runnable mUpdateMicStatusTimer =new Runnable() {
public void run() {
if(status == recoding)
updateMicStatus();
}
};
private int SPACE =100;// 间隔取样时间
/**
* 更新录音时长
*/
private void updateMicStatus() {
if (audioRecord !=null) {
if (null != audioStatusUpdateListener) {
audioStatusUpdateListener.onUpdate(0, System.currentTimeMillis() - startTime);
}
mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
}
}
public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
this.audioStatusUpdateListener = audioStatusUpdateListener;
}
/**
* 开始录音
*
*/
public void startRecord(final String fileName) {
this.fileName = fileName;
if (audioRecord==null) {
initAudio();
}
if(status == recoding)
{
LogUtils.e("正在录音~");
}
status = recoding;
Log.d("AudioRecorder", "===startRecord===" + audioRecord.getState());
/* 获取开始时间* */
startTime = System.currentTimeMillis();
updateMicStatus();
//使用线程池管理线程
mExecutorService.execute(new Runnable() {
@Override
public void run() {
writeDataTOFile(fileName);
}
});
}
/**
* 暂停录音
*/
public void pauseRecord() {
status = recodPasue;
if(audioStatusUpdateListener !=null)
{
audioStatusUpdateListener.onUpdate(0,System.currentTimeMillis() - startTime);
audioStatusUpdateListener.onStop(fileName);
}
}
/**
* 停止录音
*/
public void stopRecord() {
Log.d("AudioRecorder", "===stopRecord===");
if (status == recodEnd) {
LogUtils.e("录音尚未开始");
}else if(status == recodPasue){//暂停状态下直接释放资源
audioRecord.stop();
status = recodEnd;
release();
}else
{
//正在录音的状态下会在writeDataTOFile方法中结束释放
status = recodEnd;
}
}
/**
* 释放资源
*/
public void release() {
Log.d("AudioRecorder", "===release===");
if (audioRecord !=null) {
audioRecord.release();
audioRecord =null;
}
if(audioStatusUpdateListener !=null)
{
audioStatusUpdateListener.onUpdate(0,System.currentTimeMillis() - startTime);
audioStatusUpdateListener.onStop(fileName);
}
status = recodEnd;
}
/**
* 取消录音
*/
public void canel() {
fileName =null;
if (audioRecord !=null) {
audioRecord.release();
audioRecord =null;
}
status = recodEnd;
}
/**
* 将音频信息写入文件
*
*/
private void writeDataTOFile(String currentFileName) {
RandomAccessFile mRandomAccessFile =null;
try {
mRandomAccessFile =new RandomAccessFile(new File(
FileUtils.getRecordSdcardPCMFile(currentFileName)), "rw");
byte[] b =new byte[bufferSizeInBytes /4];
//开始录制音频
audioRecord.startRecording();
//判断是否正在录制
status = recoding;
while (status == recoding) {
audioRecord.read(b, 0, b.length);
//向文件中追加内容
mRandomAccessFile.seek(mRandomAccessFile.length());
mRandomAccessFile.write(b, 0, b.length);
//LogUtils.e("buff大小:" + b.length);
}
//停止录制
audioRecord.stop();
mRandomAccessFile.close();
//释放资源
if(status == recodEnd)
release();
LogUtils.e("保存成功");
}catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}catch (IOException e) {
// TODO Auto-generated catch block
LogUtils.e("异常关闭文件流");
e.printStackTrace();
}
}
public interface OnAudioStatusUpdateListener {
/**
* 录音中...
*
* @param db 当前声音分贝
* @param time 录音时长
*/
public void onUpdate(double db, long time);
/**
* 停止录音
*
* @param filePath 保存路径
*/
public void onStop(String filePath);
}
}
因为录制出来的是PCM文件,无法直接播放,为了和IOS端兼容并且考虑文件大小的问题,选用相同时间内容较小的.aac格式。
这里使用的是android-aac-enc这个库,可以在git上搜索下载就行了。
因为是C库,需要NDK编译jni文件夹才可以正常使用,这里附上生成jni中头文件的命令好像在JDK10版本之后取消了javah的命令了,这里生成头文件使用java xxxx.java -h . 命令生成.h头文件,注意要在xxxx.java文件根目录下运行。
把上面生成的.h文件(下面红色箭头所指的文件)拖到jni根目录下
生成的文件内容是长这个样的
之后再在jnl根目录下创建c文件
这里我创建了和jnl原本库里面文件名相同的aac-enc.c如果想自己创建文件名请修改Android.mk中下图红色箭头的文件名。
到此就可以使用ndk-build命令编译生成.os文件了。
之后使用以下方法就可以把PCM录音文件转成.aac文件了
encoder.init(32000, 2, 16000, 16, FileUtils.getRecordSdcardAACFile(currentRptBean.getVoiceFile()));
//对二进制代码进行编码
encoder.encode(b);
//编码完成
encoder.uninit();