Implement nuances progressive (hairPin) '<','>','cresc','dim' on playing midi

This commit is contained in:
hasinarak3@gmail.com 2026-03-12 12:11:01 +03:00
parent 44495aa5cd
commit 810ebb38e6
3 changed files with 172 additions and 8 deletions

View file

@ -584,14 +584,22 @@ fun LazyVerticalGridTUO(
val metadataList = remember(tuoList) {
tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO ->
val markerText = oneTUO.pTemplate.markerToString()
if (markerText.isNotEmpty()) {
val hairPin = oneTUO.hasHairPin()
val finalMarker = when {
hairPin != null && markerText.isBlank() -> hairPin.toString()
hairPin != null && markerText.isNotBlank() -> "${markerText.trim()}$hairPin"
markerText.isNotBlank() -> markerText
else -> null
}
if (finalMarker != null) {
val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L }
// println("MetaData[$globalIndex] marker='$finalMarker' hairpin=$hairPin")
MidiMarkers(
myTimestamp,
globalIndex,
oneTUO.pTemplate.template,
oneTUO.pTemplate.lastCalledMarker,
markerText,
finalMarker,
oneTUO.prevTUO?.pTemplate?.template ?: "",
oneTUO.sep0,
oneTUO.tuNotes.getOrNull(1).toString()
@ -651,7 +659,7 @@ fun LazyVerticalGridTUO(
}
}
if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) {
println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}")
// println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}")
val hairPinStart = TimeUnitObject.lastHairPinStart
val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol
val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount

View file

@ -311,7 +311,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
//finalizeMarkers()
_mediaPlayer?.syncNavigationMonitor(this)
} catch(e: Exception) {
println("Erreur d'ouverture de mediaPlayer / ")
println("Erreur d'ouverture de mediaPlayer : ${e.message} ")
}
println("New media Player crée $newMidiFile")
}
@ -460,6 +460,46 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_hasMarker.value = theHasMarker
}
data class HairPinData(
val symbol: Char,
val startGrid: Int,
val endGrid: Int,
)
fun getHairPins(): List<HairPinData> {
val allMarkers = _midiMarkersList.value
val starts = allMarkers
.filter {
val m = it.marker.trim()
m == "<" ||
m == ">" ||
m.contains("cres", ignoreCase = true) ||
m.contains("dim", ignoreCase = true)
}
.sortedBy { it.gridIndex }
val ends = allMarkers
.filter { it.marker.trim().endsWith("=") }
.sortedBy { it.gridIndex }
.toMutableList()
// println("HairPins starts: ${starts.size} | ends: ${ends.size}")
return starts.mapNotNull { start ->
val startGrid = start.gridIndex ?: return@mapNotNull null
val symbol = when {
start.marker.trim() == "<" -> '<'
start.marker.trim() == ">" -> '>'
start.marker.contains("cres", ignoreCase = true) -> '<'
start.marker.contains("dim", ignoreCase = true) -> '>'
else -> return@mapNotNull null
}
val matchingEnd = ends.firstOrNull { (it.gridIndex ?: 0) > startGrid }
if (matchingEnd != null) ends.remove(matchingEnd)
val endGrid = matchingEnd?.gridIndex ?: (startGrid + 4)
// println("HairPin '$symbol' : $startGrid → $endGrid")
HairPinData(symbol, startGrid, endGrid)
}
}
fun playNext() {
val nextIndex = (_nextPlayed.value + 1) % _playlist.value.size

View file

@ -54,6 +54,11 @@ actual class FMediaPlayer actual constructor(
var alreadyDone: Boolean = false, // done
var beatInDC: Int = 1, // temps note sur le marker
val dynamic: Dynamic?= null, // velocité
// hairpin
val hairPin: Char? = null,
val hairPinEndGrid: Int = -1,
val hairPinFromFactor: Float = 1.0f,
val hairPinToFactor: Float = 1.0f,
)
private val navigationSteps = mutableListOf<NavigationStep>()
@ -152,10 +157,43 @@ actual class FMediaPlayer actual constructor(
println("applyDynamic → ${dynamic.label} | ${currentDynamicFactor}$targetFactor")
applyDynamicSmooth(targetFactor, durationMs = 300L)
}
private fun extractDynamic(marker: String): Dynamic? {
val clean = marker.trim().removeSuffix("=").trim()
return Dynamic.entries.firstOrNull { it.label == clean }
}
fun factorToDynamic(factor: Float): Dynamic = Dynamic.entries.minByOrNull { kotlin.math.abs(it.factor - factor) } ?: Dynamic.MF
fun nextDynamic(currentFactor: Float, symbol: Char): Float {
val current = factorToDynamic(currentFactor)
val index = Dynamic.entries.indexOf(current)
return if (symbol == '<') {
Dynamic.entries.getOrElse(index + 1) { Dynamic.FFF }.factor
} else {
Dynamic.entries.getOrElse(index - 1) { Dynamic.PPP }.factor
}
}
private fun applyCrescendo(fromFactor: Float, toFactor: Float, durationMs: Long) {
dynamicJob?.cancel()
dynamicJob = playerScope.launch {
val steps = 40
val stepMs = (durationMs / steps).coerceAtLeast(10L)
for (i in 1..steps) {
val progress = i.toFloat() / steps
val smooth = progress * progress * (3f - 2f * progress) // ease in-out
currentDynamicFactor = fromFactor + (toFactor - fromFactor) * smooth
applyVoiceStates()
delay(stepMs)
}
currentDynamicFactor = toFactor
applyVoiceStates()
println("applyCrescendo terminé → factor=$currentDynamicFactor")
}
}
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
val metadataList = sharedScreenModel.getFullMarkers()
navigationSteps.clear()
var lastSegno = 0
var lastFactor = Dynamic.MF.factor
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
val currentIndex = gridIndex ?: 0
@ -166,6 +204,7 @@ actual class FMediaPlayer actual constructor(
val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""")
val last_grid = sharedScreenModel.getTotalGridCount()
val hairPins = sharedScreenModel.getHairPins()
when {
// segno
marker.contains("$") -> {
@ -240,9 +279,14 @@ 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() }
extractDynamic(marker) != null && marker.trim() != "=" -> {
val dyn = extractDynamic(marker) ?: return@forEach
lastFactor = when (dyn) {
Dynamic.MF -> 1.0f
else -> dyn.factor
}
navigationSteps.add(
NavigationStep(
marker = marker,
@ -252,6 +296,62 @@ actual class FMediaPlayer actual constructor(
)
println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex")
}
// Soufflet
marker.trim() == "<" || marker.trim() == ">" ||
marker.trim().contains("cres", ignoreCase = true) ||
marker.trim().contains("dim", ignoreCase = true) -> {
val symbol = when {
marker.trim() == "<" -> '<'
marker.trim() == ">" -> '>'
marker.trim().contains("cres", ignoreCase = true) -> '<'
marker.trim().contains("dim", ignoreCase = true) -> '>'
else -> return@forEach
}
val pair = hairPins.find { it.startGrid == currentIndex } ?: return@forEach
val dynBefore = navigationSteps
.filter { it.dynamic != null && it.gridIndex <= currentIndex }
.maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF
val dynAfter = metadataList
.filter { (_, gi, _, _, mk, _, _, _) ->
(gi ?: 0) >= pair.endGrid && Dynamic.entries.any { d -> d.label == mk.trim() }
}
.minByOrNull { it.gridIndex ?: 0 }
?.let { m -> Dynamic.entries.find { d -> d.label == m.marker.trim() } }
val explicitDynAfter = metadataList
.filter { (_, gi, _, _, mk, _, _, _) ->
(gi ?: 0) >= pair.endGrid &&
extractDynamic(mk) != null
}
.minByOrNull { it.gridIndex ?: 0 }
?.let { m -> extractDynamic(m.marker) }
val fromFactor = lastFactor
val toFactor = when {
explicitDynAfter != null && symbol == '<' && explicitDynAfter.factor > fromFactor -> {
explicitDynAfter.factor
}
explicitDynAfter != null && symbol == '>' && explicitDynAfter.factor < fromFactor -> {
explicitDynAfter.factor
}
else -> nextDynamic(fromFactor, symbol)
}
lastFactor = toFactor
navigationSteps.add(
NavigationStep(
marker = marker.trim(),
gridIndex = currentIndex,
hairPin = symbol,
hairPinEndGrid = pair.endGrid,
hairPinFromFactor = fromFactor,
hairPinToFactor = toFactor
)
)
println("NavigationStep HairPin '$symbol' : $currentIndex${pair.endGrid} | ${fromFactor})→${toFactor})")
}
}
}
}
@ -279,8 +379,8 @@ actual class FMediaPlayer actual constructor(
}
if (step != null) {
// Point d'orgue
when {
// Point d'orgue
step.isHold -> {
val currentBpm = sequencer?.tempoInBPM ?: targetBpm
val beatMs = (60_000 / currentBpm).toLong()
@ -298,6 +398,22 @@ actual class FMediaPlayer actual constructor(
synthetizer?.channels?.forEach { it?.controlChange(64, 0) }
}
//Soufflet
step.hairPin != null -> {
val resolution = sequencer?.sequence?.resolution?.toLong() ?: 480L
val gridDistance = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1)
val beatDurationMs = (60_000L / targetBpm).toLong()
val durationMs = (gridDistance * beatDurationMs).coerceIn(200L, 5000L)
println("HairPin '${step.hairPin}' grille ${step.gridIndex}${step.hairPinEndGrid} | ${step.hairPinFromFactor}${step.hairPinToFactor} | durée=${durationMs}ms")
applyCrescendo(
fromFactor = step.hairPinFromFactor,
toFactor = step.hairPinToFactor,
durationMs = durationMs
)
}
//velocity
step.dynamic != null -> {
applyDynamic(step.dynamic)
@ -531,7 +647,7 @@ actual class FMediaPlayer actual constructor(
val finalVol = (127f * userVol * globalVol * currentDynamicFactor).toInt().coerceIn(0, 127)
println("Voice[$i] user=${voiceVolumes[i]} global=$currentGlobalVolume dynFactor=$currentDynamicFactor → finalVol=$finalVol")
// println("Voice[$i] user=${voiceVolumes[i]} global=$currentGlobalVolume dynFactor=$currentDynamicFactor → finalVol=$finalVol")
channels[i].controlChange(7, finalVol)
channels[i].controlChange(11, finalVol)