Android AudioTrack周期回调不按时触发问题排查及方案咨询
问题分析与解决方案:AudioTrack 回调延迟/合并问题(API 19-22)
你遇到的这个AudioTrack周期性回调偶发延迟、首次回调缺失甚至两次合并触发的问题,在API 19-22(KitKat到Lollipop早期)的模拟器(甚至部分真实设备)上确实是个常见的坑。你的CPU频率缩放猜测完全正确,再加上代码里的一些细节问题,共同导致了这个现象。下面我来详细拆解原因,并给出几个更可靠的解决办法:
一、现象背后的原因
CPU调频器调度限制
API 19-22的系统音频线程调度优先级在低功耗场景下不够高,当CPU处于降频状态时,音频回调线程可能被延迟调度,导致第一个周期的回调信号被积压,直到第二个周期时才一起触发,这和你提到的视频里的分析完全吻合。代码时序与参数不匹配
- 你在子线程的
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
相关产品推荐
相关产品推荐

