Implements modulation&tempo change MidiPlayer - android

This commit is contained in:
Hasinjato 2026-08-07 16:23:50 +03:00
parent 41fbb204bb
commit 4698bc6a2c

View file

@ -4,6 +4,7 @@ import SharedScreenModel
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import kotlinx.coroutines.* import kotlinx.coroutines.*
import mg.dot.feufaro.provideSettings import mg.dot.feufaro.provideSettings
import mg.dot.feufaro.solfa.Transpose
import org.billthefarmer.mididriver.MidiDriver import org.billthefarmer.mididriver.MidiDriver
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -132,6 +133,7 @@ actual class FMediaPlayer actual constructor(
private var currentTickPos: Long = 0L private var currentTickPos: Long = 0L
private var lastEventNano: Long = System.nanoTime() private var lastEventNano: Long = System.nanoTime()
private var targetBpm: Float = 120f private var targetBpm: Float = 120f
private var initialBpm: Float = 120f
@Volatile private var currentPlaybackBpm: Float = 120f @Volatile private var currentPlaybackBpm: Float = 120f
@Volatile private var isInTempoChange: Boolean = false @Volatile private var isInTempoChange: Boolean = false
@ -140,6 +142,7 @@ actual class FMediaPlayer actual constructor(
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
private var transpositionSemitones: Int = 0
private var pointA: Long = -1L private var pointA: Long = -1L
private var pointB: Long = -1L private var pointB: Long = -1L
@ -171,7 +174,9 @@ actual class FMediaPlayer actual constructor(
val isTempoChange: Boolean = false, val isTempoChange: Boolean = false,
val tempoType: String = "", val tempoType: String = "",
val endGridForTempo: Int = -1, val endGridForTempo: Int = -1,
val targetTempoMultiplier: Float = 0.6f val targetTempoMultiplier: Float = 0.6f,
val newBpm: Float? = null,
val newKey: String? = null
) )
private val navigationSteps = mutableListOf<NavigationStep>() private val navigationSteps = mutableListOf<NavigationStep>()
private val prefs: Settings = provideSettings() private val prefs: Settings = provideSettings()
@ -283,6 +288,12 @@ actual class FMediaPlayer actual constructor(
) )
} }
} }
private fun resetTempoToInitial() {
tempoChangeJob?.cancel()
isInTempoChange = false
targetBpm = initialBpm
boundModel?.setBpmFlow(initialBpm)
}
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()
@ -345,6 +356,7 @@ actual class FMediaPlayer actual constructor(
} else { } else {
isRunning = false isRunning = false
allNotesOff() allNotesOff()
resetTempoToNormal()
onFinished() onFinished()
break break
} }
@ -386,9 +398,15 @@ actual class FMediaPlayer actual constructor(
} }
when (ev.type) { when (ev.type) {
0x80 -> send(0x80 or ev.channel, ev.data1, ev.data2) 0x80 -> {
0x90 -> send(0x90 or ev.channel, ev.data1, val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127)
(ev.data2 * currentDynamicFactor).toInt().coerceIn(0, 127)) send(0x80 or ev.channel, transposedNote, ev.data2)
}
0x90 -> {
val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127)
send(0x90 or ev.channel, transposedNote,
(ev.data2 * currentDynamicFactor).toInt().coerceIn(0, 127))
}
0xB0 -> if (ev.data1 != 7 && ev.data1 != 11) 0xB0 -> if (ev.data1 != 7 && ev.data1 != 11)
send(0xB0 or ev.channel, ev.data1, ev.data2) send(0xB0 or ev.channel, ev.data1, ev.data2)
0xC0 -> send(0xC0 or ev.channel, ev.data1) 0xC0 -> send(0xC0 or ev.channel, ev.data1)
@ -419,10 +437,21 @@ actual class FMediaPlayer actual constructor(
currentTickPos = gridIndex.toLong() * resolution currentTickPos = gridIndex.toLong() * resolution
lastEventNano = System.nanoTime() lastEventNano = System.nanoTime()
val initialOrPreviousBpm = navigationSteps
.filter { it.newBpm != null && it.gridIndex <= gridIndex }
.maxByOrNull { it.gridIndex }?.newBpm ?: initialBpm
targetBpm = initialOrPreviousBpm
sharedScreenModel.setBpmFlow(initialOrPreviousBpm)
val lastDyn = navigationSteps val lastDyn = navigationSteps
.filter { it.dynamic != null && it.gridIndex <= gridIndex } .filter { it.dynamic != null && it.gridIndex <= gridIndex }
.maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF
currentDynamicFactor = if (lastDyn == Dynamic.MF) 1.0f else lastDyn.factor currentDynamicFactor = if (lastDyn == Dynamic.MF) 1.0f else lastDyn.factor
navigationSteps.forEach { step ->
if (step.gridIndex > gridIndex) {
step.alreadyDone = false
}
}
applyVoiceStates() applyVoiceStates()
needsClockSync = true needsClockSync = true
@ -456,6 +485,8 @@ actual class FMediaPlayer actual constructor(
val dcG = Regex("""D\.?C\.?_GROUP_PART""")*/ val dcG = Regex("""D\.?C\.?_GROUP_PART""")*/
val finReg = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE) val finReg = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE)
val modulaRegex = Regex("""D(ô|o)?\s*dia\s*(C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B)""", RegexOption.IGNORE_CASE)
val tempoRegex = Regex("""^♩\s*=\s*(\d+)""", RegexOption.IGNORE_CASE)
metadataList.forEach { midiMarker -> metadataList.forEach { midiMarker ->
val ci = midiMarker.gridIndex ?: 0 val ci = midiMarker.gridIndex ?: 0
val last_grid = sharedScreenModel.getTotalGridCount() val last_grid = sharedScreenModel.getTotalGridCount()
@ -475,6 +506,37 @@ actual class FMediaPlayer actual constructor(
val isDsFin = hasDs && hasFin val isDsFin = hasDs && hasFin
val isDcFin = hasDc && hasFin val isDcFin = hasDc && hasFin
// Changement de Tempo ♩=
val tempoMatch = tempoRegex.find(mTrim)
if (tempoMatch != null) {
val bpmVal = tempoMatch.groupValues[1].toFloatOrNull()
if (bpmVal != null) {
navigationSteps.add(
NavigationStep(
marker = mTrim,
gridIndex = ci,
newBpm = bpmVal
)
)
println("Changement de BPM mémorisé à la grille $ci -> $bpmVal BPM")
}
}
// Modulation Do dia ..
val modMatch = modulaRegex.find(mTrim)
if (modMatch != null) {
val targetKey = modMatch.groupValues[2]
navigationSteps.add(
NavigationStep(
marker = mTrim,
gridIndex = ci,
newKey = targetKey
)
)
println("Modulation mémorisée à la grille $ci -> $targetKey")
}
when { when {
isDsFin || isDcFin -> { isDsFin || isDcFin -> {
val target = if (lastSegno > 0) lastSegno else 0 val target = if (lastSegno > 0) lastSegno else 0
@ -625,6 +687,22 @@ actual class FMediaPlayer actual constructor(
} }
for(step in currentSteps) { for(step in currentSteps) {
when { when {
//♩
step.newBpm != null -> {
step.alreadyDone = true
println("Exécution changement de tempo : ${step.newBpm} BPM à la grille $currentIndex")
targetBpm = step.newBpm
sharedScreenModel.setBpmFlow(step.newBpm)
}
// Modulation
step.newKey != null -> {
step.alreadyDone = true
val baseKeyIdx = Transpose.keyToNumber.indexOf("C").coerceAtLeast(0)
val targetKeyIdx = Transpose.keyToNumber.indexOf(step.newKey).coerceAtLeast(0)
transpositionSemitones = targetKeyIdx - baseKeyIdx
}
// ── Point d'orgue ───────────────────── // ── Point d'orgue ─────────────────────
step.isHold -> { step.isHold -> {
val beatMs = (60_000 / targetBpm).toLong() val beatMs = (60_000 / targetBpm).toLong()
@ -839,6 +917,7 @@ actual class FMediaPlayer actual constructor(
resetNavigationFlags(); navigationSteps.clear(); clearLoop() resetNavigationFlags(); navigationSteps.clear(); clearLoop()
currentDynamicFactor = Dynamic.MF.factor currentDynamicFactor = Dynamic.MF.factor
resetTempoToNormal() resetTempoToNormal()
resetTempoToInitial()
} }
actual fun release() { actual fun release() {
stop(); midiDriver.stop(); playerScope.cancel() stop(); midiDriver.stop(); playerScope.cancel()