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 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue