上一章介绍了如何对
Pcm
编码成aac
,本篇就来介绍如何解码aac
,并使用AudioTrack
进行播放
一、MediaCodec
上一章的介绍中,MediaCodec
不光能编码,同样也能解码,关于MediaCodec
,同样,对于解码,我们再来回顾下Google
官网上的流程图
之前编码,我们是将AudioRecord
中读取数据送入编码器,接着再从编码器中获取输出的数据,这样循环,最终得到编码后的aac
文件
那么对于解码,我们就可以将编码后的aac
文件传入解码器,接着再从解码器中获取输出的数据传入AudioTrack
,这样循环,就可以完整的播放该aac
文件
对于AudioTrack
,前面的章节也就介绍,不清楚的同学可以去看看Android 音视频开发【音频篇】【三】音频播放
此时产生了一个问题,我们如何将aac
文件送入解码器呢,直接读文件是否可以,就像AudioTrack
播放Pcm
文件时,直接读文件数据,传入AudioTrack
中播放
当然,直接读文件是可以的,我们只需要设置好音频解码的相关参数,比如采样率、声道设置、编码格式等
不过,Android提供了一种更为方便的组件,MediaExtractor
,它可以从文件中获取编码的音频或视频,并一帧一帧解析出来
那么,下面我们将使用MediaExtractor
来进行aac
音频文件的读取
首先,介绍下MediaExtractor
的使用步骤:
- 初始化实例
- 设置文件路径
- 遍历所有的track,根据需要,找到指定的track,并调用
selectTrack()
方法 - 调用
readSampleData()
方法读取当前帧数据 - 读取下一帧,调用
advance()
- 读取结束,则释放资源
二、音频解码
通过上面的描述,我们对音频解码的步骤有了一定了解,我们先来回顾下AudioTrack
的使用步骤:
- 开启子线程
- 构建实例
- 开始播放
- 循环读取文件数据,并写入
AudioTrack
- 停止播放,释放资源
那么,对于音频解码播放,步骤如下:
- 开启子线程
- 构建实例
- 从
MediaExtractor
中找到指定的track - 开始播放、解码
- 循环从
MediaExtractor
中读取数据,并送入解码器 - 解码完成后,数据写入
AudioTrack
中进行播放 - 停止播放、解码,释放资源
下面,就对上述步骤,一一讲解
2.1 开启子线程
解码的整个流程也是要在子线程中执行的
private static class DecodeThread extends Thread {
public DecodeThread(){}
}
2.2 配置必要参数
private static class DecodeThread extends Thread {
private static final long TIMEOUT_MS = 2000L;
private MediaExtractor mediaExtractor;
private MediaCodec mediaCodec;
private AudioTrack audioTrack;
private final String path;
/**
* 音频流格式(一般使用music)
*/
private final int streamType;
/**
* 采样率
*/
private final int sampleRateInHz;
/**
* 声道设置
*/
private final int channelConfig;
/**
* 编码格式
*/
private final int audioFormat;
/**
* 播放模式(一般使用流模式)
*/
private final int mod;
/**
* 音频缓存大小
*/
private int bufferSizeInBytes;
/**
* 音频格式
*/
private MediaFormat format;
private String mime;
/**
* 是否停止解码
*/
private boolean isStopDecode = false;
/**
* 构造方法(传入必要的参数)
*/
public DecodeThread(String path,
int streamType,
int sampleRateInHz,
int channelConfig,
int audioFormat,
int mod
) {
this.path = path;
this.streamType = streamType;
this.sampleRateInHz = sampleRateInHz;
this.channelConfig = channelConfig;
this.audioFormat = audioFormat;
this.mod = mod;
}
}
参数和AudioTrack
播放Pcm
时基本一致,只是多了两个组件MediaExtractor
和MediaCodec
2.3 初始化
重写Thread
的run
方法
@Override
public void run() {
super.run();
initMediaExtractor();
initMediaCodec();
initAudioTrack();
decode();
}
run
方法中,进行了一系列的初始化操作,最后一个方法decode()
是真正开始解码
# initMediaExtractor()
private void initMediaExtractor() {
if (TextUtils.isEmpty(path)) {
return;
}
try {
mediaExtractor = new MediaExtractor();
mediaExtractor.setDataSource(path);
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = mediaExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (!TextUtils.isEmpty(mime) && mime.startsWith("audio/")) {
mediaExtractor.selectTrack(i);
this.format = format;
this.mime = mime;
break;
}
}
} catch (IOException e) {
Log.e(TAG, "initMediaExtractor: ", e);
mediaExtractor = null;
format = null;
mime = null;
}
}
在该方法中,我们对MediaExtractor
做了初始化操作,并找到了对应audio
的track
# initMediaCodec
private void initMediaCodec() {
if (TextUtils.isEmpty(mime) || format == null) {
return;
}
try {
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(format, null, null, 0);
} catch (IOException e) {
e.printStackTrace();
mediaCodec = null;
}
}
可以看到,初始化MediaCodec
的逻辑比较简单,这是因为我们利用了从MediaExtractor
中获取的MediaFormat
去配置MediaCodec
,如果是从文件读取的话,MediaFormat
还需要我们自己配置
# initAudioTrack()
private void initAudioTrack() {
bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
audioTrack = new AudioTrack(streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mod);
}
2.4 播放
进入到decode()
方法后,就正式开始播放
# decode()
private void decode() {
if (mediaExtractor == null || mediaCodec == null || audioTrack == null) {
return;
}
long startMs = System.currentTimeMillis();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
mediaCodec.start();
audioTrack.play();
for (; ; ) {
if (isStopDecode) {
release();
break;
}
int inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_MS);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
int readSize = -1;
if (inputBuffer != null) {
readSize = mediaExtractor.readSampleData(inputBuffer, 0);
}
if (readSize <= 0) {
mediaCodec.queueInputBuffer(
inputBufferId,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isStopDecode = true;
} else {
mediaCodec.queueInputBuffer(inputBufferId, 0, readSize, mediaExtractor.getSampleTime(), 0);
mediaExtractor.advance();
}
}
int outputBufferId = mediaCodec.dequeueOutputBuffer(info, TIMEOUT_MS);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
if (outputBuffer != null && info.size > 0) {
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
byte[] data = new byte[info.size];
outputBuffer.get(data);
outputBuffer.clear();
audioTrack.write(data, 0, info.size);
}
mediaCodec.releaseOutputBuffer(outputBufferId, false);
}
}
}
解码过程中,也是通过死循环不断的读取数据,并将解码后的数据传入AudioTrack
播放的过程,不过,需要注意的一点是,在解码完成后,数据不能马上传入AudioTrack
,而是要通过解码时间与当前时间做对比,进行延时解码
,如果不进行这一操作,则播放的音频会很快