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

Android AudioTrack周期回调不按时触发问题排查及方案咨询

问题分析与解决方案:AudioTrack 回调延迟/合并问题(API 19-22)

你遇到的这个AudioTrack周期性回调偶发延迟、首次回调缺失甚至两次合并触发的问题,在API 19-22(KitKat到Lollipop早期)的模拟器(甚至部分真实设备)上确实是个常见的坑。你的CPU频率缩放猜测完全正确,再加上代码里的一些细节问题,共同导致了这个现象。下面我来详细拆解原因,并给出几个更可靠的解决办法:


一、现象背后的原因

  1. CPU调频器调度限制
    API 19-22的系统音频线程调度优先级在低功耗场景下不够高,当CPU处于降频状态时,音频回调线程可能被延迟调度,导致第一个周期的回调信号被积压,直到第二个周期时才一起触发,这和你提到的视频里的分析完全吻合。

  2. 代码时序与参数不匹配

    • 你在子线程的write循环中才设置PlaybackPositionUpdateListener,而此时audioTrack.play()已经启动,音频流已经开始播放,很容易错过第一个周期的触发信号。
    • 你的buffer参数计算有误:AudioFormat.ENCODING_PCM_16BIT是每个样本占2字节,1秒单声道的PCM数据应该是44100 * 2 = 88200字节,但你硬编码了bufferSizeInBytes = 44100,导致每次write仅写入0.5秒的数据,和setPositionNotificationPeriod(44100)(1秒样本数)的周期不匹配,加重了底层时序混乱。

二、更可靠的回调实现方案

针对API 19-22的兼容性问题,推荐以下几种稳定的替代方案:

1. 修正基础逻辑:提前设置监听器+匹配参数

这是最基础的修复,先把监听器设置提前到音频播放启动前,同时修正buffer和周期参数的匹配:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // ... 其他初始化代码 ...

    // 提前设置监听器,确保播放一开始就生效
    audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener(){
        @Override
        public void onMarkerReached(AudioTrack arg0) { }
        
        @Override
        public void onPeriodicNotification(AudioTrack arg0) {
            Log.i("XXX","onPeriodicNotification() was called");
            flipBackgroundColor();
        }
    });

    // 用系统API计算最小buffer,再确保是1秒的数据长度
    int minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, 
        AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
    bufferSizeInBytes = Math.max(minBufferSize, sampleRateInHz * 2); // 1秒PCM_16BIT单声道
    audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, 
        AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, 
        bufferSizeInBytes, AudioTrack.MODE_STREAM);
    audioTrack.setPositionNotificationPeriod(sampleRateInHz); // 1秒对应样本数
}

2. API21+推荐:用AudioTimestamp获取精确播放位置

API21及以上可以直接读取硬件级的播放位置,不受CPU调度影响,精度和可靠性远高于系统回调:

private long lastReportedPosition = -1;
private Handler mainHandler = new Handler(Looper.getMainLooper());

private void start() {
    isPlaying = true;
    audioTrack.reloadStaticData();
    audioTrack.play();
    lastReportedPosition = audioTrack.getPlaybackHeadPosition();

    Runnable r = new Runnable() {
        public void run() {
            while(isPlaying) {
                int written = audioTrack.write(silenceArray,0,silenceArray.length);
                if (written < 0) {
                    Log.e("XXX","写入音频数据失败");
                    break;
                }

                // 轮询获取精确播放位置
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    AudioTimestamp timestamp = new AudioTimestamp();
                    if (audioTrack.getTimestamp(timestamp) == AudioTrack.SUCCESS) {
                        long currentPosition = timestamp.framePosition;
                        // 检查是否到达1秒间隔
                        if (currentPosition - lastReportedPosition >= sampleRateInHz) {
                            Log.i("XXX","通过AudioTimestamp触发回调");
                            // 切换UI要在主线程
                            mainHandler.post(() -> flipBackgroundColor());
                            lastReportedPosition = currentPosition;
                        }
                    }
                }
            }
            audioTrack.stop();
        }
    };
    new Thread(r).start();
}

3. 兜底方案:Handler手动计时(辅助校验)

如果系统回调还是不稳定,可以用Handler做手动计时补充,和AudioTrack的回调互相校验:

private Runnable periodicRunnable = new Runnable() {
    @Override
    public void run() {
        if (isPlaying) {
            Log.i("XXX","手动计时触发回调");
            flipBackgroundColor();
            mainHandler.postDelayed(this, 1000);
        }
    }
};

// 在start()中启动
mainHandler.postDelayed(periodicRunnable, 1000);
// 在stop()中销毁
mainHandler.removeCallbacks(periodicRunnable);

注意:这种方式依赖系统时钟,可能和实际播放位置有微小偏差,适合作为补充方案。


三、额外代码优化建议

  • 避免重复设置监听器:原代码每次start()都会创建新的监听器实例,可能导致旧监听器仍在生效,引发重复回调。
  • 及时释放资源:在Activity的onDestroy()中调用audioTrack.release(),避免内存泄漏。
  • 处理写入错误:原代码未处理audioTrack.write()的错误返回值,建议添加错误判断,避免播放异常。

内容的提问来源于stack exchange,提问作者Nerdy Bunz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:18:07