Implement Rit. & Rall. on playing Midi - Android
This commit is contained in:
parent
0c0cf4d43c
commit
b3d7429b59
1 changed files with 146 additions and 13 deletions
|
|
@ -111,7 +111,7 @@ actual class FMediaPlayer actual constructor(
|
|||
private val midiDriver: MidiDriver = MidiDriver.getInstance()
|
||||
private var sequence: MidiSequence? = null
|
||||
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 isHolding = false
|
||||
|
|
@ -119,6 +119,10 @@ actual class FMediaPlayer actual constructor(
|
|||
private var lastEventNano: Long = System.nanoTime()
|
||||
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 var currentGlobalVolume: Float = 0.8f
|
||||
private var currentDynamicFactor: Float = Dynamic.MF.factor
|
||||
|
|
@ -148,6 +152,11 @@ actual class FMediaPlayer actual constructor(
|
|||
val hairPinToFactor: Float = 1.0f,
|
||||
val isFarany: 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>()
|
||||
|
||||
|
|
@ -206,6 +215,49 @@ actual class FMediaPlayer actual constructor(
|
|||
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 {
|
||||
it.label == marker.trim().removeSuffix("=").trim()
|
||||
}
|
||||
|
|
@ -229,8 +281,14 @@ actual class FMediaPlayer actual constructor(
|
|||
var clockTick = currentTickPos
|
||||
|
||||
while (isActive && isRunning) {
|
||||
if (needsClockSync) {
|
||||
clockNano = System.nanoTime()
|
||||
clockTick = currentTickPos
|
||||
needsClockSync = false
|
||||
}
|
||||
if (isHolding) {
|
||||
delay(10)
|
||||
yield()
|
||||
// delay(10)
|
||||
clockNano = System.nanoTime()
|
||||
clockTick = currentTickPos
|
||||
continue
|
||||
|
|
@ -252,10 +310,24 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
|
||||
val ev = events[idx]
|
||||
val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000)
|
||||
.toLong()) - System.nanoTime()
|
||||
if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L))
|
||||
// val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000)
|
||||
// .toLong()) - System.nanoTime()
|
||||
|
||||
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
|
||||
lastEventNano = System.nanoTime()
|
||||
|
||||
|
|
@ -278,6 +350,10 @@ actual class FMediaPlayer actual constructor(
|
|||
0xC0 -> send(0xC0 or ev.channel, ev.data1)
|
||||
0xFF -> { /* tempo ignoré */ }
|
||||
}
|
||||
val currentGrid = (ev.tickAbsolute / resolution).toLong()
|
||||
if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
|
||||
boundModel?.updateActiveIndex(currentGrid)
|
||||
}
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
|
@ -311,12 +387,17 @@ actual class FMediaPlayer actual constructor(
|
|||
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
|
||||
val metadataList = sharedScreenModel.getFullMarkers()
|
||||
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 dsR = Regex("""D\.?S\.?"""); val dcR = Regex("""D\.?C\.?""")
|
||||
val dsG = Regex("""D\.?S\.?_GROUP_PART"""); val dcG = Regex("""D\.?C\.?_GROUP_PART""")
|
||||
val dsR = Regex("""D\.?S\.?""");
|
||||
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 hairPins = sharedScreenModel.getHairPins()
|
||||
|
||||
|
|
@ -327,7 +408,32 @@ actual class FMediaPlayer actual constructor(
|
|||
val beat = if(note.contains('•')) 2 else 1
|
||||
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()) -> {
|
||||
val target = if(lastSegno>0) lastSegno else 0
|
||||
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) {
|
||||
var lastProcessedIndex = -1
|
||||
navigationJob?.cancel()
|
||||
navigationJob = playerScope.launch(Dispatchers.Default) {
|
||||
sharedScreenModel.activeIndex.collect { currentIndex ->
|
||||
if (currentIndex <= lastProcessedIndex) {
|
||||
return@collect
|
||||
}
|
||||
lastProcessedIndex = currentIndex
|
||||
if (!isRunning || currentIndex < 0) return@collect
|
||||
|
||||
val step = navigationSteps.find {
|
||||
it.gridIndex == currentIndex && !it.alreadyDone
|
||||
} ?: return@collect
|
||||
|
||||
}
|
||||
println("pas: ${currentIndex}")
|
||||
if (step != null) {
|
||||
when {
|
||||
// ── Farany ────────────────────────────
|
||||
step.isFarany -> {
|
||||
|
|
@ -443,6 +555,25 @@ actual class FMediaPlayer actual constructor(
|
|||
for (ch in 0 until 4) controlChange(ch, 64, 0)
|
||||
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 ──────────────────────────
|
||||
step.hairPin != null -> {
|
||||
|
|
@ -489,6 +620,7 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSyncLoop(sharedScreenModel: SharedScreenModel) {
|
||||
syncJob?.cancel()
|
||||
|
|
@ -522,6 +654,7 @@ actual class FMediaPlayer actual constructor(
|
|||
allNotesOff(); currentTickPos = 0L
|
||||
resetNavigationFlags(); navigationSteps.clear(); clearLoop()
|
||||
currentDynamicFactor = Dynamic.MF.factor
|
||||
resetTempoToNormal()
|
||||
}
|
||||
actual fun release() {
|
||||
stop(); midiDriver.stop(); playerScope.cancel()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue