最近完成了一个需求,录制一段音频,并且在界面上绘制对应的波形图,分享一下我的心路历程
首先,Android提供录制音频的有两个类。MediaRecorder和AudioRecord,这两个类都可以实现录制音频的功能,但又有一定的区别。前者封装度较高,也就意味着使用简单,设置几个对应的录制的参数,设置输出文件的路径,开始录制,结束录制,就可以得到一个音频文件。后者的优势在于,录制的过程中会把read和write的过程暴露出来,我们拿到这些数据,就可以进行一些比较复杂的需求,比如录制的同时把声音的波形图绘制出来
结合上面的分析,实现这个需求我自然要使用AudioRecord,下来我们结合实例谈一谈AudioRecord的使用。
首先,我先概括的总结一下AudioRecord的使用,1.初始化AudioRecord对象,2. 创建一个buffer数组和输出文件,3.开始录制,4,结束录制。
下面,我将结合具体的代码,详细的解释一下这四个步骤。
在这里,我还想谈一下代码封装,我们可以把AudioRecord的初始化放在Activity中,但这就意味着相关代码与Activity严重耦合,一方面不利于Activity代码的可读性,另一方面如果另一个Activity需要录音功能,就会需要copy大量相同的代码,所以这里我选择封装一个AudioRecordManager类。
1.初始化AudioRecord对象
private static final int FREQUENCY = 16000;// 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
private static final int CHANNELCONGIFIGURATION = AudioFormat.CHANNEL_IN_MONO;// 设置单声道声道
private static final int AUDIOENCODING = AudioFormat.ENCODING_PCM_16BIT;// 音频数据格式:每个样本16位
public final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;// 音频获取源
private int recBufSize;// 录音最小buffer大小
private AudioRecord audioRecord;
private void init() {
recBufSize = AudioRecord.getMinBufferSize(FREQUENCY,
CHANNELCONGIFIGURATION, AUDIOENCODING);// 录音组件
audioRecord = new AudioRecord(AUDIO_SOURCE,// 指定音频来源,这里为麦克风
FREQUENCY, // 16000HZ采样频率
CHANNELCONGIFIGURATION,// 录制通道
AUDIOENCODING,// 录制编码格式
recBufSize);// 录制缓冲区大小 //先修改
}
这里的代码无需赘述,就是通过设置几个参数来创建一个AudioRecord对象,值得注意的是有一个音频输出文件的格式,我们这里输出的是PCM文件,这是一种非常基础的音频文件,也就是说理论上他可以转成各种我们常用的音频格式,mp3,aac,wav都是可以的,另一方面,由于它非常基础,它本身是不能直接播放的。
2.创建一个buffer数组和输出文件
这里具体代码我就不贴上来了,比较简单,我说一下他们的作用,buffer数组,是用来存储我们每次从micro phone里面读取的数据的,读到了数据之后就写入到我们创建的输出文件中,这样,停止录制的时候,我们就可以得到一个音频文件了。
3.开始录制
开始录制的时候具体做了以下几个步骤
- 调用audioRecord.startRecording(),开始录制音频
- 将标识位isRecording设置为true
- 开启子线程,只要isRecording为true,则不停的调用audioRecord.read方法读取数据
- 开启另一个子线程,将读取到的数据写入我们上一步中创建的音频文件
- 从每次read到的数据中抽取一小部分,将这些数据加工之后绘制到界面上
这里之所以开启两个子线程,是为了读操作与写操作的分离,这样绘制界面的时候能够得到更加流程的体验。因为ui是只关心read这个操作的,如果不分离,write这个过程就会阻塞下一次读到的数据的绘制,造成页面不流畅。
4.结束录制
调用audioRecord.stop(),停止实际的录制动作,然后将标识位isRecording设置为false,停掉while循环,也就是结束了不停读取数据的子线程。
最后我把完整的代码放在下面,给大家参考学习
public class AudioRecordManager {
private static final int FREQUENCY = 16000;// 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
private static final int CHANNELCONGIFIGURATION = AudioFormat.CHANNEL_IN_MONO;// 设置单声道声道
private static final int AUDIOENCODING = AudioFormat.ENCODING_PCM_16BIT;// 音频数据格式:每个样本16位
public final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;// 音频获取源
private int recBufSize;// 录音最小buffer大小
private AudioRecord audioRecord;
private boolean isRecording;
private int read;
private FileOutputStream os;
public boolean isRecording() {
return isRecording;
}
public void setRecording(boolean recording) {
isRecording = recording;
}
private void init() {
recBufSize = AudioRecord.getMinBufferSize(FREQUENCY,
CHANNELCONGIFIGURATION, AUDIOENCODING);// 录音组件
audioRecord = new AudioRecord(AUDIO_SOURCE,// 指定音频来源,这里为麦克风
FREQUENCY, // 16000HZ采样频率
CHANNELCONGIFIGURATION,// 录制通道
AUDIOENCODING,// 录制编码格式
recBufSize);// 录制缓冲区大小 //先修改
}
public String start(Observer> observer) {
if (audioRecord == null) {
init();
}
audioRecord.startRecording();
isRecording = true;
File dir = new File(Environment.getExternalStorageDirectory());
if (!dir.exists()) {
dir.mkdirs();
}
String filePath = dir + "/audio_" + System.currentTimeMillis() + ".pcm";
try {
os = new FileOutputStream(filePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Observable.create((ObservableOnSubscribe) emitter -> {
//构造读取数据的数组
short data[] = new short[recBufSize];
while (isRecording) {
//读取数据
read = audioRecord.read(data, 0, recBufSize);
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
//如果读取的数据有效,发射数据
emitter.onNext(data);
}
}
emitter.onComplete();
}).flatMap(new Function>() {
@Override
public ObservableSource apply(short[] shorts) throws Exception {
return Observable.create(new ObservableOnSubscribe() {
@Override
public void subscribe(ObservableEmitter e) throws Exception {
e.onNext(shorts);
byte[] bytes = new byte[read * 2];
for (int i = 0; i < read; i++) {
byte ss[] = getBytes(shorts[i]);
bytes[i * 2] = ss[0];
bytes[i * 2 + 1] = ss[1];
}
os.write(bytes);
}
}).subscribeOn(Schedulers.io());
}
}).map(new Function>() {
@Override
public List apply(short[] shorts) throws Exception {
List list = new ArrayList<>();
for (int i = 0; i < read; i += 500) {
list.add(shorts[i]);
}
return list;
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(observer);
return filePath;
}
public void stop() {
if (audioRecord == null)
return;
audioRecord.stop();
isRecording = false;
}
public void release() {
if (audioRecord == null)
return;
audioRecord.release();
audioRecord = null;
}
public static byte[] getBytes(short s) {
byte[] buf = new byte[2];
for (int i = 0; i < buf.length; i++) {
buf[i] = (byte) (s & 0x00ff);
s >>= 8;
}
return buf;
}
}