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:
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)
}