本文讲解 AAC 硬解码 + AudioTrack 播放 的完整基类 BaseAudioDecoder,是实时语音通话、对讲、教育硬件、IoT 设备的标准音频播放方案。
接收网络 AAC 数据 → MediaCodec 硬解码为 PCM → AudioTrack 播放出声。
一、这个类是干嘛的?
BaseAudioDecoder 是音频解码与播放基类,负责:
- 接收网络端传来的 AAC 音频数据
- 使用
MediaCodec硬件解码为 PCM 原始音频 - 使用
AudioTrack实时播放 - 支持 共享 AudioSessionId(实现通话回声消除)
- 支持 静音、停止、释放 等控制
二、核心流程(最关键)
网络 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 件事:
- 初始化解码参数
- 初始化 AudioTrack 播放器
- 初始化 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 ()
这是外部喂数据的入口,所有网络音频包都从这里进入。
流程:
- 将 AAC 数据送入解码器输入缓冲区
- 解码得到 PCM
- 将 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 ()
安全释放顺序(避免崩溃、杂音、占用):
- 停止线程池
- 停止并释放 AudioTrack
- 停止并释放 MediaCodec
- 释放音效处理器
scss
mAudioTrack?.stop()
mAudioTrack?.release()
mAudioCodec?.stop()
mAudioCodec?.release()
九、亮点
- 标准 AAC 硬解码,全机型兼容
- 单线程解码,音频不乱序、不爆音
- 共享 AudioSessionId,完美支持系统 AEC 回声消除
- 自动适配采样率 / 声道 / 位宽
- 安全释放机制,避免崩溃
- 支持静音、动态音量
- 可作为基类被继承扩展
- 支持裸播 PCM + 解码 AAC 两种模式
十、踩坑记录
坑 1:不共享 AudioSessionId,导致小程序端回音严重
- 现象:设备端扬声器播放小程序端声音,小程序端又能听到自己的回声。
- 根因:AudioRecord 和 AudioTrack 各自独立的 SessionId,系统 AEC 无法关联输入输出。
- 解决 :Encoder 创建 AudioRecord 后,把
audioSessionId传给 Decoder 的 AudioTrack,共用同一会话。
坑 2:解码器未就绪时直接 write 原始 AAC 数据,扬声器发出噪音
- 现象:通话建立初期,扬声器发出"哒哒哒"的爆音。
- 根因 :
decoderAAC里mAudioCodec == null时直接mAudioTrack.write(data),但 data 是 AAC 不是 PCM。 - 解决:增加格式判断,非 PCM 数据不直接播放;或等解码器初始化完成后再开始收包。
坑 3:频繁 start/stop AudioTrack 导致崩溃
- 现象:通话中切换扬声器/听筒,应用崩溃。
- 根因 :
AudioTrack.stop()后立即release(),但底层缓冲区还在回调。 - 解决 :用
setVolume(0f)代替stop()实现静音,避免频繁启停。
坑 4:pts 单位错误导致音画不同步
- 现象:视频正常,但声音延迟或加速。
- 根因 :网络层传的是毫秒,MediaCodec 需要微秒,忘记
* 1000。 - 解决 :
queueInputBuffer时pts * 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) // 恢复默认音量
}
}
}