diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 7e7019b..7fe11a6 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index b6e545e..00cf015 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -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 { + 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 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 55e2695..9a79948 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -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() @@ -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)