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) {
|
val metadataList = remember(tuoList) {
|
||||||
tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO ->
|
tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO ->
|
||||||
val markerText = oneTUO.pTemplate.markerToString()
|
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 }
|
val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L }
|
||||||
|
// println("MetaData[$globalIndex] marker='$finalMarker' hairpin=$hairPin")
|
||||||
MidiMarkers(
|
MidiMarkers(
|
||||||
myTimestamp,
|
myTimestamp,
|
||||||
globalIndex,
|
globalIndex,
|
||||||
oneTUO.pTemplate.template,
|
oneTUO.pTemplate.template,
|
||||||
oneTUO.pTemplate.lastCalledMarker,
|
oneTUO.pTemplate.lastCalledMarker,
|
||||||
markerText,
|
finalMarker,
|
||||||
oneTUO.prevTUO?.pTemplate?.template ?: "",
|
oneTUO.prevTUO?.pTemplate?.template ?: "",
|
||||||
oneTUO.sep0,
|
oneTUO.sep0,
|
||||||
oneTUO.tuNotes.getOrNull(1).toString()
|
oneTUO.tuNotes.getOrNull(1).toString()
|
||||||
|
|
@ -651,7 +659,7 @@ fun LazyVerticalGridTUO(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) {
|
if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) {
|
||||||
println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}")
|
// println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}")
|
||||||
val hairPinStart = TimeUnitObject.lastHairPinStart
|
val hairPinStart = TimeUnitObject.lastHairPinStart
|
||||||
val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol
|
val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol
|
||||||
val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount
|
val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
//finalizeMarkers()
|
//finalizeMarkers()
|
||||||
_mediaPlayer?.syncNavigationMonitor(this)
|
_mediaPlayer?.syncNavigationMonitor(this)
|
||||||
} catch(e: Exception) {
|
} catch(e: Exception) {
|
||||||
println("Erreur d'ouverture de mediaPlayer / ")
|
println("Erreur d'ouverture de mediaPlayer : ${e.message} ")
|
||||||
}
|
}
|
||||||
println("New media Player crée $newMidiFile")
|
println("New media Player crée $newMidiFile")
|
||||||
}
|
}
|
||||||
|
|
@ -460,6 +460,46 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
_hasMarker.value = theHasMarker
|
_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() {
|
fun playNext() {
|
||||||
val nextIndex = (_nextPlayed.value + 1) % _playlist.value.size
|
val nextIndex = (_nextPlayed.value + 1) % _playlist.value.size
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ actual class FMediaPlayer actual constructor(
|
||||||
var alreadyDone: Boolean = false, // done
|
var alreadyDone: Boolean = false, // done
|
||||||
var beatInDC: Int = 1, // temps note sur le marker
|
var beatInDC: Int = 1, // temps note sur le marker
|
||||||
val dynamic: Dynamic?= null, // velocité
|
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>()
|
private val navigationSteps = mutableListOf<NavigationStep>()
|
||||||
|
|
||||||
|
|
@ -152,10 +157,43 @@ actual class FMediaPlayer actual constructor(
|
||||||
println("applyDynamic → ${dynamic.label} | ${currentDynamicFactor} → $targetFactor")
|
println("applyDynamic → ${dynamic.label} | ${currentDynamicFactor} → $targetFactor")
|
||||||
applyDynamicSmooth(targetFactor, durationMs = 300L)
|
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) {
|
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
|
||||||
val metadataList = sharedScreenModel.getFullMarkers()
|
val metadataList = sharedScreenModel.getFullMarkers()
|
||||||
navigationSteps.clear()
|
navigationSteps.clear()
|
||||||
var lastSegno = 0
|
var lastSegno = 0
|
||||||
|
var lastFactor = Dynamic.MF.factor
|
||||||
|
|
||||||
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
|
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
|
||||||
val currentIndex = gridIndex ?: 0
|
val currentIndex = gridIndex ?: 0
|
||||||
|
|
@ -166,6 +204,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""")
|
val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""")
|
||||||
|
|
||||||
val last_grid = sharedScreenModel.getTotalGridCount()
|
val last_grid = sharedScreenModel.getTotalGridCount()
|
||||||
|
val hairPins = sharedScreenModel.getHairPins()
|
||||||
when {
|
when {
|
||||||
// segno
|
// segno
|
||||||
marker.contains("$") -> {
|
marker.contains("$") -> {
|
||||||
|
|
@ -240,9 +279,14 @@ actual class FMediaPlayer actual constructor(
|
||||||
)
|
)
|
||||||
println("Lien DC créé : $marker à $indx vers le début")
|
println("Lien DC créé : $marker à $indx vers le début")
|
||||||
}
|
}
|
||||||
|
|
||||||
// velocité
|
// velocité
|
||||||
Dynamic.entries.find { it.label == marker.trim() } != null -> {
|
extractDynamic(marker) != null && marker.trim() != "=" -> {
|
||||||
val dyn = Dynamic.entries.first { it.label == marker.trim() }
|
val dyn = extractDynamic(marker) ?: return@forEach
|
||||||
|
lastFactor = when (dyn) {
|
||||||
|
Dynamic.MF -> 1.0f
|
||||||
|
else -> dyn.factor
|
||||||
|
}
|
||||||
navigationSteps.add(
|
navigationSteps.add(
|
||||||
NavigationStep(
|
NavigationStep(
|
||||||
marker = marker,
|
marker = marker,
|
||||||
|
|
@ -252,6 +296,62 @@ actual class FMediaPlayer actual constructor(
|
||||||
)
|
)
|
||||||
println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex")
|
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) {
|
if (step != null) {
|
||||||
// Point d'orgue
|
|
||||||
when {
|
when {
|
||||||
|
// Point d'orgue
|
||||||
step.isHold -> {
|
step.isHold -> {
|
||||||
val currentBpm = sequencer?.tempoInBPM ?: targetBpm
|
val currentBpm = sequencer?.tempoInBPM ?: targetBpm
|
||||||
val beatMs = (60_000 / currentBpm).toLong()
|
val beatMs = (60_000 / currentBpm).toLong()
|
||||||
|
|
@ -298,6 +398,22 @@ actual class FMediaPlayer actual constructor(
|
||||||
synthetizer?.channels?.forEach { it?.controlChange(64, 0) }
|
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
|
//velocity
|
||||||
step.dynamic != null -> {
|
step.dynamic != null -> {
|
||||||
applyDynamic(step.dynamic)
|
applyDynamic(step.dynamic)
|
||||||
|
|
@ -531,7 +647,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
|
|
||||||
val finalVol = (127f * userVol * globalVol * currentDynamicFactor).toInt().coerceIn(0, 127)
|
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(7, finalVol)
|
||||||
channels[i].controlChange(11, finalVol)
|
channels[i].controlChange(11, finalVol)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue