Fix (player): resolve 𝄐 hold on playing midi - Android

This commit is contained in:
Hasinjato 2026-10-15 15:07:20 +03:00
parent d5d8068c24
commit 3bc0d72548

View file

@ -183,6 +183,9 @@ actual class FMediaPlayer actual constructor(
@Volatile private var userCustomTempo: Boolean = false @Volatile private var userCustomTempo: Boolean = false
@Volatile private var pendingHoldDurationMs: Long = 0L
@Volatile private var pendingHoldGrid: Int = -1
init { init {
midiDriver.start() midiDriver.start()
loadVoiceVolumes() 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) { private fun send(vararg bytes: Int) {
midiDriver.write(ByteArray(bytes.size) { bytes[it].toByte() }) midiDriver.write(ByteArray(bytes.size) { bytes[it].toByte() })
} }
@ -314,9 +338,7 @@ actual class FMediaPlayer actual constructor(
} }
private fun startPlaybackLoop() { private fun startPlaybackLoop() {
val seq = sequence ?: run { val seq = sequence ?: return
return
}
isRunning = true isRunning = true
needsClockSync = true needsClockSync = true
@ -348,6 +370,10 @@ actual class FMediaPlayer actual constructor(
} }
if (idx >= events.size) { if (idx >= events.size) {
if (pendingHoldGrid >= 0) {
executeFermataHold(pendingHoldDurationMs)
pendingHoldGrid = -1
}
isRunning = false isRunning = false
allNotesOff() allNotesOff()
resetTempoToNormal() resetTempoToNormal()
@ -356,6 +382,31 @@ actual class FMediaPlayer actual constructor(
} }
val ev = events[idx] 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 targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong()
val now = System.nanoTime() val now = System.nanoTime()
val waitNano = targetNano - now val waitNano = targetNano - now
@ -390,6 +441,12 @@ actual class FMediaPlayer actual constructor(
continue 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) { when (ev.type) {
0x80 -> { 0x80 -> {
val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127) 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()) { if (boundModel?.activeIndex?.value != currentGrid) {
boundModel?.updateActiveIndexByIndex(currentGrid.toInt()) boundModel?.updateActiveIndexByIndex(currentGrid)
} }
//println("C= ${currentGrid.toInt()} et shrd ${boundModel?.activeIndex?.value}") //println("C= ${currentGrid.toInt()} et shrd ${boundModel?.activeIndex?.value}")
idx++ 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) { actual fun seekToGrid(gridIndex: Int) {
currentTickPos = gridIndex.toLong() * resolution currentTickPos = gridIndex.toLong() * resolution
@ -709,24 +801,20 @@ actual class FMediaPlayer actual constructor(
val beatMs = (60_000 / targetBpm).toLong() val beatMs = (60_000 / targetBpm).toLong()
val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2 val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2
isHolding = true //println("⏸️ [HOLD-PREPARE] Grille $currentIndex | Durée réservée: ${holdDuration}ms")
for (ch in 0 until 4) controlChange(ch, 64, 127) pendingHoldGrid = currentIndex
delay(holdDuration) pendingHoldDurationMs = holdDuration
for (ch in 0 until 4) controlChange(ch, 64, 0)
isHolding = false
println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms")
} }
// Rit. / Rall. , ... // Rit. / Rall. , ...
step.isTempoChange -> { step.isTempoChange -> {
step.alreadyDone = true
val startBpm = currentPlaybackBpm val startBpm = currentPlaybackBpm
val endBpm = targetBpm * step.targetTempoMultiplier val endBpm = targetBpm * step.targetTempoMultiplier
val gridDistance = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1) val gridDistance = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1)
val beatDurationMs = (60_000L / targetBpm).toLong() val beatDurationMs = (60_000L / targetBpm).toLong()
val durationMs = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L) val durationMs = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L)
println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}") // println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}")
println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms") // println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms")
applyTempoChange( applyTempoChange(
startBpm = startBpm, startBpm = startBpm,
@ -738,7 +826,6 @@ actual class FMediaPlayer actual constructor(
// ── Soufflet ────────────────────────── // ── Soufflet ──────────────────────────
step.hairPin != null -> { step.hairPin != null -> {
step.alreadyDone = true
val dist = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1) val dist = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1)
val beatMs = (60_000L / targetBpm).toLong() val beatMs = (60_000L / targetBpm).toLong()
applyCrescendo(step.hairPinFromFactor, step.hairPinToFactor, applyCrescendo(step.hairPinFromFactor, step.hairPinToFactor,
@ -747,7 +834,6 @@ actual class FMediaPlayer actual constructor(
// ── Dynamique ───────────────────────── // ── Dynamique ─────────────────────────
step.dynamic != null -> { step.dynamic != null -> {
step.alreadyDone = true
applyDynamic(step.dynamic) applyDynamic(step.dynamic)
} }