From 44495aa5cd3ca04042479d892025ed6f3a99761d Mon Sep 17 00:00:00 2001 From: "hasinarak3@gmail.com" Date: Wed, 11 Mar 2026 14:47:30 +0300 Subject: [PATCH] Implement velocity {ppp,pp,p,mp,mf,f,ff,fff} --- .../kotlin/mg/dot/feufaro/midi/Dynamic.kt | 17 ++ .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 145 +++++++++++++----- 2 files changed, 127 insertions(+), 35 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt new file mode 100644 index 0000000..aeae57a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/Dynamic.kt @@ -0,0 +1,17 @@ +package mg.dot.feufaro.midi + +enum class Dynamic(val velocity: Int, val label: String, val factor: Float) { + PPP(16, "ppp", 16f / 80f), + PP (33, "pp", 33f / 80f), + P (49, "p", 49f / 80f), + MP (64, "mp", 64f / 80f), + MF (80, "mf", 1.00f), + F (96, "f", 96f / 80f), + FF (112, "ff", 112f / 80f), + FFF(126, "fff", 126f / 80f); + + companion object { + fun fromVelocity(v: Int): Dynamic = + entries.minByOrNull { kotlin.math.abs(it.velocity - v) } ?: MF + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 966baa7..55e2695 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -37,6 +37,8 @@ actual class FMediaPlayer actual constructor( private val voiceVolumes = FloatArray(4) { 127f } private var currentGlobalVolume: Float = 0.8f + private var currentDynamicVelocity: Int = Dynamic.MF.velocity + private var currentDynamicFactor: Float = Dynamic.MF.factor private var currentTempo: Float = 1.0f private var targetBpm: Float = 120f @@ -51,6 +53,7 @@ actual class FMediaPlayer actual constructor( val isHold: Boolean = false, // point d'orgue var alreadyDone: Boolean = false, // done var beatInDC: Int = 1, // temps note sur le marker + val dynamic: Dynamic?= null, // velocité ) private val navigationSteps = mutableListOf() @@ -119,6 +122,36 @@ actual class FMediaPlayer actual constructor( private fun resetNavigationFlags() { navigationSteps.forEach { it.alreadyDone = false } } + + private var dynamicJob: Job? = null + + private fun applyDynamicSmooth(targetFactor: Float, durationMs: Long = 300L) { + dynamicJob?.cancel() + dynamicJob = playerScope.launch { + val startFactor = currentDynamicFactor + val steps = 30 + val stepMs = durationMs / steps + + for (i in 1..steps) { + val progress = i.toFloat() / steps + val smooth = progress * progress * (3f - 2f * progress) + + currentDynamicFactor = startFactor + (targetFactor - startFactor) * smooth + applyVoiceStates() + delay(stepMs) + } + currentDynamicFactor = targetFactor + applyVoiceStates() + } + } + private fun applyDynamic(dynamic: Dynamic) { + val targetFactor = when (dynamic) { + Dynamic.MF -> 1.0f + else -> dynamic.factor + } + println("applyDynamic → ${dynamic.label} | ${currentDynamicFactor} → $targetFactor") + applyDynamicSmooth(targetFactor, durationMs = 300L) + } private fun prepareNavigation(sharedScreenModel: SharedScreenModel) { val metadataList = sharedScreenModel.getFullMarkers() navigationSteps.clear() @@ -134,10 +167,12 @@ actual class FMediaPlayer actual constructor( val last_grid = sharedScreenModel.getTotalGridCount() when { + // segno marker.contains("$") -> { lastSegno = currentIndex println("Cible ($) mémorisée au $lastSegno") } + // Point d'orgue marker.contains("\uD834\uDD10") -> { val beat = if(note.contains('•')) 2 else 1 // demi-ton sur .) ou non navigationSteps.add( @@ -205,6 +240,18 @@ actual class FMediaPlayer actual constructor( ) println("Lien DC créé : $marker à $indx vers le début") } + // velocité + Dynamic.entries.find { it.label == marker.trim() } != null -> { + val dyn = Dynamic.entries.first { it.label == marker.trim() } + navigationSteps.add( + NavigationStep( + marker = marker, + gridIndex = currentIndex, + dynamic = dyn + ) + ) + println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex") + } } } } @@ -233,42 +280,50 @@ actual class FMediaPlayer actual constructor( if (step != null) { // Point d'orgue - if (step.isHold) { - val currentBpm = sequencer?.tempoInBPM ?: targetBpm - val beatMs = (60_000 / currentBpm).toLong() - val holdDuration = if (step.beatInDC == 2) beatMs / 2 else beatMs * 2 + when { + step.isHold -> { + val currentBpm = sequencer?.tempoInBPM ?: targetBpm + val beatMs = (60_000 / currentBpm).toLong() + val holdDuration = if (step.beatInDC == 2) beatMs / 2 else beatMs * 2 - println("POINT D'ORGUE sur Grille $currentIndex | Durée: ${holdDuration}ms") + println("POINT D'ORGUE sur Grille $currentIndex | Durée: ${holdDuration}ms") - synthetizer?.channels?.forEach { it?.controlChange(64, 127) } - val previousFactor = sequencer?.tempoFactor ?: 1.0f - sequencer?.tempoFactor = 0.0001f + synthetizer?.channels?.forEach { it?.controlChange(64, 127) } + val previousFactor = sequencer?.tempoFactor ?: 1.0f + sequencer?.tempoFactor = 0.0001f - delay(holdDuration) + delay(holdDuration) - sequencer?.tempoFactor = previousFactor - synthetizer?.channels?.forEach { it?.controlChange(64, 0) } - } else { - // $ dc ds ... - step.alreadyDone = true - - val currentBpm = targetBpm - val beatDuration = (60_000 / currentBpm).toLong() - println("avant de sauter bpm=$targetBpm") - - synthetizer?.channels?.forEach { it?.controlChange(64, 127) } - sequencer?.tempoFactor = 0.0001f - delay(beatDuration) - -// println("et là je saut vers ${step.targetGrid}") - seekToGrid(step.targetGrid) - synthetizer?.channels?.forEach { it?.controlChange(64, 0) } - sequencer?.tempoFactor = 1f - if (sequencer?.isRunning == false) { - sequencer?.tempoInBPM = targetBpm - sequencer?.start() + sequencer?.tempoFactor = previousFactor + synthetizer?.channels?.forEach { it?.controlChange(64, 0) } + } + + //velocity + step.dynamic != null -> { + applyDynamic(step.dynamic) + } + else -> { + // $ dc ds ... + step.alreadyDone = true + + val currentBpm = targetBpm + val beatDuration = (60_000 / currentBpm).toLong() + println("avant de sauter bpm=$targetBpm") + + synthetizer?.channels?.forEach { it?.controlChange(64, 127) } + sequencer?.tempoFactor = 0.0001f + delay(beatDuration) + + // println("et là je saut vers ${step.targetGrid}") + seekToGrid(step.targetGrid) + synthetizer?.channels?.forEach { it?.controlChange(64, 0) } + sequencer?.tempoFactor = 1f + if (sequencer?.isRunning == false) { + sequencer?.tempoInBPM = targetBpm + sequencer?.start() + } + // println("Après de sauter bpm=$targetBpm") } -// println("Après de sauter bpm=$targetBpm") } } } @@ -296,6 +351,13 @@ actual class FMediaPlayer actual constructor( sequencer?.tickPosition = tick applyBpm() + // Voir les velocity avant + val lastDynamic = navigationSteps + .filter { it.dynamic != null && it.gridIndex <= gridIndex } + .maxByOrNull { it.gridIndex } + ?.dynamic ?: Dynamic.MF + currentDynamicFactor = lastDynamic.factor + applyVoiceStates() } actual fun play(){ @@ -367,6 +429,7 @@ actual class FMediaPlayer actual constructor( resetNavigationFlags() navigationSteps.clear() clearLoop() + currentDynamicFactor = Dynamic.MF.factor // release() } actual fun getDuration(): Long { @@ -387,7 +450,8 @@ actual class FMediaPlayer actual constructor( actual fun setVolume(level: Float) { try { this.currentGlobalVolume = level - val volumeInt = (level * 127).toInt().coerceIn(0, 127) + applyVoiceStates() + /*val volumeInt = (level * 127).toInt().coerceIn(0, 127) synthetizer?.channels?.forEachIndexed { index, channel -> if (index < 4) { @@ -396,7 +460,7 @@ actual class FMediaPlayer actual constructor( } else { channel?.controlChange(7, volumeInt) } - } + }*/ val mixer = AudioSystem.getMixer(null) val lines = mixer.sourceLines for (line in lines) { @@ -462,14 +526,25 @@ actual class FMediaPlayer actual constructor( synthetizer?.channels?.let { channels -> for (i in 0 until 4) { if (i < channels.size) { - val targetVolume = voiceVolumes[i].toInt() + val userVol = voiceVolumes[i] / 127f + val globalVol = currentGlobalVolume + + val finalVol = (127f * userVol * globalVol * currentDynamicFactor).toInt().coerceIn(0, 127) + + println("Voice[$i] user=${voiceVolumes[i]} global=$currentGlobalVolume dynFactor=$currentDynamicFactor → finalVol=$finalVol") + + channels[i].controlChange(7, finalVol) + channels[i].controlChange(11, finalVol) + + if (finalVol == 0) channels[i].controlChange(123, 0) + /*val targetVolume = voiceVolumes[i].toInt() channels[i].controlChange(7, targetVolume) channels[i].controlChange(11, targetVolume) if (targetVolume == 0) { channels[i].controlChange(123, 0) - } + }*/ } // println("SATB $i Volumes: ${voiceVolumes[i]}") }