Android 音视频开发【音频篇】【五】音频解码

2,967 阅读3分钟

上一章介绍了如何对Pcm编码成aac,本篇就来介绍如何解码aac,并使用AudioTrack进行播放

一、MediaCodec

上一章的介绍中,MediaCodec不光能编码,同样也能解码,关于MediaCodec,同样,对于解码,我们再来回顾下Google官网上的流程图

MediaCodec流程图.png

之前编码,我们是将AudioRecord中读取数据送入编码器,接着再从编码器中获取输出的数据,这样循环,最终得到编码后的aac文件

那么对于解码,我们就可以将编码后的aac文件传入解码器,接着再从解码器中获取输出的数据传入AudioTrack,这样循环,就可以完整的播放该aac文件

对于AudioTrack,前面的章节也就介绍,不清楚的同学可以去看看Android 音视频开发【音频篇】【三】音频播放

此时产生了一个问题,我们如何将aac文件送入解码器呢,直接读文件是否可以,就像AudioTrack播放Pcm文件时,直接读文件数据,传入AudioTrack中播放

当然,直接读文件是可以的,我们只需要设置好音频解码的相关参数,比如采样率、声道设置、编码格式等

不过,Android提供了一种更为方便的组件,MediaExtractor,它可以从文件中获取编码的音频或视频,并一帧一帧解析出来

那么,下面我们将使用MediaExtractor来进行aac音频文件的读取

首先,介绍下MediaExtractor的使用步骤:

  • 初始化实例
  • 设置文件路径
  • 遍历所有的track,根据需要,找到指定的track,并调用selectTrack()方法
  • 调用readSampleData()方法读取当前帧数据
  • 读取下一帧,调用advance()
  • 读取结束,则释放资源

二、音频解码

通过上面的描述,我们对音频解码的步骤有了一定了解,我们先来回顾下AudioTrack的使用步骤:

  1. 开启子线程
  2. 构建实例
  3. 开始播放
  4. 循环读取文件数据,并写入AudioTrack
  5. 停止播放,释放资源

那么,对于音频解码播放,步骤如下:

  1. 开启子线程
  2. 构建实例
  3. MediaExtractor中找到指定的track
  4. 开始播放、解码
  5. 循环从MediaExtractor中读取数据,并送入解码器
  6. 解码完成后,数据写入AudioTrack中进行播放
  7. 停止播放、解码,释放资源

下面,就对上述步骤,一一讲解

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时基本一致,只是多了两个组件MediaExtractorMediaCodec

2.3 初始化

重写Threadrun方法

@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做了初始化操作,并找到了对应audiotrack # 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,而是要通过解码时间与当前时间做对比,进行延时解码,如果不进行这一操作,则播放的音频会很快

三、GitHub

PcmDecoder.java

PcmActivity.java