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,24 +411,28 @@ 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
markerList.forEach { marker ->
val mTrim = marker.trim()
when { when {
marker.contains("$") -> lastSegno = ci 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) -> { ritRegex.containsMatchIn(marker) -> {
val endGrid = if(lastCallerMarker == 0) last_grid else lastCallerMarker val endGrid = if(midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker
navigationSteps.add( navigationSteps.add(
NavigationStep( NavigationStep(
marker = marker, marker = marker,
@ -441,34 +457,40 @@ actual class FMediaPlayer actual constructor(
) )
) )
} }
dsG.matches(marker.trim()) -> { dsG.matches(mTrim) -> {
val target = if(lastSegno>0) lastSegno else 0 val target = if(lastSegno>0) lastSegno else 0
val indx = if((last_grid-ci)<=0) ci-1 else ci val indx = if((last_grid-ci)<=0) ci-1 else ci
navigationSteps.add(NavigationStep(marker, indx, targetGrid=target)) navigationSteps.add(NavigationStep(marker, indx, targetGrid=target))
} }
dsR.matches(marker.trim()) || marker == "DSFin" -> { dsR.matches(mTrim) || marker == "DSFin" -> {
navigationSteps.add(NavigationStep(marker, ci, navigationSteps.add(NavigationStep(marker, ci,
targetGrid = if(lastSegno>0) lastSegno else 0)) targetGrid = if(lastSegno>0) lastSegno else 0))
} }
dcG.matches(marker.trim()) -> { dcG.matches(mTrim) -> {
val indx = if((last_grid-ci)<=0) ci-1 else ci val indx = if((last_grid-ci)<=0) ci-1 else ci
navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) navigationSteps.add(NavigationStep(marker, indx, targetGrid=0))
println("DC_GROUP créé à $indx → 0") println("DC_GROUP créé à $indx → 0")
} }
dcR.matches(marker.trim()) && !marker.contains("DC_GROUP_PART") -> { dcR.matches(mTrim) && !marker.contains("DC_GROUP_PART") -> {
val indx = if((last_grid-ci)<=0) ci else ci val indx = if((last_grid-ci)<=0) ci else ci
navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) navigationSteps.add(NavigationStep(marker, indx, targetGrid=0))
println("DC créé à $indx → 0") println("DC créé à $indx → 0")
} }
// ── Farany ──────────────────────────────── // ── Farany ────────────────────────────────
marker.trim().equals("Farany_GROUP_PART", ignoreCase = true) -> { finReg.containsMatchIn(mTrim) -> {
val hasDcAfter = metadataList.any { (_, gi, _, _, mk, _, _, _) -> val hasDcAfter = metadataList.any { m ->
(gi ?: 0) > ci && val after = (m.gridIndex ?: 0) > ci
(mk.contains(Regex("""D\.?C\.?""")) || mk.contains("DC_GROUP_PART"))
val hasMarker = m.marker.any { mk ->
val value = mk.trim()
val ok = dcG.matches(value) || dcR.matches(value)
ok
}
after && hasMarker
} }
if (hasDcAfter) { if (hasDcAfter) {
navigationSteps.add(NavigationStep( navigationSteps.add(NavigationStep(
@ -480,27 +502,43 @@ actual class FMediaPlayer actual constructor(
)) ))
println("Farany mémorisé à $ci") println("Farany mémorisé à $ci")
} else { } else {
println("Farany ignoré à $ci") 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 && marker.trim() != "=" -> { extractDynamic(marker) != null && mTrim != "=" -> {
val dyn = extractDynamic(marker) ?: return@forEach val dyn = extractDynamic(marker) ?: return@forEach
lastFactor = if(dyn==Dynamic.MF) 1.0f else dyn.factor lastFactor = if(dyn==Dynamic.MF) 1.0f else dyn.factor
navigationSteps.add(NavigationStep(marker=marker, gridIndex=ci, dynamic=dyn)) navigationSteps.add(NavigationStep(marker=marker, gridIndex=ci, dynamic=dyn))
} }
marker.trim()=="<" || marker.trim()==">" || mTrim=="<" || mTrim==">" ||
marker.trim().contains("cres", ignoreCase=true) || mTrim.contains("cres", ignoreCase=true) ||
marker.trim().contains("dim", ignoreCase=true) -> { mTrim.contains("dim", ignoreCase=true) -> {
val sym = when { val sym = when {
marker.trim()=="<" -> '<'; marker.trim()==">" -> '>' mTrim=="<" -> '<'; mTrim==">" -> '>'
marker.trim().contains("cres",ignoreCase=true) -> '<'; else -> '>' mTrim.contains("cres",ignoreCase=true) -> '<'; else -> '>'
} }
val pair = hairPins.find{it.startGrid==ci} ?: return@forEach val pair = hairPins.find{it.startGrid==ci} ?: return@forEach
val explicitAfter = metadataList val explicitAfter = metadataList
.filter{(_,gi,_,_,mk,_,_,_)->(gi?:0)>=pair.endGrid && extractDynamic(mk)!=null} .filter{ m ->(m.gridIndex ?:0) >= pair.endGrid }
.minByOrNull{it.gridIndex?:0}?.let{m->extractDynamic(m.marker)} .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 from = lastFactor
val to = when { val to = when {
explicitAfter!=null && sym=='<' && explicitAfter.factor>from -> explicitAfter.factor explicitAfter!=null && sym=='<' && explicitAfter.factor>from -> explicitAfter.factor
@ -509,7 +547,7 @@ actual class FMediaPlayer actual constructor(
} }
lastFactor = to lastFactor = to
navigationSteps.add(NavigationStep( navigationSteps.add(NavigationStep(
marker=marker.trim(), gridIndex=ci, hairPin=sym, marker=mTrim, gridIndex=ci, hairPin=sym,
hairPinEndGrid=pair.endGrid, hairPinEndGrid=pair.endGrid,
hairPinFromFactor=from, hairPinToFactor=to hairPinFromFactor=from, hairPinToFactor=to
)) ))
@ -517,50 +555,45 @@ actual class FMediaPlayer actual constructor(
} }
} }
} }
}
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
step.alreadyDone = true else -> 3
isHolding = false
isRunning = false
playJob?.cancel()
allNotesOff()
onFinished()
} else {
println("Farany 1er passage — DC pas encore vu → on continue ✅")
} }
} }
for(step in currentSteps) {
when {
// ── Point d'orgue ───────────────────── // ── Point d'orgue ─────────────────────
step.isHold -> { step.isHold -> {
val beatMs = (60_000 / targetBpm).toLong() val beatMs = (60_000 / targetBpm).toLong()
val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2 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 isHolding = true
for (ch in 0 until 4) controlChange(ch, 64, 127)
delay(holdDuration) delay(holdDuration)
isHolding = false
for (ch in 0 until 4) controlChange(ch, 64, 0) for (ch in 0 until 4) controlChange(ch, 64, 0)
step.alreadyDone = true isHolding = false
println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms")
} }
// Rit. / Rall. , ... // Rit. / Rall. , ...
step.isTempoChange -> { step.isTempoChange -> {
@ -597,12 +630,29 @@ actual class FMediaPlayer actual constructor(
applyDynamic(step.dynamic) 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,7 +671,6 @@ 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")
) )
), ),
@ -126,8 +120,11 @@ private val markerGroups = listOf(
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,38 +345,26 @@ 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
) {
Text(text = tooltipText, fontSize = MaterialTheme.typography.titleSmall.fontSize)
} }
}, },
state = rememberTooltipState() state = rememberTooltipState()
) { ) {
IconButton( IconButton(
modifier = Modifier.size(30.dp) modifier = Modifier.size(25.dp),
.onGloballyPositioned { coordinates ->
markerButtonPos = coordinates.positionInParent()
.let {
IntOffset(
it.x.toInt(),
it.y.toInt()
)
}
},
onClick = { onClick = {
if(hasMarker) {
val state = TUOEditState( val state = TUOEditState(
tuoIndex = globalIndex, tuoIndex = globalIndex,
notesByVoice = notes.toMap(), notesByVoice = notes.toMap(),
@ -383,16 +372,45 @@ fun TUODetailDialog(
originalLyricsByStanza = originalLyricsByStz.toMutableMap(), originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
lyricsByStanza = mutableMapOf(), lyricsByStanza = mutableMapOf(),
templateFragment = templateFragment, templateFragment = templateFragment,
marker = "_", marker = "_", // On remet le marqueur à vide
originalSep = initialSep, originalSep = initialSep,
sep = newSep sep = newSep
) )
onSave(state) onSave(state)
} }
showMarkerPopup = true ) {
}) {
Icon( Icon(
imageVector = if(hasMarker) Icons.Default.Remove else Icons.Default.Add, imageVector = Icons.Sharp.Delete,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = "Enlever"
)
}
}
}
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), tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = null contentDescription = null
) )
@ -401,6 +419,8 @@ fun TUODetailDialog(
} }
} }
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
val activeMessages = (0..3).mapNotNull { voice -> val activeMessages = (0..3).mapNotNull { voice ->
@ -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
@ -767,7 +772,6 @@ fun LazyVerticalGridTUO(
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()
@ -821,24 +825,37 @@ 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 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) 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 { when {
musicForPtSerifBIRegex.containsMatchIn(text) -> { musicForPtSerifBIRegex.matches(marker) -> {
markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value
markerFontFamily = ptSerifBoldItalic markerFontFamily = ptSerifBoldItalic
} }
musicForEmmentRegex.containsMatchIn(text) -> {
musicForEmmentRegex.matches(marker) -> {
markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value + 11 markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value + 11
markerFontFamily = emmentaler markerFontFamily = emmentaler
} }
else -> { else -> {
markerFontFamily = ptSerif markerFontFamily = ptSerif
} }
} }
Text(text = text, Text(
text = marker,
modifier = Modifier modifier = Modifier
.wrapContentSize(unbounded = true, align = Alignment.CenterStart,), .wrapContentSize(unbounded = true, align = Alignment.CenterStart,),
softWrap = false, softWrap = false,
@ -851,6 +868,12 @@ fun LazyVerticalGridTUO(
fontFamily = markerFontFamily fontFamily = markerFontFamily
) )
) )
if (marker != markerList.last()) {
Spacer(modifier = Modifier.width(2.dp))
}
}
}
}
} }
} }
} }
@ -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,89 +431,129 @@ 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
var currentMarker = marker
var newGridIndex = marker.gridIndex ?: 0
val newMarkerList = mutableListOf<String>()
originalMarker.forEach { markerText->
val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI
val isNearEnd = index >= (tuos.size - 2) val isNearEnd = index >= (tuos.size - 2)
// println("MARKERS:> $markerText\n")
val isDC = markerText.contains(Regex("""D\.?C\.?""")) val isDC = markerText.contains(Regex("""D\.?C\.?"""))
val isDS = markerText.contains(Regex("""D\.?S\.?""")) val isDS = markerText.contains(Regex("""D\.?S\.?"""))
val isFarany = markerText.trim().contains(Regex("""^(fine|fin|farany|end)$""", RegexOption.IGNORE_CASE)) val isFarany = markerText.trim().contains(Regex("""^(fine|fin|farany|end)$""", RegexOption.IGNORE_CASE))
val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
val isRall = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
var resultMarker: MidiMarkers = marker
if (isFarany) { if (isFarany) {
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
resultMarker = marker.copy( newMarkerList.add("Farany_GROUP_PART")
gridIndex = forwardIndex - 1,
marker = "Farany_GROUP_PART"
)
println("Farany finalisé : grille $index${forwardIndex - 1}")
break break
} }
forwardIndex++ forwardIndex++
} }
println("Farany finalisé : grille $index${forwardIndex - 1}")
} else if (isDC) { } else if (isDC) {
var found = false
var forwardIndex = index var forwardIndex = index
while (forwardIndex < tuos.size) { while (forwardIndex < tuos.size) {
val currentTuo = tuos.getOrNull(forwardIndex) if (tuos.getOrNull(forwardIndex)?.sep0 == "/") {
val currentNote = currentTuo?.tuNotes?.getOrNull(1)?.toString() ?: "" newGridIndex = forwardIndex - 1
val currentSep = currentTuo?.sep0 ?: "" newMarkerList.add("${markerText.trim()}_GROUP_PART")
println("DC gp finalisé grille $newGridIndex")
// println("Je suis sur $forwardIndex note $currentNote Sep $currentSep \t condition: ${(currentNote == "―")} || ${(currentSep == "/")}") found = true
// Tant que fin de mesure
if (currentSep == "/") {
resultMarker = marker.copy(
gridIndex = forwardIndex-1,
marker = "${markerText.trim()}_GROUP_PART"
)
break break
} }
forwardIndex++ forwardIndex++
} }
if (!found) {
newMarkerList.add(markerText)
}
} else if (isDS && isNearEnd) { } else if (isDS && isNearEnd) {
val currentNote = tuos.getOrNull(index)?.tuNotes?.getOrNull(1)?.toString() ?: "" val currentNote = tuos.getOrNull(index)?.tuNotes?.getOrNull(1)?.toString() ?: ""
val nextNote = tuos.getOrNull(index + 1)?.tuNotes?.getOrNull(1)?.toString() ?: "" val nextNote = tuos.getOrNull(index + 1)?.tuNotes?.getOrNull(1)?.toString() ?: ""
// Cas où le marker et la note suivante sont vides if (currentNote.isBlank() && nextNote.isBlank()) {
val isEmptySituation = currentNote.trim().isEmpty() && nextNote.trim().isEmpty()
if (isEmptySituation) {
// vers arrière une note non vide
var backwardIndex = index - 1 var backwardIndex = index - 1
while (backwardIndex >= 0) { while (backwardIndex >= 0) {
val note = tuos.getOrNull(backwardIndex)?.tuNotes?.getOrNull(1)?.toString() ?: "" if (tuos.getOrNull(backwardIndex)?.tuNotes?.getOrNull(1)?.toString()?.isNotBlank() == true) {
if (note.trim().isNotEmpty()) { newGridIndex = backwardIndex
val cleanText = if (markerText.trim() == "DSFin") "DS" else markerText.trim() newMarkerList.add("${if (markerText.trim() == "DSFin") "DS" else markerText.trim()}_GROUP_PART")
println("DS finalisé grille $newGridIndex")
resultMarker = marker.copy(
gridIndex = backwardIndex,
marker = "${cleanText}_GROUP_PART"
)
break break
} }
backwardIndex-- backwardIndex--
} }
} else { } 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
val cleanText = if (markerText.trim() == "DSFin") "DS" else markerText.trim() newMarkerList.add("${if (markerText.trim() == "DSFin") "DS" else markerText.trim()}_GROUP_PART")
println("DS gp finalisé grille $newGridIndex")
resultMarker = marker.copy(
gridIndex = forwardIndex,
marker = "${cleanText}_GROUP_PART"
)
break break
} }
forwardIndex++ forwardIndex++
@ -520,30 +561,40 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
} }
} else if (isRit.containsMatchIn(markerText)) { } else if (isRit.containsMatchIn(markerText)) {
var forwardIndex = index + 1 var forwardIndex = index + 1
var foundSeparator = false
while (forwardIndex < tuos.size) { while (forwardIndex < tuos.size) {
val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: "" if (tuos.getOrNull(forwardIndex)?.sep0 == "/") {
if (sep == "/") { currentMarker = currentMarker.copy(lastCallerMarker = forwardIndex - 1)
resultMarker = marker.copy( newMarkerList.add("Ritenuto")
gridIndex = index,
lastCallerMarker = forwardIndex - 1,
marker = "Ritenuto"
)
println("Rit finalisé : grille $index${forwardIndex - 1}")
foundSeparator = true
break break
} }
forwardIndex++ forwardIndex++
} }
} else { println("Rit finalisé : grille $index${forwardIndex - 1}")
marker
} 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
} }
resultMarker forwardIndex++
}
println("Rall finalisé : grille $index${forwardIndex - 1}")
} else {
newMarkerList.add(markerText)
}
}
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
val starts = allMarkers
.filter { fun isStartToken(s: String): Boolean {
val m = it.marker.trim() val m = s.trim()
m == "<" || return m == "<" ||
m == ">" || m == ">" ||
m.contains("cres", ignoreCase = true) || m.contains("cres", ignoreCase = true) ||
m.contains("dim", ignoreCase = true) m.contains("dim", ignoreCase = true)
} }
val starts = allMarkers
.filter { markerObj ->
markerObj.marker.any { isStartToken(it) }
}
.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 startString = start.marker.firstOrNull { isStartToken(it) } ?: return@mapNotNull null
val symbol = when { val symbol = when {
start.marker.trim() == "<" -> '<' startString.contains("<") || startString.contains("cres", ignoreCase = true) -> '<'
start.marker.trim() == ">" -> '>' startString.contains(">") || startString.contains("dim", ignoreCase = true) -> '>'
start.marker.contains("cres", ignoreCase = true) -> '<'
start.marker.contains("dim", ignoreCase = true) -> '>'
else -> return@mapNotNull null else -> return@mapNotNull null
} }

View file

@ -255,16 +255,20 @@ 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("$") -> {
@ -273,7 +277,7 @@ actual class FMediaPlayer actual constructor(
} }
// Point d'orgue // Point d'orgue
marker.contains("\uD834\uDD10") -> { marker.contains("\uD834\uDD10") -> {
val beat = if(note.contains('•')) 2 else 1 // demi-ton sur .) ou non val beat = if (midiMarker.note.contains('•')) 2 else 1 // demi-ton sur .) ou non
navigationSteps.add( navigationSteps.add(
FMediaPlayer.NavigationStep( FMediaPlayer.NavigationStep(
marker, marker,
@ -282,11 +286,11 @@ actual class FMediaPlayer actual constructor(
beatInDC = beat beatInDC = beat
) )
) )
println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° $gridIndex") println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° ${midiMarker.gridIndex}")
} }
// Rit ... // Rit ...
ritRegex.containsMatchIn(marker) -> { ritRegex.containsMatchIn(marker) -> {
val endGrid = if(lastCallerMarker == 0) last_grid else lastCallerMarker val endGrid = if (midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker
navigationSteps.add( navigationSteps.add(
NavigationStep( NavigationStep(
@ -316,7 +320,7 @@ actual class FMediaPlayer actual constructor(
println("Rallentando mémorisé à grille $currentIndex jusqu'à $endGrid") println("Rallentando mémorisé à grille $currentIndex jusqu'à $endGrid")
} }
// DS // DS
dsGPattern.matches(marker.trim())-> { dsGPattern.matches(mTrim) -> {
val target = if (lastSegno > 0) lastSegno else 0 val target = if (lastSegno > 0) lastSegno else 0
var indx = if ((last_grid - currentIndex) <= 0) { var indx = if ((last_grid - currentIndex) <= 0) {
currentIndex - 1 currentIndex - 1
@ -330,7 +334,8 @@ actual class FMediaPlayer actual constructor(
) )
println("Lien créé : $marker à grille n° $indx vers cible $target ........") println("Lien créé : $marker à grille n° $indx vers cible $target ........")
} }
dsRegex.matches(marker.trim()) || marker == "DSFin" -> {
dsRegex.matches(mTrim) || marker == "DSFin" -> {
val target = if (lastSegno > 0) lastSegno else 0 val target = if (lastSegno > 0) lastSegno else 0
navigationSteps.add( navigationSteps.add(
@ -338,12 +343,13 @@ actual class FMediaPlayer actual constructor(
marker, marker,
currentIndex, currentIndex,
targetGrid = target targetGrid = target
)) )
println("Lien DS créé : Saut immédiat à $gridIndex vers Segno $target") )
println("Lien DS créé : Saut immédiat à ${midiMarker.gridIndex} vers Segno $target")
} }
// DC // DC
dcGPattern.matches(marker.trim()) -> { dcGPattern.matches(mTrim) -> {
val indx = if ((last_grid - currentIndex) <= 0) { val indx = if ((last_grid - currentIndex) <= 0) {
currentIndex - 1 currentIndex - 1
} else currentIndex } else currentIndex
@ -357,7 +363,8 @@ actual class FMediaPlayer actual constructor(
) )
println("Lien créé : $marker à $indx vers cible 0 ") println("Lien créé : $marker à $indx vers cible 0 ")
} }
(dcRegex.matches(marker.trim()) && !marker.contains("DC_GROUP_PART")) -> {
(dcRegex.matches(mTrim) && !marker.contains("DC_GROUP_PART")) -> {
println("dernier grille $last_grid") println("dernier grille $last_grid")
var indx = if ((last_grid - currentIndex) <= 0) { var indx = if ((last_grid - currentIndex) <= 0) {
currentIndex /*- 1*/ currentIndex /*- 1*/
@ -373,11 +380,24 @@ actual class FMediaPlayer actual constructor(
} }
// Farany // Farany
marker.trim().equals("Farany_GROUP_PART", ignoreCase = true) -> { finReg.containsMatchIn(mTrim) -> {
val hasDcAfter = metadataList.any { (_, gi, _, _, mk, _, _, _) -> val hasDcAfter = metadataList.any { m ->
(gi ?: 0) > currentIndex && val after = (m.gridIndex ?: 0) > currentIndex
(dcGPattern.matches(mk.trim()) ||
dcRegex.matches(mk.trim())) 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) { if (hasDcAfter) {
navigationSteps.add( navigationSteps.add(
@ -390,10 +410,22 @@ actual class FMediaPlayer actual constructor(
) )
) )
println("Farany mémorisé à la grille $currentIndex") 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é // velocité
extractDynamic(marker) != null && marker.trim() != "=" -> { extractDynamic(marker) != null && mTrim != "=" -> {
val dyn = extractDynamic(marker) ?: return@forEach val dyn = extractDynamic(marker) ?: return@forEach
lastFactor = when (dyn) { lastFactor = when (dyn) {
Dynamic.MF -> 1.0f Dynamic.MF -> 1.0f
@ -409,14 +441,14 @@ actual class FMediaPlayer actual constructor(
println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex") println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex")
} }
// Soufflet // Soufflet
marker.trim() == "<" || marker.trim() == ">" || mTrim == "<" || mTrim == ">" ||
marker.trim().contains("cres", ignoreCase = true) || mTrim.contains("cres", ignoreCase = true) ||
marker.trim().contains("dim", ignoreCase = true) -> { mTrim.contains("dim", ignoreCase = true) -> {
val symbol = when { val symbol = when {
marker.trim() == "<" -> '<' mTrim == "<" -> '<'
marker.trim() == ">" -> '>' mTrim == ">" -> '>'
marker.trim().contains("cres", ignoreCase = true) -> '<' mTrim.contains("cres", ignoreCase = true) -> '<'
marker.trim().contains("dim", ignoreCase = true) -> '>' mTrim.contains("dim", ignoreCase = true) -> '>'
else -> return@forEach else -> return@forEach
} }
val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach
@ -426,35 +458,43 @@ actual class FMediaPlayer actual constructor(
.maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF
val dynAfter = metadataList val dynAfter = metadataList
.filter { (_, gi, _, _, mk, _, _, _) -> .filter { m -> (m.gridIndex ?: 0) >= pair.endGrid }
(gi ?: 0) >= pair.endGrid && Dynamic.entries.any { d -> d.label == mk.trim() } .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.gridIndex ?: 0 } .minByOrNull { it.first }
?.let { m -> Dynamic.entries.find { d -> d.label == m.marker.trim() } } ?.second
val explicitDynAfter = metadataList val explicitDynAfter = metadataList
.filter { (_, gi, _, _, mk, _, _, _) -> .filter { m -> (m.gridIndex ?: 0) >= pair.endGrid }
(gi ?: 0) >= pair.endGrid && .mapNotNull { m ->
extractDynamic(mk) != null val dynamic = m.marker.firstNotNullOfOrNull { mk -> extractDynamic(mk) }
if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null
} }
.minByOrNull { it.gridIndex ?: 0 } .minByOrNull { it.first }
?.let { m -> extractDynamic(m.marker) } ?.second
val fromFactor = lastFactor val fromFactor = lastFactor
val toFactor = when { val toFactor = when {
explicitDynAfter != null && symbol == '<' && explicitDynAfter.factor > fromFactor -> { explicitDynAfter != null && symbol == '<' && explicitDynAfter.factor > fromFactor -> {
explicitDynAfter.factor explicitDynAfter.factor
} }
explicitDynAfter != null && symbol == '>' && explicitDynAfter.factor < fromFactor -> { explicitDynAfter != null && symbol == '>' && explicitDynAfter.factor < fromFactor -> {
explicitDynAfter.factor explicitDynAfter.factor
} }
else -> nextDynamic(fromFactor, symbol) else -> nextDynamic(fromFactor, symbol)
} }
lastFactor = toFactor lastFactor = toFactor
navigationSteps.add( navigationSteps.add(
NavigationStep( NavigationStep(
marker = marker.trim(), marker = mTrim,
gridIndex = currentIndex, gridIndex = currentIndex,
hairPin = symbol, hairPin = symbol,
hairPinEndGrid = pair.endGrid, hairPinEndGrid = pair.endGrid,
@ -467,6 +507,7 @@ actual class FMediaPlayer actual constructor(
} }
} }
} }
}
private fun getPendingDcStep(): NavigationStep? { private fun getPendingDcStep(): NavigationStep? {
return navigationSteps.firstOrNull { step -> return navigationSteps.firstOrNull { step ->
!step.alreadyDone && !step.alreadyDone &&
@ -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())
} }