7、音频编码实战:AAC编码器配置、AudioRecord与MediaCodec联动、编码数据回调

音频编码这块,说实话是音视频开发里最容易被忽视的环节。很多人觉得视频编码难,音频嘛,随便搞搞就行。但我在项目中吃过亏——有一次直播推流,视频清晰度没问题,但声音断断续续,用户直接开骂。查了半天,发现是AudioRecord和MediaCodec的配合出了问题。

嗯,今天我们就来把这块彻底讲透。从AAC编码器的配置,到AudioRecord怎么喂数据给MediaCodec,再到编码后的数据怎么回调出来。你跟着走一遍,以后遇到音频编码的问题,心里就有底了。

7.1 AAC编码器配置:参数不是随便填的

先说说AAC编码器的配置。MediaCodec创建编码器时,有几个关键参数需要你认真对待。

核心参数一览:

参数 推荐值 说明
MIME类型 MediaFormat.MIMETYPE_AUDIO_AAC 固定值,别写错
采样率 44100 Hz 兼容性最好,大部分设备都支持
声道数 1(单声道)或 2(立体声) 直播场景建议单声道,省带宽
比特率 64000 ~ 128000 bps 看你的网络状况,我一般用96000
AAC Profile AACObjectLC 低复杂度,兼容性最好

配置代码长这样:

MediaFormat format = MediaFormat.createAudioFormat(
    MediaFormat.MIMETYPE_AUDIO_AAC, 
    44100,  // 采样率
    1       // 单声道
);
format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, 
    MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 4096);

MediaCodec encoder = MediaCodec.createEncoderByType(
    MediaFormat.MIMETYPE_AUDIO_AAC);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

个人经验:KEY_MAX_INPUT_SIZE这个参数,我建议你显式设置一下。有些设备默认值很小,会导致输入缓冲区不够用,编码直接报错。我曾经在小米某款机型上踩过这个坑,折腾了两天才发现是这个问题。

7.2 AudioRecord与MediaCodec联动:数据流怎么走?

配置好编码器,接下来就是怎么把麦克风采集到的数据送进去。这里有个关键点:AudioRecord采集的是PCM裸数据,而MediaCodec需要的是特定格式的输入。

说白了,就是你要把PCM数据塞进MediaCodec的输入缓冲区。流程是这样的:

  1. AudioRecord从麦克风读取PCM数据
  2. 把PCM数据写入MediaCodec的输入队列
  3. MediaCodec编码后,从输出队列取出AAC数据

我画了一张图,帮你理清这个流程:

AudioRecord → MediaCodec 数据流转 麦克风 AudioRecord PCM数据 输入缓冲区 dequeueInputBuffer queueInputBuffer MediaCodec AAC编码 输出缓冲区 dequeueOutputBuffer 编码数据回调 AAC裸流 整个流程:采集 → 入队 → 编码 → 出队 → 回调

你看,整个流程其实不复杂。但实际写代码时,有几个细节要注意。

7.3 实战代码:AudioRecord采集线程

先写一个采集线程,专门从麦克风读数据:

class AudioCaptureThread extends Thread {
    private AudioRecord audioRecord;
    private MediaCodec encoder;
    private volatile boolean isRunning = true;
    
    @Override
    public void run() {
        int bufferSize = AudioRecord.getMinBufferSize(
            44100, 
            AudioFormat.CHANNEL_IN_MONO, 
            AudioFormat.ENCODING_PCM_16BIT
        );
        
        audioRecord = new AudioRecord(
            MediaRecorder.AudioSource.MIC,
            44100,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSize
        );
        
        audioRecord.startRecording();
        
        byte[] pcmBuffer = new byte[4096];
        
        while (isRunning) {
            int bytesRead = audioRecord.read(pcmBuffer, 0, pcmBuffer.length);
            if (bytesRead > 0) {
                feedEncoder(pcmBuffer, bytesRead);
            }
        }
        
        audioRecord.stop();
        audioRecord.release();
    }
    
    private void feedEncoder(byte[] data, int length) {
        int inputIndex = encoder.dequeueInputBuffer(10000);
        if (inputIndex >= 0) {
            ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
            inputBuffer.clear();
            inputBuffer.put(data, 0, length);
            
            encoder.queueInputBuffer(
                inputIndex, 
                0, 
                length, 
                System.nanoTime() / 1000, 
                0
            );
        }
    }
}

注意:AudioRecord的bufferSize不要随便写。用getMinBufferSize获取最小值,然后根据你的需求适当放大。太小了会导致采集丢数据,太大了延迟会变高。我一般取最小值的2倍,平衡得比较好。

7.4 编码数据回调:怎么拿到AAC?

数据送进编码器后,我们需要从输出端把编码好的AAC数据取出来。这里我习惯用一个单独的线程来轮询输出缓冲区:

class AudioEncodeThread extends Thread {
    private MediaCodec encoder;
    private volatile boolean isRunning = true;
    
    @Override
    public void run() {
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        
        while (isRunning) {
            int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
            
            if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // 暂时没有数据,等一下
                continue;
            } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 编码器格式变化,一般只触发一次
                MediaFormat newFormat = encoder.getOutputFormat();
                // 这里可以获取到AAC的配置信息(ADTS头等)
                continue;
            }
            
            if (outputIndex >= 0) {
                ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
                byte[] aacData = new byte[bufferInfo.size];
                outputBuffer.get(aacData);
                
                // 这里拿到的是纯AAC数据,没有ADTS头
                // 如果需要ADTS头,需要自己添加
                onAacDataAvailable(aacData, bufferInfo);
                
                encoder.releaseOutputBuffer(outputIndex, false);
            }
        }
    }
    
    private void onAacDataAvailable(byte[] data, MediaCodec.BufferInfo info) {
        // 回调给上层,比如写入文件或推流
        if (listener != null) {
            listener.onAacFrame(data, info);
        }
    }
}

避坑指南:MediaCodec输出的AAC数据默认不带ADTS头。如果你要直接写入文件或者推流,需要自己加上ADTS头。我曾经在这个问题上浪费了半天,播放器一直报错,后来才发现是缺少了ADTS头。

加ADTS头的代码网上很多,核心就是计算帧长度,然后填充7个字节的头信息。这里不展开了,你记得有这回事就行。

7.5 完整联动:两个线程怎么配合?

采集线程和编码线程是独立运行的。采集线程负责往输入队列塞数据,编码线程负责从输出队列取数据。它们通过MediaCodec内部的缓冲区进行通信,不需要额外的同步机制。

启动顺序要注意:先启动编码器,再启动采集线程。否则可能出现采集线程已经塞数据了,编码器还没准备好的情况。

// 正确的启动顺序
encoder.start();
captureThread.start();
encodeThread.start();

停止时顺序反过来:先停止采集,再停止编码器。确保所有数据都处理完:

// 停止顺序
captureThread.stopCapture();
encodeThread.stopEncode();
encoder.stop();
encoder.release();

核心要点总结:

  • AAC编码器配置时,采样率用44100,比特率96000,Profile用LC
  • AudioRecord的bufferSize取最小值的2倍,平衡延迟和稳定性
  • 编码输出的AAC数据不带ADTS头,需要自己添加
  • 启动顺序:编码器先启动,采集后启动
  • 停止顺序:采集先停止,编码器后停止

嗯,音频编码这块其实不难,但细节很多。你按照这个流程走一遍,基本不会出大问题。如果遇到编码器报错,先检查参数配置,再检查数据格式,大部分问题都能解决。


公众号:蓝海资料掘金营,微信deep3321