Android音视频之使用MediaCodec编解码AAC

96
落英坠露
0.5 2019.05.03 21:57 字数 1275

现在的短视频、音视频通话都离不开编码和解码,今天就来聊一下音频的编解码。

1. 音频的基本概念

在音频开发中,有些基本概念是需要了解的。

  • 采样率(SampleRate):每秒采集声音的数量,它用赫兹(Hz)来表示。采样频率越高,音频质量越好。常用的音频采样频率有:8kHz、16kHz、44.1kHz、48kHz 等。
  • 声道数(Channel):一般表示声音录制时的音源数量或回放时相应的扬声器数量。常用的是单声道(Mono)和双声道(Stereo)。
  • 采样精度(BitDepth):每个采样点用多少数据量表示,它以位(Bit)为单位。位数越多,表示得就越精细,声音质量自然就越好,当然数据量也越大。常见的位宽是:8bit 或者 16bit。
  • 比特率(BitRate):每秒音频占用的比特数量,单位是 bps(Bit Per Second),比特率越高,压缩比越小,声音质量越好,音频体积也越大。

AAC 是应用非常广泛的音频压缩格式,Android 硬件编码天生支持 AAC。我们采集的原始 PCM 音频,一般不直接用来网络传输,而是经过编码器压缩成 AAC,这样就提高了传输效率,节省了网络带宽。

简言之,编码就是压缩,解码就是解压。编码的目的是减小数据的体积,方便网络传输和本地存储。编码后的数据是不能直接使用的,必须先解码成原来的样子。就像 zip 压缩文件里面有张图片,我们用图片查看器是无法打开的,必须先解压文件,恢复图片原来的数据,这样才能查看。音视频编解码也是同样的道理。

2. MediaCodec 介绍

Android 在 API 16 后引入的音视频编解码 API,Android 应用层统一由 MediaCodec API 提供音视频编解码的功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等。由于使用硬件编解码,兼容性有不少问题,据说 MediaCodec 坑比较多。

MediaCodec 采用了基于环形缓冲区的「生产者-消费者」模型,异步处理数据。在 input 端,Client 是这个环形缓冲区「生产者」,MediaCodec 是「消费者」。在 output 端,MediaCodec 是这个环形缓冲区「生产者」,而 Client 则变成了「消费者」。

工作流程是这样的:

(1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]

(2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer]

(3)MediaCodec 从 input 缓冲区队列取一帧数据进行编解码处理

(4)处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列

(5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]

(6)Client 对编解码后的 buffer 进行渲染/播放

(7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]

MediaCodeC

MediaCodec 基本使用流程:

- createEncoderByType/createDecoderByType
- configure
- start
- while(true) {
    - dequeueInputBuffer
    - queueInputBuffer
    - dequeueOutputBuffer
    - releaseOutputBuffer
}
- stop
- release

3. 实时采集音频并编码

我们将使用 AudioRecord 和 MediaCodec 实现这个功能,关于 AudioRecord 的使用可以参考之前的文章:Android音视频之使用AudioRecord采集音频

为了保证兼容性,推荐的配置是 44.1kHz、单通道、16 位精度。首先创建并配置 AudioRecord 和 MediaCodec。

    // 输入源 麦克风
    private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    // 采样率 44.1kHz,所有设备都支持
    private final static int SAMPLE_RATE = 44100;
    // 通道 单声道,所有设备都支持
    private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    // 精度 16 位,所有设备都支持
    private final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    // 通道数 单声道
    private static final int CHANNEL_COUNT = 1;
    // 比特率
    private static final int BIT_RATE = 96000;

public void createAudio() {
        mBufferSizeInBytes = AudioRecord.getMinBufferSize(AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT);
        if (mBufferSizeInBytes <= 0) {
            throw new RuntimeException("AudioRecord is not available, minBufferSize: " + mBufferSizeInBytes);
        }
        Log.i(TAG, "createAudioRecord minBufferSize: " + mBufferSizeInBytes);

        mAudioRecord = new AudioRecord(AudioEncoder.AUDIO_SOURCE, AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT, mBufferSizeInBytes);
        int state = mAudioRecord.getState();
        Log.i(TAG, "createAudio state: " + state + ", initialized: " + (state == AudioRecord.STATE_INITIALIZED));
    }
    
        public void createMediaCodec() throws IOException {
        MediaCodecInfo mediaCodecInfo = CodecUtils.selectCodec(MIMETYPE_AUDIO_AAC);
        if (mediaCodecInfo == null) {
            throw new RuntimeException(MIMETYPE_AUDIO_AAC + " encoder is not available");
        }
        Log.i(TAG, "createMediaCodec: mediaCodecInfo " + mediaCodecInfo.getName());

        MediaFormat format = MediaFormat.createAudioFormat(MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT);
        format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);

        mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC);
        mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    }

然后开始录音,得到原始音频数据,再编码为 AAC 格式。这个地方会阻塞调用的线程,而且编码比较耗时,一定要在主线程之外调用。

   public void start(File outFile) throws IOException {
        Log.d(TAG, "start() called with: outFile = [" + outFile + "]");
        mStopped = false;
        FileOutputStream fos = new FileOutputStream(outFile);
        mMediaCodec.start();
        mAudioRecord.startRecording();
        byte[] buffer = new byte[mBufferSizeInBytes];
        ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
        try {
            while (!mStopped) {
                int readSize = mAudioRecord.read(buffer, 0, mBufferSizeInBytes);
                if (readSize > 0) {
                    int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
                    if (inputBufferIndex >= 0) {
                        ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                        inputBuffer.clear();
                        inputBuffer.put(buffer);
                        inputBuffer.limit(buffer.length);
                        mMediaCodec.queueInputBuffer(inputBufferIndex, 0, readSize, 0, 0);
                    }

                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                    int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                    while (outputBufferIndex >= 0) {
                        ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                        outputBuffer.position(bufferInfo.offset);
                        outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                        byte[] chunkAudio = new byte[bufferInfo.size + 7];// 7 is ADTS size
                        addADTStoPacket(chunkAudio, chunkAudio.length);
                        outputBuffer.get(chunkAudio, 7, bufferInfo.size);
                        outputBuffer.position(bufferInfo.offset);
                        fos.write(chunkAudio);
                        mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                        outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                    }
                } else {
                    Log.w(TAG, "read audio buffer error:" + readSize);
                    break;
                }
            }
        } finally {
            Log.i(TAG, "released");
            mAudioRecord.stop();
            mAudioRecord.release();
            mMediaCodec.stop();
            mMediaCodec.release();
            fos.close();
        }
    }

AAC 是一种压缩格式,可以直接使用播放器播放。为了实现流式播放,也就是做到边下边播,我们采用 ADTS 格式。给每帧加上 7 个字节的头信息。加上头信息就是为了告诉解码器,这帧音频长度、采样率、通道是多少,每帧都携带头信息,解码器随时都可以解码播放。我们这里采用单通道、44.1KHz 采样率的头信息配置。

    private void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2;  //AAC LC
        int freqIdx = 4;  //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;
    }

4. AAC 解码

我们可以利用 MediaExtractor 和 MediaCodec 来提取编码后的音频数据,并解压成音频源数据。

    /**
     * AAC 格式解码成 PCM 数据
     *
     * @param aacFile
     * @param pcmFile
     * @throws IOException
     */
    public static void decodeAacToPcm(File aacFile, File pcmFile) throws IOException {
        MediaExtractor extractor = new MediaExtractor();
        extractor.setDataSource(aacFile.getAbsolutePath());
        MediaFormat mediaFormat = null;
        for (int i = 0; i < extractor.getTrackCount(); i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("audio/")) {
                extractor.selectTrack(i);
                mediaFormat = format;
                break;
            }
        }
        if (mediaFormat == null) {
            Log.e(TAG, "Invalid file with audio track.");
            extractor.release();
            return;
        }

        FileOutputStream fosDecoder = new FileOutputStream(pcmFile);
        String mediaMime = mediaFormat.getString(MediaFormat.KEY_MIME);
        Log.i(TAG, "decodeAacToPcm: mimeType: " + mediaMime);
        MediaCodec codec = MediaCodec.createDecoderByType(mediaMime);
        codec.configure(mediaFormat, null, null, 0);
        codec.start();
        ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
        ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
        final long kTimeOutUs = 10_000;
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        boolean sawInputEOS = false;
        boolean sawOutputEOS = false;

        try {
            while (!sawOutputEOS) {
                if (!sawInputEOS) {
                    int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
                    if (inputBufIndex >= 0) {
                        ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
                        int sampleSize = extractor.readSampleData(dstBuf, 0);
                        if (sampleSize < 0) {
                            Log.i(TAG, "saw input EOS.");
                            sawInputEOS = true;
                            codec.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        } else {
                            codec.queueInputBuffer(inputBufIndex, 0, sampleSize, extractor.getSampleTime(), 0);
                            extractor.advance();
                        }
                    }
                }

                int outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
                if (outputBufferIndex >= 0) {
                    // Simply ignore codec config buffers.
                    if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                        Log.i(TAG, "audio encoder: codec config buffer");
                        codec.releaseOutputBuffer(outputBufferIndex, false);
                        continue;
                    }

                    if (info.size != 0) {
                        ByteBuffer outBuf = codecOutputBuffers[outputBufferIndex];
                        outBuf.position(info.offset);
                        outBuf.limit(info.offset + info.size);
                        byte[] data = new byte[info.size];
                        outBuf.get(data);
                        fosDecoder.write(data);
                    }

                    codec.releaseOutputBuffer(outputBufferIndex, false);
                    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        Log.i(TAG, "saw output EOS.");
                        sawOutputEOS = true;
                    }
                } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                    codecOutputBuffers = codec.getOutputBuffers();
                    Log.i(TAG, "output buffers have changed.");
                } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    MediaFormat oformat = codec.getOutputFormat();
                    Log.i(TAG, "output format has changed to " + oformat);
                }
            }
        } finally {
            Log.i(TAG, "decodeAacToPcm finish");
            codec.stop();
            codec.release();
            extractor.release();
            fosDecoder.close();
        }
    }

源码在 GitHub上,欢迎交流。

参考资料:

Android音视频