Convert marker to List to allow multiple markers 1 grid & static marker list & fine supported

This commit is contained in:
Hasinjato 2026-07-15 15:24:03 +03:00
parent 2ec69a8b13
commit 295ec5100e
9 changed files with 879 additions and 615 deletions

View file

@ -291,6 +291,9 @@ actual class FMediaPlayer actual constructor(
if (needsClockSync) { if (needsClockSync) {
clockNano = System.nanoTime() clockNano = System.nanoTime()
clockTick = currentTickPos clockTick = currentTickPos
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
.takeIf { it >= 0 } ?: events.size
needsClockSync = false needsClockSync = false
} }
if (isHolding) { if (isHolding) {
@ -304,7 +307,6 @@ actual class FMediaPlayer actual constructor(
if (idx >= events.size) { if (idx >= events.size) {
val dc = getPendingDcStep() val dc = getPendingDcStep()
if (dc != null) { if (dc != null) {
dc.alreadyDone = true
allNotesOff() allNotesOff()
seekToGrid(dc.targetGrid) seekToGrid(dc.targetGrid)
clockNano = System.nanoTime(); clockTick = currentTickPos clockNano = System.nanoTime(); clockTick = currentTickPos
@ -312,7 +314,10 @@ actual class FMediaPlayer actual constructor(
.takeIf { it >= 0 } ?: events.size .takeIf { it >= 0 } ?: events.size
continue continue
} else { } else {
isRunning = false; allNotesOff(); onFinished(); break isRunning = false
allNotesOff()
onFinished()
break
} }
} }
@ -335,6 +340,10 @@ actual class FMediaPlayer actual constructor(
yield() yield()
} }
} }
if (needsClockSync) {
continue
}
currentTickPos = ev.tickAbsolute currentTickPos = ev.tickAbsolute
lastEventNano = System.nanoTime() lastEventNano = System.nanoTime()
@ -357,7 +366,7 @@ actual class FMediaPlayer actual constructor(
0xC0 -> send(0xC0 or ev.channel, ev.data1) 0xC0 -> send(0xC0 or ev.channel, ev.data1)
0xFF -> { /* tempo ignoré */ } 0xFF -> { /* tempo ignoré */ }
} }
val currentGrid = (ev.tickAbsolute / resolution).toLong() val currentGrid = (currentTickPos/* ev.tickAbsolute*/ / resolution).toLong()
if (boundModel?.activeIndex?.value != currentGrid.toInt()) { if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
boundModel?.updateActiveIndex(currentGrid) boundModel?.updateActiveIndex(currentGrid)
} }
@ -368,12 +377,15 @@ actual class FMediaPlayer actual constructor(
actual fun seekToGrid(gridIndex: Int) { actual fun seekToGrid(gridIndex: Int) {
currentTickPos = gridIndex.toLong() * resolution currentTickPos = gridIndex.toLong() * resolution
lastEventNano = System.nanoTime() // ← recaler l'horloge d'interpolation lastEventNano = System.nanoTime()
val lastDyn = navigationSteps val lastDyn = navigationSteps
.filter { it.dynamic != null && it.gridIndex <= gridIndex } .filter { it.dynamic != null && it.gridIndex <= gridIndex }
.maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF
currentDynamicFactor = if (lastDyn == Dynamic.MF) 1.0f else lastDyn.factor currentDynamicFactor = if (lastDyn == Dynamic.MF) 1.0f else lastDyn.factor
applyVoiceStates() applyVoiceStates()
needsClockSync = true
} }
private fun ticksToMs(ticks: Long) = (ticks * usPerTick / 1000.0).toLong() private fun ticksToMs(ticks: Long) = (ticks * usPerTick / 1000.0).toLong()
private fun msToTicks(ms: Long) = (ms * 1000.0 / usPerTick).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 ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE) val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> metadataList.forEach { midiMarker ->
val ci = gridIndex ?: 0 val ci = midiMarker.gridIndex ?: 0
val dsR = Regex("""D\.?S\.?"""); val dsR = Regex("""D\.?S\.?""");
val dcR = Regex("""D\.?C\.?""") val dcR = Regex("""D\.?C\.?""")
val dsG = Regex("""D\.?S\.?_GROUP_PART"""); 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 last_grid = sharedScreenModel.getTotalGridCount()
val hairPins = sharedScreenModel.getHairPins() val hairPins = sharedScreenModel.getHairPins()
val markerList = midiMarker.marker
when { markerList.forEach { marker ->
marker.contains("$") -> lastSegno = ci val mTrim = marker.trim()
when {
marker.contains("$") -> lastSegno = ci
marker.contains("\uD834\uDD10") -> { marker.contains("\uD834\uDD10") -> {
val beat = if(note.contains('•')) 2 else 1 val beat = if(midiMarker.note.contains('•')) 2 else 1
navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat)) 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"))
} }
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<from -> explicitAfter.factor
else -> nextDynamic(from, sym)
}
lastFactor = to
navigationSteps.add(NavigationStep( navigationSteps.add(NavigationStep(
marker = marker, marker=mTrim, gridIndex=ci, hairPin=sym,
gridIndex = ci, hairPinEndGrid=pair.endGrid,
targetGrid = last_grid, hairPinFromFactor=from, hairPinToFactor=to
isFarany = true,
faranyActive = false
)) ))
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<from -> 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) { private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) {
var lastProcessedIndex = -1 var lastProcessedIndex = -1
var isJumping = false
var pendingFin = false
navigationJob?.cancel() navigationJob?.cancel()
navigationJob = playerScope.launch(Dispatchers.Default) { navigationJob = playerScope.launch(Dispatchers.Default) {
sharedScreenModel.activeIndex.collect { currentIndex -> sharedScreenModel.activeIndex.collect { currentIndex ->
if (currentIndex <= lastProcessedIndex) { //if (currentIndex <= lastProcessedIndex && lastProcessedIndex != -1) return@collect
return@collect
}
lastProcessedIndex = currentIndex lastProcessedIndex = currentIndex
if (!isRunning || currentIndex < 0) return@collect if (!isRunning || currentIndex < 0) return@collect
val currentSteps = navigationSteps.filter {
val step = navigationSteps.find {
it.gridIndex == currentIndex && !it.alreadyDone it.gridIndex == currentIndex && !it.alreadyDone
} }.sortedBy { step ->
println("pas: ${currentIndex}")
if (step != null) {
when { when {
// ── Farany ──────────────────────────── step.isHold -> 0
step.isFarany -> { step.isTempoChange -> 1
if (step.faranyActive) { step.isFarany -> 1
println("Farany activé → STOP à grille $currentIndex") 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 step.alreadyDone = true
isHolding = false val startBpm = currentPlaybackBpm
isRunning = false val endBpm = targetBpm * step.targetTempoMultiplier
playJob?.cancel() val gridDistance = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1)
allNotesOff() val beatDurationMs = (60_000L / targetBpm).toLong()
onFinished() val durationMs = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L)
} else {
println("Farany 1er passage — DC pas encore vu → on continue ✅") 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 ───────────────────── // ── Soufflet ──────────────────────────
step.isHold -> { step.hairPin != null -> {
val beatMs = (60_000 / targetBpm).toLong() step.alreadyDone = true
val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2 val dist = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1)
println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms") val beatMs = (60_000L / targetBpm).toLong()
for (ch in 0 until 4) controlChange(ch, 64, 127) applyCrescendo(step.hairPinFromFactor, step.hairPinToFactor,
isHolding = true (dist * beatMs).coerceIn(200L, 5000L))
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)
println("${step.tempoType.uppercase()} : Grille ${step.gridIndex}${step.endGridForTempo}") // ── Dynamique ─────────────────────────
println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms") step.dynamic != null -> {
step.alreadyDone = true
applyTempoChange( applyDynamic(step.dynamic)
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)
}
// ── 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 ─────────────────────────── // ── DC / DS ───────────────────────────
else -> { else -> {
step.alreadyDone = true step.alreadyDone = true
val beatMs = (60_000 / targetBpm).toLong() val beatMs = (60_000 / targetBpm).toLong()
println("avant de sauter bpm=$targetBpm") println("avant de sauter bpm=$targetBpm")
//lastProcessedIndex = -1
for (ch in 0 until 4) controlChange(ch, 64, 127) for (ch in 0 until 4) controlChange(ch, 64, 127)
isHolding = true isHolding = true
delay(beatMs) delay(beatMs)
@ -621,10 +671,9 @@ actual class FMediaPlayer actual constructor(
} }
startPlaybackLoop() startPlaybackLoop()
println("DC/DS → grille ${step.targetGrid}") }
} }
} }
}
} }
} }
} }

View file

@ -7,27 +7,12 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircleOutline import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.RemoveCircleOutline import androidx.compose.material.icons.filled.RemoveCircleOutline
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.*
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.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -52,6 +37,13 @@ data class MarkerGroup(
val markers: List<Marker> val markers: List<Marker>
) )
fun getAllMarker(): List<String> {
return markerGroups
.flatMap { group -> group.markers }
.map { marker -> marker.abbr }
.filter { it != "♩=" && it != "Dô dia" }
.sortedByDescending { it.length }
}
private val markerGroups = listOf( private val markerGroups = listOf(
MarkerGroup( MarkerGroup(
title = "Nuances", title = "Nuances",
@ -71,6 +63,8 @@ private val markerGroups = listOf(
Marker("dim.", "Diminuendo"), Marker("dim.", "Diminuendo"),
Marker(">", "Decrescendo"), Marker(">", "Decrescendo"),
Marker("poco", "Un peu (modifie la nuance suivante)"),
Marker("organa", "Style ornementé"),
Marker("sf", "Accent soudain"), Marker("sf", "Accent soudain"),
Marker("sfz", "Sforzato"), Marker("sfz", "Sforzato"),
Marker("fz", "Forzando"), Marker("fz", "Forzando"),
@ -106,11 +100,11 @@ private val markerGroups = listOf(
Marker("string.", "Presser le tempo"), Marker("string.", "Presser le tempo"),
Marker("♩=", "Changement tempo", canEditValue = true, defaultValue = "60"), Marker("♩=", "Changement tempo", canEditValue = true, defaultValue = "60"),
Marker("allarg.", "Élargir le tempo"), 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("Tempo I", "Retour au premier tempo"),
Marker("rubato", "Tempo libre"), Marker("rubato", "Tempo libre"),
Marker("meno mosso", "Moins rapide"), Marker("meno_mosso", "Moins rapide"),
Marker("più mosso", "Plus rapide") Marker("più_mosso", "Plus rapide")
) )
), ),
@ -118,16 +112,19 @@ private val markerGroups = listOf(
title = "Reprises", title = "Reprises",
markers = listOf( markers = listOf(
Marker("D.C.", "Da Capo"), Marker("D.C.", "Da Capo"),
Marker("D.C. Fin", "Retour au début jusqu'à Fine"), 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.alCoda", "Retour au début puis aller à la Coda"),
Marker("D.S.", "Retour au Segno"), Marker("D.S.", "Retour au Segno"),
Marker("End", "Fin"), 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("Farany", "Fin"),
Marker("$", "Segno"), Marker("$", "Segno"),
Marker("Fine", "Fin"), Marker("Fine", "Fin"),
Marker("Fiverenana", "Réfrain"),
Marker("Isan\'andininy", "Réfrain"),
Marker("Fine", "Fin"),
Marker("𝄌", "Coda"), Marker("𝄌", "Coda"),
Marker("To Coda", "Aller à la Coda"), Marker("To_Coda", "Aller à la Coda"),
Marker("||:", "Début de reprise"), Marker("||:", "Début de reprise"),
Marker(":||", "Fin de reprise"), Marker(":||", "Fin de reprise"),
Marker("Bis", "Rejouer") Marker("Bis", "Rejouer")

View file

@ -36,6 +36,61 @@ class PTemplate (val template: String, val separatorAfter: String, private val m
.replace(Regex("^c:(.*)"), "Do dia $1") .replace(Regex("^c:(.*)"), "Do dia $1")
} }
} }
fun markerToList(): List<String> {
if (markers.isEmpty()) {
return emptyList()
}
val mString = markerToString()
return parseMarkers(mString)
}
fun parseMarkers(rawText: String): List<String> {
val tokens = mutableListOf<String>()
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 { fun hasKeyChange(): String {
var returnFun = "" var returnFun = ""
markers.map { markers.map {

View file

@ -1,5 +1,6 @@
package mg.dot.feufaro package mg.dot.feufaro
import SharedScreenModel
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* 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.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.sharp.Delete
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
@ -48,6 +48,7 @@ fun TUODetailDialog(
globalIndex: Int, globalIndex: Int,
isEditable: Boolean, isEditable: Boolean,
canAdd: Boolean, canAdd: Boolean,
sharedScreenModel: SharedScreenModel,
onIndexChanged: (Int) -> Unit, onIndexChanged: (Int) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSave: (TUOEditState) -> Unit onSave: (TUOEditState) -> Unit
@ -344,61 +345,80 @@ fun TUODetailDialog(
) )
} }
if(!templateFragment.isNullOrBlank() && templateFragment !="-") {
if (!templateFragment.isNullOrBlank() && templateFragment != "-") {
val hasMarker = marker.isNotEmpty() val hasMarker = marker.isNotEmpty()
val tooltipText = if (hasMarker) {
"Enlever la marqueur" Row(verticalAlignment = Alignment.CenterVertically) {
} else {
"Ajouter une marqueur" // 1. Bouton "Remove" (affiché uniquement si hasMarker est true)
} if (hasMarker) {
TooltipBox( TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { tooltip = {
PlainTooltip( PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) {
containerColor = Color.DarkGray, Text(text = "Enlever le marqueur", fontSize = MaterialTheme.typography.titleSmall.fontSize)
contentColor = Color.White }
},
state = rememberTooltipState()
) { ) {
Text(text = tooltipText, fontSize = MaterialTheme.typography.titleSmall.fontSize) IconButton(
} modifier = Modifier.size(25.dp),
}, onClick = {
state = rememberTooltipState() val state = TUOEditState(
) { tuoIndex = globalIndex,
IconButton( notesByVoice = notes.toMap(),
modifier = Modifier.size(30.dp) originalNotes = originalNotes.toMap(),
.onGloballyPositioned { coordinates -> originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
markerButtonPos = coordinates.positionInParent() lyricsByStanza = mutableMapOf(),
.let { templateFragment = templateFragment,
IntOffset( marker = "_", // On remet le marqueur à vide
it.x.toInt(), originalSep = initialSep,
it.y.toInt() sep = newSep
) )
} onSave(state)
}, }
onClick = { ) {
if(hasMarker) { Icon(
val state = TUOEditState( imageVector = Icons.Sharp.Delete,
tuoIndex = globalIndex, tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
notesByVoice = notes.toMap(), contentDescription = "Enlever"
originalNotes = originalNotes.toMap(),
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
lyricsByStanza = mutableMapOf(),
templateFragment = templateFragment,
marker = "_",
originalSep = initialSep,
sep = newSep
) )
onSave(state)
} }
showMarkerPopup = true }
}) { }
Icon(
imageVector = if(hasMarker) Icons.Default.Remove else Icons.Default.Add, TooltipBox(
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
contentDescription = null 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)) Spacer(modifier = Modifier.height(8.dp))
@ -595,8 +615,20 @@ fun TUODetailDialog(
onDismiss = { onDismiss = {
showMarkerPopup = false showMarkerPopup = false
}, },
onMarkerSelected = { marker -> onMarkerSelected = { newMarker ->
showMarkerPopup = false 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( val state = TUOEditState(
tuoIndex = globalIndex, tuoIndex = globalIndex,
@ -605,7 +637,7 @@ fun TUODetailDialog(
originalLyricsByStanza = originalLyricsByStz.toMutableMap(), originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
lyricsByStanza = mutableMapOf(), lyricsByStanza = mutableMapOf(),
templateFragment = templateFragment, templateFragment = templateFragment,
marker = marker, marker = markerToSave,
originalSep = initialSep, originalSep = initialSep,
sep = newSep sep = newSep
) )

View file

@ -43,6 +43,7 @@ import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import feufaro.composeapp.generated.resources.Emmentaler import feufaro.composeapp.generated.resources.Emmentaler
import feufaro.composeapp.generated.resources.PTSerif_Bold import feufaro.composeapp.generated.resources.PTSerif_Bold
import feufaro.composeapp.generated.resources.PT_Serif_Bold_Italic import feufaro.composeapp.generated.resources.PT_Serif_Bold_Italic
@ -646,29 +647,34 @@ fun LazyVerticalGridTUO(
val currentStanza = viewModel.stanza val currentStanza = viewModel.stanza
val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() 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) val measures = tuoList.drop(1).chunked(gridColumnCount)
// Avant column affichage: // Avant column affichage:
val metadataList = remember(tuoList) { val metadataList = remember(tuoList) {
tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO -> tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO ->
val markerText = oneTUO.pTemplate.markerToString() val rawMarker = oneTUO.pTemplate.markerToString()
val hairPin = oneTUO.hasHairPin() val hairPin = oneTUO.hasHairPin()
val finalMarker = when { val markerList = sharedScreenModel.parseMarkers(rawMarker)
/*val finalMarker = when {
hairPin != null && markerText.isBlank() -> hairPin.toString() hairPin != null && markerText.isBlank() -> hairPin.toString()
hairPin != null && markerText.isNotBlank() -> "${markerText.trim()}$hairPin" hairPin != null && markerText.isNotBlank() -> "${markerText.trim()}$hairPin"
markerText.isNotBlank() -> markerText markerText.isNotBlank() -> markerText
else -> null else -> null
}*/
if (hairPin != null) {
markerList.add(hairPin.toString())
} }
if (finalMarker != null) {
if (markerList.isNotEmpty()) {
val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L }
// println("MetaData[$globalIndex] marker='$finalMarker' hairpin=$hairPin") // println("MetaData[$globalIndex] marker='${markerList.joinToString(", ")}' hairpin=$hairPin")
MidiMarkers( MidiMarkers(
myTimestamp, myTimestamp,
globalIndex, globalIndex,
oneTUO.pTemplate.template, oneTUO.pTemplate.template,
oneTUO.pTemplate.lastCalledMarker, oneTUO.pTemplate.lastCalledMarker,
finalMarker, markerList,
oneTUO.prevTUO?.pTemplate?.template ?: "", oneTUO.prevTUO?.pTemplate?.template ?: "",
oneTUO.sep0, oneTUO.sep0,
oneTUO.tuNotes.getOrNull(1).toString() oneTUO.tuNotes.getOrNull(1).toString()
@ -697,7 +703,6 @@ fun LazyVerticalGridTUO(
var emmentaler = FontFamily(Font(Res.font.Emmentaler)) var emmentaler = FontFamily(Font(Res.font.Emmentaler))
var ptSerifBoldItalic = FontFamily(Font(Res.font.PT_Serif_Bold_Italic)) var ptSerifBoldItalic = FontFamily(Font(Res.font.PT_Serif_Bold_Italic))
var ptSerif = FontFamily(Font(Res.font.PTSerif_Bold)) var ptSerif = FontFamily(Font(Res.font.PTSerif_Bold))
var markerFontFamily: FontFamily = FontFamily.Default
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
val containerWidthDp = gridWidthDp / gridColumnCount val containerWidthDp = gridWidthDp / gridColumnCount
@ -764,18 +769,17 @@ fun LazyVerticalGridTUO(
if (TimeUnitObject._hasMarker) { if (TimeUnitObject._hasMarker) {
val lineHeight = 20.sp val lineHeight = 20.sp
val density = LocalDensity.current val density = LocalDensity.current
val lineHeightDp : Dp = with(density) { val lineHeightDp: Dp = with(density) {
lineHeight.toDp() lineHeight.toDp()
} }
var markerFontSize: Float = MaterialTheme.typography.titleMedium.fontSize.value
var fontStyle = FontStyle.Normal var fontStyle = FontStyle.Normal
var fontWeight = FontWeight.Normal var fontWeight = FontWeight.Normal
val hairPinSymbol = tuo.hasHairPin() val hairPinSymbol = tuo.hasHairPin()
val yHeight = with(density) { lineHeightDp.toPx()} val yHeight = with(density) { lineHeightDp.toPx() }
if (tuo.isTriolet()) { if (tuo.isTriolet()) {
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
val arcWidth = with(density) { size.width * 0.75f} val arcWidth = with(density) { size.width * 0.75f }
drawArc( drawArc(
color = FEUFAROO_TRIOLET_COLOR, color = FEUFAROO_TRIOLET_COLOR,
startAngle = 200f, startAngle = 200f,
@ -792,26 +796,26 @@ fun LazyVerticalGridTUO(
// println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}") // println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}")
val hairPinStart = TimeUnitObject.lastHairPinStart val hairPinStart = TimeUnitObject.lastHairPinStart
val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol
val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount
val hairPinEndLine: Int = (tuo.numBlock - 1) / gridColumnCount val hairPinEndLine: Int = (tuo.numBlock - 1) / gridColumnCount
// if (hairPinStartLine == hairPinEndLine) { // if (hairPinStartLine == hairPinEndLine) {
Canvas( Canvas(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
val xStart = if (lastHairPinSymbol == '>') -size.width * (tuo.numBlock - hairPinStart) else size.width/2 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) val xEnd = if (lastHairPinSymbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairPinStart)
drawLine( drawLine(
Color.DarkGray, Color.DarkGray,
start = Offset(x=xStart, y=0f), start = Offset(x = xStart, y = 0f),
end = Offset(xEnd, yHeight/2) end = Offset(xEnd, yHeight / 2)
) )
drawLine( drawLine(
Color.DarkGray, Color.DarkGray,
start = Offset(xStart, yHeight), start = Offset(xStart, yHeight),
end = Offset(xEnd, yHeight/2) end = Offset(xEnd, yHeight / 2)
) )
} }
TimeUnitObject.endHairPin() TimeUnitObject.endHairPin()
// } // }
} }
@ -821,36 +825,55 @@ fun LazyVerticalGridTUO(
// @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne // @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne
// problème si template = $QD:,-$QD // problème si template = $QD:,-$QD
tuo.pTemplate.resetCalledMarker() 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 { 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)
musicForPtSerifBIRegex.containsMatchIn(text) -> { val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE)
markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value
markerFontFamily = ptSerifBoldItalic val markerList = tuo.pTemplate.markerToList()
} Row(
musicForEmmentRegex.containsMatchIn(text) -> { modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart),
markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value+11 verticalAlignment = Alignment.CenterVertically
markerFontFamily = emmentaler ) {
} markerList.forEach { marker ->
else -> { Column {
markerFontFamily = ptSerif 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, globalIndex = globalIndex,
isEditable = canEdit, isEditable = canEdit,
canAdd = canAdd, canAdd = canAdd,
sharedScreenModel = sharedScreenModel,
onDismiss = { onDismiss = {
showAddDialog = false showAddDialog = false
showDetailDialog = false showDetailDialog = false

View file

@ -48,9 +48,6 @@ fun Settings(
) { ) {
var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) } var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) }
var refreshTrigger by remember { mutableStateOf(0) } 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 isFullScreenEnabled by sharedScreenModel.isFullScreen.collectAsState()
val platform = getPlatform() val platform = getPlatform()
val isAndroid = platform.name.startsWith("Android") val isAndroid = platform.name.startsWith("Android")
@ -72,6 +69,16 @@ fun Settings(
var emmentaler = FontFamily(Font(Res.font.Emmentaler)) var emmentaler = FontFamily(Font(Res.font.Emmentaler))
val player = sharedScreenModel.mediaPlayer 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() } val instruments = remember { player?.getAvalaibleInstruments() ?: emptyList() }
var globalInstrumentProgram by remember { var globalInstrumentProgram by remember {
mutableStateOf(player?.getVoiceInstrument() ?: 1) mutableStateOf(player?.getVoiceInstrument() ?: 1)
@ -117,7 +124,9 @@ fun Settings(
SettingsAccordion( SettingsAccordion(
title = "Paramètres Général", title = "Paramètres Général",
isExpanded = expandedGeneral, isExpanded = expandedGeneral,
onToggle = { expandedGeneral = !expandedGeneral } onToggle = {
openExclusive("General")
}
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
@ -327,7 +336,9 @@ fun Settings(
SettingsAccordion( SettingsAccordion(
title = "Paramètres Impression", title = "Paramètres Impression",
isExpanded = expandedPrint, isExpanded = expandedPrint,
onToggle = { expandedPrint = !expandedPrint } onToggle = {
openExclusive("Print")
}
) { ) {
Column( Column(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
@ -346,7 +357,9 @@ fun Settings(
SettingsAccordion( SettingsAccordion(
title = "Paramètres Audio & Nuances", title = "Paramètres Audio & Nuances",
isExpanded = expandedAudio, isExpanded = expandedAudio,
onToggle = { expandedAudio = !expandedAudio } onToggle = {
openExclusive("Audio")
}
) { ) {
Column(modifier = Modifier.padding(top = 8.dp), Column(modifier = Modifier.padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)

View file

@ -5,7 +5,7 @@ data class MidiMarkers(
val gridIndex: Int? = 0, val gridIndex: Int? = 0,
val template: String, val template: String,
val lastCallerMarker: Int, val lastCallerMarker: Int,
val marker: String, val marker: List<String>,
val noteBefore: String, val noteBefore: String,
val separat: String, val separat: String,
val note: String val note: String

View file

@ -23,6 +23,7 @@ import mg.dot.feufaro.data.getCombinedList
import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.midi.FMediaPlayer import mg.dot.feufaro.midi.FMediaPlayer
import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.solfa.TUOEditState
import mg.dot.feufaro.solfa.getAllMarker
import mg.dot.feufaro.viewmodel.MidiMarkers import mg.dot.feufaro.viewmodel.MidiMarkers
import java.io.File import java.io.File
@ -430,120 +431,170 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun getTotalGridCount(): Int { fun getTotalGridCount(): Int {
return _tuoList.value.drop(1).size - 1 return _tuoList.value.drop(1).size - 1
} }
fun parseMarkers(rawText: String): MutableList<String> {
val tokens = mutableListOf<String>()
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<MidiMarkers>) { fun updateAndFinalizeMidiData(rawList: List<MidiMarkers>) {
// val timestamps = _tuoTimestamps.value // val timestamps = _tuoTimestamps.value
val tuos = _tuoList.value.drop(1) val tuos = _tuoList.value.drop(1)
val finalizedList = rawList.map { marker -> val finalizedList = rawList.map { marker ->
var markerText = marker.marker val originalMarker = marker.marker
val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI var currentMarker = marker
val isNearEnd = index >= (tuos.size - 2)
val isDC = markerText.contains(Regex("""D\.?C\.?""")) var newGridIndex = marker.gridIndex ?: 0
val isDS = markerText.contains(Regex("""D\.?S\.?""")) val newMarkerList = mutableListOf<String>()
val isFarany = markerText.trim().contains(Regex("""^(fine|fin|farany|end)$""", RegexOption.IGNORE_CASE))
val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
var resultMarker: MidiMarkers = marker originalMarker.forEach { markerText->
if(isFarany) { val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI
var forwardIndex = index val isNearEnd = index >= (tuos.size - 2)
while (forwardIndex < tuos.size) { // println("MARKERS:> $markerText\n")
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 ?: ""
// println("Je suis sur $forwardIndex note $currentNote Sep $currentSep \t condition: ${(currentNote == "―")} || ${(currentSep == "/")}") val isDC = markerText.contains(Regex("""D\.?C\.?"""))
// Tant que fin de mesure val isDS = markerText.contains(Regex("""D\.?S\.?"""))
if (currentSep == "/") { val isFarany = markerText.trim().contains(Regex("""^(fine|fin|farany|end)$""", RegexOption.IGNORE_CASE))
resultMarker = marker.copy( val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
gridIndex = forwardIndex-1, val isRall = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
marker = "${markerText.trim()}_GROUP_PART"
)
break
}
forwardIndex++
}
} else if (isDS && isNearEnd) { if (isFarany) {
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 "/"
var forwardIndex = index var forwardIndex = index
while (forwardIndex < tuos.size) { while (forwardIndex < tuos.size) {
val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: "" if (tuos.getOrNull(forwardIndex)?.sep0 == "/") {
if (sep == "/") { newGridIndex = forwardIndex - 1
val cleanText = if (markerText.trim() == "DSFin") "DS" else markerText.trim() newMarkerList.add("Farany_GROUP_PART")
resultMarker = marker.copy(
gridIndex = forwardIndex,
marker = "${cleanText}_GROUP_PART"
)
break break
} }
forwardIndex++ forwardIndex++
} }
} println("Farany finalisé : grille $index${forwardIndex - 1}")
} else if(isRit.containsMatchIn(markerText)) { } else if (isDC) {
var forwardIndex = index + 1 var found = false
var foundSeparator = false var forwardIndex = index
while (forwardIndex < tuos.size) {
while (forwardIndex < tuos.size) { if (tuos.getOrNull(forwardIndex)?.sep0 == "/") {
val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: "" newGridIndex = forwardIndex - 1
if (sep == "/") { newMarkerList.add("${markerText.trim()}_GROUP_PART")
resultMarker = marker.copy( println("DC gp finalisé grille $newGridIndex")
gridIndex = index, found = true
lastCallerMarker = forwardIndex - 1, break
marker = "Ritenuto" }
) forwardIndex++
println("Rit finalisé : grille $index${forwardIndex - 1}")
foundSeparator = true
break
} }
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 _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<TUOEditState?>(null) private val _tuoEditState = MutableStateFlow<TUOEditState?>(null)
@ -709,6 +760,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun setStanza(theStanza: Int) { fun setStanza(theStanza: Int) {
try { try {
_stanza.value = theStanza _stanza.value = theStanza
_showMidiCtrl.value = false
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
_stanza.value = 0 _stanza.value = 0
} }
@ -758,28 +810,33 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun getHairPins(): List<HairPinData> { fun getHairPins(): List<HairPinData> {
val allMarkers = _midiMarkersList.value 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 val starts = allMarkers
.filter { .filter { markerObj ->
val m = it.marker.trim() markerObj.marker.any { isStartToken(it) }
m == "<" ||
m == ">" ||
m.contains("cres", ignoreCase = true) ||
m.contains("dim", ignoreCase = true)
} }
.sortedBy { it.gridIndex } .sortedBy { it.gridIndex }
val ends = allMarkers val ends = allMarkers
.filter { it.marker.trim().endsWith("=") } .filter { markerObjt ->
markerObjt.marker.any { it.trim().endsWith("=") }
}
.sortedBy { it.gridIndex } .sortedBy { it.gridIndex }
.toMutableList() .toMutableList()
// println("HairPins starts: ${starts.size} | ends: ${ends.size}") // println("HairPins starts: ${starts.size} | ends: ${ends.size}")
return starts.mapNotNull { start -> return starts.mapNotNull { start ->
val startGrid = start.gridIndex ?: return@mapNotNull null val startGrid = start.gridIndex ?: return@mapNotNull null
val symbol = when { val startString = start.marker.firstOrNull { isStartToken(it) } ?: return@mapNotNull null
start.marker.trim() == "<" -> '<' val symbol = when {
start.marker.trim() == ">" -> '>' startString.contains("<") || startString.contains("cres", ignoreCase = true) -> '<'
start.marker.contains("cres", ignoreCase = true) -> '<' startString.contains(">") || startString.contains("dim", ignoreCase = true) -> '>'
start.marker.contains("dim", ignoreCase = true) -> '>'
else -> return@mapNotNull null else -> return@mapNotNull null
} }

View file

@ -255,214 +255,255 @@ actual class FMediaPlayer actual constructor(
val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE) val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> metadataList.forEach { midiMarker ->
val currentIndex = gridIndex ?: 0 val currentIndex = midiMarker.gridIndex ?: 0
val markerList = midiMarker.marker
val last_grid = sharedScreenModel.getTotalGridCount()
val hairPins = sharedScreenModel.getHairPins()
val dsRegex = Regex("""D\.?S\.?""") val dsRegex = Regex("""D\.?S\.?""")
val dcRegex = Regex("""D\.?C\.?""") val dcRegex = Regex("""D\.?C\.?""")
val dsGPattern = Regex("""D\.?S\.?_GROUP_PART""") val dsGPattern = Regex("""D\.?S\.?_GROUP_PART""")
val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""") val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""")
val finReg = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE)
val last_grid = sharedScreenModel.getTotalGridCount() markerList.forEach { marker ->
val hairPins = sharedScreenModel.getHairPins() val mTrim = marker.trim()
when { when {
// segno // segno
marker.contains("$") -> { marker.contains("$") -> {
lastSegno = currentIndex lastSegno = currentIndex
println("Cible ($) mémorisée au $lastSegno") 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()))
} }
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( navigationSteps.add(
NavigationStep( FMediaPlayer.NavigationStep(
marker = marker, marker,
gridIndex = currentIndex, currentIndex,
targetGrid = last_grid, isHold = true,
isFin = true, beatInDC = beat
finActive = false
) )
) )
println("Farany mémorisé à la grille $currentIndex") println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° ${midiMarker.gridIndex}")
} }
} // Rit ...
// velocité ritRegex.containsMatchIn(marker) -> {
extractDynamic(marker) != null && marker.trim() != "=" -> { val endGrid = if (midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker
val dyn = extractDynamic(marker) ?: return@forEach
lastFactor = when (dyn) { navigationSteps.add(
Dynamic.MF -> 1.0f NavigationStep(
else -> dyn.factor marker = marker,
} gridIndex = currentIndex,
navigationSteps.add( isTempoChange = true,
NavigationStep( tempoType = "rit",
marker = marker, endGridForTempo = endGrid,
gridIndex = currentIndex, targetTempoMultiplier = 0.55f // 55% du tempo initial
dynamic = dyn )
) )
) println("Ritardando mémorisé à grille $currentIndex jusqu'à $endGrid")
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
} }
val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach rallRegex.containsMatchIn(marker) -> {
val endGrid = last_grid
val dynBefore = navigationSteps navigationSteps.add(
.filter { it.dynamic != null && it.gridIndex <= currentIndex } NavigationStep(
.maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF marker = marker,
gridIndex = currentIndex,
val dynAfter = metadataList isTempoChange = true,
.filter { (_, gi, _, _, mk, _, _, _) -> tempoType = "rall",
(gi ?: 0) >= pair.endGrid && Dynamic.entries.any { d -> d.label == mk.trim() } endGridForTempo = endGrid,
} targetTempoMultiplier = 0.50f // 50% du tempo initial
.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
) )
) println("Rallentando mémorisé à grille $currentIndex jusqu'à $endGrid")
println("NavigationStep HairPin '$symbol' : $currentIndex${pair.endGrid} | ${fromFactor})→${toFactor})") }
// 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?.cancel()
navigationJob = playerScope.launch(Dispatchers.Default) { navigationJob = playerScope.launch(Dispatchers.Default) {
sharedScreenModel.activeIndex.collect { currentIndex -> sharedScreenModel.activeIndex.collect { currentIndex ->
val availableIndices = navigationSteps.map { it.gridIndex }
// println("bpm:$targetBpm _ ${sequencer?.tempoInBPM}| Index en mémoire : $availableIndices") // println("bpm:$targetBpm _ ${sequencer?.tempoInBPM}| Index en mémoire : $availableIndices")
// println("i $currentIndex ") // println("i $currentIndex ")
@ -494,12 +533,10 @@ actual class FMediaPlayer actual constructor(
} }
if (currentIndex < 0) return@collect if (currentIndex < 0) return@collect
val step = navigationSteps.find { val currentSteps = navigationSteps.filter {
it.gridIndex == currentIndex && it.gridIndex == currentIndex && !it.alreadyDone
!it.alreadyDone
} }
currentSteps.forEach { step ->
if (step != null) {
if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) { if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) {
forceTempo(targetBpm.toDouble()) forceTempo(targetBpm.toDouble())
} }