Android 音视频通话核心 —— 音频解码(AAC → PCM → 播放)完整解析

本文讲解 AAC 硬解码 + AudioTrack 播放 的完整基类 BaseAudioDecoder,是实时语音通话、对讲、教育硬件、IoT 设备的标准音频播放方案。

接收网络 AAC 数据 → MediaCodec 硬解码为 PCM → AudioTrack 播放出声。


一、这个类是干嘛的?

BaseAudioDecoder音频解码与播放基类,负责:

  1. 接收网络端传来的 AAC 音频数据
  2. 使用 MediaCodec 硬件解码为 PCM 原始音频
  3. 使用 AudioTrack 实时播放
  4. 支持 共享 AudioSessionId(实现通话回声消除)
  5. 支持 静音、停止、释放 等控制

二、核心流程(最关键)

网络 AAC 数据 → decoderAAC () → MediaCodec 解码 → PCM 数据 → AudioTrack 播放


三、核心成员变量解释

java 复制代码
// 音效(AEC回声消除、NS噪声抑制,一般在Encoder侧开启,Decoder侧共享)
private var mAcousticEchoCanceler: AcousticEchoCanceler? = null
private var mNoiseSuppressor: NoiseSuppressor? = null

// 解码参数
private var mAudioDecoderParam = AudioDecoderParam()

// 解码线程(单线程保证顺序)
private var mAudioExecutor = Executors.newSingleThreadExecutor()

// 系统播放器
private var mAudioTrack: AudioTrack? = null

// AAC 硬解码器
private var mAudioCodec: MediaCodec? = null

// 解码配置
private var mAudioFormat: MediaFormat? = null

// 停止标记
@Volatile private var isStopped = false

四、启动解码流程:startAudio ()

外部调用 startAudio() 后,内部做 3 件事:

  1. 初始化解码参数
  2. 初始化 AudioTrack 播放器
  3. 初始化 MediaCodec AAC 解码器

1. 初始化解码参数 initAudioDecoderParam ()

统一配置:

  • 声道:单声道 / 立体声
  • 位宽:16bit(标准)
  • 采样率:16000 / 44100 / 48000(自动规整)
perl 复制代码
sampleRateInHz = when (sampleRate) {
    in 7999 until 16001 -> 16000
    in 16000 until 44101 -> 44100
    ...
}

2. 初始化 AudioTrack 播放器 initAudioTrack ()

作用:创建系统音频播放轨,支持共享 AudioSessionId(用于回声消除)。

scss 复制代码
mAudioTrack = AudioTrack(
    sampleRate,
    channelConfig,
    audioFormat,
    minBufSize,
    mode,
    sharedSessionId // 关键:共享录音端的 SessionId
).apply {
    play() // 立刻启动播放
}

3. 初始化 AAC 解码器 initAudio ()

创建 MediaCodec 解码器,配置:

  • AAC LC 格式
  • 采样率
  • 声道数
  • 关键:KEY_IS_ADTS = 1(必须!编码时带了 ADTS 头)
ini 复制代码
mAudioCodec = MediaCodec.createDecoderByType(MIMETYPE_AUDIO_AAC)

五、最核心:解码 + 播放 decoderAAC ()

这是外部喂数据的入口,所有网络音频包都从这里进入。

流程:

  1. 将 AAC 数据送入解码器输入缓冲区
  2. 解码得到 PCM
  3. 将 PCM 写入 AudioTrack 播放
kotlin 复制代码
open fun decoderAAC(data: ByteArray, len: Int, pts: Long): Int {
    mAudioExecutor?.submit {
        // 1. 送入解码器
        val inputBufferIndex = mAudioCodec?.dequeueInputBuffer(0)
        if (inputBufferIndex >= 0) {
            val inputBuffer = mAudioCodec?.getInputBuffer(inputIndex)
            inputBuffer?.put(data)
            mAudioCodec?.queueInputBuffer(...)
        }

        // 2. 取出解码后的 PCM
        var outputBufferIndex = mAudioCodec?.dequeueOutputBuffer(bufferInfo, 0)
        while (outputBufferIndex >= 0) {
            val outData = ByteArray(bufferInfo.size)
            outputBuffer?.get(outData)

            // 3. 播放
            mAudioTrack?.write(outData, 0, outData.size)

            mAudioCodec?.releaseOutputBuffer(outputIndex, false)
            outputBufferIndex = mAudioCodec?.dequeueOutputBuffer(...)
        }
    }
}

关键点:

  • 单线程解码,保证音频不乱序、不爆音
  • 异步解码,不阻塞主线程
  • 自动判断解码器是否存在,不存在则直接裸播 PCM

六、ADTS 配置

编码时加了 ADTS 头,解码时必须配置:

scss 复制代码
setInteger(MediaFormat.KEY_IS_ADTS, 1)

并传入 csd-0 配置信息(ADTS 头信息):

less 复制代码
setByteBuffer("csd-0", createAdtsData())

作用:告诉解码器这是带 ADTS 头的 AAC 流,否则无法正常解码。


七、共享 AudioSessionId(通话回声消除核心)

这是微信 / 钉钉通话能消除回声的关键:

  • 录音(Encoder) 创建 AudioSession
  • 播放(Decoder) 共享同一个 SessionId
  • 系统 AEC 才能识别 "哪些声音是自己播放出去的"

所以代码里:

scss 复制代码
// Decoder 传入 Encoder 的 sessionId
startAudio(..., sharedSessionId)

Decoder 侧不再重复创建 AEC/NS,避免冲突。


八、停止与释放 stopAudio ()

安全释放顺序(避免崩溃、杂音、占用):

  1. 停止线程池
  2. 停止并释放 AudioTrack
  3. 停止并释放 MediaCodec
  4. 释放音效处理器
scss 复制代码
mAudioTrack?.stop()
mAudioTrack?.release()

mAudioCodec?.stop()
mAudioCodec?.release()

九、亮点

  1. 标准 AAC 硬解码,全机型兼容
  2. 单线程解码,音频不乱序、不爆音
  3. 共享 AudioSessionId,完美支持系统 AEC 回声消除
  4. 自动适配采样率 / 声道 / 位宽
  5. 安全释放机制,避免崩溃
  6. 支持静音、动态音量
  7. 可作为基类被继承扩展
  8. 支持裸播 PCM + 解码 AAC 两种模式

十、踩坑记录

坑 1:不共享 AudioSessionId,导致小程序端回音严重

  • 现象:设备端扬声器播放小程序端声音,小程序端又能听到自己的回声。
  • 根因:AudioRecord 和 AudioTrack 各自独立的 SessionId,系统 AEC 无法关联输入输出。
  • 解决 :Encoder 创建 AudioRecord 后,把 audioSessionId 传给 Decoder 的 AudioTrack,共用同一会话。

坑 2:解码器未就绪时直接 write 原始 AAC 数据,扬声器发出噪音

  • 现象:通话建立初期,扬声器发出"哒哒哒"的爆音。
  • 根因decoderAACmAudioCodec == null 时直接 mAudioTrack.write(data),但 data 是 AAC 不是 PCM。
  • 解决:增加格式判断,非 PCM 数据不直接播放;或等解码器初始化完成后再开始收包。

坑 3:频繁 start/stop AudioTrack 导致崩溃

  • 现象:通话中切换扬声器/听筒,应用崩溃。
  • 根因AudioTrack.stop() 后立即 release(),但底层缓冲区还在回调。
  • 解决 :用 setVolume(0f) 代替 stop() 实现静音,避免频繁启停。

坑 4:pts 单位错误导致音画不同步

  • 现象:视频正常,但声音延迟或加速。
  • 根因 :网络层传的是毫秒,MediaCodec 需要微秒,忘记 * 1000
  • 解决queueInputBufferpts * 1000
kotlin 复制代码
open class BaseAudioDecoder : IAudioDecoder {
    private val TAG = "BaseAudioDecoder"
    private var mAcousticEchoCanceler: AcousticEchoCanceler? = null  //回声消除器
    private var mNoiseSuppressor: NoiseSuppressor? = null  //回声消除器 噪声抑制

    private var mAudioDecoderParam = AudioDecoderParam()
    private var mAudioExecutor = Executors.newSingleThreadExecutor()
    private var mAudioTrack: AudioTrack? = null
    private var mAudioCodec: MediaCodec? = null
    private var mAudioFormat: MediaFormat? = null

    open fun setContext(context: Context) {}

    @Volatile
    private var isStopped = false

    override fun startAudio(type: Int, mode: Int, width: Int, sampleRate: Int) {
        // 内部转发到 5 参数版本,默认生成新的 SessionId
        startAudio(type, mode, width, sampleRate, AudioManager.AUDIO_SESSION_ID_GENERATE)
    }

    /**
     * 开放给外部传入共享 SessionId 的重载方法(非 override,可带默认值)
     */
    open fun startAudio(type: Int, mode: Int, width: Int, sampleRate: Int, sharedSessionId: Int) {
        isStopped = false
        initAudioDecoderParam(mode, width, sampleRate)
        initAudioTrack(gain = 0.8f, sharedSessionId = sharedSessionId)
        initAudio(type, mode.plus(1))
    }

    /**
     * 初始化 音频解码的参数
     */
    private fun initAudioDecoderParam(mode: Int, width: Int, sampleRate: Int): AudioDecoderParam {
        mAudioDecoderParam.apply {
            channelConfig = if (mode == 1) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO
            audioFormat = if (width == 1) AudioFormat.ENCODING_PCM_16BIT else AudioFormat.ENCODING_PCM_8BIT
            sampleRateInHz = when (sampleRate) {
                in 7999 until 16001 -> 16000
                in 16000 until 44101 -> 44100
                in 44100 until 48001 -> 48000
                else -> sampleRate
            }
        }
        LogUtil.i(msg = "initAudioDecoderParam 初始化音频解码的参数完成 mode =$mode width =$width  sampleRate =$sampleRate")
        return mAudioDecoderParam
    }

    /**
     * 初始化  AudioTrack(用于播放实时音频流)
     * @param gain 音量增益,通常是一个浮动类型的数值。这个值控制音量的大小,范围通常是 0.0 到 1.0,或者更大的值。0.0 表示静音,1.0 表示默认音量(通常是最大音量)
     */
    private fun initAudioTrack(gain: Float = 1.0f, sharedSessionId: Int = AudioManager.AUDIO_SESSION_ID_GENERATE): AudioTrack? {
        val minBufSize = AudioTrack.getMinBufferSize(
            mAudioDecoderParam.sampleRateInHz,
            mAudioDecoderParam.channelConfig,
            mAudioDecoderParam.audioFormat
        )
        mAudioTrack = AudioTrack(
            mAudioDecoderParam.streamType,
            mAudioDecoderParam.sampleRateInHz,
            mAudioDecoderParam.channelConfig,
            mAudioDecoderParam.audioFormat,
            minBufSize,
            mAudioDecoderParam.audioMode,
            sharedSessionId  // 使用 Encoder 传过来的 SessionId
        ).apply {
            // 删除 initAEC(audioSessionId)  ------ 共享 SessionId 后,Encoder 侧已创建
            // 删除 initNoiseSuppressor(audioSessionId)
            setVolume(gain)
            play()
        }
        LogUtil.i(msg = "initAudioTrack 初始化 AudioTrack完成 sessionId=$sharedSessionId")
        return mAudioTrack
    }

    /**
     * @param channelCount  1:CHANNEL_OUT_STEREO(立体声) 2:CHANNEL_OUT_MONO(单声道)
     * @param audioMimeType 音频解码器类型  默认 MIMETYPE_AUDIO_AAC模式
     */
    private fun initAudio(type: Int, channelCount: Int, audioMimeType: String = MediaFormat.MIMETYPE_AUDIO_AAC) {
        LogUtil.i(msg = "BaseAudioDecoder initAudio type =$type  channelCount =$channelCount  audioMimeType =$audioMimeType")
        if (type == 4) {
            LogUtil.i(msg = "initAudio 创建解码器$audioMimeType 音频解码器,解码类型详见 MediaFormat")
            try {
                mAudioCodec = MediaCodec.createDecoderByType(audioMimeType)
                initAudioFormat(audioMimeType, channelCount)
                mAudioCodec?.apply {
                    configure(mAudioFormat, null, null, 0)
                    start()
                    LogUtil.i(msg = "initAudio 启动$audioMimeType 音频解码器,解码类型详见 MediaFormat")
                }
            } catch (e: IOException) {
                throw RuntimeException(e)
            }

        }
    }

    /**
     * 初始化 音频格式
     * @param channelCount  1:CHANNEL_OUT_STEREO(立体声) 2:CHANNEL_OUT_MONO(单声道)
     * @param audioMimeType 音频解码器类型  默认 MIMETYPE_AUDIO_AAC模式
     */
    private fun initAudioFormat(audioMimeType: String, channelCount: Int): MediaFormat? {
        mAudioFormat = MediaFormat.createAudioFormat(audioMimeType, mAudioDecoderParam.sampleRateInHz, channelCount).apply {
            setInteger(MediaFormat.KEY_PCM_ENCODING, mAudioDecoderParam.audioFormat)
            setInteger(MediaFormat.KEY_IS_ADTS, 1)
            setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 256 * 1024)
            setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
            setByteBuffer("csd-0", createAdtsData())
        }
        LogUtil.i(msg = "initAudioFormat 初始化音频格式完成 当前音频解码器类型$audioMimeType 解码类型详见 MediaFormat")
        return mAudioFormat
    }


    /**
     * 回声消除器 麦克风
     * 作用:消除或减少由扬声器音频回馈到麦克风的回声(语音通话、视频通话中的回声消除)
     * @param audioSessionId
     */
    private fun initAEC(audioSessionId: Int) {
        if (AcousticEchoCanceler.isAvailable()) {
            if (mAcousticEchoCanceler == null) {
                mAcousticEchoCanceler = AcousticEchoCanceler.create(audioSessionId)
            }
            mAcousticEchoCanceler?.enabled = true
            LogUtil.d(TAG, "initAEC 开启回音消除")
        } else {
            LogUtil.d(TAG, "initAEC 当前设备不支持回音消除")
        }
    }

    /**
     * 开启噪声抑制
     * 作用:有些设备的回声是由于环境噪音导致的,可以使用 NoiseSuppressor 进行降噪
     * @param audioSessionId
     */
    private fun initNoiseSuppressor(audioSessionId: Int) {
        if (NoiseSuppressor.isAvailable()) {
            if (mNoiseSuppressor == null) {
                mNoiseSuppressor = NoiseSuppressor.create(audioSessionId)
            }
            mNoiseSuppressor?.enabled = true
            LogUtil.d(TAG, "initNoiseSuppressor 开启噪声抑制")
        } else {
            LogUtil.d(TAG, "initNoiseSuppressor 当前设备不支持噪声抑制")
        }
    }

    /**
     * 创建Adts 数据
     */
    private fun createAdtsData(): ByteBuffer {
        val adtsData = ByteArray(2)
        val sampleRateIdx = when (mAudioDecoderParam.sampleRateInHz) {
            16000 -> 0x08
            8000 -> 0x0B
            44100 -> 0x04
            48000 -> 0x03
            else -> 0
        }
        adtsData[0] = ((MediaCodecInfo.CodecProfileLevel.AACObjectLC shl 3) or (sampleRateIdx shr 1)).toByte()
        adtsData[1] = ((sampleRateIdx shl 7) and 0x80 or (mAudioDecoderParam.channelConfig shl 3)).toByte()
        return ByteBuffer.wrap(adtsData)
    }

    open fun setSpeakerOn(speakerOn: Boolean) {}
    open fun isSpeakerOn(): Boolean = false
    open fun decoderAAC(data: ByteArray, len: Int, pts: Long): Int {
        if (isStopped || mAudioExecutor?.isShutdown == true) {
            return -1
        }
        if (mAudioTrack == null) {
            LogUtil.w(msg = "decoderAAC  mAudioTrack is null")
            return -2
        }

        mAudioExecutor?.submit {
            try {
                if (isStopped || mAudioCodec == null) {
                    mAudioTrack?.write(data, 0, len)
                    return@submit
                }
                val inputBufferIndex = mAudioCodec?.dequeueInputBuffer(0) ?: -1
                if (inputBufferIndex >= 0) {
                    val inputBuffer = mAudioCodec?.getInputBuffer(inputBufferIndex)
                    inputBuffer?.apply {
                        clear()
                        put(data)
                        mAudioCodec?.queueInputBuffer(inputBufferIndex, 0, len, pts * 1000, 0)
                    }
                } else {
                    LogUtil.e(TAG, msg = "audio inputBufferIndex invalid: $inputBufferIndex")
                }

                val bufferInfo = MediaCodec.BufferInfo()
                var outputBufferIndex = mAudioCodec?.dequeueOutputBuffer(bufferInfo, 0) ?: -1
                while (outputBufferIndex >= 0) {
                    val outputBuffer = mAudioCodec?.getOutputBuffer(outputBufferIndex)
                    outputBuffer?.let {
                        val outData = ByteArray(bufferInfo.size)
                        it.get(outData)
                        if (mAudioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) {
                            mAudioTrack?.write(outData, 0, outData.size)
                        } else {
                            LogUtil.w(TAG, "AudioTrack is not playing, skipping write.")
                        }
                        mAudioCodec?.releaseOutputBuffer(outputBufferIndex, false)
                    }
                    outputBufferIndex = mAudioCodec?.dequeueOutputBuffer(bufferInfo, 0) ?: -1
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        return 0
    }

    override fun stopAudio() {
        if (isStopped) return
        isStopped = true

        LogUtil.d(TAG, "stopAudio start")
        tryCatch<BaseAudioDecoder> {
            mAudioExecutor?.let {
                it.shutdown()
                try {
                    if (!it.awaitTermination(200, TimeUnit.MILLISECONDS)) {
                        it.shutdownNow()
                    }
                } catch (e: InterruptedException) {
                    it.shutdownNow()
                }
                mAudioExecutor = null
            }
            mAudioTrack?.let {
                if (it.playState == AudioTrack.PLAYSTATE_PLAYING) {
                    it.stop()
                }
                it.release()
                mAudioTrack = null
            }
            mAudioCodec?.let {
                try {
                    it.stop()
                } catch (e: Exception) { }
                try {
                    it.release()
                } catch (e: Exception) { }
                mAudioCodec = null
            }
            mAcousticEchoCanceler?.let {
                try { it.enabled = false } catch (e: Exception) { }
                try { it.release() } catch (e: Exception) { }
                mAcousticEchoCanceler = null
            }
            mNoiseSuppressor?.let {
                try { it.enabled = false } catch (e: Exception) { }
                try { it.release() } catch (e: Exception) { }
                mNoiseSuppressor = null
            }
        }
        LogUtil.d(TAG, "stopAudio end")
    }

    open fun setMute(enableMute:Boolean){
        if(enableMute){
            mAudioTrack?.setVolume(0f)
        } else {
            mAudioTrack?.setVolume(1f) // 恢复默认音量
        }
    }

}

相关推荐
Coffeeee2 小时前
Android功耗优化,哪些代码会让你的机子发热又耗电
android·架构·kotlin
Android打工仔2 小时前
Kotlin 编译器是如何把 suspend 函数变成状态机的?
kotlin
Android打工仔2 小时前
一个 suspend 函数,为什么会变成一个普通函数?
kotlin
苦瓜花1 天前
【Kotlin】初入门
android·开发语言·kotlin
Kapaseker1 天前
Rust 是如何处理异常的
rust·kotlin
冬日砥砺前行1 天前
Kotlin 协程:几个曾经被我误解的点
kotlin
plainGeekDev3 天前
libs 目录 → Gradle 依赖管理
android·java·kotlin
监督者修3 天前
从零构建 GIS 数据引擎:方案驱动架构的设计与实践
android·架构·kotlin
urkay-4 天前
Kotlin Flow分类
android·开发语言·kotlin
alexhilton5 天前
Kotlin DSL深度解析:从Gradle脚本到构建你自己的DSL
android·kotlin·android jetpack