fix: atomic PlayState replaces non-atomic seekVersion+elapsed+progress
PlayState data class bundles playId, elapsedMs, progress into one atomic snapshot. LaunchedEffect keyed on playId — when it changes (seek/resume), the timer reads elapsedMs and progress from the SAME snapshot, eliminating the race between seekVersion++ and elapsedMs=seekMs.
This commit is contained in:
1 parent
1481d1244b
commit
e4af56856c
1 file changed
+18
-17
+18
-17
@@ -79,29 +79,30 @@ fun MessageBubble(message: Message, onPlayVoice: (String, () -> Unit, () -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
private data class PlayState(val playId: Int, val elapsedMs: Int, val progress: Float)
|
||||
|
||||
@Composable
|
||||
private fun VoiceBubble(message: Message, textColor: Color,
|
||||
onPlay: (String, () -> Unit, () -> Unit) -> Unit,
|
||||
onSeek: (String, Int, () -> Unit) -> Unit) {
|
||||
val wf = message.waveform // List<Float> 0..1, 100 elements
|
||||
val wf = message.waveform
|
||||
val wfCount = wf.size
|
||||
val durMs = message.voiceDurationMs
|
||||
var playing by remember { mutableStateOf(false) }
|
||||
var paused by remember { mutableStateOf(false) }
|
||||
var seekVersion by remember { mutableStateOf(0) }
|
||||
var elapsedMs by remember { mutableStateOf(0) }
|
||||
var progress by remember { mutableStateOf(0f) }
|
||||
var state by remember { mutableStateOf(PlayState(0, 0, 0f)) }
|
||||
|
||||
val onDone = remember { { playing = false; paused = false; elapsedMs = 0; progress = 0f } }
|
||||
val onDone = remember { { playing = false; paused = false; state = PlayState(0, 0, 0f) } }
|
||||
val onPaused = remember { { paused = true; playing = false } }
|
||||
|
||||
/* elapsed timer while playing */
|
||||
LaunchedEffect(playing, seekVersion) {
|
||||
/* elapsed timer while playing — keyed on atomic playId */
|
||||
LaunchedEffect(state.playId) {
|
||||
if (playing && !paused) {
|
||||
val start = System.currentTimeMillis() - elapsedMs
|
||||
val start = System.currentTimeMillis() - state.elapsedMs
|
||||
while (playing && !paused) {
|
||||
elapsedMs = (System.currentTimeMillis() - start).toInt().coerceIn(0, durMs)
|
||||
if (durMs > 0) progress = (elapsedMs.toFloat() / durMs.toFloat()).coerceIn(0f, 1f)
|
||||
val ms = (System.currentTimeMillis() - start).toInt().coerceIn(0, durMs)
|
||||
val frac = if (durMs > 0) ms.toFloat() / durMs.toFloat() else 0f
|
||||
state = state.copy(elapsedMs = ms, progress = frac.coerceIn(0f, 1f))
|
||||
delay(80)
|
||||
}
|
||||
}
|
||||
@@ -117,7 +118,7 @@ private fun VoiceBubble(message: Message, textColor: Color,
|
||||
if (playing && !paused) {
|
||||
onPlay(message.filePath, onDone, onPaused)
|
||||
} else {
|
||||
seekVersion++
|
||||
state = state.copy(playId = state.playId + 1)
|
||||
playing = true; paused = false
|
||||
onPlay(message.filePath, onDone, onPaused)
|
||||
}
|
||||
@@ -138,8 +139,8 @@ private fun VoiceBubble(message: Message, textColor: Color,
|
||||
if (message.filePath.isEmpty()) return@detectTapGestures
|
||||
val seekFrac = (offset.x / size.width).coerceIn(0f, 1f)
|
||||
val seekMs = (seekFrac * durMs).toInt()
|
||||
seekVersion++
|
||||
playing = true; paused = false; elapsedMs = seekMs; progress = seekFrac
|
||||
state = PlayState(state.playId + 1, seekMs, seekFrac)
|
||||
playing = true; paused = false
|
||||
onSeek(message.filePath, seekMs, onDone)
|
||||
}
|
||||
}
|
||||
@@ -148,7 +149,7 @@ private fun VoiceBubble(message: Message, textColor: Color,
|
||||
val barW = size.width / wfCount
|
||||
val ph = size.height
|
||||
val showCursor = playing || paused
|
||||
val playedIdx = if (showCursor && progress > 0f) (progress * wfCount).toInt() else 0
|
||||
val playedIdx = if (showCursor && state.progress > 0f) (state.progress * wfCount).toInt() else 0
|
||||
for (i in 0 until wfCount) {
|
||||
val level = wf[i].coerceIn(0f, 1f)
|
||||
val barH = (2f + level * (ph - 2f)).coerceAtMost(ph)
|
||||
@@ -161,8 +162,8 @@ private fun VoiceBubble(message: Message, textColor: Color,
|
||||
size = Size(barW * 0.75f, barH)
|
||||
)
|
||||
}
|
||||
if (showCursor && progress > 0f && progress < 1f) {
|
||||
val cursorX = progress * size.width
|
||||
if (showCursor && state.progress > 0f && state.progress < 1f) {
|
||||
val cursorX = state.progress * size.width
|
||||
drawLine(Color(0xFFFFD600), Offset(cursorX, 0f), Offset(cursorX, ph), strokeWidth = 2.5f)
|
||||
}
|
||||
}
|
||||
@@ -179,7 +180,7 @@ private fun VoiceBubble(message: Message, textColor: Color,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
if (playing || paused) "${formatDurationMs(elapsedMs)} / ${formatDurationMs(durMs)}"
|
||||
if (playing || paused) "${formatDurationMs(state.elapsedMs)} / ${formatDurationMs(durMs)}"
|
||||
else formatDurationMs(durMs),
|
||||
fontSize = 12.sp,
|
||||
color = textColor.copy(alpha = 0.55f)
|
||||
|
||||
Reference in new issue
Block a user