Implement nuances progressive (hairPin) '<','>','cresc','dim' on playing midi
This commit is contained in:
parent
44495aa5cd
commit
810ebb38e6
3 changed files with 172 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue