From b605fc87fc0d36b4386ad57281dd8101e87831b4 Mon Sep 17 00:00:00 2001 From: "hasinarak3@gmail.com" Date: Tue, 17 Mar 2026 16:08:11 +0300 Subject: [PATCH] Implement Rit. & Rall on playing Midi --- .../feufaro/viewmodel/SharedScreenModel.kt | 20 ++++ .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 106 ++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index 391771e..83acf35 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -213,6 +213,8 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode val isDS = markerText.contains(Regex("""D\.?S\.?""")) val isFarany = markerText.trim().equals("Farany", ignoreCase = true) + val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE) + var resultMarker: MidiMarkers = marker if(isFarany) { var forwardIndex = index @@ -287,6 +289,24 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode forwardIndex++ } } + } else if(isRit.containsMatchIn(markerText)) { + var forwardIndex = index + 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 + } + forwardIndex++ + } } else { marker } diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 1537ae9..ad58e74 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -46,6 +46,9 @@ actual class FMediaPlayer actual constructor( private var abJob: Job? = null private var navigationJob: Job? = null + + private var isInTempoChange: Boolean = false + private var tempoChangeJob: Job? = null private data class NavigationStep( val marker: String, val gridIndex: Int, // L'ancre absolue @@ -61,6 +64,12 @@ actual class FMediaPlayer actual constructor( val hairPinToFactor: Float = 1.0f, val isFin: Boolean = false, // Farany D.C. var finActive: Boolean =false, + // rit & rall + val isTempoChange: Boolean = false, + val tempoType: String = "", + val endGridForTempo: Int = -1, + val targetTempoMultiplier: Float = 0.6f + ) private val navigationSteps = mutableListOf() @@ -195,11 +204,54 @@ actual class FMediaPlayer actual constructor( println("applyCrescendo terminé → factor=$currentDynamicFactor") } } + 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 + val currentBpm = startBpm + (endBpm - startBpm) * smooth + + sequencer?.tempoInBPM = currentBpm + + delay(stepMs) + if (!isActive) break + } + + sequencer?.tempoInBPM = endBpm + isInTempoChange = false + } + } + + private fun resetTempoToNormal() { + if (sequencer?.tempoInBPM != targetBpm) { +// println("Retour au tempo normal: ${sequencer?.tempoInBPM} → $targetBpm BPM") + applyTempoChange( + startBpm = sequencer?.tempoInBPM ?: targetBpm, + endBpm = targetBpm, + durationMs = 800L, + tempoType = "Retour tempo" + ) + } + } private fun prepareNavigation(sharedScreenModel: SharedScreenModel) { val metadataList = sharedScreenModel.getFullMarkers() navigationSteps.clear() 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 { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) -> val currentIndex = gridIndex ?: 0 @@ -230,6 +282,37 @@ actual class FMediaPlayer actual constructor( ) 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 @@ -415,6 +498,9 @@ actual class FMediaPlayer actual constructor( } if (step != null) { + if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) { + forceTempo(targetBpm.toDouble()) + } when { step.isFin -> { if(step.finActive) { @@ -463,6 +549,25 @@ actual class FMediaPlayer actual constructor( ) } + //Rit | Rall + step.isTempoChange -> { + step.alreadyDone = true + val currentBpm = sequencer?.tempoInBPM ?: targetBpm + 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(" ${currentBpm} BPM → $endBpm BPM sur ${durationMs}ms") + + applyTempoChange( + startBpm = currentBpm, + endBpm = endBpm, + durationMs = durationMs, + tempoType = step.tempoType + ) + } //velocity step.dynamic != null -> { applyDynamic(step.dynamic) @@ -601,6 +706,7 @@ actual class FMediaPlayer actual constructor( navigationSteps.clear() clearLoop() currentDynamicFactor = Dynamic.MF.factor + resetTempoToNormal() // release() } actual fun getDuration(): Long {