AudioRecord 录制音频
MediaCodec 把录制的 PCM 流硬编为 aac (Advanced Audio Coding)
裸的 PCM 流是不能直接播放的,要加上 ADTS (Audio Data Transport Stream) 头
大部分播放器可以播放 aac
codec 5.0 以上推荐异步获取 buffer。这里用 3 个线程一个录制线程、一个 codec 线程、一个写 aac 文件的线程。用两个阻塞队列 (ArrayBlockingQueue)来做线程间的数据传递。
private static final int QUEUE_SIZE = 10;
private volatile boolean isRecording = false;
private volatile boolean isCodecComplete = false;
ArrayBlockingQueue inputQueue = new ArrayBlockingQueue<>(QUEUE_SIZE); // codec 输入队列
ArrayBlockingQueue outputQueue = new ArrayBlockingQueue<>(QUEUE_SIZE); // codec 输出队列
录制线程
class RecordRunnable implements Runnable {
int bufferSize;
RecordRunnable(int bufferSize) {
this.bufferSize = bufferSize;
}
@Override
public void run() {
byte[] buffer = new byte[bufferSize];
while (isRecording) {
int len = audioRecord.read(buffer, 0, bufferSize);
if (len <= 0) {
continue;
}
byte[] data = new byte[len];
System.arraycopy(buffer, 0, data, 0, len);
try {
Log.d(TAG, "inputQueue 等待入队");
inputQueue.put(data); // 阻塞方法
Log.d(TAG, "inputQueue 入队了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
audioRecord.stop();
audioRecord.release();
Log.d(TAG, "录制线程结束");
}
}
codec 线程(这里是主线程)
// 默认在调用它的线程中运行,这里是主线程。也可以传一个 handle 进去让它运行在 handler 所在的线程
// 异步模式需要 API Level 21(5.0) 以上
// API Level 23 (6.0) 以上才支持传 handler 参数
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
// input buffer 用完后要及时提交(queueInputBuffer)到 codec ,否则可能导致 input buffer 被占满
Log.d(TAG, "onInputBufferAvailable");
byte[] data = null;
if (isRecording) {
try {
data = inputQueue.take(); // 阻塞方法,不要过多的 take, 队列为空时 take 会阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 录制结束后如果队列非空要处理完队列中的剩余数据
Log.d(TAG, "11111111111111111 " + inputQueue.size());
data = inputQueue.poll();
}
if (data != null) {
// 只要有数据就放入 codec (无论录制是否结束)
ByteBuffer inputBuffer = codec.getInputBuffer(index);
inputBuffer.clear();
inputBuffer.put(data);
codec.queueInputBuffer(index, 0, data.length, System.currentTimeMillis() * 1000, 0);
Log.d(TAG, "codec 入队 " + Thread.currentThread().getName());
} else if (!isRecording) {
// 录制结束且队列中没有数据说明已经没有输入了,用一个带 BUFFER_FLAG_END_OF_STREAM 的空 buffer 标志输入结束
mediaCodec.queueInputBuffer(index, 0, 0, System.currentTimeMillis() * 1000, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
Log.d(TAG, "codec 最后一个空 buffer 了");
}
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
// output buffer 用完后要及时释放(releaseOutputBuffer)回 codec, 否则可能导致 output buffer 被占满
Log.d(TAG, "codec 出队 " + Thread.currentThread().getName() + " size " + info.size + " flag " + info.flags);
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
byte[] outData = new byte[info.size];
outputBuffer.get(outData);
codec.releaseOutputBuffer(index, false);
try {
Log.d(TAG, "outputQueue 等待入队");
outputQueue.put(outData);
Log.d(TAG, "outputQueue 入队");
} catch (InterruptedException e) {
e.printStackTrace();
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "flags " + info.flags);
Log.d(TAG, "codec 处理完毕了");
isCodecComplete = true;
mediaCodec.stop();
mediaCodec.release();
}
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
Log.d(TAG, "codec onError " + e.getMessage());
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
// 输出格式改变时被调用
}
});
aac 写文件线程
class WriteRunnable implements Runnable {
@Override
public void run() {
File file = new File(getExternalFilesDir(null), "demo.aac");
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (out == null) {
return;
}
try {
byte[] header = new byte[7];
while (true) {
Log.d(TAG, "write thread running");
byte[] data;
if (!isCodecComplete) {
Log.d(TAG, "outputQueue 等待出队");
data = outputQueue.take(); // 阻塞方法,不要过多的 take, 否则线程会阻塞无法退出
Log.d(TAG, "outputQueue 出队");
} else {
// codec 结束后如果队列非空要处理完队列中的剩余数据
Log.d(TAG, "222222222222222222 " + inputQueue.size());
data = outputQueue.poll();
}
if (data != null) {
// 队列非空就处理(不论 codec 是否处理完成)
addADTStoPacket(header, data.length + 7);
out.write(header);
out.write(data);
Log.d(TAG, "aac 写入文件");
} else if (isCodecComplete) {
// 队列空且 codec 结束说明所有数据都写完了
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Log.d(TAG, "aac写线程结束");
}
}
我用非阻塞方法,同步获取 codec buffer 也写了个 demo
class RecordRunnable implements Runnable {
int bufferSize;
RecordRunnable(int bufferSize) {
this.bufferSize = bufferSize;
}
@Override
public void run() {
byte[] buffer = new byte[bufferSize];
while (isRecording) {
int len = audioRecord.read(buffer, 0, bufferSize);
if (len <= 0) {
continue;
}
byte[] data = new byte[len];
System.arraycopy(buffer, 0, data, 0, len);
if (inputQueue.size() == QUEUE_SIZE) {
try {
Thread.sleep(100);
Log.d(TAG, "inputQueue 满了等待。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
inputQueue.offer(data);
Log.d(TAG, "inputQueue 入队 " + data.length);
}
}
audioRecord.stop();
audioRecord.release();
Log.d(TAG, "录制线程结束");
}
}
class CodecRunnable implements Runnable {
@Override
public void run() {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
for (; ; ) {
Log.d(TAG, "codec thread running...");
byte[] data = inputQueue.poll();
if (data != null) {
// 只要有数据就放入 codec (无论录制是否结束)
// dequeueInputBuffer 后要及时提交(queueInputBuffer)到 codec,否则 input buffer 用尽后 dequeueInputBuffer 会一直返回 -1
int inputBufferId = mediaCodec.dequeueInputBuffer(1000);
Log.d(TAG, "inputBufferId " + inputBufferId);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
inputBuffer.clear();
inputBuffer.put(data);
mediaCodec.queueInputBuffer(inputBufferId, 0, data.length, System.currentTimeMillis() * 1000, 0);
Log.d(TAG, "codec 入队 ");
} else {
Log.d(TAG, "codec 入队失败数据丢失,没有可用 buffer");
}
} else if (!isRecording) {
// 录制结束且队列中没有数据说明已经没有输入了,用一个带 BUFFER_FLAG_END_OF_STREAM 的空 buffer 标志输入结束
int inputBufferId = mediaCodec.dequeueInputBuffer(1000);
Log.d(TAG, "inputBufferId2 " + inputBufferId);
if (inputBufferId >= 0) {
mediaCodec.queueInputBuffer(inputBufferId, 0, 0, System.currentTimeMillis() * 1000, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
Log.d(TAG, "codec 最后一个空 buffer 了");
} else {
Log.d(TAG, "最后一个空 buffer 入队失败,没有可用 buffer");
}
} else {
// 录制未结束,队列为空,这里什么也不做
// 这里不要 sleep ,因为没有输入时还要处理输出呢
Log.d(TAG, "inputQueue 为空。。。");
}
// dequeueOutputBuffer 用完后要及时 releaseOutputBuffer ,否则 out buffer 会用尽后 dequeueOutputBuffer 会一直返回 -1
int outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, 1000);
Log.d(TAG, "outputBufferId " + outputBufferId);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
mediaCodec.releaseOutputBuffer(outputBufferId, false);
Log.d(TAG, "codec 出队 size " + bufferInfo.size + " flag " + bufferInfo.flags);
if (outputQueue.size() == QUEUE_SIZE) {
try {
// outputQueue 满后要等一下,否则解码后数据会丢失
// 睡一小会对 codec 线程影响不大
Thread.sleep(100);
Log.d(TAG, "outputQueue 满了等待。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
outputQueue.offer(outData);
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "flags " + bufferInfo.flags);
Log.d(TAG, "codec 处理完毕了");
isCodecComplete = true;
break;
}
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
}
}
mediaCodec.stop();
mediaCodec.release();
Log.d(TAG, "codec 线程结束");
}
}
class WriteRunnable implements Runnable {
@Override
public void run() {
File file = new File(getExternalFilesDir(null), "demo.aac");
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (out == null) {
return;
}
try {
byte[] header = new byte[7];
while (true) {
byte[] data = outputQueue.poll();
Log.d(TAG, "write thread running");
if (data != null) {
// 队列非空就处理(不论 codec 是否处理完成)
addADTStoPacket(header, data.length + 7);
out.write(header);
out.write(data);
Log.d(TAG, "aac 写入文件");
} else if (isCodecComplete) {
// 队列空且 codec 结束说明所有数据都写完了
break;
} else {
// 队列空且 codec 未结束,这时要等待一下
try {
Thread.sleep(200);
Log.d(TAG, "暂无数据 aac 写线程等待。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Log.d(TAG, "aac写线程结束");
}
}
多线程交互比较复杂,如果测试不充分很容易出现问题。经常出现线程无法退出的情况。自己水平有限,如果有 bug 请指出我再修改。
源码 https://github.com/lesliebeijing/audio_video_learn/tree/master/MediaCodecDemo