From 3bc0d7254834ada34d7ecc0cf26a4982f7d3f89f Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Thu, 15 Oct 2026 15:07:20 +0300 Subject: [PATCH] =?UTF-8?q?Fix=20(player):=20resolve=20=F0=9D=84=90=20hold?= =?UTF-8?q?=20on=20playing=20midi=20-=20Android?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 120 +++++++++++++++--- 1 file changed, 103 insertions(+), 17 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 5a9138d..3e5468f 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -183,6 +183,9 @@ actual class FMediaPlayer actual constructor( @Volatile private var userCustomTempo: Boolean = false + @Volatile private var pendingHoldDurationMs: Long = 0L + @Volatile private var pendingHoldGrid: Int = -1 + init { midiDriver.start() loadVoiceVolumes() @@ -208,6 +211,27 @@ actual class FMediaPlayer actual constructor( } } + private fun getVoiceLabel(channel: Int): String { + return when (channel) { + 0 -> "S" + 1 -> "A" + 2 -> "T" + 3 -> "B" + else -> "$channel" + } + } + private fun midiNoteToSolfa(midiNote: Int): String { + val solfaNames = arrayOf("d", "di", "r", "ri", "m", "f", "fi", "s", "si", "l", "ta", "t") + val octave = (midiNote / 12) - 1 + val noteIndex = midiNote % 12 + val baseName = solfaNames[noteIndex] + + return when { + octave == 4 -> baseName + octave > 4 -> baseName.uppercase() + else -> "${baseName},${4 - octave}" + } + } private fun send(vararg bytes: Int) { midiDriver.write(ByteArray(bytes.size) { bytes[it].toByte() }) } @@ -314,9 +338,7 @@ actual class FMediaPlayer actual constructor( } private fun startPlaybackLoop() { - val seq = sequence ?: run { - return - } + val seq = sequence ?: return isRunning = true needsClockSync = true @@ -348,6 +370,10 @@ actual class FMediaPlayer actual constructor( } if (idx >= events.size) { + if (pendingHoldGrid >= 0) { + executeFermataHold(pendingHoldDurationMs) + pendingHoldGrid = -1 + } isRunning = false allNotesOff() resetTempoToNormal() @@ -356,6 +382,31 @@ actual class FMediaPlayer actual constructor( } val ev = events[idx] + val currentGrid = (ev.tickAbsolute / resolution).toInt() + + if (pendingHoldGrid >= 0 && currentGrid > pendingHoldGrid) { + val duration = pendingHoldDurationMs + val holdGrid = pendingHoldGrid + pendingHoldGrid = -1 + pendingHoldDurationMs = 0L + + executeFermataHold(duration) + + val holdMaxTick = (holdGrid + 1).toLong() * resolution + idx = events.indexOfFirst { it.tickAbsolute >= holdMaxTick } + .takeIf { it >= 0 } ?: events.size + + if (idx < events.size) { + currentTickPos = events[idx].tickAbsolute + } + + clockNano = System.nanoTime() + clockTick = currentTickPos + lastEventNano = System.nanoTime() + needsClockSync = false + continue + } + val targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong() val now = System.nanoTime() val waitNano = targetNano - now @@ -390,6 +441,12 @@ actual class FMediaPlayer actual constructor( continue } + if (ev.type == 0x90 && ev.data2 > 0) { + val voiceLabel = getVoiceLabel(ev.channel) + val noteSolfa = midiNoteToSolfa(ev.data1 + transpositionSemitones) + //println("[PLAY-EVENT] Tick: $currentTickPos (Grille $currentGrid) | Voix: $voiceLabel | Note: $noteSolfa (${ev.data1}) | Vel: ${ev.data2}") + } + when (ev.type) { 0x80 -> { val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127) @@ -416,15 +473,50 @@ actual class FMediaPlayer actual constructor( } } } - val currentGrid = (currentTickPos / resolution).toLong() - if (boundModel?.activeIndex?.value != currentGrid.toInt()) { - boundModel?.updateActiveIndexByIndex(currentGrid.toInt()) + + if (boundModel?.activeIndex?.value != currentGrid) { + boundModel?.updateActiveIndexByIndex(currentGrid) } //println("C= ${currentGrid.toInt()} et shrd ${boundModel?.activeIndex?.value}") idx++ } } } + private suspend fun executeFermataHold(durationMs: Long) { + //println("⏸️ [FERMATA-START] Début tenue nette à tickPos=$currentTickPos (Durée: ${durationMs}ms)") + isHolding = true + + delay(20) + + for (ch in 0 until 4) { + controlChange(ch, 64, 127) + } + + val steps = 35 + val stepDelay = ((durationMs - 20) / steps).coerceAtLeast(10L) + val startExpression = 127 + val endExpression = 45 + + for (i in 1..steps) { + val progress = i.toFloat() / steps + val smooth = progress * progress * (3f - 2f * progress) + val currentExp = (startExpression - (startExpression - endExpression) * smooth).toInt() + + for (ch in 0 until 4) { + controlChange(ch, 11, currentExp) + } + delay(stepDelay) + } + + for (ch in 0 until 4) { + controlChange(ch, 64, 0) + controlChange(ch, 11, 127) + } + + lastEventNano = System.nanoTime() + isHolding = false + println("▶️ [FERMATA-END] Fin tenue, reprise fluide") + } actual fun seekToGrid(gridIndex: Int) { currentTickPos = gridIndex.toLong() * resolution @@ -709,24 +801,20 @@ actual class FMediaPlayer actual constructor( 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") + //println("⏸️ [HOLD-PREPARE] Grille $currentIndex | Durée réservée: ${holdDuration}ms") + pendingHoldGrid = currentIndex + pendingHoldDurationMs = holdDuration } // 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") +// println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}") +// println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms") applyTempoChange( startBpm = startBpm, @@ -738,7 +826,6 @@ actual class FMediaPlayer actual constructor( // ── 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, @@ -747,7 +834,6 @@ actual class FMediaPlayer actual constructor( // ── Dynamique ───────────────────────── step.dynamic != null -> { - step.alreadyDone = true applyDynamic(step.dynamic) }