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 57eec87cd4e91b34ced9c0dcf39061ddcba8e47a
parent de3476b54d273df9aaa78c7941b3d739f8b3f14f
Author: Jean Schurger <jean@schurger.org>
Date:   Fri, 16 Oct 2026 15:28:04 -0400

A loop next to a skip zone extends over it

Marks A B C D with B..C skipped: looping from C..D loops A..D, honoring the
skip, instead of C..D. Moving to the next loop region steps over skipped spans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141Zm4UQq8PATQAG98MdNCc

Diffstat:
McomposeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_view_model.kt | 37+++++++++++++++++++++++++++----------
1 file changed, 27 insertions(+), 10 deletions(-)

diff --git a/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_view_model.kt b/composeApp/src/commonMain/kotlin/ubi2/sonodrill/ui/player/player_view_model.kt @@ -182,13 +182,17 @@ class PlayerViewModel( if (ls.is_mark_loop() && ls.mark_b != null) { val current_end = ls.mark_b val sorted = _marks.value.sortedBy { it.position_ms } - val next_start = sorted.firstOrNull { it.position_ms >= current_end.position_ms && it.id != current_end.id } - if (next_start != null) { - val next_end = sorted.firstOrNull { it.position_ms > next_start.position_ms } - if (next_end != null) { - loop_between(next_start, next_end) - return + var next_start = sorted.firstOrNull { it.position_ms >= current_end.position_ms && it.id != current_end.id } + while (next_start != null) { + val start = next_start + val next_end = sorted.firstOrNull { it.position_ms > start.position_ms } ?: break + // A skipped span is not a loop region: go on to the following one + if (_skip_zones.value.any { it.start_ms == start.position_ms && it.end_ms == next_end.position_ms }) { + next_start = next_end + continue } + loop_between(start, next_end) + return } // No next loop region available, disable loop loop.clear() @@ -217,14 +221,27 @@ class PlayerViewModel( val sorted = _marks.value.sortedBy { it.position_ms } val prev = sorted.lastOrNull { it.position_ms <= position_ms } val next = sorted.firstOrNull { it.position_ms > position_ms } - if (prev != null && next != null) loop_between(prev, next) + if (prev != null && next != null) loop_between(loop_start_mark(prev, sorted), next) + } + + // Start of a loop whose previous mark is prev: a skip zone ending at prev is not a + // boundary, the loop extends to the mark before that zone (marks A B C D with B..C + // skipped: from C..D the loop is A..D) and the skip is honored inside it + private fun loop_start_mark(prev: MarkInfo, sorted: List<MarkInfo>): MarkInfo { + var start = prev + repeat(_skip_zones.value.size) { + val skip = _skip_zones.value.firstOrNull { it.end_ms == start.position_ms } ?: return start + start = sorted.lastOrNull { it.position_ms < skip.start_ms } ?: return start + } + return start } fun add_mark_and_loop() { val pos = position.value val new_mark = add_mark_at(pos) ?: return - val prev = _marks.value.lastOrNull { it.position_ms < pos } ?: return - val zone = LoopZone(prev.position_ms, new_mark.position_ms) + val sorted = _marks.value.sortedBy { it.position_ms } + val prev = sorted.lastOrNull { it.position_ms < pos } ?: return + val zone = LoopZone(loop_start_mark(prev, sorted).position_ms, new_mark.position_ms) add_loop_zone(zone) activate_loop_zone(zone) } @@ -241,7 +258,7 @@ class PlayerViewModel( val prev = sorted.lastOrNull { it.position_ms <= pos + 500 } val next = sorted.firstOrNull { it.position_ms > pos - 500 && it != prev } if (prev != null && next != null) { - val zone = LoopZone(prev.position_ms, next.position_ms) + val zone = LoopZone(loop_start_mark(prev, sorted).position_ms, next.position_ms) add_loop_zone(zone) activate_loop_zone(zone) }