在实现录制音频需求的过程中的一些笔记,参考了很多有用的文章,希望能帮到他人
Android 系统 Java 层提供两个 Recorder Api, MediaRecorder 与 AudioRecorder,前者能够生成编码后的录音文件,而后者则是 PCM Audio RAW Data,我们希望通过对 PCM 的操作,根据需求完成任何操作。
AudioRecorder 基本使用方法
- 在 Recorder Thread 中创建 Recorder 与相关的 Encoder
- loop 读取 Recorder 里面的 PCM data,不断地将 PCM 喂入 Encoder中
- 外部停止录音后,将 run 这个 flag 置为 false 跳出循环,并且 close 相关 Encoder 并且保存他们的结果。
- release 相关资源。
遇到的问题:
在 loop 中 Encoder的process block 时间太长,会导致来不及读取 AudioRecorder buffer,最终导致录制的音频丢失数据
为了支持更多的音频格式,Recorder 就需要挂载不同的 Encoder
所有encode的流程是相仿的,如初始化阶段,编码阶段,结束阶段,释放资源阶段。只是其中某些的具体实现步骤不同,因此通过AudioProcessor接口按照流程抽离出具体实现类encoder,将其注入到Recorder中,我们只需要挂载新的 Encoder,即可编码成我们所需要的文件格式
解决方法:
所以我们创建一个ProcessThread, 让 Encoder 在 ProcessThread 中执行,这样 RecoderThread 不会因为 Encoder 而 block 导致数据丢失。
由于项目的需要,要把pcm转码为aac音频,目前大致两种方案,FFmpeg和MediaCodec,我们这里使用MediaCodec
接下来就是具体的encode阶段,先初始化编码器 ,和解码器的MediaFormat直接在音频文件内获取不同,编码器的MediaFormat需要自己来创建,对MediaCodec还不是很了解的同学,可以参考这篇文章http://www.jianshu.com/p/30e596112015
下文引用了很多部分
用编码器把PCM转为AAC
我们在AudioProcessor的初始化方法中,完成创建输出文件和初始化编码器的步骤.创建了一个MediaCodec对象,此时MediaCodec处于Uninitialized状态。首先,需要使用configure(…)方法对MediaCodec进行配置,这时MediaCodec转为Configured状态。然后调用start()方法使其转入Executing状态。
@Override
public void start() {
try {
fos = new FileOutputStream(filePath);
bos = new BufferedOutputStream(fos, 200 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
try {
MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 16000, 1);//参数对应-> mime type、采样率、声道数
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128 * 100);//比特率
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16*1024);
codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
codec.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
e.printStackTrace();
}
if (codec == null) {
Log.e(TAG, "create mediaEncode failed");
return;
}
//调用MediaCodec的start()方法,此时MediaCodec处于Executing状态
codec.start();
}
初始化结束我们就可以loop喂数据给我们的AudioProcessor,AudioProcessor会调用我们的这个方法,进行具体的编码工作.
Executing状态包含三个子状态: Flushed、 Running 以及End-of-Stream。
- 在调用start()方法后MediaCodec立即进入Flushed子状态,此时MediaCodec会拥有所有的缓存。
- 一旦第一个输入缓存(input buffer)被移出队列,MediaCodec就转入Running子状态,这种状态占据了MediaCodec的大部分生命周期。
- 当你将一个带有end-of-stream marker标记的输入缓存入队列时,MediaCodec将转入End-of-Stream子状态。在这种状态下,MediaCodec不再接收之后的输入缓存,但它仍然产生输出缓存直到end-of- stream标记输出。
@Override
public void flow(byte[] bytes, int size) {
int inputIndex;
ByteBuffer inputBuffer;
int outputIndex;
ByteBuffer outputBuffer;
byte[] chunkAudio;
int outBitSize;
int outPacketSize;
//通过getInputBuffers()方法和getOutputBuffers()方法获取缓存队列
encodeInputBuffers = codec.getInputBuffers();
encodeOutputBuffers = codec.getOutputBuffers();
//用于存储ByteBuffer的信息
encodeBufferInfo = new MediaCodec.BufferInfo();
//首先通过dequeueInputBuffer(long timeoutUs)请求一个输入缓存,timeoutUs代表等待时间,设置为-1代表无限等待
int inputBufferIndex = codec.dequeueInputBuffer(-1);
//返回的整型变量为请求到的输入缓存的index,通过getInputBuffers()得到的输入缓存数组,再用index和输入缓存数组即可得到当前请求的输入缓存
if (inputBufferIndex >= 0) {
inputBuffer = encodeInputBuffers[inputBufferIndex];
//使用之前要clear一下,避免之前的缓存数据影响当前数据
inputBuffer.clear();
//把数据添加到输入缓存中,
inputBuffer.put(bytes);
//并调用queueInputBuffer()把缓存数据入队
codec.queueInputBuffer(inputBufferIndex, 0, size, 0, 0);
}
//通过dequeueOutputBuffer(BufferInfo info, long timeoutUs)来请求一个输出缓存,传入一个上面的BufferInfo对象
outputIndex = codec.dequeueOutputBuffer(encodeBufferInfo, 10000);
//然后通过返回的index得到输出缓存,并通过BufferInfo获取ByteBuffer的信息
while (outputIndex >= 0) {
outBitSize = encodeBufferInfo.size;
//添加ADTS头,ADTS头包含了AAC文件的采样率、通道数、帧数据长度等信息。
outPacketSize = outBitSize + 7;//7为ADTS头部的大小
outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
outputBuffer.position(encodeBufferInfo.offset);
outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
chunkAudio = new byte[outPacketSize];
addADTStoPacket(chunkAudio, outPacketSize);//添加ADTS 代码后面会贴上
outputBuffer.get(chunkAudio, 7, outBitSize);//将编码得到的AAC数据 取出到byte[]中偏移量offset=7
outputBuffer.position(encodeBufferInfo.offset);
//showLog("outPacketSize:" + outPacketSize + " encodeOutBufferRemain:" + outputBuffer.remaining());
try {
bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 将文件保存到内存卡中 *.aac
} catch (IOException e) {
e.printStackTrace();
}
//releaseOutputBuffer方法必须调用
codec.releaseOutputBuffer(outputIndex, false);
outputIndex = codec.dequeueOutputBuffer(encodeBufferInfo, 10000);
}
}
/**
* 添加ADTS头
*
* @param packet
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int freqIdx = 8; // 44.1KHz
int chanCfg = 1; // CPE
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
结束MediaCodec,并释放掉占用资源
@Override
public void end() {
try {
if (bos != null) {
bos.flush();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
bos=null;
}
}
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
fos=null;
}
if (codec != null) {
codec.stop();
codec.release();
codec=null;
}
}
参考:
https://juejin.im/entry/58fd31b75c497d005802c5e3
http://www.jianshu.com/p/30e596112015