From b3d7429b593f88930daa2f869d344576cf1b6855 Mon Sep 17 00:00:00 2001 From: "hasinarak3@gmail.com" Date: Tue, 7 Apr 2026 15:38:44 +0300 Subject: [PATCH] Implement Rit. & Rall. on playing Midi - Android --- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 159 ++++++++++++++++-- 1 file changed, 146 insertions(+), 13 deletions(-) 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 5d1c6a6..1293ff6 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -111,7 +111,7 @@ actual class FMediaPlayer actual constructor( private val midiDriver: MidiDriver = MidiDriver.getInstance() private var sequence: MidiSequence? = null private var resolution: Int = 480 - private var usPerTick: Double = 500_000.0 / 480.0 + @Volatile private var usPerTick: Double = (60_000_000.0 / 120.0) / 480.0 @Volatile private var isRunning = false @Volatile private var isHolding = false @@ -119,6 +119,10 @@ actual class FMediaPlayer actual constructor( private var lastEventNano: Long = System.nanoTime() private var targetBpm: Float = 120f + @Volatile private var currentPlaybackBpm: Float = 120f + @Volatile private var isInTempoChange: Boolean = false + @Volatile private var needsClockSync = false + private var tempoChangeJob: Job? = null private val voiceVolumes = FloatArray(4) { 127f } private var currentGlobalVolume: Float = 0.8f private var currentDynamicFactor: Float = Dynamic.MF.factor @@ -148,6 +152,11 @@ actual class FMediaPlayer actual constructor( val hairPinToFactor: Float = 1.0f, val isFarany: Boolean = false, var faranyActive: Boolean = false, + // rit & rall + val isTempoChange: Boolean = false, + val tempoType: String = "", + val endGridForTempo: Int = -1, + val targetTempoMultiplier: Float = 0.6f ) private val navigationSteps = mutableListOf() @@ -206,6 +215,49 @@ actual class FMediaPlayer actual constructor( currentDynamicFactor = toFactor; applyVoiceStates() } } + + private fun applyTempoChange( + startBpm: Float, + endBpm: Float, + durationMs: Long, + tempoType: String + ) { + tempoChangeJob?.cancel() + isInTempoChange = true + tempoChangeJob = playerScope.launch { + val steps = 50 + val stepMs = (durationMs / steps).coerceAtLeast(20L) + val startTime = System.currentTimeMillis() + + for (i in 1..steps) { + val elapsed = System.currentTimeMillis() - startTime + val progress = (elapsed.toFloat() / durationMs).coerceIn(0f, 1f) + val smooth = progress * progress * (3f - 2f * progress) // ease-in-out + currentPlaybackBpm = startBpm + (endBpm - startBpm) * smooth + usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution + needsClockSync = true + delay(stepMs) + if (!isActive) break + } + + currentPlaybackBpm = endBpm + usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution + isInTempoChange = false + println("$tempoType terminé → $endBpm BPM") + } + } + + private fun resetTempoToNormal() { + if (kotlin.math.abs(currentPlaybackBpm - targetBpm) > 0.5f) { + applyTempoChange( + startBpm = currentPlaybackBpm, + endBpm = targetBpm, + durationMs = 800L, + tempoType = "Retour tempo" + ) + } + } + private fun extractDynamic(marker: String) = Dynamic.entries.firstOrNull { it.label == marker.trim().removeSuffix("=").trim() } @@ -229,8 +281,14 @@ actual class FMediaPlayer actual constructor( var clockTick = currentTickPos while (isActive && isRunning) { + if (needsClockSync) { + clockNano = System.nanoTime() + clockTick = currentTickPos + needsClockSync = false + } if (isHolding) { - delay(10) + yield() +// delay(10) clockNano = System.nanoTime() clockTick = currentTickPos continue @@ -252,10 +310,24 @@ actual class FMediaPlayer actual constructor( } val ev = events[idx] - val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000) - .toLong()) - System.nanoTime() - if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L)) +// val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000) +// .toLong()) - System.nanoTime() + val targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong() + val now = System.nanoTime() + val waitNano = targetNano - now + +// if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L)) + if (waitNano > 0) { + // Thread.sleep est beaucoup plus précis que delay() pour les micro-délais MIDI + val ms = waitNano / 1_000_000 + val ns = (waitNano % 1_000_000).toInt() + try { + Thread.sleep(ms, ns) + } catch (e: Exception) { + yield() + } + } currentTickPos = ev.tickAbsolute lastEventNano = System.nanoTime() @@ -278,6 +350,10 @@ actual class FMediaPlayer actual constructor( 0xC0 -> send(0xC0 or ev.channel, ev.data1) 0xFF -> { /* tempo ignoré */ } } + val currentGrid = (ev.tickAbsolute / resolution).toLong() + if (boundModel?.activeIndex?.value != currentGrid.toInt()) { + boundModel?.updateActiveIndex(currentGrid) + } idx++ } } @@ -311,12 +387,17 @@ actual class FMediaPlayer actual constructor( private fun prepareNavigation(sharedScreenModel: SharedScreenModel) { val metadataList = sharedScreenModel.getFullMarkers() navigationSteps.clear() - var lastSegno=0; var lastFactor=Dynamic.MF.factor + var lastSegno=0; + var lastFactor=Dynamic.MF.factor + val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) + val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE) - metadataList.forEach { (_, gridIndex, _, _, marker, _, _, note) -> + metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> val ci = gridIndex ?: 0 - val dsR = Regex("""D\.?S\.?"""); val dcR = Regex("""D\.?C\.?""") - val dsG = Regex("""D\.?S\.?_GROUP_PART"""); val dcG = Regex("""D\.?C\.?_GROUP_PART""") + val dsR = Regex("""D\.?S\.?"""); + val dcR = Regex("""D\.?C\.?""") + val dsG = Regex("""D\.?S\.?_GROUP_PART"""); + val dcG = Regex("""D\.?C\.?_GROUP_PART""") val last_grid = sharedScreenModel.getTotalGridCount() val hairPins = sharedScreenModel.getHairPins() @@ -327,7 +408,32 @@ actual class FMediaPlayer actual constructor( 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 @@ -406,16 +512,22 @@ actual class FMediaPlayer actual constructor( } private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) { + var lastProcessedIndex = -1 navigationJob?.cancel() navigationJob = playerScope.launch(Dispatchers.Default) { sharedScreenModel.activeIndex.collect { currentIndex -> + if (currentIndex <= lastProcessedIndex) { + return@collect + } + lastProcessedIndex = currentIndex if (!isRunning || currentIndex < 0) return@collect val step = navigationSteps.find { it.gridIndex == currentIndex && !it.alreadyDone - } ?: return@collect - - when { + } + println("pas: ${currentIndex}") + if (step != null) { + when { // ── Farany ──────────────────────────── step.isFarany -> { if (step.faranyActive) { @@ -443,6 +555,25 @@ actual class FMediaPlayer actual constructor( for (ch in 0 until 4) controlChange(ch, 64, 0) step.alreadyDone = true } + // Rit. / Rall. , ... + step.isTempoChange -> { + step.alreadyDone = true + val startBpm = currentPlaybackBpm + val endBpm = targetBpm * step.targetTempoMultiplier + val gridDistance = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1) + val beatDurationMs = (60_000L / targetBpm).toLong() + val durationMs = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L) + + println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}") + println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms") + + applyTempoChange( + startBpm = startBpm, + endBpm = endBpm, + durationMs = durationMs, + tempoType = step.tempoType + ) + } // ── Soufflet ────────────────────────── step.hairPin != null -> { @@ -486,6 +617,7 @@ actual class FMediaPlayer actual constructor( println("DC/DS → grille ${step.targetGrid}") } } + } } } } @@ -522,6 +654,7 @@ actual class FMediaPlayer actual constructor( allNotesOff(); currentTickPos = 0L resetNavigationFlags(); navigationSteps.clear(); clearLoop() currentDynamicFactor = Dynamic.MF.factor + resetTempoToNormal() } actual fun release() { stop(); midiDriver.stop(); playerScope.cancel()