diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 8545870..36fc437 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -291,6 +291,9 @@ actual class FMediaPlayer actual constructor( if (needsClockSync) { clockNano = System.nanoTime() clockTick = currentTickPos + idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos } + .takeIf { it >= 0 } ?: events.size + needsClockSync = false } if (isHolding) { @@ -304,7 +307,6 @@ actual class FMediaPlayer actual constructor( if (idx >= events.size) { val dc = getPendingDcStep() if (dc != null) { - dc.alreadyDone = true allNotesOff() seekToGrid(dc.targetGrid) clockNano = System.nanoTime(); clockTick = currentTickPos @@ -312,7 +314,10 @@ actual class FMediaPlayer actual constructor( .takeIf { it >= 0 } ?: events.size continue } else { - isRunning = false; allNotesOff(); onFinished(); break + isRunning = false + allNotesOff() + onFinished() + break } } @@ -335,6 +340,10 @@ actual class FMediaPlayer actual constructor( yield() } } + if (needsClockSync) { + continue + } + currentTickPos = ev.tickAbsolute lastEventNano = System.nanoTime() @@ -357,7 +366,7 @@ actual class FMediaPlayer actual constructor( 0xC0 -> send(0xC0 or ev.channel, ev.data1) 0xFF -> { /* tempo ignoré */ } } - val currentGrid = (ev.tickAbsolute / resolution).toLong() + val currentGrid = (currentTickPos/* ev.tickAbsolute*/ / resolution).toLong() if (boundModel?.activeIndex?.value != currentGrid.toInt()) { boundModel?.updateActiveIndex(currentGrid) } @@ -368,12 +377,15 @@ actual class FMediaPlayer actual constructor( actual fun seekToGrid(gridIndex: Int) { currentTickPos = gridIndex.toLong() * resolution - lastEventNano = System.nanoTime() // ← recaler l'horloge d'interpolation + lastEventNano = System.nanoTime() + val lastDyn = navigationSteps .filter { it.dynamic != null && it.gridIndex <= gridIndex } .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF currentDynamicFactor = if (lastDyn == Dynamic.MF) 1.0f else lastDyn.factor applyVoiceStates() + + needsClockSync = true } private fun ticksToMs(ticks: Long) = (ticks * usPerTick / 1000.0).toLong() private fun msToTicks(ms: Long) = (ms * 1000.0 / usPerTick).toLong() @@ -399,210 +411,248 @@ actual class FMediaPlayer actual constructor( val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE) - metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> - val ci = gridIndex ?: 0 + metadataList.forEach { midiMarker -> + val ci = midiMarker.gridIndex ?: 0 val dsR = Regex("""D\.?S\.?"""); - val dcR = Regex("""D\.?C\.?""") + val dcR = Regex("""D\.?C\.?""") val dsG = Regex("""D\.?S\.?_GROUP_PART"""); - val dcG = Regex("""D\.?C\.?_GROUP_PART""") + val dcG = Regex("""D\.?C\.?_GROUP_PART""") + val finReg = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE) val last_grid = sharedScreenModel.getTotalGridCount() val hairPins = sharedScreenModel.getHairPins() + val markerList = midiMarker.marker - when { - marker.contains("$") -> lastSegno = ci + markerList.forEach { marker -> + val mTrim = marker.trim() + when { + marker.contains("$") -> lastSegno = ci - marker.contains("\uD834\uDD10") -> { - val beat = if(note.contains('•')) 2 else 1 - navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat)) - } - ritRegex.containsMatchIn(marker) -> { - val endGrid = if(lastCallerMarker == 0) last_grid else lastCallerMarker - navigationSteps.add( - NavigationStep( - marker = marker, - gridIndex = ci, - isTempoChange = true, - tempoType = "rit", - endGridForTempo = endGrid, - targetTempoMultiplier = 0.55f - ) - ) - } - rallRegex.containsMatchIn(marker) -> { - val endGrid = last_grid - navigationSteps.add( - NavigationStep( - marker = marker, - gridIndex = ci, - isTempoChange = true, - tempoType = "rall", - endGridForTempo = endGrid, - targetTempoMultiplier = 0.50f - ) - ) - } - dsG.matches(marker.trim()) -> { - val target = if(lastSegno>0) lastSegno else 0 - val indx = if((last_grid-ci)<=0) ci-1 else ci - navigationSteps.add(NavigationStep(marker, indx, targetGrid=target)) - } - - dsR.matches(marker.trim()) || marker == "DSFin" -> { - navigationSteps.add(NavigationStep(marker, ci, - targetGrid = if(lastSegno>0) lastSegno else 0)) - } - - dcG.matches(marker.trim()) -> { - val indx = if((last_grid-ci)<=0) ci-1 else ci - navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) - println("DC_GROUP créé à $indx → 0") - } - - dcR.matches(marker.trim()) && !marker.contains("DC_GROUP_PART") -> { - val indx = if((last_grid-ci)<=0) ci else ci - navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) - println("DC créé à $indx → 0") - } - - // ── Farany ──────────────────────────────── - marker.trim().equals("Farany_GROUP_PART", ignoreCase = true) -> { - val hasDcAfter = metadataList.any { (_, gi, _, _, mk, _, _, _) -> - (gi ?: 0) > ci && - (mk.contains(Regex("""D\.?C\.?""")) || mk.contains("DC_GROUP_PART")) + marker.contains("\uD834\uDD10") -> { + val beat = if(midiMarker.note.contains('•')) 2 else 1 + navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat)) } - if (hasDcAfter) { + ritRegex.containsMatchIn(marker) -> { + val endGrid = if(midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker + navigationSteps.add( + NavigationStep( + marker = marker, + gridIndex = ci, + isTempoChange = true, + tempoType = "rit", + endGridForTempo = endGrid, + targetTempoMultiplier = 0.55f + ) + ) + } + rallRegex.containsMatchIn(marker) -> { + val endGrid = last_grid + navigationSteps.add( + NavigationStep( + marker = marker, + gridIndex = ci, + isTempoChange = true, + tempoType = "rall", + endGridForTempo = endGrid, + targetTempoMultiplier = 0.50f + ) + ) + } + dsG.matches(mTrim) -> { + val target = if(lastSegno>0) lastSegno else 0 + val indx = if((last_grid-ci)<=0) ci-1 else ci + navigationSteps.add(NavigationStep(marker, indx, targetGrid=target)) + } + + dsR.matches(mTrim) || marker == "DSFin" -> { + navigationSteps.add(NavigationStep(marker, ci, + targetGrid = if(lastSegno>0) lastSegno else 0)) + } + + dcG.matches(mTrim) -> { + val indx = if((last_grid-ci)<=0) ci-1 else ci + navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) + println("DC_GROUP créé à $indx → 0") + } + + dcR.matches(mTrim) && !marker.contains("DC_GROUP_PART") -> { + val indx = if((last_grid-ci)<=0) ci else ci + navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) + println("DC créé à $indx → 0") + } + + // ── Farany ──────────────────────────────── + finReg.containsMatchIn(mTrim) -> { + val hasDcAfter = metadataList.any { m -> + val after = (m.gridIndex ?: 0) > ci + + val hasMarker = m.marker.any { mk -> + val value = mk.trim() + val ok = dcG.matches(value) || dcR.matches(value) + ok + } + after && hasMarker + } + if (hasDcAfter) { + navigationSteps.add(NavigationStep( + marker = marker, + gridIndex = ci, + targetGrid = last_grid, + isFarany = true, + faranyActive = false + )) + println("Farany mémorisé à $ci") + } else { + val verse = sharedScreenModel.stanza.value + val verses = sharedScreenModel.nbStanzas.value + println("Farany ignoré à $ci i $verse == $verses") + navigationSteps.add( + NavigationStep( + marker="${marker}_STZ", + gridIndex=ci, + isFarany = true, + faranyActive = if(verse == verses) false else true + ) + ) + } + } + + extractDynamic(marker) != null && mTrim != "=" -> { + val dyn = extractDynamic(marker) ?: return@forEach + lastFactor = if(dyn==Dynamic.MF) 1.0f else dyn.factor + navigationSteps.add(NavigationStep(marker=marker, gridIndex=ci, dynamic=dyn)) + } + + mTrim=="<" || mTrim==">" || + mTrim.contains("cres", ignoreCase=true) || + mTrim.contains("dim", ignoreCase=true) -> { + val sym = when { + mTrim=="<" -> '<'; mTrim==">" -> '>' + mTrim.contains("cres",ignoreCase=true) -> '<'; else -> '>' + } + val pair = hairPins.find{it.startGrid==ci} ?: return@forEach + val explicitAfter = metadataList + .filter{ m ->(m.gridIndex ?:0) >= pair.endGrid } + .mapNotNull { m -> + val dynamic = m.marker.firstNotNullOfOrNull { mk -> extractDynamic(mk) } + if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null + } + .minByOrNull { it.first } + ?.second + + val from = lastFactor + val to = when { + explicitAfter!=null && sym=='<' && explicitAfter.factor>from -> explicitAfter.factor + explicitAfter!=null && sym=='>' && explicitAfter.factor explicitAfter.factor + else -> nextDynamic(from, sym) + } + lastFactor = to navigationSteps.add(NavigationStep( - marker = marker, - gridIndex = ci, - targetGrid = last_grid, - isFarany = true, - faranyActive = false + marker=mTrim, gridIndex=ci, hairPin=sym, + hairPinEndGrid=pair.endGrid, + hairPinFromFactor=from, hairPinToFactor=to )) - println("Farany mémorisé à $ci") - } else { - println("Farany ignoré à $ci") } } - - extractDynamic(marker) != null && marker.trim() != "=" -> { - val dyn = extractDynamic(marker) ?: return@forEach - lastFactor = if(dyn==Dynamic.MF) 1.0f else dyn.factor - navigationSteps.add(NavigationStep(marker=marker, gridIndex=ci, dynamic=dyn)) - } - - marker.trim()=="<" || marker.trim()==">" || - marker.trim().contains("cres", ignoreCase=true) || - marker.trim().contains("dim", ignoreCase=true) -> { - val sym = when { - marker.trim()=="<" -> '<'; marker.trim()==">" -> '>' - marker.trim().contains("cres",ignoreCase=true) -> '<'; else -> '>' - } - val pair = hairPins.find{it.startGrid==ci} ?: return@forEach - val explicitAfter = metadataList - .filter{(_,gi,_,_,mk,_,_,_)->(gi?:0)>=pair.endGrid && extractDynamic(mk)!=null} - .minByOrNull{it.gridIndex?:0}?.let{m->extractDynamic(m.marker)} - val from = lastFactor - val to = when { - explicitAfter!=null && sym=='<' && explicitAfter.factor>from -> explicitAfter.factor - explicitAfter!=null && sym=='>' && explicitAfter.factor explicitAfter.factor - else -> nextDynamic(from, sym) - } - lastFactor = to - navigationSteps.add(NavigationStep( - marker=marker.trim(), gridIndex=ci, hairPin=sym, - hairPinEndGrid=pair.endGrid, - hairPinFromFactor=from, hairPinToFactor=to - )) - } } } } private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) { var lastProcessedIndex = -1 + var isJumping = false + var pendingFin = false navigationJob?.cancel() + navigationJob = playerScope.launch(Dispatchers.Default) { sharedScreenModel.activeIndex.collect { currentIndex -> - if (currentIndex <= lastProcessedIndex) { - return@collect - } + //if (currentIndex <= lastProcessedIndex && lastProcessedIndex != -1) return@collect + lastProcessedIndex = currentIndex + if (!isRunning || currentIndex < 0) return@collect - - val step = navigationSteps.find { + val currentSteps = navigationSteps.filter { it.gridIndex == currentIndex && !it.alreadyDone - } - println("pas: ${currentIndex}") - if (step != null) { + }.sortedBy { step -> when { - // ── Farany ──────────────────────────── - step.isFarany -> { - if (step.faranyActive) { - println("Farany activé → STOP à grille $currentIndex") + step.isHold -> 0 + step.isTempoChange -> 1 + step.isFarany -> 1 + step.dynamic != null || step.hairPin != null -> 1 + else -> 3 + } + } + for(step in currentSteps) { + when { + // ── Point d'orgue ───────────────────── + step.isHold -> { + val beatMs = (60_000 / targetBpm).toLong() + val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2 + + isHolding = true + for (ch in 0 until 4) controlChange(ch, 64, 127) + delay(holdDuration) + for (ch in 0 until 4) controlChange(ch, 64, 0) + isHolding = false + println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms") + } + // Rit. / Rall. , ... + step.isTempoChange -> { step.alreadyDone = true - isHolding = false - isRunning = false - playJob?.cancel() - allNotesOff() - onFinished() - } else { - println("Farany 1er passage — DC pas encore vu → on continue ✅") + val startBpm = currentPlaybackBpm + val endBpm = targetBpm * step.targetTempoMultiplier + val gridDistance = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1) + val beatDurationMs = (60_000L / targetBpm).toLong() + val durationMs = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L) + + println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}") + println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms") + + applyTempoChange( + startBpm = startBpm, + endBpm = endBpm, + durationMs = durationMs, + tempoType = step.tempoType + ) } - } - // ── Point d'orgue ───────────────────── - step.isHold -> { - val beatMs = (60_000 / targetBpm).toLong() - val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2 - println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms") - for (ch in 0 until 4) controlChange(ch, 64, 127) - isHolding = true - delay(holdDuration) - isHolding = false - for (ch in 0 until 4) controlChange(ch, 64, 0) - step.alreadyDone = true - } - // Rit. / Rall. , ... - step.isTempoChange -> { - step.alreadyDone = true - val startBpm = currentPlaybackBpm - val endBpm = targetBpm * step.targetTempoMultiplier - val gridDistance = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1) - val beatDurationMs = (60_000L / targetBpm).toLong() - val durationMs = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L) + // ── Soufflet ────────────────────────── + step.hairPin != null -> { + step.alreadyDone = true + val dist = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1) + val beatMs = (60_000L / targetBpm).toLong() + applyCrescendo(step.hairPinFromFactor, step.hairPinToFactor, + (dist * beatMs).coerceIn(200L, 5000L)) + } - println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}") - println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms") - - applyTempoChange( - startBpm = startBpm, - endBpm = endBpm, - durationMs = durationMs, - tempoType = step.tempoType - ) - } - - // ── Soufflet ────────────────────────── - step.hairPin != null -> { - step.alreadyDone = true - val dist = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1) - val beatMs = (60_000L / targetBpm).toLong() - applyCrescendo(step.hairPinFromFactor, step.hairPinToFactor, - (dist * beatMs).coerceIn(200L, 5000L)) - } - - // ── Dynamique ───────────────────────── - step.dynamic != null -> { - step.alreadyDone = true - applyDynamic(step.dynamic) - } + // ── Dynamique ───────────────────────── + step.dynamic != null -> { + step.alreadyDone = true + applyDynamic(step.dynamic) + } + // ── Farany ───────────────────────── + step.isFarany -> { + println("C Farany ok $step") + if (pendingFin || step.faranyActive) { + println("Farany activé → STOP à grille $currentIndex") + isRunning = false + allNotesOff() + onFinished() + return@collect + } else { + println("Farany 1er passage — DC pas encore vu") + pendingFin = true + step.alreadyDone = true + continue + } + } // ── DC / DS ─────────────────────────── else -> { step.alreadyDone = true val beatMs = (60_000 / targetBpm).toLong() println("avant de sauter bpm=$targetBpm") + //lastProcessedIndex = -1 for (ch in 0 until 4) controlChange(ch, 64, 127) isHolding = true delay(beatMs) @@ -621,10 +671,9 @@ actual class FMediaPlayer actual constructor( } startPlaybackLoop() - println("DC/DS → grille ${step.targetGrid}") + } } } - } } } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt index 74b971a..29bc204 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt @@ -7,27 +7,12 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddCircleOutline import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.RemoveCircleOutline -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.PlainTooltip -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TooltipBox -import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.rememberTooltipState +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,6 +37,13 @@ data class MarkerGroup( val markers: List ) +fun getAllMarker(): List { + return markerGroups + .flatMap { group -> group.markers } + .map { marker -> marker.abbr } + .filter { it != "♩=" && it != "Dô dia" } + .sortedByDescending { it.length } +} private val markerGroups = listOf( MarkerGroup( title = "Nuances", @@ -71,6 +63,8 @@ private val markerGroups = listOf( Marker("dim.", "Diminuendo"), Marker(">", "Decrescendo"), + Marker("poco", "Un peu (modifie la nuance suivante)"), + Marker("organa", "Style ornementé"), Marker("sf", "Accent soudain"), Marker("sfz", "Sforzato"), Marker("fz", "Forzando"), @@ -106,11 +100,11 @@ private val markerGroups = listOf( Marker("string.", "Presser le tempo"), Marker("♩=", "Changement tempo", canEditValue = true, defaultValue = "60"), Marker("allarg.", "Élargir le tempo"), - Marker("a tempo", "Retour au tempo initial"), + Marker("a_tempo", "Retour au tempo initial"), Marker("Tempo I", "Retour au premier tempo"), Marker("rubato", "Tempo libre"), - Marker("meno mosso", "Moins rapide"), - Marker("più mosso", "Plus rapide") + Marker("meno_mosso", "Moins rapide"), + Marker("più_mosso", "Plus rapide") ) ), @@ -118,16 +112,19 @@ private val markerGroups = listOf( title = "Reprises", markers = listOf( Marker("D.C.", "Da Capo"), - Marker("D.C. Fin", "Retour au début jusqu'à Fine"), - Marker("D.C. al Coda", "Retour au début puis aller à la Coda"), + Marker("D.C.Fin", "Retour au début jusqu'à Fine"), + Marker("D.C.alCoda", "Retour au début puis aller à la Coda"), Marker("D.S.", "Retour au Segno"), Marker("End", "Fin"), - Marker("D.S. Fin", "Retour au Segno jusqu'à Fine"), + Marker("D.S.Fin", "Retour au Segno jusqu'à Fine"), Marker("Farany", "Fin"), Marker("$", "Segno"), Marker("Fine", "Fin"), + Marker("Fiverenana", "Réfrain"), + Marker("Isan\'andininy", "Réfrain"), + Marker("Fine", "Fin"), Marker("𝄌", "Coda"), - Marker("To Coda", "Aller à la Coda"), + Marker("To_Coda", "Aller à la Coda"), Marker("||:", "Début de reprise"), Marker(":||", "Fin de reprise"), Marker("Bis", "Rejouer") diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/PTemplate.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/PTemplate.kt index fbec31b..5a8b7dd 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/PTemplate.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/PTemplate.kt @@ -36,6 +36,61 @@ class PTemplate (val template: String, val separatorAfter: String, private val m .replace(Regex("^c:(.*)"), "Do dia $1") } } + fun markerToList(): List { + if (markers.isEmpty()) { + return emptyList() + } + val mString = markerToString() + return parseMarkers(mString) + } + fun parseMarkers(rawText: String): List { + val tokens = mutableListOf() + if (rawText.isBlank()) return tokens + + var remaining = rawText.replace(",", " ") + .replace(Regex("""\b(DC)\b""", RegexOption.IGNORE_CASE), "D.C.") + .replace(Regex("""\b(DS)\b""", RegexOption.IGNORE_CASE), "D.S.") + .replace(Regex("""(D\.?C\.?)\s*(Fin)""", RegexOption.IGNORE_CASE), "D.C.Fin") + .replace(Regex("""(D\.?S\.?)\s*(Fin)""", RegexOption.IGNORE_CASE), "D.S.Fin") + .replace(Regex("""(D\.?C\.?)\s*(al\s*Coda)""", RegexOption.IGNORE_CASE), "D.C.alCoda") + .replace(Regex("""(D\.?S\.?)\s*(al\s*Coda)""", RegexOption.IGNORE_CASE), "D.S.alCoda") + + val allStaticMarkers = getAllMarker() + val tempoRegex = Regex("""^(♩\s*=\s*\d+)""", RegexOption.IGNORE_CASE) + val modulaRegex = Regex("""^(D(?:ô|o)?\s*dia\s*(?:C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B))""", RegexOption.IGNORE_CASE) + + // Lexer + while (remaining.isNotEmpty()) { + remaining = remaining.trimStart() + if (remaining.isEmpty()) break + + val tempoMatch = tempoRegex.find(remaining) + if (tempoMatch != null) { + tokens.add(tempoMatch.value) + remaining = remaining.substring(tempoMatch.value.length) + continue + } + + val modulaMatch = modulaRegex.find(remaining) + if (modulaMatch != null) { + tokens.add(modulaMatch.value) + remaining = remaining.substring(modulaMatch.value.length) + continue + } + + val matchedMarker = allStaticMarkers.find { remaining.startsWith(it, ignoreCase = true) } + if (matchedMarker != null) { + tokens.add(matchedMarker) + remaining = remaining.substring(matchedMarker.length) + } else { + val unknownChunk = remaining.takeWhile { !it.isWhitespace() } + tokens.add(unknownChunk) + remaining = remaining.substring(unknownChunk.length) + } + } + return tokens + } + fun hasKeyChange(): String { var returnFun = "" markers.map { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt index ae77caa..1348a69 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -1,5 +1,6 @@ package mg.dot.feufaro +import SharedScreenModel import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* @@ -12,17 +13,16 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.sharp.Delete import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -48,6 +48,7 @@ fun TUODetailDialog( globalIndex: Int, isEditable: Boolean, canAdd: Boolean, + sharedScreenModel: SharedScreenModel, onIndexChanged: (Int) -> Unit, onDismiss: () -> Unit, onSave: (TUOEditState) -> Unit @@ -344,61 +345,80 @@ fun TUODetailDialog( ) } - if(!templateFragment.isNullOrBlank() && templateFragment !="-") { + + if (!templateFragment.isNullOrBlank() && templateFragment != "-") { val hasMarker = marker.isNotEmpty() - val tooltipText = if (hasMarker) { - "Enlever la marqueur" - } else { - "Ajouter une marqueur" - } - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip( - containerColor = Color.DarkGray, - contentColor = Color.White + + Row(verticalAlignment = Alignment.CenterVertically) { + + // 1. Bouton "Remove" (affiché uniquement si hasMarker est true) + if (hasMarker) { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { + Text(text = "Enlever le marqueur", fontSize = MaterialTheme.typography.titleSmall.fontSize) + } + }, + state = rememberTooltipState() ) { - Text(text = tooltipText, fontSize = MaterialTheme.typography.titleSmall.fontSize) - } - }, - state = rememberTooltipState() - ) { - IconButton( - modifier = Modifier.size(30.dp) - .onGloballyPositioned { coordinates -> - markerButtonPos = coordinates.positionInParent() - .let { - IntOffset( - it.x.toInt(), - it.y.toInt() - ) - } - }, - onClick = { - if(hasMarker) { - val state = TUOEditState( - tuoIndex = globalIndex, - notesByVoice = notes.toMap(), - originalNotes = originalNotes.toMap(), - originalLyricsByStanza = originalLyricsByStz.toMutableMap(), - lyricsByStanza = mutableMapOf(), - templateFragment = templateFragment, - marker = "_", - originalSep = initialSep, - sep = newSep + IconButton( + modifier = Modifier.size(25.dp), + onClick = { + val state = TUOEditState( + tuoIndex = globalIndex, + notesByVoice = notes.toMap(), + originalNotes = originalNotes.toMap(), + originalLyricsByStanza = originalLyricsByStz.toMutableMap(), + lyricsByStanza = mutableMapOf(), + templateFragment = templateFragment, + marker = "_", // On remet le marqueur à vide + originalSep = initialSep, + sep = newSep + ) + onSave(state) + } + ) { + Icon( + imageVector = Icons.Sharp.Delete, + tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), + contentDescription = "Enlever" ) - onSave(state) } - showMarkerPopup = true - }) { - Icon( - imageVector = if(hasMarker) Icons.Default.Remove else Icons.Default.Add, - tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), - contentDescription = null - ) + } + } + + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { + Text( + text = "Ajouter un marqueur", + fontSize = MaterialTheme.typography.titleSmall.fontSize + ) + } + }, + state = rememberTooltipState() + ) { + IconButton( + modifier = Modifier.size(25.dp) + .onGloballyPositioned { coordinates -> + markerButtonPos = coordinates.positionInParent().let { + IntOffset(it.x.toInt(), it.y.toInt()) + } + }, + onClick = { showMarkerPopup = true } + ) { + Icon( + imageVector = Icons.Default.Edit, + tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), + contentDescription = null + ) + } } } } + } Spacer(modifier = Modifier.height(8.dp)) @@ -595,8 +615,20 @@ fun TUODetailDialog( onDismiss = { showMarkerPopup = false }, - onMarkerSelected = { marker -> + onMarkerSelected = { newMarker -> showMarkerPopup = false + val cleanMarker = marker.trim().replace(Regex("""\s+"""), " ") + val modulaRegex = Regex("""D(ô|o)?\s*dia\s*(C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B)""", RegexOption.IGNORE_CASE) + val formattedExisting = modulaRegex.replace(cleanMarker) { match -> + "c:${match.groupValues[2]}" + } + val formattedMod = modulaRegex.replace(newMarker) { match -> "c:${match.groupValues[2]}"} + + val markerToSave = if (TimeUnitObject._hasMarker) { + "${formattedExisting.trim()} ${formattedMod.trim()} " + } else { + "${formattedMod.trim()} " + } val state = TUOEditState( tuoIndex = globalIndex, @@ -605,7 +637,7 @@ fun TUODetailDialog( originalLyricsByStanza = originalLyricsByStz.toMutableMap(), lyricsByStanza = mutableMapOf(), templateFragment = templateFragment, - marker = marker, + marker = markerToSave, originalSep = initialSep, sep = newSep ) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 82eadbc..057bec6 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import androidx.compose.ui.window.Popup +import androidx.lifecycle.compose.collectAsStateWithLifecycle import feufaro.composeapp.generated.resources.Emmentaler import feufaro.composeapp.generated.resources.PTSerif_Bold import feufaro.composeapp.generated.resources.PT_Serif_Bold_Italic @@ -646,29 +647,34 @@ fun LazyVerticalGridTUO( val currentStanza = viewModel.stanza val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() - val activeRowIndex by sharedScreenModel.activeIndex.collectAsState() + val activeRowIndex by sharedScreenModel.activeIndex.collectAsStateWithLifecycle() val measures = tuoList.drop(1).chunked(gridColumnCount) // Avant column affichage: val metadataList = remember(tuoList) { tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO -> - val markerText = oneTUO.pTemplate.markerToString() + val rawMarker = oneTUO.pTemplate.markerToString() val hairPin = oneTUO.hasHairPin() - val finalMarker = when { + val markerList = sharedScreenModel.parseMarkers(rawMarker) + /*val finalMarker = when { hairPin != null && markerText.isBlank() -> hairPin.toString() hairPin != null && markerText.isNotBlank() -> "${markerText.trim()}$hairPin" markerText.isNotBlank() -> markerText else -> null + }*/ + if (hairPin != null) { + markerList.add(hairPin.toString()) } - if (finalMarker != null) { + + if (markerList.isNotEmpty()) { val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } -// println("MetaData[$globalIndex] marker='$finalMarker' hairpin=$hairPin") +// println("MetaData[$globalIndex] marker='${markerList.joinToString(", ")}' hairpin=$hairPin") MidiMarkers( myTimestamp, globalIndex, oneTUO.pTemplate.template, oneTUO.pTemplate.lastCalledMarker, - finalMarker, + markerList, oneTUO.prevTUO?.pTemplate?.template ?: "", oneTUO.sep0, oneTUO.tuNotes.getOrNull(1).toString() @@ -697,7 +703,6 @@ fun LazyVerticalGridTUO( var emmentaler = FontFamily(Font(Res.font.Emmentaler)) var ptSerifBoldItalic = FontFamily(Font(Res.font.PT_Serif_Bold_Italic)) var ptSerif = FontFamily(Font(Res.font.PTSerif_Bold)) - var markerFontFamily: FontFamily = FontFamily.Default val textMeasurer = rememberTextMeasurer() val containerWidthDp = gridWidthDp / gridColumnCount @@ -764,18 +769,17 @@ fun LazyVerticalGridTUO( if (TimeUnitObject._hasMarker) { val lineHeight = 20.sp val density = LocalDensity.current - val lineHeightDp : Dp = with(density) { + val lineHeightDp: Dp = with(density) { lineHeight.toDp() } - var markerFontSize: Float = MaterialTheme.typography.titleMedium.fontSize.value var fontStyle = FontStyle.Normal var fontWeight = FontWeight.Normal val hairPinSymbol = tuo.hasHairPin() - val yHeight = with(density) { lineHeightDp.toPx()} + val yHeight = with(density) { lineHeightDp.toPx() } if (tuo.isTriolet()) { Canvas(modifier = Modifier.fillMaxSize()) { - val arcWidth = with(density) { size.width * 0.75f} + val arcWidth = with(density) { size.width * 0.75f } drawArc( color = FEUFAROO_TRIOLET_COLOR, startAngle = 200f, @@ -792,26 +796,26 @@ fun LazyVerticalGridTUO( // println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}") val hairPinStart = TimeUnitObject.lastHairPinStart val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol - val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount + val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount val hairPinEndLine: Int = (tuo.numBlock - 1) / gridColumnCount // if (hairPinStartLine == hairPinEndLine) { - Canvas( - modifier = Modifier.fillMaxSize() - ) { - val xStart = if (lastHairPinSymbol == '>') -size.width * (tuo.numBlock - hairPinStart) else size.width/2 - val xEnd = if (lastHairPinSymbol == '>') size.width/2 else -size.width * (tuo.numBlock - hairPinStart) + Canvas( + modifier = Modifier.fillMaxSize() + ) { + val xStart = if (lastHairPinSymbol == '>') -size.width * (tuo.numBlock - hairPinStart) else size.width / 2 + val xEnd = if (lastHairPinSymbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairPinStart) drawLine( Color.DarkGray, - start = Offset(x=xStart, y=0f), - end = Offset(xEnd, yHeight/2) + start = Offset(x = xStart, y = 0f), + end = Offset(xEnd, yHeight / 2) ) - drawLine( - Color.DarkGray, - start = Offset(xStart, yHeight), - end = Offset(xEnd, yHeight/2) - ) - } - TimeUnitObject.endHairPin() + drawLine( + Color.DarkGray, + start = Offset(xStart, yHeight), + end = Offset(xEnd, yHeight / 2) + ) + } + TimeUnitObject.endHairPin() // } } @@ -821,36 +825,55 @@ fun LazyVerticalGridTUO( // @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne // problème si template = $QD:,-$QD tuo.pTemplate.resetCalledMarker() - val text = tuo.pTemplate.markerToString() - val musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""", RegexOption.IGNORE_CASE) - val musicForEmmentRegex = Regex("""\b(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""", RegexOption.IGNORE_CASE) - when { - musicForPtSerifBIRegex.containsMatchIn(text) -> { - markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value - markerFontFamily = ptSerifBoldItalic - } - musicForEmmentRegex.containsMatchIn(text) -> { - markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value+11 - markerFontFamily = emmentaler - } - else -> { - markerFontFamily = ptSerif + val musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""",RegexOption.IGNORE_CASE) + val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE) + + val markerList = tuo.pTemplate.markerToList() + Row( + modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart), + verticalAlignment = Alignment.CenterVertically + ) { + markerList.forEach { marker -> + Column { + var markerFontFamily: FontFamily = FontFamily.Default + var markerFontSize: Float = MaterialTheme.typography.titleMedium.fontSize.value + + when { + musicForPtSerifBIRegex.matches(marker) -> { + markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value + markerFontFamily = ptSerifBoldItalic + } + + musicForEmmentRegex.matches(marker) -> { + markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value + 11 + markerFontFamily = emmentaler + } + + else -> { + markerFontFamily = ptSerif + } + } + Text( + text = marker, + modifier = Modifier + .wrapContentSize(unbounded = true, align = Alignment.CenterStart,), + softWrap = false, + maxLines = 1, + fontStyle = fontStyle, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSecondary, + fontSize = markerFontSize.sp, + fontFamily = markerFontFamily + ) + ) + if (marker != markerList.last()) { + Spacer(modifier = Modifier.width(2.dp)) + } + } } } - Text(text = text, - modifier = Modifier - .wrapContentSize(unbounded = true, align = Alignment.CenterStart,), - softWrap = false, - maxLines = 1, - fontStyle = fontStyle, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium.copy( - color = MaterialTheme.colorScheme.onSecondary, - fontSize = markerFontSize.sp, - fontFamily = markerFontFamily - ) - ) } } } @@ -1053,6 +1076,7 @@ fun LazyVerticalGridTUO( globalIndex = globalIndex, isEditable = canEdit, canAdd = canAdd, + sharedScreenModel = sharedScreenModel, onDismiss = { showAddDialog = false showDetailDialog = false diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt index 5b26958..59dd4ae 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt @@ -48,9 +48,6 @@ fun Settings( ) { var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) } var refreshTrigger by remember { mutableStateOf(0) } - var expandedGeneral by remember { mutableStateOf(true) } - var expandedPrint by remember { mutableStateOf(false) } - var expandedAudio by remember { mutableStateOf(false) } val isFullScreenEnabled by sharedScreenModel.isFullScreen.collectAsState() val platform = getPlatform() val isAndroid = platform.name.startsWith("Android") @@ -72,6 +69,16 @@ fun Settings( var emmentaler = FontFamily(Font(Res.font.Emmentaler)) val player = sharedScreenModel.mediaPlayer + var expandedAudio by remember { mutableStateOf(player?.getCurrentPosition() != 0L) } + var expandedGeneral by remember { mutableStateOf(true) } + var expandedPrint by remember { mutableStateOf(false) } + + fun openExclusive(target: String) { + expandedGeneral = (target == "General") + expandedPrint = (target == "Print") + expandedAudio = (target == "Audio") + } + val instruments = remember { player?.getAvalaibleInstruments() ?: emptyList() } var globalInstrumentProgram by remember { mutableStateOf(player?.getVoiceInstrument() ?: 1) @@ -117,7 +124,9 @@ fun Settings( SettingsAccordion( title = "Paramètres Général", isExpanded = expandedGeneral, - onToggle = { expandedGeneral = !expandedGeneral } + onToggle = { + openExclusive("General") + } ) { Column( modifier = Modifier.padding(16.dp), @@ -327,7 +336,9 @@ fun Settings( SettingsAccordion( title = "Paramètres Impression", isExpanded = expandedPrint, - onToggle = { expandedPrint = !expandedPrint } + onToggle = { + openExclusive("Print") + } ) { Column( modifier = Modifier.padding(8.dp), @@ -346,7 +357,9 @@ fun Settings( SettingsAccordion( title = "Paramètres Audio & Nuances", isExpanded = expandedAudio, - onToggle = { expandedAudio = !expandedAudio } + onToggle = { + openExclusive("Audio") + } ) { Column(modifier = Modifier.padding(top = 8.dp), verticalArrangement = Arrangement.spacedBy(2.dp) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt index 8fc62b5..738e44e 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt @@ -5,7 +5,7 @@ data class MidiMarkers( val gridIndex: Int? = 0, val template: String, val lastCallerMarker: Int, - val marker: String, + val marker: List, val noteBefore: String, val separat: String, val note: String diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index c462856..f8cfc5a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -23,6 +23,7 @@ import mg.dot.feufaro.data.getCombinedList import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.midi.FMediaPlayer import mg.dot.feufaro.solfa.TUOEditState +import mg.dot.feufaro.solfa.getAllMarker import mg.dot.feufaro.viewmodel.MidiMarkers import java.io.File @@ -430,120 +431,170 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode fun getTotalGridCount(): Int { return _tuoList.value.drop(1).size - 1 } + + fun parseMarkers(rawText: String): MutableList { + val tokens = mutableListOf() + if (rawText.isBlank()) return tokens + + var remaining = rawText.replace(",", " ") + .replace(Regex("""\b(DC)\b""", RegexOption.IGNORE_CASE), "D.C.") + .replace(Regex("""\b(DS)\b""", RegexOption.IGNORE_CASE), "D.S.") + .replace(Regex("""(D\.?C\.?)\s*(Fin)""", RegexOption.IGNORE_CASE), "D.C.Fin") + .replace(Regex("""(D\.?S\.?)\s*(Fin)""", RegexOption.IGNORE_CASE), "D.S.Fin") + .replace(Regex("""(D\.?C\.?)\s*(al\s*Coda)""", RegexOption.IGNORE_CASE), "D.C.alCoda") + .replace(Regex("""(D\.?S\.?)\s*(al\s*Coda)""", RegexOption.IGNORE_CASE), "D.S.alCoda") + + val allStaticMarkers = getAllMarker() + + val tempoRegex = Regex("""^(♩\s*=\s*\d+)""", RegexOption.IGNORE_CASE) + val modulaRegex = Regex("""^(D(?:ô|o)?\s*dia\s*(?:C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B))""", RegexOption.IGNORE_CASE) + + // Lexer + while (remaining.isNotEmpty()) { + remaining = remaining.trimStart() + if (remaining.isEmpty()) break + + val tempoMatch = tempoRegex.find(remaining) + if (tempoMatch != null) { + tokens.add(tempoMatch.value) + remaining = remaining.substring(tempoMatch.value.length) + continue + } + + val modulaMatch = modulaRegex.find(remaining) + if (modulaMatch != null) { + tokens.add(modulaMatch.value) + remaining = remaining.substring(modulaMatch.value.length) + continue + } + + val matchedMarker = allStaticMarkers.find { remaining.startsWith(it, ignoreCase = true) } + if (matchedMarker != null) { + tokens.add(matchedMarker) + remaining = remaining.substring(matchedMarker.length) + } else { + val unknownChunk = remaining.takeWhile { !it.isWhitespace() } + tokens.add(unknownChunk) + remaining = remaining.substring(unknownChunk.length) + } + } + +// println("Sortie: ${tokens.joinToString(" | ")}") + return tokens + } + fun updateAndFinalizeMidiData(rawList: List) { // val timestamps = _tuoTimestamps.value val tuos = _tuoList.value.drop(1) val finalizedList = rawList.map { marker -> - var markerText = marker.marker - val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI - val isNearEnd = index >= (tuos.size - 2) + val originalMarker = marker.marker + var currentMarker = marker - val isDC = markerText.contains(Regex("""D\.?C\.?""")) - val isDS = markerText.contains(Regex("""D\.?S\.?""")) - val isFarany = markerText.trim().contains(Regex("""^(fine|fin|farany|end)$""", RegexOption.IGNORE_CASE)) - val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) + var newGridIndex = marker.gridIndex ?: 0 + val newMarkerList = mutableListOf() - var resultMarker: MidiMarkers = marker - if(isFarany) { - var forwardIndex = index - while (forwardIndex < tuos.size) { - val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: "" - if (sep == "/") { - resultMarker = marker.copy( - gridIndex = forwardIndex - 1, - marker = "Farany_GROUP_PART" - ) - println("Farany finalisé : grille $index → ${forwardIndex - 1}") - break - } - forwardIndex++ - } - } else if (isDC) { - var forwardIndex = index - while (forwardIndex < tuos.size) { - val currentTuo = tuos.getOrNull(forwardIndex) - val currentNote = currentTuo?.tuNotes?.getOrNull(1)?.toString() ?: "" - val currentSep = currentTuo?.sep0 ?: "" + originalMarker.forEach { markerText-> + val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI + val isNearEnd = index >= (tuos.size - 2) +// println("MARKERS:> $markerText\n") -// println("Je suis sur $forwardIndex note $currentNote Sep $currentSep \t condition: ${(currentNote == "―")} || ${(currentSep == "/")}") - // Tant que fin de mesure - if (currentSep == "/") { - resultMarker = marker.copy( - gridIndex = forwardIndex-1, - marker = "${markerText.trim()}_GROUP_PART" - ) - break - } - forwardIndex++ - } + val isDC = markerText.contains(Regex("""D\.?C\.?""")) + val isDS = markerText.contains(Regex("""D\.?S\.?""")) + val isFarany = markerText.trim().contains(Regex("""^(fine|fin|farany|end)$""", RegexOption.IGNORE_CASE)) + val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) + val isRall = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE) - } else if (isDS && isNearEnd) { - val currentNote = tuos.getOrNull(index)?.tuNotes?.getOrNull(1)?.toString() ?: "" - val nextNote = tuos.getOrNull(index + 1)?.tuNotes?.getOrNull(1)?.toString() ?: "" - - // Cas où le marker et la note suivante sont vides - val isEmptySituation = currentNote.trim().isEmpty() && nextNote.trim().isEmpty() - - if (isEmptySituation) { - // vers arrière une note non vide - var backwardIndex = index - 1 - while (backwardIndex >= 0) { - val note = tuos.getOrNull(backwardIndex)?.tuNotes?.getOrNull(1)?.toString() ?: "" - if (note.trim().isNotEmpty()) { - val cleanText = if (markerText.trim() == "DSFin") "DS" else markerText.trim() - - resultMarker = marker.copy( - gridIndex = backwardIndex, - marker = "${cleanText}_GROUP_PART" - ) - break - } - backwardIndex-- - } - } else { - // vers avant le séparateur "/" + if (isFarany) { var forwardIndex = index while (forwardIndex < tuos.size) { - val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: "" - if (sep == "/") { - val cleanText = if (markerText.trim() == "DSFin") "DS" else markerText.trim() - - resultMarker = marker.copy( - gridIndex = forwardIndex, - marker = "${cleanText}_GROUP_PART" - ) + if (tuos.getOrNull(forwardIndex)?.sep0 == "/") { + newGridIndex = forwardIndex - 1 + newMarkerList.add("Farany_GROUP_PART") break } forwardIndex++ } - } - } else if(isRit.containsMatchIn(markerText)) { - var forwardIndex = index + 1 - var foundSeparator = false - - while (forwardIndex < tuos.size) { - val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: "" - if (sep == "/") { - resultMarker = marker.copy( - gridIndex = index, - lastCallerMarker = forwardIndex - 1, - marker = "Ritenuto" - ) - println("Rit finalisé : grille $index → ${forwardIndex - 1}") - foundSeparator = true - break + println("Farany finalisé : grille $index → ${forwardIndex - 1}") + } else if (isDC) { + var found = false + var forwardIndex = index + while (forwardIndex < tuos.size) { + if (tuos.getOrNull(forwardIndex)?.sep0 == "/") { + newGridIndex = forwardIndex - 1 + newMarkerList.add("${markerText.trim()}_GROUP_PART") + println("DC gp finalisé grille $newGridIndex") + found = true + break + } + forwardIndex++ } - forwardIndex++ + if (!found) { + newMarkerList.add(markerText) + } + } else if (isDS && isNearEnd) { + val currentNote = tuos.getOrNull(index)?.tuNotes?.getOrNull(1)?.toString() ?: "" + val nextNote = tuos.getOrNull(index + 1)?.tuNotes?.getOrNull(1)?.toString() ?: "" + + if (currentNote.isBlank() && nextNote.isBlank()) { + var backwardIndex = index - 1 + while (backwardIndex >= 0) { + if (tuos.getOrNull(backwardIndex)?.tuNotes?.getOrNull(1)?.toString()?.isNotBlank() == true) { + newGridIndex = backwardIndex + newMarkerList.add("${if (markerText.trim() == "DSFin") "DS" else markerText.trim()}_GROUP_PART") + println("DS finalisé grille $newGridIndex") + break + } + backwardIndex-- + } + } else { + var forwardIndex = index + while (forwardIndex < tuos.size) { + if (tuos.getOrNull(forwardIndex)?.sep0 == "/") { + newGridIndex = forwardIndex + newMarkerList.add("${if (markerText.trim() == "DSFin") "DS" else markerText.trim()}_GROUP_PART") + println("DS gp finalisé grille $newGridIndex") + break + } + forwardIndex++ + } + } + } else if (isRit.containsMatchIn(markerText)) { + var forwardIndex = index + 1 + while (forwardIndex < tuos.size) { + if (tuos.getOrNull(forwardIndex)?.sep0 == "/") { + currentMarker = currentMarker.copy(lastCallerMarker = forwardIndex - 1) + newMarkerList.add("Ritenuto") + break + } + forwardIndex++ + } + println("Rit finalisé : grille $index → ${forwardIndex - 1}") + + } else if (isRall.containsMatchIn(markerText)) { + var forwardIndex = index + 1 + while (forwardIndex < tuos.size) { + if (tuos.getOrNull(forwardIndex)?.sep0 == "/") { + currentMarker = currentMarker.copy(lastCallerMarker = forwardIndex - 1) + newMarkerList.add("Rallentando") + break + } + forwardIndex++ + } + println("Rall finalisé : grille $index → ${forwardIndex - 1}") + + } else { + newMarkerList.add(markerText) } - } else { - marker } - resultMarker + currentMarker.copy( + gridIndex = newGridIndex, + marker = newMarkerList + ) } _midiMarkersList.value = finalizedList - println("Markers finalisés et mis à jour : ${finalizedList.size}") + println("Markers finalisés et mis à jour : ${finalizedList.size} ${finalizedList.joinToString(", ")}") } private val _tuoEditState = MutableStateFlow(null) @@ -709,6 +760,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode fun setStanza(theStanza: Int) { try { _stanza.value = theStanza + _showMidiCtrl.value = false } catch (e: NumberFormatException) { _stanza.value = 0 } @@ -758,28 +810,33 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode fun getHairPins(): List { val allMarkers = _midiMarkersList.value + + fun isStartToken(s: String): Boolean { + val m = s.trim() + return m == "<" || + m == ">" || + m.contains("cres", ignoreCase = true) || + m.contains("dim", ignoreCase = true) + } val starts = allMarkers - .filter { - val m = it.marker.trim() - m == "<" || - m == ">" || - m.contains("cres", ignoreCase = true) || - m.contains("dim", ignoreCase = true) + .filter { markerObj -> + markerObj.marker.any { isStartToken(it) } } .sortedBy { it.gridIndex } val ends = allMarkers - .filter { it.marker.trim().endsWith("=") } + .filter { markerObjt -> + markerObjt.marker.any { it.trim().endsWith("=") } + } .sortedBy { it.gridIndex } .toMutableList() // println("HairPins starts: ${starts.size} | ends: ${ends.size}") return starts.mapNotNull { start -> val startGrid = start.gridIndex ?: return@mapNotNull null - val symbol = when { - start.marker.trim() == "<" -> '<' - start.marker.trim() == ">" -> '>' - start.marker.contains("cres", ignoreCase = true) -> '<' - start.marker.contains("dim", ignoreCase = true) -> '>' + val startString = start.marker.firstOrNull { isStartToken(it) } ?: return@mapNotNull null + val symbol = when { + startString.contains("<") || startString.contains("cres", ignoreCase = true) -> '<' + startString.contains(">") || startString.contains("dim", ignoreCase = true) -> '>' else -> return@mapNotNull null } diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index a5247a9..7f0ffbd 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -255,214 +255,255 @@ actual class FMediaPlayer actual constructor( val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE) - metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> - val currentIndex = gridIndex ?: 0 + metadataList.forEach { midiMarker -> + val currentIndex = midiMarker.gridIndex ?: 0 + val markerList = midiMarker.marker + val last_grid = sharedScreenModel.getTotalGridCount() + val hairPins = sharedScreenModel.getHairPins() val dsRegex = Regex("""D\.?S\.?""") val dcRegex = Regex("""D\.?C\.?""") val dsGPattern = Regex("""D\.?S\.?_GROUP_PART""") val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""") + val finReg = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE) - val last_grid = sharedScreenModel.getTotalGridCount() - val hairPins = sharedScreenModel.getHairPins() - when { - // segno - marker.contains("$") -> { - lastSegno = currentIndex - println("Cible ($) mémorisée au $lastSegno") - } - // Point d'orgue - marker.contains("\uD834\uDD10") -> { - val beat = if(note.contains('•')) 2 else 1 // demi-ton sur .) ou non - navigationSteps.add( - FMediaPlayer.NavigationStep( - marker, - currentIndex, - isHold = true, - beatInDC = beat - ) - ) - println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° $gridIndex") - } - // Rit ... - ritRegex.containsMatchIn(marker) -> { - val endGrid = if(lastCallerMarker == 0) last_grid else lastCallerMarker - - navigationSteps.add( - NavigationStep( - marker = marker, - gridIndex = currentIndex, - isTempoChange = true, - tempoType = "rit", - endGridForTempo = endGrid, - targetTempoMultiplier = 0.55f // 55% du tempo initial - ) - ) - println("Ritardando mémorisé à grille $currentIndex jusqu'à $endGrid") - } - rallRegex.containsMatchIn(marker) -> { - val endGrid = last_grid - - navigationSteps.add( - NavigationStep( - marker = marker, - gridIndex = currentIndex, - isTempoChange = true, - tempoType = "rall", - endGridForTempo = endGrid, - targetTempoMultiplier = 0.50f // 50% du tempo initial - ) - ) - println("Rallentando mémorisé à grille $currentIndex jusqu'à $endGrid") - } - // DS - dsGPattern.matches(marker.trim())-> { - val target = if (lastSegno > 0) lastSegno else 0 - var indx = if((last_grid - currentIndex) <= 0) { - currentIndex - 1 - } else currentIndex - navigationSteps.add( - NavigationStep( - marker, - gridIndex = indx, - targetGrid = target, - ) - ) - println("Lien créé : $marker à grille n° $indx vers cible $target ........") - } - dsRegex.matches(marker.trim()) || marker == "DSFin" -> { - val target = if (lastSegno > 0) lastSegno else 0 - - navigationSteps.add( - NavigationStep( - marker, - currentIndex, - targetGrid = target - )) - println("Lien DS créé : Saut immédiat à $gridIndex vers Segno $target") - } - - // DC - dcGPattern.matches(marker.trim()) -> { - val indx = if((last_grid - currentIndex) <= 0) { - currentIndex - 1 - } else currentIndex -// println("monn index est $indx car dernier est $last_grid et curr $currentIndex") - navigationSteps.add( - NavigationStep( - marker, - indx, - targetGrid = 0 - ) - ) - println("Lien créé : $marker à $indx vers cible 0 ") - } - (dcRegex.matches(marker.trim()) && !marker.contains("DC_GROUP_PART")) -> { - println("dernier grille $last_grid") - var indx = if((last_grid - currentIndex) <= 0) { - currentIndex /*- 1*/ - } else currentIndex - navigationSteps.add( - NavigationStep( - marker, - indx, - targetGrid = 0 - ) - ) - println("Lien DC créé : $marker à $indx vers le début") - } - - // Farany - marker.trim().equals("Farany_GROUP_PART", ignoreCase = true) -> { - val hasDcAfter = metadataList.any { (_, gi, _, _, mk, _, _, _) -> - (gi ?: 0) > currentIndex && - (dcGPattern.matches(mk.trim()) || - dcRegex.matches(mk.trim())) + markerList.forEach { marker -> + val mTrim = marker.trim() + when { + // segno + marker.contains("$") -> { + lastSegno = currentIndex + println("Cible ($) mémorisée au $lastSegno") } - if (hasDcAfter) { + // Point d'orgue + marker.contains("\uD834\uDD10") -> { + val beat = if (midiMarker.note.contains('•')) 2 else 1 // demi-ton sur .) ou non navigationSteps.add( - NavigationStep( - marker = marker, - gridIndex = currentIndex, - targetGrid = last_grid, - isFin = true, - finActive = false + FMediaPlayer.NavigationStep( + marker, + currentIndex, + isHold = true, + beatInDC = beat ) ) - println("Farany mémorisé à la grille $currentIndex") + println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° ${midiMarker.gridIndex}") } - } - // velocité - extractDynamic(marker) != null && marker.trim() != "=" -> { - val dyn = extractDynamic(marker) ?: return@forEach - lastFactor = when (dyn) { - Dynamic.MF -> 1.0f - else -> dyn.factor - } - navigationSteps.add( - NavigationStep( - marker = marker, - gridIndex = currentIndex, - dynamic = dyn + // Rit ... + ritRegex.containsMatchIn(marker) -> { + val endGrid = if (midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker + + navigationSteps.add( + NavigationStep( + marker = marker, + gridIndex = currentIndex, + isTempoChange = true, + tempoType = "rit", + endGridForTempo = endGrid, + targetTempoMultiplier = 0.55f // 55% du tempo initial + ) ) - ) - println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex") - } - // Soufflet - marker.trim() == "<" || marker.trim() == ">" || - marker.trim().contains("cres", ignoreCase = true) || - marker.trim().contains("dim", ignoreCase = true) -> { - val symbol = when { - marker.trim() == "<" -> '<' - marker.trim() == ">" -> '>' - marker.trim().contains("cres", ignoreCase = true) -> '<' - marker.trim().contains("dim", ignoreCase = true) -> '>' - else -> return@forEach + println("Ritardando mémorisé à grille $currentIndex jusqu'à $endGrid") } - val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach + rallRegex.containsMatchIn(marker) -> { + val endGrid = last_grid - val dynBefore = navigationSteps - .filter { it.dynamic != null && it.gridIndex <= currentIndex } - .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF - - val dynAfter = metadataList - .filter { (_, gi, _, _, mk, _, _, _) -> - (gi ?: 0) >= pair.endGrid && Dynamic.entries.any { d -> d.label == mk.trim() } - } - .minByOrNull { it.gridIndex ?: 0 } - ?.let { m -> Dynamic.entries.find { d -> d.label == m.marker.trim() } } - - val explicitDynAfter = metadataList - .filter { (_, gi, _, _, mk, _, _, _) -> - (gi ?: 0) >= pair.endGrid && - extractDynamic(mk) != null - } - .minByOrNull { it.gridIndex ?: 0 } - ?.let { m -> extractDynamic(m.marker) } - - val fromFactor = lastFactor - val toFactor = when { - explicitDynAfter != null && symbol == '<' && explicitDynAfter.factor > fromFactor -> { - explicitDynAfter.factor - } - explicitDynAfter != null && symbol == '>' && explicitDynAfter.factor < fromFactor -> { - explicitDynAfter.factor - } - else -> nextDynamic(fromFactor, symbol) - } - lastFactor = toFactor - - navigationSteps.add( - NavigationStep( - marker = marker.trim(), - gridIndex = currentIndex, - hairPin = symbol, - hairPinEndGrid = pair.endGrid, - hairPinFromFactor = fromFactor, - hairPinToFactor = toFactor + navigationSteps.add( + NavigationStep( + marker = marker, + gridIndex = currentIndex, + isTempoChange = true, + tempoType = "rall", + endGridForTempo = endGrid, + targetTempoMultiplier = 0.50f // 50% du tempo initial + ) ) - ) - println("NavigationStep HairPin '$symbol' : $currentIndex→${pair.endGrid} | ${fromFactor})→${toFactor})") + println("Rallentando mémorisé à grille $currentIndex jusqu'à $endGrid") + } + // DS + dsGPattern.matches(mTrim) -> { + val target = if (lastSegno > 0) lastSegno else 0 + var indx = if ((last_grid - currentIndex) <= 0) { + currentIndex - 1 + } else currentIndex + navigationSteps.add( + NavigationStep( + marker, + gridIndex = indx, + targetGrid = target, + ) + ) + println("Lien créé : $marker à grille n° $indx vers cible $target ........") + } + + dsRegex.matches(mTrim) || marker == "DSFin" -> { + val target = if (lastSegno > 0) lastSegno else 0 + + navigationSteps.add( + NavigationStep( + marker, + currentIndex, + targetGrid = target + ) + ) + println("Lien DS créé : Saut immédiat à ${midiMarker.gridIndex} vers Segno $target") + } + + // DC + dcGPattern.matches(mTrim) -> { + val indx = if ((last_grid - currentIndex) <= 0) { + currentIndex - 1 + } else currentIndex + // println("monn index est $indx car dernier est $last_grid et curr $currentIndex") + navigationSteps.add( + NavigationStep( + marker, + indx, + targetGrid = 0 + ) + ) + println("Lien créé : $marker à $indx vers cible 0 ") + } + + (dcRegex.matches(mTrim) && !marker.contains("DC_GROUP_PART")) -> { + println("dernier grille $last_grid") + var indx = if ((last_grid - currentIndex) <= 0) { + currentIndex /*- 1*/ + } else currentIndex + navigationSteps.add( + NavigationStep( + marker, + indx, + targetGrid = 0 + ) + ) + println("Lien DC créé : $marker à $indx vers le début") + } + + // Farany + finReg.containsMatchIn(mTrim) -> { + val hasDcAfter = metadataList.any { m -> + val after = (m.gridIndex ?: 0) > currentIndex + + println( + "grid=${m.gridIndex}, current=$currentIndex, after=$after" + ) + + val hasMarker = m.marker.any { mk -> + val value = mk.trim() + val ok = dcGPattern.matches(value) || dcRegex.matches(value) + println("marker=$value -> $ok") + ok + } + + println("after=$after, hasMarker=$hasMarker, result=${after && hasMarker}") + + after && hasMarker + } + if (hasDcAfter) { + navigationSteps.add( + NavigationStep( + marker = marker, + gridIndex = currentIndex, + targetGrid = last_grid, + isFin = true, + finActive = false + ) + ) + println("Farany mémorisé à la grille $currentIndex") + } else { + val verse = sharedScreenModel.stanza.value + val verses = sharedScreenModel.nbStanzas.value + println("$verse == $verses") + navigationSteps.add( + NavigationStep( + marker="${marker}_STZ", + gridIndex = currentIndex, + isFin = true, + finActive = if(verse == verses) false else true + ) + ) + } + } + // velocité + extractDynamic(marker) != null && mTrim != "=" -> { + val dyn = extractDynamic(marker) ?: return@forEach + lastFactor = when (dyn) { + Dynamic.MF -> 1.0f + else -> dyn.factor + } + navigationSteps.add( + NavigationStep( + marker = marker, + gridIndex = currentIndex, + dynamic = dyn + ) + ) + println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex") + } + // Soufflet + mTrim == "<" || mTrim == ">" || + mTrim.contains("cres", ignoreCase = true) || + mTrim.contains("dim", ignoreCase = true) -> { + val symbol = when { + mTrim == "<" -> '<' + mTrim == ">" -> '>' + mTrim.contains("cres", ignoreCase = true) -> '<' + mTrim.contains("dim", ignoreCase = true) -> '>' + else -> return@forEach + } + val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach + + val dynBefore = navigationSteps + .filter { it.dynamic != null && it.gridIndex <= currentIndex } + .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF + + val dynAfter = metadataList + .filter { m -> (m.gridIndex ?: 0) >= pair.endGrid } + .mapNotNull { m -> + val label = m.marker.firstOrNull { mk -> Dynamic.entries.any { d -> d.label == mk.trim() } } + val dynamic = label?.let { l -> Dynamic.entries.find { d -> d.label == l.trim() } } + if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null + } + .minByOrNull { it.first } + ?.second + + + val explicitDynAfter = metadataList + .filter { m -> (m.gridIndex ?: 0) >= pair.endGrid } + .mapNotNull { m -> + val dynamic = m.marker.firstNotNullOfOrNull { mk -> extractDynamic(mk) } + if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null + } + .minByOrNull { it.first } + ?.second + + + val fromFactor = lastFactor + val toFactor = when { + explicitDynAfter != null && symbol == '<' && explicitDynAfter.factor > fromFactor -> { + explicitDynAfter.factor + } + + explicitDynAfter != null && symbol == '>' && explicitDynAfter.factor < fromFactor -> { + explicitDynAfter.factor + } + + else -> nextDynamic(fromFactor, symbol) + } + lastFactor = toFactor + + navigationSteps.add( + NavigationStep( + marker = mTrim, + gridIndex = currentIndex, + hairPin = symbol, + hairPinEndGrid = pair.endGrid, + hairPinFromFactor = fromFactor, + hairPinToFactor = toFactor + ) + ) + println("NavigationStep HairPin '$symbol' : $currentIndex→${pair.endGrid} | ${fromFactor})→${toFactor})") + } } } } @@ -483,8 +524,6 @@ actual class FMediaPlayer actual constructor( navigationJob?.cancel() navigationJob = playerScope.launch(Dispatchers.Default) { sharedScreenModel.activeIndex.collect { currentIndex -> - - val availableIndices = navigationSteps.map { it.gridIndex } // println("bpm:$targetBpm _ ${sequencer?.tempoInBPM}| Index en mémoire : $availableIndices") // println("i $currentIndex ") @@ -494,12 +533,10 @@ actual class FMediaPlayer actual constructor( } if (currentIndex < 0) return@collect - val step = navigationSteps.find { - it.gridIndex == currentIndex && - !it.alreadyDone + val currentSteps = navigationSteps.filter { + it.gridIndex == currentIndex && !it.alreadyDone } - - if (step != null) { + currentSteps.forEach { step -> if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) { forceTempo(targetBpm.toDouble()) }