Implement Rit. & Rall. on playing Midi - Android

This commit is contained in:
hasinarak3@gmail.com 2026-04-07 15:38:44 +03:00
parent 0c0cf4d43c
commit b3d7429b59

View file

@ -111,7 +111,7 @@ actual class FMediaPlayer actual constructor(
private val midiDriver: MidiDriver = MidiDriver.getInstance() private val midiDriver: MidiDriver = MidiDriver.getInstance()
private var sequence: MidiSequence? = null private var sequence: MidiSequence? = null
private var resolution: Int = 480 private var resolution: Int = 480
private var usPerTick: Double = 500_000.0 / 480.0 @Volatile private var usPerTick: Double = (60_000_000.0 / 120.0) / 480.0
@Volatile private var isRunning = false @Volatile private var isRunning = false
@Volatile private var isHolding = false @Volatile private var isHolding = false
@ -119,6 +119,10 @@ actual class FMediaPlayer actual constructor(
private var lastEventNano: Long = System.nanoTime() private var lastEventNano: Long = System.nanoTime()
private var targetBpm: Float = 120f private var targetBpm: Float = 120f
@Volatile private var currentPlaybackBpm: Float = 120f
@Volatile private var isInTempoChange: Boolean = false
@Volatile private var needsClockSync = false
private var tempoChangeJob: Job? = null
private val voiceVolumes = FloatArray(4) { 127f } private val voiceVolumes = FloatArray(4) { 127f }
private var currentGlobalVolume: Float = 0.8f private var currentGlobalVolume: Float = 0.8f
private var currentDynamicFactor: Float = Dynamic.MF.factor private var currentDynamicFactor: Float = Dynamic.MF.factor
@ -148,6 +152,11 @@ actual class FMediaPlayer actual constructor(
val hairPinToFactor: Float = 1.0f, val hairPinToFactor: Float = 1.0f,
val isFarany: Boolean = false, val isFarany: Boolean = false,
var faranyActive: Boolean = false, var faranyActive: Boolean = false,
// rit & rall
val isTempoChange: Boolean = false,
val tempoType: String = "",
val endGridForTempo: Int = -1,
val targetTempoMultiplier: Float = 0.6f
) )
private val navigationSteps = mutableListOf<NavigationStep>() private val navigationSteps = mutableListOf<NavigationStep>()
@ -206,6 +215,49 @@ actual class FMediaPlayer actual constructor(
currentDynamicFactor = toFactor; applyVoiceStates() currentDynamicFactor = toFactor; applyVoiceStates()
} }
} }
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
currentPlaybackBpm = startBpm + (endBpm - startBpm) * smooth
usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution
needsClockSync = true
delay(stepMs)
if (!isActive) break
}
currentPlaybackBpm = endBpm
usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution
isInTempoChange = false
println("$tempoType terminé → $endBpm BPM")
}
}
private fun resetTempoToNormal() {
if (kotlin.math.abs(currentPlaybackBpm - targetBpm) > 0.5f) {
applyTempoChange(
startBpm = currentPlaybackBpm,
endBpm = targetBpm,
durationMs = 800L,
tempoType = "Retour tempo"
)
}
}
private fun extractDynamic(marker: String) = Dynamic.entries.firstOrNull { private fun extractDynamic(marker: String) = Dynamic.entries.firstOrNull {
it.label == marker.trim().removeSuffix("=").trim() it.label == marker.trim().removeSuffix("=").trim()
} }
@ -229,8 +281,14 @@ actual class FMediaPlayer actual constructor(
var clockTick = currentTickPos var clockTick = currentTickPos
while (isActive && isRunning) { while (isActive && isRunning) {
if (needsClockSync) {
clockNano = System.nanoTime()
clockTick = currentTickPos
needsClockSync = false
}
if (isHolding) { if (isHolding) {
delay(10) yield()
// delay(10)
clockNano = System.nanoTime() clockNano = System.nanoTime()
clockTick = currentTickPos clockTick = currentTickPos
continue continue
@ -252,10 +310,24 @@ actual class FMediaPlayer actual constructor(
} }
val ev = events[idx] val ev = events[idx]
val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000) // val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000)
.toLong()) - System.nanoTime() // .toLong()) - System.nanoTime()
if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L))
val targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong()
val now = System.nanoTime()
val waitNano = targetNano - now
// if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L))
if (waitNano > 0) {
// Thread.sleep est beaucoup plus précis que delay() pour les micro-délais MIDI
val ms = waitNano / 1_000_000
val ns = (waitNano % 1_000_000).toInt()
try {
Thread.sleep(ms, ns)
} catch (e: Exception) {
yield()
}
}
currentTickPos = ev.tickAbsolute currentTickPos = ev.tickAbsolute
lastEventNano = System.nanoTime() lastEventNano = System.nanoTime()
@ -278,6 +350,10 @@ actual class FMediaPlayer actual constructor(
0xC0 -> send(0xC0 or ev.channel, ev.data1) 0xC0 -> send(0xC0 or ev.channel, ev.data1)
0xFF -> { /* tempo ignoré */ } 0xFF -> { /* tempo ignoré */ }
} }
val currentGrid = (ev.tickAbsolute / resolution).toLong()
if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
boundModel?.updateActiveIndex(currentGrid)
}
idx++ idx++
} }
} }
@ -311,12 +387,17 @@ actual class FMediaPlayer actual constructor(
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) { private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
val metadataList = sharedScreenModel.getFullMarkers() val metadataList = sharedScreenModel.getFullMarkers()
navigationSteps.clear() navigationSteps.clear()
var lastSegno=0; var lastFactor=Dynamic.MF.factor 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 { (_, gridIndex, _, _, marker, _, _, note) -> metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
val ci = gridIndex ?: 0 val ci = gridIndex ?: 0
val dsR = Regex("""D\.?S\.?"""); val dcR = Regex("""D\.?C\.?""") val dsR = Regex("""D\.?S\.?""");
val dsG = Regex("""D\.?S\.?_GROUP_PART"""); val dcG = Regex("""D\.?C\.?_GROUP_PART""") val dcR = Regex("""D\.?C\.?""")
val dsG = Regex("""D\.?S\.?_GROUP_PART""");
val dcG = Regex("""D\.?C\.?_GROUP_PART""")
val last_grid = sharedScreenModel.getTotalGridCount() val last_grid = sharedScreenModel.getTotalGridCount()
val hairPins = sharedScreenModel.getHairPins() val hairPins = sharedScreenModel.getHairPins()
@ -327,7 +408,32 @@ actual class FMediaPlayer actual constructor(
val beat = if(note.contains('•')) 2 else 1 val beat = if(note.contains('•')) 2 else 1
navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat)) navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat))
} }
ritRegex.containsMatchIn(marker) -> {
val endGrid = if(lastCallerMarker == 0) last_grid else lastCallerMarker
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = ci,
isTempoChange = true,
tempoType = "rit",
endGridForTempo = endGrid,
targetTempoMultiplier = 0.55f
)
)
}
rallRegex.containsMatchIn(marker) -> {
val endGrid = last_grid
navigationSteps.add(
NavigationStep(
marker = marker,
gridIndex = ci,
isTempoChange = true,
tempoType = "rall",
endGridForTempo = endGrid,
targetTempoMultiplier = 0.50f
)
)
}
dsG.matches(marker.trim()) -> { dsG.matches(marker.trim()) -> {
val target = if(lastSegno>0) lastSegno else 0 val target = if(lastSegno>0) lastSegno else 0
val indx = if((last_grid-ci)<=0) ci-1 else ci val indx = if((last_grid-ci)<=0) ci-1 else ci
@ -406,15 +512,21 @@ actual class FMediaPlayer actual constructor(
} }
private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) { private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) {
var lastProcessedIndex = -1
navigationJob?.cancel() navigationJob?.cancel()
navigationJob = playerScope.launch(Dispatchers.Default) { navigationJob = playerScope.launch(Dispatchers.Default) {
sharedScreenModel.activeIndex.collect { currentIndex -> sharedScreenModel.activeIndex.collect { currentIndex ->
if (currentIndex <= lastProcessedIndex) {
return@collect
}
lastProcessedIndex = currentIndex
if (!isRunning || currentIndex < 0) return@collect if (!isRunning || currentIndex < 0) return@collect
val step = navigationSteps.find { val step = navigationSteps.find {
it.gridIndex == currentIndex && !it.alreadyDone it.gridIndex == currentIndex && !it.alreadyDone
} ?: return@collect }
println("pas: ${currentIndex}")
if (step != null) {
when { when {
// ── Farany ──────────────────────────── // ── Farany ────────────────────────────
step.isFarany -> { step.isFarany -> {
@ -443,6 +555,25 @@ actual class FMediaPlayer actual constructor(
for (ch in 0 until 4) controlChange(ch, 64, 0) for (ch in 0 until 4) controlChange(ch, 64, 0)
step.alreadyDone = true step.alreadyDone = true
} }
// 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")
applyTempoChange(
startBpm = startBpm,
endBpm = endBpm,
durationMs = durationMs,
tempoType = step.tempoType
)
}
// ── Soufflet ────────────────────────── // ── Soufflet ──────────────────────────
step.hairPin != null -> { step.hairPin != null -> {
@ -489,6 +620,7 @@ actual class FMediaPlayer actual constructor(
} }
} }
} }
}
private fun startSyncLoop(sharedScreenModel: SharedScreenModel) { private fun startSyncLoop(sharedScreenModel: SharedScreenModel) {
syncJob?.cancel() syncJob?.cancel()
@ -522,6 +654,7 @@ actual class FMediaPlayer actual constructor(
allNotesOff(); currentTickPos = 0L allNotesOff(); currentTickPos = 0L
resetNavigationFlags(); navigationSteps.clear(); clearLoop() resetNavigationFlags(); navigationSteps.clear(); clearLoop()
currentDynamicFactor = Dynamic.MF.factor currentDynamicFactor = Dynamic.MF.factor
resetTempoToNormal()
} }
actual fun release() { actual fun release() {
stop(); midiDriver.stop(); playerScope.cancel() stop(); midiDriver.stop(); playerScope.cancel()