commit 4dcca08530ea4e8be46ddfd5417a81bfa2560efe
parent 2bee00465c559bda184ee24260b26af75b3d2c6f
Author: Jean Schurger <jean@schurger.org>
Date: Mon, 6 Apr 2026 17:53:06 -0400
Fix ghost progression, player reuse after release, and loop zone seek
- Don't cancel player scope on release (allows reuse)
- Guard position updates with playing check (no ghost progress)
- Seek to zone start when changing loop zone, tap position when same zone
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 20 insertions(+), 6 deletions(-)
diff --git a/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_screen.kt b/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_screen.kt
@@ -87,22 +87,30 @@ fun PlayerScreen(vm: PlayerViewModel, on_back: () -> Unit) {
loop_zones = loop_zones,
loop_state = loop_state,
on_seek = { tap_ms ->
- // Check if tapping inside a loop zone first
+ // Check if tapping inside a loop zone
val tapped_zone = loop_zones.firstOrNull { tap_ms >= it.start_ms && tap_ms < it.end_ms }
if (tapped_zone != null) {
+ val ls = loop_state
+ val already_active = ls.enabled && ls.mark_a?.position_ms == tapped_zone.start_ms && ls.mark_b?.position_ms == tapped_zone.end_ms
vm.tap_in_loop_zone(tap_ms)
- vm.seek(tap_ms)
+ // If changing zone, seek to start; if same zone, seek to tap position
+ vm.seek(if (already_active) tap_ms else tapped_zone.start_ms)
return@ProgressTimeline
}
- vm.seek(tap_ms)
- // If mark loop is active and tap is outside current loop, re-loop around tap position
+ // If mark loop is active and tap is outside current loop
val ls = loop_state
if (ls.enabled && ls.mark_a != null && ls.mark_a!!.id != "_full_a") {
val a = ls.mark_a!!.position_ms
val b = ls.mark_b?.position_ms ?: a
if (tap_ms < a || tap_ms > b) {
vm.loop_around(tap_ms)
+ val new_ls = vm.loop_state.value
+ vm.seek(new_ls.mark_a?.position_ms ?: tap_ms)
+ } else {
+ vm.seek(tap_ms)
}
+ } else {
+ vm.seek(tap_ms)
}
},
on_mark_label_click = { mark -> mark_to_rename = mark },
diff --git a/composeApp/src/iosMain/kotlin/ubi2/sonodrill/audio/ios_audio_player.kt b/composeApp/src/iosMain/kotlin/ubi2/sonodrill/audio/ios_audio_player.kt
@@ -111,13 +111,19 @@ class IosAudioPlayer : AudioPlayer {
_speed.value = rate; time_pitch?.rate = rate
}
override fun skip(delta_ms: Long) = seek((_position.value + delta_ms).coerceIn(0, _duration.value))
- override fun release() { release_engine(); scope.cancel() }
+ override fun release() { release_engine() }
- private fun release_engine() { stop_position_updates(); player_node?.stop(); engine?.stop(); engine = null; player_node = null; time_pitch = null; audio_file = null }
+ private fun release_engine() {
+ stop_position_updates()
+ player_node?.stop(); engine?.stop()
+ engine = null; player_node = null; time_pitch = null; audio_file = null
+ _playing.value = false
+ }
private fun start_position_updates() { position_job?.cancel(); position_job = scope.launch { while (isActive) { update_current_position(); delay(100) } } }
private fun stop_position_updates() { position_job?.cancel(); position_job = null }
private fun update_current_position() {
+ if (!_playing.value) return
val elapsed_ms = (play_start_mark.elapsedNow().inWholeMilliseconds * _speed.value).toLong()
val pos = play_start_position_ms + elapsed_ms
if (pos >= _duration.value) {