Attach lyrics on midi export; stz loop midi

This commit is contained in:
Hasinjato 2026-10-15 11:03:24 +03:00
parent 81515a40f1
commit d5d8068c24
4 changed files with 246 additions and 24 deletions

View file

@ -53,7 +53,7 @@ class MidiWriterKotlin constructor(private val fileRepository: FileRepository)
tick = 0 tick = 0
pitches.forEach { pitches.forEach {
if (it.metaType > 0) { if (it.metaType > 0) {
val metaDataString = String(it.metaBytes, Charsets.ISO_8859_1) val metaDataString = String(it.metaBytes, Charsets.UTF_8)
addMetaMessage(it.metaType, it.tick, it.metaByteSize, metaDataString) addMetaMessage(it.metaType, it.tick, it.metaByteSize, metaDataString)
} else if (it.pitch != "") { } else if (it.pitch != "") {
addNote(it.voiceNumber, it.pitch.toInt(), 100, it.tick.toLong()) addNote(it.voiceNumber, it.pitch.toInt(), 100, it.tick.toLong())

View file

@ -68,6 +68,106 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
sequence.addControlChange(channel, ctrl, valByte, tickPosition) sequence.addControlChange(channel, ctrl, valByte, tickPosition)
} }
//val dcGridIndex = (dcMarker.gridIndex ?: 0)+1
//val endRepeatGridIndex = (fineMarker?.gridIndex ?: dcGridIndex)+1
/**
* Déplie séquentiellement tous les sauts (D.C. et D.S. / DSFin) dans l'ordre chronologique.
*/
fun expandDaCapoAndSegno(
initialPitches: List<MidiPitch>,
initialMarkers: List<MidiMarkers>
): Pair<List<MidiPitch>, List<MidiMarkers>> {
val isDC = Regex("""D\.?C\.?""", RegexOption.IGNORE_CASE)
val isDS = Regex("""D\.?S\.?""", RegexOption.IGNORE_CASE)
val isFine = Regex("""\b(fine|fin|farany|end)\b""", RegexOption.IGNORE_CASE)
var currentPitches = initialPitches
var currentMarkers = initialMarkers
var maxIterations = 20
while (maxIterations > 0) {
maxIterations--
val sortedMarkers = currentMarkers.sortedBy { it.gridIndex ?: 0 }
val candidateJumpMarker = sortedMarkers.firstOrNull { m ->
m.marker.any { text ->
val clean = text.trim()
isDC.containsMatchIn(clean) || isDS.containsMatchIn(clean)
}
} ?: break
val candidateGridIdx = candidateJumpMarker.gridIndex ?: 0
val jumpGridIndex = candidateGridIdx + 1
val jumpRawTick = jumpGridIndex * 480L
val isDsJump = candidateJumpMarker.marker.any { text -> isDS.containsMatchIn(text.trim()) }
currentMarkers = currentMarkers.map { m ->
if (m === candidateJumpMarker || (m.gridIndex == candidateGridIdx && m.marker == candidateJumpMarker.marker)) {
m.copy(marker = m.marker.map { text ->
text.replace(Regex("""D\.?C\.?""", RegexOption.IGNORE_CASE), "")
.replace(Regex("""D\.?S\.?""", RegexOption.IGNORE_CASE), "")
})
} else {
m
}
}
val startRepeatGridIndex = if (isDsJump) {
val segnoMarker = currentMarkers.lastOrNull { m ->
(m.gridIndex ?: 0) <= jumpGridIndex && m.marker.any { it.contains("$") }
}
segnoMarker?.gridIndex ?: 0
} else {
0
}
val startRepeatRawTick = startRepeatGridIndex * 480L
val fineMarker = currentMarkers.firstOrNull { m ->
val gIdx = m.gridIndex ?: 0
gIdx >= startRepeatGridIndex && gIdx <= jumpGridIndex && m.marker.any { text -> isFine.containsMatchIn(text.trim()) }
}
val hasFine = fineMarker != null
val endRepeatGridIndex = (fineMarker?.gridIndex ?: candidateGridIdx)
val endRepeatRawTick = endRepeatGridIndex * 480L
val shiftGridIndex = jumpGridIndex - startRepeatGridIndex
val shiftInPitchTick: Int = shiftGridIndex * 60
val pitchesBeforeJump = currentPitches.filter { (it.tick * 8L) <= jumpRawTick }
val pitchesToRepeat = currentPitches.filter {
val raw = it.tick * 8L
raw >= startRepeatRawTick && raw <= endRepeatRawTick
}
val duplicatedPitches = pitchesToRepeat.map { pitch ->
pitch.copy(tick = pitch.tick + shiftInPitchTick)
}
val repeatDurationGridIndex = endRepeatGridIndex - startRepeatGridIndex
val durationShiftInPitchTick: Int = repeatDurationGridIndex * 60
val shiftedPitchesAfterJump = currentPitches.filter { (it.tick * 8L) > jumpRawTick }.map { pitch ->
pitch.copy(tick = pitch.tick + durationShiftInPitchTick)
}
currentPitches = pitchesBeforeJump + duplicatedPitches + shiftedPitchesAfterJump
val markersBeforeJump = currentMarkers.filter { (it.gridIndex ?: 0) <= jumpGridIndex }
val markersToRepeat = currentMarkers.filter {
val gIdx = it.gridIndex ?: 0
gIdx >= startRepeatGridIndex && gIdx <= endRepeatGridIndex
}
val duplicatedMarkers = markersToRepeat.map { m ->
m.copy(gridIndex = (m.gridIndex ?: 0) + shiftGridIndex)
}
val shiftedMarkersAfterJump = currentMarkers.filter { (it.gridIndex ?: 0) > jumpGridIndex }.map { m ->
m.copy(gridIndex = (m.gridIndex ?: 0) + repeatDurationGridIndex)
}
currentMarkers = markersBeforeJump + duplicatedMarkers + shiftedMarkersAfterJump
}
return Pair(currentPitches, currentMarkers)
}
fun process( fun process(
pitches: List<MidiPitch>, pitches: List<MidiPitch>,
markers: List<MidiMarkers> = emptyList(), markers: List<MidiMarkers> = emptyList(),
@ -75,6 +175,7 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
voiceInstruments: Map<Int, Int> = mapOf(1 to 0, 2 to 0, 3 to 0, 4 to 0), voiceInstruments: Map<Int, Int> = mapOf(1 to 0, 2 to 0, 3 to 0, 4 to 0),
initialKey: String = "C" initialKey: String = "C"
) { ) {
val (effectivePitches, effectiveMarkers) = expandDaCapoAndSegno(pitches, markers)
val lastTick = 0 val lastTick = 0
nextTick.clear() nextTick.clear()
// addMetaMessage(0x59, 4, 2, 2,0) // addMetaMessage(0x59, 4, 2, 2,0)
@ -96,7 +197,7 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
val modulaRegex = Regex("""D(ô|o)?\s*dia\s*(C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B)""", 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)
val tempoRegex = Regex("""^(?:♩|P)\s*[:=]\s*(\d+)""", RegexOption.IGNORE_CASE) val tempoRegex = Regex("""^(?:♩|P)\s*[:=]\s*(\d+)""", RegexOption.IGNORE_CASE)
markers.forEach { midiMarker -> effectiveMarkers.forEach { midiMarker ->
val gridIdx = (midiMarker.gridIndex ?: 0) * 480L val gridIdx = (midiMarker.gridIndex ?: 0) * 480L
midiMarker.marker.forEach { text -> midiMarker.marker.forEach { text ->
val cleanText = text.trim() val cleanText = text.trim()
@ -168,7 +269,7 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
val stepDelta = 20 val stepDelta = 20
val processedHairpins = mutableSetOf<Long>() val processedHairpins = mutableSetOf<Long>()
val pitchesByTick = pitches.groupBy { (it.tick) * 8L }.toSortedMap() val pitchesByTick = effectivePitches.groupBy { (it.tick) * 8L }.toSortedMap()
pitchesByTick.forEach { (rawTick, eventsAtTick) -> pitchesByTick.forEach { (rawTick, eventsAtTick) ->
/* Fermata CC*/ /* Fermata CC*/
@ -202,8 +303,7 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
baseVelocity = nuanceMap[rawTick] ?: Dynamic.MF.velocity baseVelocity = nuanceMap[rawTick] ?: Dynamic.MF.velocity
} }
val adjustedTick = rawTick/* + timeOffset*/ val adjustedTick = rawTick + timeOffset
// hairpin // hairpin
@ -243,40 +343,35 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
} }
} }
// Tempo Meta 0x51
val pendingTempoTicks = tempoChangeMap.keys.filter { it <= rawTick }.sorted() val pendingTempoTicks = tempoChangeMap.keys.filter { it <= rawTick }.sorted()
pendingTempoTicks.forEach { tempoRawTick -> pendingTempoTicks.forEach { tempoRawTick ->
val newBpm = tempoChangeMap[tempoRawTick] ?: initialBpm val newBpm = tempoChangeMap[tempoRawTick] ?: initialBpm
val tempoAdjustedTick = tempoRawTick/* + timeOffset*/ val tempoAdjustedTick = tempoRawTick + timeOffset
setTempoMeta(newBpm, tempoAdjustedTick.toInt()) setTempoMeta(newBpm, tempoAdjustedTick.toInt())
tempoChangeMap.remove(tempoRawTick) tempoChangeMap.remove(tempoRawTick)
} }
// Modulation Meta 0x59
val pendingKeyTicks = keyChangeMap.keys.filter { it <= rawTick }.sorted() val pendingKeyTicks = keyChangeMap.keys.filter { it <= rawTick }.sorted()
pendingKeyTicks.forEach { keyRawTick -> pendingKeyTicks.forEach { keyRawTick ->
val targetKey = keyChangeMap[keyRawTick] ?: initialKey val targetKey = keyChangeMap[keyRawTick] ?: initialKey
currentKey = targetKey currentKey = targetKey
val keyAdjustedTick = keyRawTick/* + timeOffset*/ val keyAdjustedTick = keyRawTick + timeOffset
val keyIdx = Transpose.keyToNumber.indexOf(targetKey) val keyIdx = Transpose.keyToNumber.indexOf(targetKey)
if (keyIdx != -1) { if (keyIdx != -1) {
val sf = Transpose.keyToArmature[keyIdx] val sf = Transpose.keyToArmature[keyIdx]
val keyBytes = byteArrayOf(sf.toByte(), 0.toByte()) // 0 = Majeur val keyBytes = byteArrayOf(sf.toByte(), 0.toByte())
addMetaMessage(0x59, keyAdjustedTick.toInt(), 2, keyBytes) addMetaMessage(0x59, keyAdjustedTick.toInt(), 2, keyBytes)
} }
keyChangeMap.remove(keyRawTick) keyChangeMap.remove(keyRawTick)
} }
/* Tetxe; marker */
val pendingTextTicks = textMetaMap.keys.filter { it <= rawTick }.sorted() val pendingTextTicks = textMetaMap.keys.filter { it <= rawTick }.sorted()
pendingTextTicks.forEach { textRawTick -> pendingTextTicks.forEach { textRawTick ->
val textAdjustedTick = textRawTick/* + timeOffset*/ val textAdjustedTick = textRawTick + timeOffset
textMetaMap[textRawTick]?.forEach { annotationText -> textMetaMap[textRawTick]?.forEach { annotationText ->
addTextMeta(text = annotationText, tickPosition = textAdjustedTick, metaType = 0x06) addTextMeta(text = annotationText, tickPosition = textAdjustedTick, metaType = 0x06)
@ -285,7 +380,6 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
} }
eventsAtTick.forEach { noteEvent -> eventsAtTick.forEach { noteEvent ->
// Note + transpose
val currentKeyIdx = Transpose.keyToNumber.indexOf(currentKey).coerceAtLeast(0) val currentKeyIdx = Transpose.keyToNumber.indexOf(currentKey).coerceAtLeast(0)
val transpositionSemitones = currentKeyIdx - baseKeyIdx val transpositionSemitones = currentKeyIdx - baseKeyIdx
@ -303,7 +397,7 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
} }
tempoChangeMap.forEach { (tempoRawTick, bpm) -> tempoChangeMap.forEach { (tempoRawTick, bpm) ->
val tempoAdjustedTick = tempoRawTick/* + timeOffset*/ val tempoAdjustedTick = tempoRawTick + timeOffset
setTempoMeta(bpm, tempoAdjustedTick.toInt()) setTempoMeta(bpm, tempoAdjustedTick.toInt())
} }
tempoChangeMap.clear() tempoChangeMap.clear()
@ -322,9 +416,6 @@ class MidiWriterforEXport constructor(private val fileRepository: FileRepositor
} }
return -1 return -1
} }
/**
* (Type 0x01 = Text, 0x06 = Marker, 0x05 = Lyric).
*/
fun addTextMeta(text: String, tickPosition: Long, metaType: Int = 0x01) { fun addTextMeta(text: String, tickPosition: Long, metaType: Int = 0x01) {
val bytes = text.toByteArray(Charsets.UTF_8) val bytes = text.toByteArray(Charsets.UTF_8)
addMetaMessage( addMetaMessage(

View file

@ -11,6 +11,7 @@ import mg.dot.feufaro.midi.MidiPitch
import mg.dot.feufaro.midi.MidiWriterKotlin import mg.dot.feufaro.midi.MidiWriterKotlin
import mg.dot.feufaro.midi.MidiWriterforEXport import mg.dot.feufaro.midi.MidiWriterforEXport
import mg.dot.feufaro.provideSettings import mg.dot.feufaro.provideSettings
import mg.dot.feufaro.viewmodel.MidiMarkers
import mg.dot.feufaro.viewmodel.PartitionMetadata import mg.dot.feufaro.viewmodel.PartitionMetadata
import java.io.File import java.io.File
import kotlin.math.min import kotlin.math.min
@ -261,8 +262,8 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
unparsedNote.forEach { unparsedNote.forEach {
preloadN(it) preloadN(it)
} }
val localAccumulator = mutableListOf<TimeUnitObject>()
try { try {
val localAccumulator = mutableListOf<TimeUnitObject>()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
sharedScreenModel.updateFullTUOList(emptyList()) sharedScreenModel.updateFullTUOList(emptyList())
} }
@ -270,6 +271,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} catch (e: Exception) { } catch (e: Exception) {
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}") println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
} }
attachLyricsToPitches(localAccumulator)
val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber })) val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
val midiWriter = MidiWriterKotlin(fileRepository) val midiWriter = MidiWriterKotlin(fileRepository)
midiWriter.process(pitches) midiWriter.process(pitches)
@ -289,8 +291,79 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
fun generateMidiFile(bpm: Float, outputPath: String = "whawyd3.mid", transpI: Int = 0) { fun generateMidiFile(bpm: Float, outputPath: String = "whawyd3.mid", transpI: Int = 0) {
val parseScope = CoroutineScope(Dispatchers.Default) val parseScope = CoroutineScope(Dispatchers.Default)
parseScope.launch { parseScope.launch {
val pitchesSorted = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber })) val nbStanzas = sharedScreenModel.nbStanzas.value.takeIf { it > 0 } ?: 1
val currentTuoList = sharedScreenModel.tuoList.value
val basePitches = pitches.filter { it.metaType != 0x05 }.map { it.copy() }
val baseMarkers = sharedScreenModel.getFullMarkers()
val loopedPitches = mutableListOf<MidiPitch>()
val loopedMarkers = mutableListOf<MidiMarkers>()
val midiWriter = MidiWriterforEXport(fileRepository) val midiWriter = MidiWriterforEXport(fileRepository)
val totalGridsInStanza = currentTuoList.size
var currentTickOffset = 0L
var currentGridOffset = 0
for (stanza in 1..nbStanzas) {
attachLyricsToPitches(tuoList = currentTuoList, stanzaNumber = stanza)
val stanzaLyrics = pitches.filter { it.metaType == 0x05 }.map { it.copy() }
val stanzaPitches = basePitches.map { it.copy() } + stanzaLyrics
val (expandedPitches, expandedMarkers) = midiWriter.expandDaCapoAndSegno(stanzaPitches, baseMarkers)
expandedPitches.forEach { pitch ->
loopedPitches.add(pitch.copy().apply {
tick += currentTickOffset.toInt()
metaBytes = metaBytes.copyOf()
})
}
expandedMarkers.forEach { marker ->
loopedMarkers.add(
marker.copy(
timestamp = marker.timestamp + currentTickOffset,
gridIndex = (marker.gridIndex ?: 0) + currentGridOffset
)
)
}
val lastNoteGrid = expandedPitches
.filter { it.metaType <= 0 && it.pitch.isNotEmpty() }
.maxOfOrNull { it.tick / 60 }
val maxGridFromMarkers = expandedMarkers.maxOfOrNull { it.gridIndex ?: 0 }
val maxGridFromPitches = expandedPitches.maxOfOrNull { it.tick / 60 }
val maxStanzaGrid = maxOf(maxGridFromMarkers ?: 0, maxGridFromPitches ?: 0, totalGridsInStanza - 1)
val stanzaGridCount = maxStanzaGrid + 1
val totalGridWithGap = if (stanza < nbStanzas && lastNoteGrid != null) {
val measureInt = sharedScreenModel.measureInt.value.takeIf { it > 0 } ?: 4
val gapNoires = when (measureInt) {
2, 4, 8, 10 -> 4
3, 6, 9, 12 -> 3
else -> 4
}
lastNoteGrid + gapNoires
} else {
stanzaGridCount
}
val stanzaTickDuration = totalGridWithGap * 60L
currentGridOffset += totalGridWithGap
currentTickOffset += stanzaTickDuration
}
pitches.clear()
pitches.addAll(basePitches)
attachLyricsToPitches(
tuoList = currentTuoList,
stanzaNumber = sharedScreenModel.stanza.value.takeIf { it > 0 } ?: 1
)
val pitchesSorted = loopedPitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
val instruments = mapOf( val instruments = mapOf(
1 to prefs.getInt("voice_instrument", 0), 1 to prefs.getInt("voice_instrument", 0),
@ -301,9 +374,10 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val originalKey = sharedScreenModel.songKey.value val originalKey = sharedScreenModel.songKey.value
val transposedKey = Transpose.transposeKey(baseKey = originalKey, interval = transpI) val transposedKey = Transpose.transposeKey(baseKey = originalKey, interval = transpI)
val initialSongBpm = meta["p"]?.toFloatOrNull() ?: bpm val initialSongBpm = meta["p"]?.toFloatOrNull() ?: bpm
midiWriter.process( midiWriter.process(
pitchesSorted, pitchesSorted,
sharedScreenModel.getFullMarkers(), loopedMarkers,
initialBpm = initialSongBpm, initialBpm = initialSongBpm,
instruments, instruments,
initialKey = transposedKey initialKey = transposedKey
@ -311,6 +385,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
midiWriter.save(outputPath) midiWriter.save(outputPath)
} }
} }
fun justBuild(sourceFile: String, sourceContent: String) { fun justBuild(sourceFile: String, sourceContent: String) {
currentFile = sourceFile currentFile = sourceFile
val parseScope = CoroutineScope(Dispatchers.Default) val parseScope = CoroutineScope(Dispatchers.Default)
@ -349,8 +424,8 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
unparsedNote.forEach { unparsedNote.forEach {
preloadN(it) preloadN(it)
} }
val localAccumulator = mutableListOf<TimeUnitObject>()
try { try {
val localAccumulator = mutableListOf<TimeUnitObject>()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
sharedScreenModel.updateFullTUOList(emptyList()) sharedScreenModel.updateFullTUOList(emptyList())
} }
@ -358,6 +433,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} catch (e: Exception) { } catch (e: Exception) {
println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}") println("Erreur parseScope Solfa:150 : ${e.message} iter: ${TimeUnitObject.nbBlock}")
} }
attachLyricsToPitches(localAccumulator)
val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber })) val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
val midiWriter = MidiWriterKotlin(fileRepository) val midiWriter = MidiWriterKotlin(fileRepository)
midiWriter.process(pitches) midiWriter.process(pitches)
@ -2112,6 +2188,48 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
sharedScreenModel.setBpmFlow(bpm) sharedScreenModel.setBpmFlow(bpm)
} }
private fun attachLyricsToPitches(
tuoList: List<TimeUnitObject> = sharedScreenModel.tuoList.value,
stanzaNumber: Int = sharedScreenModel.stanza.value.takeIf { it > 0 } ?: 1,
stepTicks: Int = 60,
) {
pitches.removeAll { it.metaType == 0x05 }
val cleanRegex = Regex("(\\d+\\.)+")
val validTuos = if (tuoList.size > 1 && tuoList.first().pTemplate.template.isEmpty()) {
tuoList.drop(1)
} else {
tuoList
}
var lyricsAttachedCount = 0
validTuos.forEachIndexed { i, tuo ->
val calculatedTick = i * stepTicks
val rawLines = tuo.getSingleSyllable(stanzaNumber)
val cleanText = rawLines.firstOrNull()
?.replace(cleanRegex, "")
?.trim() ?: ""
// println("Voici ${tuo.lyricsAsMultiString(stanzaNumber)}")
if (cleanText.isNotBlank()) {
val bytes = cleanText.toByteArray(Charsets.UTF_8)
val lyricMeta = MidiPitch(
voiceNumber = 1,
tick = calculatedTick,
metaType = 0x05,
metaBytes = bytes,
metaByteSize = bytes.size
)
pitches.add(lyricMeta)
lyricsAttachedCount++
// println(String.format("TUO #%-3d | Tick: %-5d | Syllable: [%s] -> ATTACHÉ",
// i, calculatedTick, cleanText))
}
}
}
private fun loadN(voiceNumber: Int, line: String, templateComments: MutableList<String>) { private fun loadN(voiceNumber: Int, line: String, templateComments: MutableList<String>) {
var midiDuration = 0 var midiDuration = 0
var nextDuration = 0 var nextDuration = 0

View file

@ -532,7 +532,20 @@ actual class FMediaPlayer actual constructor(
navigationJob = playerScope.launch(Dispatchers.Default) { navigationJob = playerScope.launch(Dispatchers.Default) {
sharedScreenModel.activeIndex.collect { currentIndex -> sharedScreenModel.activeIndex.collect { currentIndex ->
// 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 ${sequencer!!.tickPosition}")
if (currentIndex < 0) return@collect
val tick = currentIndex * 60L
val tuoList = sharedScreenModel.tuoList.value
val validTuos = if (tuoList.size > 1 && tuoList.first().pTemplate.template.isEmpty()) {
tuoList.drop(1)
} else {
tuoList
}
val currentTuo = validTuos.getOrNull(currentIndex)
val currentStanza = sharedScreenModel.stanza.value.coerceAtLeast(1)
val lyrics = currentTuo?.getSingleSyllable(currentStanza)?.joinToString(" ") ?: ""
println("Grille #%-3d | Tick: %-5d | Parole: [%s]".format(currentIndex, tick, lyrics))
if (sequencer?.isRunning == true) { if (sequencer?.isRunning == true) {
if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1) { if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1) {