diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 4f65401..52b187f 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -4,6 +4,7 @@ import SharedScreenModel import com.russhwolf.settings.Settings import kotlinx.coroutines.* import mg.dot.feufaro.provideSettings +import mg.dot.feufaro.solfa.Transpose import org.billthefarmer.mididriver.MidiDriver import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -132,6 +133,7 @@ actual class FMediaPlayer actual constructor( private var currentTickPos: Long = 0L private var lastEventNano: Long = System.nanoTime() private var targetBpm: Float = 120f + private var initialBpm: Float = 120f @Volatile private var currentPlaybackBpm: Float = 120f @Volatile private var isInTempoChange: Boolean = false @@ -140,6 +142,7 @@ actual class FMediaPlayer actual constructor( private val voiceVolumes = FloatArray(4) { 127f } private var currentGlobalVolume: Float = 0.8f private var currentDynamicFactor: Float = Dynamic.MF.factor + private var transpositionSemitones: Int = 0 private var pointA: Long = -1L private var pointB: Long = -1L @@ -171,7 +174,9 @@ actual class FMediaPlayer actual constructor( val isTempoChange: Boolean = false, val tempoType: String = "", val endGridForTempo: Int = -1, - val targetTempoMultiplier: Float = 0.6f + val targetTempoMultiplier: Float = 0.6f, + val newBpm: Float? = null, + val newKey: String? = null ) private val navigationSteps = mutableListOf() private val prefs: Settings = provideSettings() @@ -283,6 +288,12 @@ actual class FMediaPlayer actual constructor( ) } } + private fun resetTempoToInitial() { + tempoChangeJob?.cancel() + isInTempoChange = false + targetBpm = initialBpm + boundModel?.setBpmFlow(initialBpm) + } private fun extractDynamic(marker: String) = Dynamic.entries.firstOrNull { it.label == marker.trim().removeSuffix("=").trim() @@ -345,6 +356,7 @@ actual class FMediaPlayer actual constructor( } else { isRunning = false allNotesOff() + resetTempoToNormal() onFinished() break } @@ -386,9 +398,15 @@ actual class FMediaPlayer actual constructor( } when (ev.type) { - 0x80 -> send(0x80 or ev.channel, ev.data1, ev.data2) - 0x90 -> send(0x90 or ev.channel, ev.data1, - (ev.data2 * currentDynamicFactor).toInt().coerceIn(0, 127)) + 0x80 -> { + val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127) + send(0x80 or ev.channel, transposedNote, ev.data2) + } + 0x90 -> { + val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127) + send(0x90 or ev.channel, transposedNote, + (ev.data2 * currentDynamicFactor).toInt().coerceIn(0, 127)) + } 0xB0 -> if (ev.data1 != 7 && ev.data1 != 11) send(0xB0 or ev.channel, ev.data1, ev.data2) 0xC0 -> send(0xC0 or ev.channel, ev.data1) @@ -419,10 +437,21 @@ actual class FMediaPlayer actual constructor( currentTickPos = gridIndex.toLong() * resolution lastEventNano = System.nanoTime() + val initialOrPreviousBpm = navigationSteps + .filter { it.newBpm != null && it.gridIndex <= gridIndex } + .maxByOrNull { it.gridIndex }?.newBpm ?: initialBpm + + targetBpm = initialOrPreviousBpm + sharedScreenModel.setBpmFlow(initialOrPreviousBpm) 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 + navigationSteps.forEach { step -> + if (step.gridIndex > gridIndex) { + step.alreadyDone = false + } + } applyVoiceStates() needsClockSync = true @@ -456,6 +485,8 @@ actual class FMediaPlayer actual constructor( val dcG = Regex("""D\.?C\.?_GROUP_PART""")*/ val finReg = Regex("""(fine|fin|farany|end)""", 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("""^♩\s*=\s*(\d+)""", RegexOption.IGNORE_CASE) metadataList.forEach { midiMarker -> val ci = midiMarker.gridIndex ?: 0 val last_grid = sharedScreenModel.getTotalGridCount() @@ -475,6 +506,37 @@ actual class FMediaPlayer actual constructor( val isDsFin = hasDs && hasFin val isDcFin = hasDc && hasFin + + // Changement de Tempo ♩= + val tempoMatch = tempoRegex.find(mTrim) + if (tempoMatch != null) { + val bpmVal = tempoMatch.groupValues[1].toFloatOrNull() + if (bpmVal != null) { + navigationSteps.add( + NavigationStep( + marker = mTrim, + gridIndex = ci, + newBpm = bpmVal + ) + ) + println("Changement de BPM mémorisé à la grille $ci -> $bpmVal BPM") + } + } + + // Modulation Do dia .. + val modMatch = modulaRegex.find(mTrim) + if (modMatch != null) { + val targetKey = modMatch.groupValues[2] + navigationSteps.add( + NavigationStep( + marker = mTrim, + gridIndex = ci, + newKey = targetKey + ) + ) + println("Modulation mémorisée à la grille $ci -> $targetKey") + } + when { isDsFin || isDcFin -> { val target = if (lastSegno > 0) lastSegno else 0 @@ -625,6 +687,22 @@ actual class FMediaPlayer actual constructor( } for(step in currentSteps) { when { + //♩ + step.newBpm != null -> { + step.alreadyDone = true + println("Exécution changement de tempo : ${step.newBpm} BPM à la grille $currentIndex") + targetBpm = step.newBpm + sharedScreenModel.setBpmFlow(step.newBpm) + } + + // Modulation + step.newKey != null -> { + step.alreadyDone = true + val baseKeyIdx = Transpose.keyToNumber.indexOf("C").coerceAtLeast(0) + val targetKeyIdx = Transpose.keyToNumber.indexOf(step.newKey).coerceAtLeast(0) + transpositionSemitones = targetKeyIdx - baseKeyIdx + } + // ── Point d'orgue ───────────────────── step.isHold -> { val beatMs = (60_000 / targetBpm).toLong() @@ -839,6 +917,7 @@ actual class FMediaPlayer actual constructor( resetNavigationFlags(); navigationSteps.clear(); clearLoop() currentDynamicFactor = Dynamic.MF.factor resetTempoToNormal() + resetTempoToInitial() } actual fun release() { stop(); midiDriver.stop(); playerScope.cancel()