Pcm
格式的音频文件,在一般的播放器上是无法播放的,需要将其编码,本章就来学习如何对音频进行编码
一、MediaCodec
Android中,有一个名叫MediaCodec
的组件,它能够将原始数据编码成指定格式,也能够将编码格式解码为原始格式
它还能使用Surface
作为输入或者输出进行编码和解码,不过在音频中,主要还是直接使用其编码或解码的数据进行相关操作
1.1 简单介绍
先放一张Google
官网上的图
在图中我们可以看到:
- 分为了两个端,一个是
客户端
,表示调用者
,一个是服务端
,表示编解码器
调用者
先从编解码器
中请求,或者说是获取一个空的buffer
,得到这个空的buffer
后,将数据装入这个空的buffer
送入编解码器
编解码器
得到调用者
传送过来的数据,会将进行对应的处理调用者
再从编解码器
中获取输出的buffer
,此时的buffer就是处理完成的buffer,调用者
使用完成后,清空
其中的数据,并返还给编解码器
调用者在传入数据到MediaCodec
后,无需关心MediaCodec
是如何处理的,只需要关心数据是否正常即可
下面来介绍下MediaCodec
的使用方式
1.2 使用方式
MediaCodec
有两种使用方式,分别是:
- 同步方式
- 异步方式
具体的流程如下(来自Google
官网):
同步方式
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
可以看到,同步的使用方式有以下几个步骤
-
创建实例
-
配置
MediaCodec
-
启动
MediaCodec
-
开启
死循环
- 获取输入的
ByteBuffer
,填充数据后,调用queueInputBuffer
方法插入编解码器 - 获取输出的
ByetBuffer
,获取编解码后的数据,并做对应的业务处理,调用releaseOutputBuffer
方法释放该ByteBuffer
数据
- 获取输入的
-
停止编解码,释放资源
在死循环
中,我们可以使用一个标志位
来控制是否退出循环,即退出编解码
异步方式
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
可以看到,异步方式
和同步方式
存在的不同:
异步方式
在配置MediaCodec
时,会设置一个回调- 回调中,主要是在
onInputBufferAvailable
和onOutputBufferAvailable
- 在
onInputBufferAvailable
中执行插入数据的逻辑 - 在
onOutputBufferAvailable
中执行处理编解码后数据的逻辑 - 最后同样是释放资源
在了解了MediaCodec
后,下面就开始进入对Pcm
的编码
二、音频编码
前面章节中,介绍了如何使用AudioRecord
进行Pcm
数据的采集,本章同样会使用到,不过,不同的是,在采集到Pcm
数据后,需要送入编码器,进行编码操作,最终获取可以在一般设备上播放的aac
文件
回顾下之前AudioRecord
的采集Pcm
步骤
- 开启子线程
- 构建实例
- 开始录制
- 循环从
AudioRecord
读取数据,写入文件
- 停止录制,释放资源
要进行编码的话,写入文件的数据应该是编码后的数据,所以步骤如下:
- 开启子线程
- 构建实例
- 开始录制、编码
- 循环从
AudioRecord
读取数据,送入编码器 - 从编码器中取出编码后的数据,写入文件
- 停止录制、编码,释放资源
因为之前AudioRecord
是通过死循环的方式不断读取数据,那么此次编码就采用同步方式
下面,就上述步骤,一一讲解
2.1 开启子线程
同样,编码操作也需要在子线程中执行
private static class EncodeThread extends Thread {
public EncodeThread(){}
}
2.2 配置必要参数
类似于AudioRecord
,进行一些参数配置
private static class EncodeThread extends Thread {
private static final long TIMEOUT_MS = 2000L;
private AudioRecord audioRecord;
private MediaCodec mediaCodec;
/**
* 文件输出
*/
private FileOutputStream fos;
private final String path;
/**
* 声源(一般是来自麦克风)
*/
private final int audioSource;
/**
* 采样率
*/
private final int sampleRateInHz;
/**
* 声道设置
*/
private final int channelConfig;
/**
* 编码格式
*/
private final int audioFormat;
/**
* 比特率
*/
private final int bitRate;
/**
* 最大输入size
*/
private final int maxInputSize;
/**
* 编码类型
*/
private final String mine;
/**
* 声道数
*/
private int channelCount;
/**
* 音频缓存buffer
*/
private int bufferSizeInByte;
/**
* 是否停止编码
*/
private boolean isStopEncode = false;
/**
* 构造方法(传入必要的参数)
*/
public EncodeThread(String path,
int audioSource,
int sampleRateInHz,
int channelConfig,
int audioFormat,
int bitRate,
int maxInputSize,
String mime
) {
this.path = path;
this.audioSource = audioSource;
this.sampleRateInHz = sampleRateInHz;
this.channelConfig = channelConfig;
this.audioFormat = audioFormat;
this.bitRate = bitRate;
this.maxInputSize = maxInputSize;
this.mine = mime;
}
}
可以注意到,多了bitRate
、maxInputSize
、mime
参数,其中mime
表示编码的类型,用于构建编码器
2.3 初始化
重写Thread
的run
方法
# run()
@Override
public void run() {
super.run();
initIo();
initAudioRecord();
initMediaCodec();
encode();
}
里面调用了三个方法,分别是:
-
initIo()
初始化文件输出流
-
initAudioRecord()
初始化录音组件
-
initMediaCodec()
初始化编码器
-
encode()
真正开始编码
# initIo()
private void initIo() {
if (TextUtils.isEmpty(path)) {
return;
}
File file = new File(path);
if (file.exists()) {
file.delete();
}
try {
fos = new FileOutputStream(path);
} catch (FileNotFoundException e) {
e.printStackTrace();
fos = null;
}
}
# initAudioRecord()
private void initAudioRecord() {
bufferSizeInByte = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInByte);
}
# initMediaCodec()
private void initMediaCodec() {
channelCount = 1;
if (channelConfig == AudioFormat.CHANNEL_IN_MONO) {
channelCount = 1;
} else if (channelConfig == AudioFormat.CHANNEL_IN_STEREO) {
channelCount = 2;
}
MediaFormat format = MediaFormat.createAudioFormat(
mine, sampleRateInHz, channelCount);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
try {
mediaCodec = MediaCodec.createEncoderByType(mine);
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
e.printStackTrace();
mediaCodec = null;
}
}
在构建MediaCodec
实例时,有一个MediaFormat
的参数需要我们创建,其中bitRate
是必须设置的,不然编码器会报错,还需要注意的一个地方是,AudioRecord
初始化时,声道设置是个常量,而在创建MediaFormat
时,需要转换为具体的声道数
2.4 编码
接下来开始真正的编码
# encode()
private void encode() {
if (audioRecord == null || fos == null || mediaCodec == null) {
return;
}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
audioRecord.startRecording();
mediaCodec.start();
for (; ; ) {
if (isStopEncode) {
release();
break;
}
int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
int readSize = -1;
if (inputBuffer != null) {
readSize = audioRecord.read(inputBuffer, bufferSizeInByte);
}
if (readSize <= 0) {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isStopEncode = true;
} else {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
readSize,
System.nanoTime() / 1000,
0);
}
}
int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
int size = info.size;
if (outputBuffer != null && size > 0) {
byte[] data = new byte[size + 7];
addADTSHeader(data, size + 7);
outputBuffer.get(data, 7, size);
outputBuffer.clear();
try {
fos.write(data);
} catch (IOException e) {
e.printStackTrace();
}
mediaCodec.releaseOutputBuffer(outputBufferId, false);
}
}
}
}
可以注意到,编码里面的逻辑比AudioRecord
采集Pcm
时复杂多了,不过代码结构还是比较清晰的
在AudioRecord
采集到Pcm
后,之前我们是直接将其写入文件,而这里是需要送入编码器,接着在得到输出buffer后才写入数据
注意,写入文件前,需要对数据做一个addADTSHeader
的处理,也就是对编码完成的数据增加一个7字节
的ADTS
头信息
为什么要这样做呢?
如果添加该头信息,则在播放器中是无法播放的,而编码器编码出来的数据是不带这个头信息的,所以需要我们自己添加进去
# addADTSHeader()
/**
* 添加AAC帧文件头
*
* @param packet packet
* @param packetLen packetLen
*/
private void addADTSHeader(byte[] packet, int packetLen) {
int profile = 2; // AAC
int freqIdx = 4; // 44.1kHz
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (channelCount >> 2));
packet[3] = (byte) (((channelCount & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
可以注意到,添加头信息,有一些参数是固定
的,而有一些参数是可变
的,我们主要关注可变的信息,比如:编码格式
、采样率
、声道数
、帧长