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

Android节拍器APP真机时间间隔不稳定问题求助

节拍器APP真机时间间隔不稳定问题解决

问题描述

我正在开发一款节拍器APP,需求是精准循环计数4拍,第一拍带重音。在Android Studio模拟器上运行正常,但在Galaxy J3-2018(Android v9)真机上,时间间隔完全不稳定,随机性很强。

尝试过Threads、Coroutines、Handlers,都没效果;也试过Timer对象,参考相关代码重构仍失败。

当前代码(模拟器运行正常):

timer = 5000
timeSignatureTop = 4
accentPlayed = false
measureCount = 1

Thread {
    while (!stopButtonPressed) {
        Log.d("Thread running", "running")

        if (!accentPlayed) {
            mediaPlayerAccent!!.start()
            measureCount++
            accentPlayed = true
        }

        else if (measureCount < timeSignatureTop) {
            mediaPlayerBeat!!.start()
            measureCount++
        }

        else if (measureCount == timeSignatureTop) {
            mediaPlayerBeat!!.start()
            measureCount = 1
            accentPlayed = false
        }

        Thread.sleep(timer.toLong())
    }

    Log.d("Thread stopped", "stopped")
    return@Thread
}.start()

核心问题分析

用Thread.sleep()控制节拍间隔的做法,容易被Android系统调度干扰——尤其是低性能设备,后台任务、进程优先级变化都会打乱sleep的精准度,导致时间间隔不稳定。另外,MediaPlayer的start()本身存在微小耗时,累积后会让节拍误差越来越大。

修复方案

方案1:使用ScheduledExecutorService保证固定速率执行

ScheduledExecutorService的scheduleAtFixedRate方法会自动调整执行时间,抵消任务本身的耗时,维持稳定的时间间隔,适合节拍器这种需要精准定时的场景。

优化后的代码示例:

import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit

// 全局变量
private val scheduler = Executors.newSingleThreadScheduledExecutor()
private var scheduledFuture: ScheduledFuture<*>? = null
var timer = 5000L // 直接用Long类型,避免类型转换耗时
val timeSignatureTop = 4
var accentPlayed = false
var measureCount = 1

// 启动节拍器
fun startMetronome() {
    // 先终止之前的任务,避免重复启动
    scheduledFuture?.cancel(false)
    
    // 固定速率执行:首次立即执行,之后每隔timer毫秒执行一次
    scheduledFuture = scheduler.scheduleAtFixedRate({
        when {
            !accentPlayed -> {
                mediaPlayerAccent?.start()
                measureCount++
                accentPlayed = true
            }
            measureCount < timeSignatureTop -> {
                mediaPlayerBeat?.start()
                measureCount++
            }
            measureCount == timeSignatureTop -> {
                mediaPlayerBeat?.start()
                measureCount = 1
                accentPlayed = false
            }
        }
    }, 0, timer, TimeUnit.MILLISECONDS)
}

// 停止节拍器
fun stopMetronome() {
    scheduledFuture?.cancel(false)
    scheduledFuture = null
}

// Activity销毁时关闭调度器,避免内存泄漏
override fun onDestroy() {
    super.onDestroy()
    scheduler.shutdown()
}

方案2:用SoundPool替代MediaPlayer(进一步降低延迟)

MediaPlayer适合播放长音频,节拍器的短节拍音用SoundPool更高效,播放延迟更低,能提升整体精准度。

示例代码:

import android.media.SoundPool

private val soundPool = SoundPool.Builder().setMaxStreams(2).build()
private var accentSoundId: Int = 0
private var beatSoundId: Int = 0

// 在Activity初始化时加载音频资源
fun loadBeatSounds() {
    accentSoundId = soundPool.load(this, R.raw.accent_beat, 1)
    beatSoundId = soundPool.load(this, R.raw.normal_beat, 1)
}

// 播放重音
fun playAccent() {
    soundPool.play(accentSoundId, 1f, 1f, 1, 0, 1f)
}

// 播放普通节拍
fun playNormalBeat() {
    soundPool.play(beatSoundId, 1f, 1f, 1, 0, 1f)
}

// 销毁时释放资源
override fun onDestroy() {
    super.onDestroy()
    soundPool.release()
}

额外优化点

  1. 提前初始化音频资源:在APP启动或进入节拍器页面时,完成MediaPlayer或SoundPool的初始化与准备工作,避免播放时的临时初始化耗时。
  2. UI更新需切主线程:如果要在界面上显示当前节拍数,需用runOnUiThread或协程切换到主线程执行更新操作,避免跨线程异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 16:32:30