sonodrill

Sonodrill, app iOS/Android d’entraînement musical (repères, boucles, vitesse), Kotlin Multiplatform + Compose
git clone https://git.schurger.org/sonodrill.git
Log | Files | Refs

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:
McomposeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_screen.kt | 16++++++++++++----
McomposeApp/src/iosMain/kotlin/ubi2/sonodrill/audio/ios_audio_player.kt | 10++++++++--
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) {