You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android协程上下文调用AudioTrack.write致应用崩溃问题求助

AudioTrack播放音频流崩溃问题解决方案

问题重现

使用AudioTrack播放解码后的音频流时遇到三种矛盾场景:

  • 主线程直接调用write:音质正常但UI严重卡顿
  • 传入WRITE_NON_BLOCKING参数:UI恢复流畅但音频频繁中断
  • 协程IO线程调用write:音质流畅且UI无卡顿,但运行数秒或数分钟后触发Native崩溃,错误日志核心信息指向audioTrack?.write:
releaseBuffer: mUnreleased out of range, !(stepCount:952 <= mUnreleased:0 <= mFrameCount:3848), BufferSizeInFrames:3848

崩溃原因分析

该Native层崩溃属于AudioTrack内部缓冲区管理异常,常见触发点:

  • AudioTrack实例已调用release()释放,但后台协程仍持续调用write,触发野指针操作
  • 解码后的音频帧参数(采样率、声道数、位深)与AudioTrack初始化配置不匹配,导致缓冲区计数逻辑混乱
  • 协程生命周期未与AudioTrack绑定,无法在资源释放时及时终止写入任务
  • 忽略write方法的返回值,未处理写入失败场景,持续写入无效数据破坏缓冲区状态

解决方案

1. 绑定协程与AudioTrack生命周期

使用带Job的CoroutineScope,在AudioTrack释放时主动取消协程,避免无效操作:

// 定义与AudioTrack生命周期绑定的协程Scope
private val audioPlaybackScope = CoroutineScope(Dispatchers.IO + Job())

// 启动播放任务
fun startPlaying(decodedFrames: List<ByteArray>) {
    audioPlaybackScope.launch {
        try {
            decodedFrames.forEach { frame ->
                // 提前检查AudioTrack状态,避免已释放
                if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) break
                
                val writeResult = audioTrack?.write(frame, 0, frame.size)
                // 处理写入失败的情况
                if (writeResult == null || writeResult < 0) {
                    Log.e("AudioError", "Write failed with code: $writeResult")
                    break
                }
            }
        } catch (e: Exception) {
            Log.e("AudioError", e.message ?: "Unknown playback exception")
        }
    }
}

// 释放AudioTrack时同步取消协程
fun releaseAudioResources() {
    audioPlaybackScope.cancel()
    audioTrack?.stop()
    audioTrack?.release()
    audioTrack = null
}

2. 校验音频帧与AudioTrack配置匹配

确保解码后的帧参数与AudioTrack初始化参数严格一致,避免缓冲区计数错误:

// 示例AudioTrack初始化参数
val sampleRate = 44100
val channelConfig = AudioFormat.CHANNEL_OUT_STEREO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT

// 计算单帧字节大小(声道数 × 位深字节数)
val singleFrameByteSize = (if (channelConfig == AudioFormat.CHANNEL_OUT_STEREO) 2 else 1) * 2

// 写入前校验帧大小合法性
decodedFrames.forEach { frame ->
    if (frame.size % singleFrameByteSize != 0) {
        Log.w("AudioWarn", "Invalid frame size, skip this frame")
        return@forEach
    }
    // 执行写入操作
}

3. 使用回调模式驱动写入(推荐)

避免主动循环写入,改用AudioTrack的周期回调适配缓冲区节奏,减少缓冲区异常:

// 设置播放位置更新监听
audioTrack?.setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener {
    override fun onPeriodicNotification(track: AudioTrack?) {
        // 周期性回调补充数据,避免缓冲区耗尽
        audioPlaybackScope.launch {
            writeNextFrame()
        }
    }

    override fun onMarkerReached(track: AudioTrack?) {
        // 标记位触发,可用于播放完成逻辑
    }
})

// 设置周期回调的帧间隔(建议为缓冲区大小的1/4)
val periodFrames = (audioTrack?.bufferSizeInFrames ?: 0) / 4
audioTrack?.setPositionNotificationPeriod(periodFrames)

4. 正确计算AudioTrack缓冲区大小

初始化时使用系统推荐的最小缓冲区大小,避免硬编码导致的缓冲区适配问题:

val minBufferSize = AudioTrack.getMinBufferSize(
    sampleRate,
    channelConfig,
    audioFormat
)

if (minBufferSize == AudioTrack.ERROR_BAD_VALUE) {
    Log.e("AudioInitError", "Invalid AudioTrack parameters")
    return
}

// 实例化AudioTrack时,缓冲区大小建议为最小大小的2倍,提升稳定性
val audioTrack = AudioTrack(
    AudioManager.STREAM_MUSIC,
    sampleRate,
    channelConfig,
    audioFormat,
    minBufferSize * 2,
    AudioTrack.MODE_STREAM
)

内容的提问来源于stack exchange,提问作者Stefan Olteanu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 17:04:57