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

View file

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

View file

@ -36,6 +36,61 @@ class PTemplate (val template: String, val separatorAfter: String, private val m
.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 {
var returnFun = ""
markers.map {

View file

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

View file

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

View file

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

View file

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

View file

@ -255,214 +255,255 @@ actual class FMediaPlayer actual constructor(
val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
val currentIndex = gridIndex ?: 0
metadataList.forEach { midiMarker ->
val currentIndex = midiMarker.gridIndex ?: 0
val markerList = midiMarker.marker
val last_grid = sharedScreenModel.getTotalGridCount()
val hairPins = sharedScreenModel.getHairPins()
val dsRegex = Regex("""D\.?S\.?""")
val dcRegex = Regex("""D\.?C\.?""")
val dsGPattern = Regex("""D\.?S\.?_GROUP_PART""")
val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""")
val finReg = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE)
val last_grid = sharedScreenModel.getTotalGridCount()
val hairPins = sharedScreenModel.getHairPins()
when {
// segno
marker.contains("$") -> {
lastSegno = currentIndex
println("Cible ($) mémorisée au $lastSegno")
}
// Point d'orgue
marker.contains("\uD834\uDD10") -> {
val beat = if(note.contains('•')) 2 else 1 // demi-ton sur .) ou non
navigationSteps.add(
FMediaPlayer.NavigationStep(
marker,
currentIndex,
isHold = true,
beatInDC = beat
)
)
println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° $gridIndex")
}
// Rit ...
ritRegex.containsMatchIn(marker) -> {
val endGrid = if(lastCallerMarker == 0) last_grid else lastCallerMarker
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
isTempoChange = true,
tempoType = "rit",
endGridForTempo = endGrid,
targetTempoMultiplier = 0.55f // 55% du tempo initial
)
)
println("Ritardando mémorisé à grille $currentIndex jusqu'à $endGrid")
}
rallRegex.containsMatchIn(marker) -> {
val endGrid = last_grid
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
isTempoChange = true,
tempoType = "rall",
endGridForTempo = endGrid,
targetTempoMultiplier = 0.50f // 50% du tempo initial
)
)
println("Rallentando mémorisé à grille $currentIndex jusqu'à $endGrid")
}
// DS
dsGPattern.matches(marker.trim())-> {
val target = if (lastSegno > 0) lastSegno else 0
var indx = if((last_grid - currentIndex) <= 0) {
currentIndex - 1
} else currentIndex
navigationSteps.add(
NavigationStep(
marker,
gridIndex = indx,
targetGrid = target,
)
)
println("Lien créé : $marker à grille n° $indx vers cible $target ........")
}
dsRegex.matches(marker.trim()) || marker == "DSFin" -> {
val target = if (lastSegno > 0) lastSegno else 0
navigationSteps.add(
NavigationStep(
marker,
currentIndex,
targetGrid = target
))
println("Lien DS créé : Saut immédiat à $gridIndex vers Segno $target")
}
// DC
dcGPattern.matches(marker.trim()) -> {
val indx = if((last_grid - currentIndex) <= 0) {
currentIndex - 1
} else currentIndex
// println("monn index est $indx car dernier est $last_grid et curr $currentIndex")
navigationSteps.add(
NavigationStep(
marker,
indx,
targetGrid = 0
)
)
println("Lien créé : $marker à $indx vers cible 0 ")
}
(dcRegex.matches(marker.trim()) && !marker.contains("DC_GROUP_PART")) -> {
println("dernier grille $last_grid")
var indx = if((last_grid - currentIndex) <= 0) {
currentIndex /*- 1*/
} else currentIndex
navigationSteps.add(
NavigationStep(
marker,
indx,
targetGrid = 0
)
)
println("Lien DC créé : $marker à $indx vers le début")
}
// Farany
marker.trim().equals("Farany_GROUP_PART", ignoreCase = true) -> {
val hasDcAfter = metadataList.any { (_, gi, _, _, mk, _, _, _) ->
(gi ?: 0) > currentIndex &&
(dcGPattern.matches(mk.trim()) ||
dcRegex.matches(mk.trim()))
markerList.forEach { marker ->
val mTrim = marker.trim()
when {
// segno
marker.contains("$") -> {
lastSegno = currentIndex
println("Cible ($) mémorisée au $lastSegno")
}
if (hasDcAfter) {
// Point d'orgue
marker.contains("\uD834\uDD10") -> {
val beat = if (midiMarker.note.contains('•')) 2 else 1 // demi-ton sur .) ou non
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
targetGrid = last_grid,
isFin = true,
finActive = false
FMediaPlayer.NavigationStep(
marker,
currentIndex,
isHold = true,
beatInDC = beat
)
)
println("Farany mémorisé à la grille $currentIndex")
println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° ${midiMarker.gridIndex}")
}
}
// velocité
extractDynamic(marker) != null && marker.trim() != "=" -> {
val dyn = extractDynamic(marker) ?: return@forEach
lastFactor = when (dyn) {
Dynamic.MF -> 1.0f
else -> dyn.factor
}
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
dynamic = dyn
// Rit ...
ritRegex.containsMatchIn(marker) -> {
val endGrid = if (midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
isTempoChange = true,
tempoType = "rit",
endGridForTempo = endGrid,
targetTempoMultiplier = 0.55f // 55% du tempo initial
)
)
)
println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex")
}
// Soufflet
marker.trim() == "<" || marker.trim() == ">" ||
marker.trim().contains("cres", ignoreCase = true) ||
marker.trim().contains("dim", ignoreCase = true) -> {
val symbol = when {
marker.trim() == "<" -> '<'
marker.trim() == ">" -> '>'
marker.trim().contains("cres", ignoreCase = true) -> '<'
marker.trim().contains("dim", ignoreCase = true) -> '>'
else -> return@forEach
println("Ritardando mémorisé à grille $currentIndex jusqu'à $endGrid")
}
val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach
rallRegex.containsMatchIn(marker) -> {
val endGrid = last_grid
val dynBefore = navigationSteps
.filter { it.dynamic != null && it.gridIndex <= currentIndex }
.maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF
val dynAfter = metadataList
.filter { (_, gi, _, _, mk, _, _, _) ->
(gi ?: 0) >= pair.endGrid && Dynamic.entries.any { d -> d.label == mk.trim() }
}
.minByOrNull { it.gridIndex ?: 0 }
?.let { m -> Dynamic.entries.find { d -> d.label == m.marker.trim() } }
val explicitDynAfter = metadataList
.filter { (_, gi, _, _, mk, _, _, _) ->
(gi ?: 0) >= pair.endGrid &&
extractDynamic(mk) != null
}
.minByOrNull { it.gridIndex ?: 0 }
?.let { m -> extractDynamic(m.marker) }
val fromFactor = lastFactor
val toFactor = when {
explicitDynAfter != null && symbol == '<' && explicitDynAfter.factor > fromFactor -> {
explicitDynAfter.factor
}
explicitDynAfter != null && symbol == '>' && explicitDynAfter.factor < fromFactor -> {
explicitDynAfter.factor
}
else -> nextDynamic(fromFactor, symbol)
}
lastFactor = toFactor
navigationSteps.add(
NavigationStep(
marker = marker.trim(),
gridIndex = currentIndex,
hairPin = symbol,
hairPinEndGrid = pair.endGrid,
hairPinFromFactor = fromFactor,
hairPinToFactor = toFactor
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
isTempoChange = true,
tempoType = "rall",
endGridForTempo = endGrid,
targetTempoMultiplier = 0.50f // 50% du tempo initial
)
)
)
println("NavigationStep HairPin '$symbol' : $currentIndex${pair.endGrid} | ${fromFactor})→${toFactor})")
println("Rallentando mémorisé à grille $currentIndex jusqu'à $endGrid")
}
// DS
dsGPattern.matches(mTrim) -> {
val target = if (lastSegno > 0) lastSegno else 0
var indx = if ((last_grid - currentIndex) <= 0) {
currentIndex - 1
} else currentIndex
navigationSteps.add(
NavigationStep(
marker,
gridIndex = indx,
targetGrid = target,
)
)
println("Lien créé : $marker à grille n° $indx vers cible $target ........")
}
dsRegex.matches(mTrim) || marker == "DSFin" -> {
val target = if (lastSegno > 0) lastSegno else 0
navigationSteps.add(
NavigationStep(
marker,
currentIndex,
targetGrid = target
)
)
println("Lien DS créé : Saut immédiat à ${midiMarker.gridIndex} vers Segno $target")
}
// DC
dcGPattern.matches(mTrim) -> {
val indx = if ((last_grid - currentIndex) <= 0) {
currentIndex - 1
} else currentIndex
// println("monn index est $indx car dernier est $last_grid et curr $currentIndex")
navigationSteps.add(
NavigationStep(
marker,
indx,
targetGrid = 0
)
)
println("Lien créé : $marker à $indx vers cible 0 ")
}
(dcRegex.matches(mTrim) && !marker.contains("DC_GROUP_PART")) -> {
println("dernier grille $last_grid")
var indx = if ((last_grid - currentIndex) <= 0) {
currentIndex /*- 1*/
} else currentIndex
navigationSteps.add(
NavigationStep(
marker,
indx,
targetGrid = 0
)
)
println("Lien DC créé : $marker à $indx vers le début")
}
// Farany
finReg.containsMatchIn(mTrim) -> {
val hasDcAfter = metadataList.any { m ->
val after = (m.gridIndex ?: 0) > currentIndex
println(
"grid=${m.gridIndex}, current=$currentIndex, after=$after"
)
val hasMarker = m.marker.any { mk ->
val value = mk.trim()
val ok = dcGPattern.matches(value) || dcRegex.matches(value)
println("marker=$value -> $ok")
ok
}
println("after=$after, hasMarker=$hasMarker, result=${after && hasMarker}")
after && hasMarker
}
if (hasDcAfter) {
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
targetGrid = last_grid,
isFin = true,
finActive = false
)
)
println("Farany mémorisé à la grille $currentIndex")
} else {
val verse = sharedScreenModel.stanza.value
val verses = sharedScreenModel.nbStanzas.value
println("$verse == $verses")
navigationSteps.add(
NavigationStep(
marker="${marker}_STZ",
gridIndex = currentIndex,
isFin = true,
finActive = if(verse == verses) false else true
)
)
}
}
// velocité
extractDynamic(marker) != null && mTrim != "=" -> {
val dyn = extractDynamic(marker) ?: return@forEach
lastFactor = when (dyn) {
Dynamic.MF -> 1.0f
else -> dyn.factor
}
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = currentIndex,
dynamic = dyn
)
)
println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex")
}
// Soufflet
mTrim == "<" || mTrim == ">" ||
mTrim.contains("cres", ignoreCase = true) ||
mTrim.contains("dim", ignoreCase = true) -> {
val symbol = when {
mTrim == "<" -> '<'
mTrim == ">" -> '>'
mTrim.contains("cres", ignoreCase = true) -> '<'
mTrim.contains("dim", ignoreCase = true) -> '>'
else -> return@forEach
}
val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach
val dynBefore = navigationSteps
.filter { it.dynamic != null && it.gridIndex <= currentIndex }
.maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF
val dynAfter = metadataList
.filter { m -> (m.gridIndex ?: 0) >= pair.endGrid }
.mapNotNull { m ->
val label = m.marker.firstOrNull { mk -> Dynamic.entries.any { d -> d.label == mk.trim() } }
val dynamic = label?.let { l -> Dynamic.entries.find { d -> d.label == l.trim() } }
if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null
}
.minByOrNull { it.first }
?.second
val explicitDynAfter = metadataList
.filter { m -> (m.gridIndex ?: 0) >= pair.endGrid }
.mapNotNull { m ->
val dynamic = m.marker.firstNotNullOfOrNull { mk -> extractDynamic(mk) }
if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null
}
.minByOrNull { it.first }
?.second
val fromFactor = lastFactor
val toFactor = when {
explicitDynAfter != null && symbol == '<' && explicitDynAfter.factor > fromFactor -> {
explicitDynAfter.factor
}
explicitDynAfter != null && symbol == '>' && explicitDynAfter.factor < fromFactor -> {
explicitDynAfter.factor
}
else -> nextDynamic(fromFactor, symbol)
}
lastFactor = toFactor
navigationSteps.add(
NavigationStep(
marker = mTrim,
gridIndex = currentIndex,
hairPin = symbol,
hairPinEndGrid = pair.endGrid,
hairPinFromFactor = fromFactor,
hairPinToFactor = toFactor
)
)
println("NavigationStep HairPin '$symbol' : $currentIndex${pair.endGrid} | ${fromFactor})→${toFactor})")
}
}
}
}
@ -483,8 +524,6 @@ actual class FMediaPlayer actual constructor(
navigationJob?.cancel()
navigationJob = playerScope.launch(Dispatchers.Default) {
sharedScreenModel.activeIndex.collect { currentIndex ->
val availableIndices = navigationSteps.map { it.gridIndex }
// println("bpm:$targetBpm _ ${sequencer?.tempoInBPM}| Index en mémoire : $availableIndices")
// println("i $currentIndex ")
@ -494,12 +533,10 @@ actual class FMediaPlayer actual constructor(
}
if (currentIndex < 0) return@collect
val step = navigationSteps.find {
it.gridIndex == currentIndex &&
!it.alreadyDone
val currentSteps = navigationSteps.filter {
it.gridIndex == currentIndex && !it.alreadyDone
}
if (step != null) {
currentSteps.forEach { step ->
if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) {
forceTempo(targetBpm.toDouble())
}