Fix (player): resolve 𝄐 hold on playing midi - Android
This commit is contained in:
parent
d5d8068c24
commit
3bc0d72548
1 changed files with 103 additions and 17 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue