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) // 恢复默认音量
        }
    }

}

相关推荐
Refrain_zc2 小时前
Android 音视频通话核心 —— Camera 采集 + 音视频编码调度
kotlin
plainGeekDev5 小时前
AlertDialog → DialogFragment
android·java·kotlin
Meteors.8 小时前
Kotlin协程序使用技巧和应用场景
android·开发语言·kotlin
黄林晴9 小时前
官方实战指南!Compose 项目无缝迁移 KMP
android·kotlin
plainGeekDev9 小时前
XML Shape/Selector → Kotlin 动态创建
android·java·kotlin
plainGeekDev9 小时前
Java 自定义 View → Kotlin 自定义 View
android·java·kotlin
zhangphil11 小时前
Android Coil 3 extend ImageRequest‘s custom method/function,Kotlin(2)
android·kotlin
Kapaseker11 小时前
五分钟搞定 Compose 用户名密码自动填充
android·kotlin
松仔log11 小时前
Jetpack——DataStore
java·kotlin