2026-01-18 16:22:42 +01:00
|
|
|
package mg.dot.feufaro.midi
|
|
|
|
|
|
2026-03-02 10:08:52 +03:00
|
|
|
import SharedScreenModel
|
2026-02-17 16:23:53 +03:00
|
|
|
import kotlinx.coroutines.*
|
2026-02-02 14:20:03 +03:00
|
|
|
import mg.dot.feufaro.getConfigDirectoryPath
|
2026-03-16 14:56:06 +03:00
|
|
|
import mg.dot.feufaro.viewmodel.MidiMarkers
|
2026-01-18 16:22:42 +01:00
|
|
|
import java.io.File
|
2026-01-19 14:53:39 +03:00
|
|
|
import java.util.prefs.Preferences
|
2026-03-04 15:25:02 +03:00
|
|
|
import javax.sound.midi.MetaMessage
|
2026-01-18 16:22:42 +01:00
|
|
|
import javax.sound.midi.MidiSystem
|
2026-03-02 10:08:52 +03:00
|
|
|
import javax.sound.midi.Sequence
|
2026-01-18 16:22:42 +01:00
|
|
|
import javax.sound.midi.Sequencer
|
2026-03-16 14:56:06 +03:00
|
|
|
import javax.sound.midi.ShortMessage
|
2026-01-19 14:53:39 +03:00
|
|
|
import javax.sound.midi.Synthesizer
|
2026-03-16 14:56:06 +03:00
|
|
|
import javax.sound.midi.Track
|
2026-01-19 14:06:55 +03:00
|
|
|
import javax.sound.sampled.AudioSystem
|
|
|
|
|
import javax.sound.sampled.FloatControl
|
2026-01-18 16:22:42 +01:00
|
|
|
|
2026-02-16 17:20:32 +01:00
|
|
|
actual class FMediaPlayer actual constructor(
|
2026-01-18 16:22:42 +01:00
|
|
|
private val filename: String,
|
|
|
|
|
private val onFinished: () -> Unit
|
|
|
|
|
) {
|
|
|
|
|
private var sequencer: Sequencer? = try {
|
|
|
|
|
MidiSystem.getSequencer(false)
|
|
|
|
|
} catch (e: Exception){
|
2026-01-19 14:06:55 +03:00
|
|
|
|
2026-01-18 16:22:42 +01:00
|
|
|
println("Erreur impossible obtenir ${e.message}")
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 14:53:39 +03:00
|
|
|
private val prefs = Preferences.userRoot().node("mg.dot.feufaro")
|
2026-01-19 14:06:55 +03:00
|
|
|
private var synthetizer = MidiSystem.getSynthesizer() as Synthesizer?
|
2026-01-18 16:22:42 +01:00
|
|
|
|
|
|
|
|
private var pointA: Long = -1L
|
|
|
|
|
private var pointB: Long = -1L
|
|
|
|
|
private var isLoopingAB: Boolean = false
|
|
|
|
|
|
2026-02-17 16:23:53 +03:00
|
|
|
private val voiceVolumes = FloatArray(4) { 127f }
|
2026-01-18 16:22:42 +01:00
|
|
|
private var currentGlobalVolume: Float = 0.8f
|
2026-03-11 14:47:30 +03:00
|
|
|
private var currentDynamicVelocity: Int = Dynamic.MF.velocity
|
|
|
|
|
private var currentDynamicFactor: Float = Dynamic.MF.factor
|
2026-01-18 16:22:42 +01:00
|
|
|
|
|
|
|
|
private var currentTempo: Float = 1.0f
|
2026-03-10 09:33:21 +03:00
|
|
|
private var targetBpm: Float = 120f
|
2026-01-28 16:06:53 +03:00
|
|
|
private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
|
|
|
private var abJob: Job? = null
|
2026-01-18 16:22:42 +01:00
|
|
|
|
2026-03-04 15:25:02 +03:00
|
|
|
private var navigationJob: Job? = null
|
|
|
|
|
private data class NavigationStep(
|
2026-03-10 13:59:52 +03:00
|
|
|
val marker: String,
|
|
|
|
|
val gridIndex: Int, // L'ancre absolue
|
|
|
|
|
val targetGrid: Int = 0,
|
2026-03-04 15:25:02 +03:00
|
|
|
val isHold: Boolean = false, // point d'orgue
|
|
|
|
|
var alreadyDone: Boolean = false, // done
|
|
|
|
|
var beatInDC: Int = 1, // temps note sur le marker
|
2026-03-11 14:47:30 +03:00
|
|
|
val dynamic: Dynamic?= null, // velocité
|
2026-03-12 12:11:01 +03:00
|
|
|
// hairpin
|
|
|
|
|
val hairPin: Char? = null,
|
|
|
|
|
val hairPinEndGrid: Int = -1,
|
|
|
|
|
val hairPinFromFactor: Float = 1.0f,
|
|
|
|
|
val hairPinToFactor: Float = 1.0f,
|
2026-03-04 15:25:02 +03:00
|
|
|
)
|
|
|
|
|
private val navigationSteps = mutableListOf<NavigationStep>()
|
|
|
|
|
|
2026-03-10 13:59:52 +03:00
|
|
|
private var boundModel: SharedScreenModel? = null
|
2026-01-18 16:22:42 +01:00
|
|
|
init {
|
2026-01-19 14:06:55 +03:00
|
|
|
try {
|
|
|
|
|
sequencer?.open()
|
|
|
|
|
synthetizer?.open()
|
|
|
|
|
val transmitter = sequencer?.transmitter
|
|
|
|
|
val synthReceiver = synthetizer?.receiver
|
|
|
|
|
transmitter?.receiver = synthReceiver
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val file = File(filename)
|
2026-03-02 10:08:52 +03:00
|
|
|
if (file.exists()) {
|
|
|
|
|
val mySequence = MidiSystem.getSequence(file)
|
|
|
|
|
sequencer?.sequence = mySequence
|
2026-03-10 13:59:52 +03:00
|
|
|
stripTempoEvents(mySequence)
|
|
|
|
|
sequencer?.tempoFactor = 1.0f
|
2026-02-18 10:44:24 +03:00
|
|
|
loadVoiceVolumes()
|
2026-01-19 14:06:55 +03:00
|
|
|
applyVoiceStates()
|
|
|
|
|
sequencer?.addMetaEventListener { meta ->
|
|
|
|
|
if(meta.type == 47){
|
2026-03-13 14:51:45 +03:00
|
|
|
val pendingDc = getPendingDcStep()
|
|
|
|
|
|
2026-03-16 14:56:06 +03:00
|
|
|
if (pendingDc != null) {
|
2026-03-13 14:51:45 +03:00
|
|
|
println("onFinished : DC pending → saut direct vers ${pendingDc.targetGrid}")
|
|
|
|
|
pendingDc.alreadyDone = true
|
|
|
|
|
seekToGrid(pendingDc.targetGrid)
|
|
|
|
|
play()
|
|
|
|
|
} else {
|
|
|
|
|
onFinished()
|
2026-03-10 13:59:52 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-13 14:51:45 +03:00
|
|
|
}
|
2026-02-02 14:20:03 +03:00
|
|
|
} else {
|
|
|
|
|
// Créeons une fichier vide au 1er lancement de l'application, après MidiWriterKotlin l'écrasera
|
|
|
|
|
val f0file = File("${getConfigDirectoryPath()}whawyd3.mid")
|
|
|
|
|
f0file.createNewFile()
|
2026-01-19 14:06:55 +03:00
|
|
|
}
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
2026-01-19 14:06:55 +03:00
|
|
|
|
2026-03-10 13:59:52 +03:00
|
|
|
private fun stripTempoEvents(sequence: Sequence) {
|
|
|
|
|
for (track in sequence.tracks) {
|
|
|
|
|
val eventsToRemove = mutableListOf<javax.sound.midi.MidiEvent>()
|
|
|
|
|
for (i in 0 until track.size()) {
|
|
|
|
|
val event = track.get(i)
|
|
|
|
|
val message = event.message
|
|
|
|
|
if (message is MetaMessage && message.type == 0x51) { // 0x51 = Set Tempo
|
|
|
|
|
eventsToRemove.add(event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
eventsToRemove.forEach { track.remove(it) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private fun applyBpm() {
|
|
|
|
|
sequencer?.tempoInBPM = targetBpm
|
|
|
|
|
sequencer?.tempoFactor = 1.0f
|
|
|
|
|
}
|
|
|
|
|
private fun forceTempo(bpm: Double) {
|
|
|
|
|
sequencer?.tempoInBPM = bpm.toFloat()
|
|
|
|
|
sequencer?.tempoFactor = 1.0f
|
|
|
|
|
}
|
2026-03-04 15:25:02 +03:00
|
|
|
private fun resetNavigationFlags() {
|
|
|
|
|
navigationSteps.forEach { it.alreadyDone = false }
|
|
|
|
|
}
|
2026-03-11 14:47:30 +03:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-12 12:11:01 +03:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-04 15:25:02 +03:00
|
|
|
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
|
|
|
|
|
val metadataList = sharedScreenModel.getFullMarkers()
|
|
|
|
|
navigationSteps.clear()
|
2026-03-10 13:59:52 +03:00
|
|
|
var lastSegno = 0
|
2026-03-12 12:11:01 +03:00
|
|
|
var lastFactor = Dynamic.MF.factor
|
2026-03-10 13:59:52 +03:00
|
|
|
|
|
|
|
|
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
|
|
|
|
|
val currentIndex = gridIndex ?: 0
|
2026-03-04 15:25:02 +03:00
|
|
|
|
2026-03-10 13:59:52 +03:00
|
|
|
val dsRegex = Regex("""D\.?S\.?""")
|
|
|
|
|
val dcRegex = Regex("""D\.?C\.?""")
|
|
|
|
|
val dsGPattern = Regex("""D\.?S\.?_GROUP_PART""")
|
|
|
|
|
val dcGPattern = Regex("""D\.?C\.?_GROUP_PART""")
|
2026-03-04 15:25:02 +03:00
|
|
|
|
2026-03-10 13:59:52 +03:00
|
|
|
val last_grid = sharedScreenModel.getTotalGridCount()
|
2026-03-12 12:11:01 +03:00
|
|
|
val hairPins = sharedScreenModel.getHairPins()
|
2026-03-04 15:25:02 +03:00
|
|
|
when {
|
2026-03-11 14:47:30 +03:00
|
|
|
// segno
|
2026-03-10 13:59:52 +03:00
|
|
|
marker.contains("$") -> {
|
|
|
|
|
lastSegno = currentIndex
|
|
|
|
|
println("Cible ($) mémorisée au $lastSegno")
|
|
|
|
|
}
|
2026-03-11 14:47:30 +03:00
|
|
|
// Point d'orgue
|
2026-03-04 15:25:02 +03:00
|
|
|
marker.contains("\uD834\uDD10") -> {
|
|
|
|
|
val beat = if(note.contains('•')) 2 else 1 // demi-ton sur .) ou non
|
2026-03-10 13:59:52 +03:00
|
|
|
navigationSteps.add(
|
|
|
|
|
FMediaPlayer.NavigationStep(
|
|
|
|
|
marker,
|
|
|
|
|
currentIndex,
|
|
|
|
|
isHold = true,
|
|
|
|
|
beatInDC = beat
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
println("Point d'orgue (\uD834\uDD10) mémorisée au grille n° $gridIndex")
|
2026-03-04 15:25:02 +03:00
|
|
|
}
|
2026-03-10 16:09:54 +03:00
|
|
|
// DS
|
2026-03-10 13:59:52 +03:00
|
|
|
dsGPattern.matches(marker.trim())-> {
|
|
|
|
|
val target = if (lastSegno > 0) lastSegno else 0
|
|
|
|
|
var indx = if((last_grid - currentIndex) <= 0) {
|
2026-03-10 16:09:54 +03:00
|
|
|
currentIndex - 1
|
2026-03-10 13:59:52 +03:00
|
|
|
} else currentIndex
|
|
|
|
|
navigationSteps.add(
|
|
|
|
|
NavigationStep(
|
|
|
|
|
marker,
|
|
|
|
|
gridIndex = indx,
|
|
|
|
|
targetGrid = target,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
println("Lien créé : $marker à grille n° $indx vers cible $target ........")
|
2026-03-04 15:25:02 +03:00
|
|
|
}
|
2026-03-10 13:59:52 +03:00
|
|
|
dsRegex.matches(marker.trim()) || marker == "DSFin" -> {
|
|
|
|
|
val target = if (lastSegno > 0) lastSegno else 0
|
|
|
|
|
|
|
|
|
|
navigationSteps.add(
|
|
|
|
|
NavigationStep(
|
|
|
|
|
marker,
|
|
|
|
|
currentIndex,
|
|
|
|
|
targetGrid = target
|
|
|
|
|
))
|
|
|
|
|
println("Lien DS créé : Saut immédiat à $gridIndex vers Segno $target")
|
2026-03-10 16:09:54 +03:00
|
|
|
}
|
2026-03-10 13:59:52 +03:00
|
|
|
|
|
|
|
|
// DC
|
|
|
|
|
dcGPattern.matches(marker.trim()) -> {
|
2026-03-13 14:51:45 +03:00
|
|
|
val indx = if((last_grid - currentIndex) <= 0) {
|
2026-03-10 13:59:52 +03:00
|
|
|
currentIndex - 1
|
|
|
|
|
} else currentIndex
|
2026-03-13 14:51:45 +03:00
|
|
|
// println("monn index est $indx car dernier est $last_grid et curr $currentIndex")
|
2026-03-10 13:59:52 +03:00
|
|
|
navigationSteps.add(
|
|
|
|
|
NavigationStep(
|
|
|
|
|
marker,
|
|
|
|
|
indx,
|
|
|
|
|
targetGrid = 0
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
println("Lien créé : $marker à $indx vers cible 0 ")
|
|
|
|
|
}
|
|
|
|
|
(dcRegex.matches(marker.trim()) && !marker.contains("DC_GROUP_PART")) -> {
|
|
|
|
|
println("dernier grille $last_grid")
|
|
|
|
|
var indx = if((last_grid - currentIndex) <= 0) {
|
|
|
|
|
currentIndex /*- 1*/
|
|
|
|
|
} else currentIndex
|
|
|
|
|
navigationSteps.add(
|
|
|
|
|
NavigationStep(
|
|
|
|
|
marker,
|
|
|
|
|
indx,
|
|
|
|
|
targetGrid = 0
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
println("Lien DC créé : $marker à $indx vers le début")
|
2026-03-04 15:25:02 +03:00
|
|
|
}
|
2026-03-12 12:11:01 +03:00
|
|
|
|
2026-03-11 14:47:30 +03:00
|
|
|
// velocité
|
2026-03-12 12:11:01 +03:00
|
|
|
extractDynamic(marker) != null && marker.trim() != "=" -> {
|
|
|
|
|
val dyn = extractDynamic(marker) ?: return@forEach
|
|
|
|
|
lastFactor = when (dyn) {
|
|
|
|
|
Dynamic.MF -> 1.0f
|
|
|
|
|
else -> dyn.factor
|
|
|
|
|
}
|
2026-03-11 14:47:30 +03:00
|
|
|
navigationSteps.add(
|
|
|
|
|
NavigationStep(
|
|
|
|
|
marker = marker,
|
|
|
|
|
gridIndex = currentIndex,
|
|
|
|
|
dynamic = dyn
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
println("Dynamique '${dyn.label}' (vel=${dyn.velocity}) mémorisée à la grille $currentIndex")
|
|
|
|
|
}
|
2026-03-12 12:11:01 +03:00
|
|
|
// 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})")
|
|
|
|
|
}
|
2026-03-04 15:25:02 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-13 14:51:45 +03:00
|
|
|
private fun getPendingDcStep(): NavigationStep? {
|
|
|
|
|
return navigationSteps.firstOrNull { step ->
|
|
|
|
|
!step.alreadyDone &&
|
|
|
|
|
step.hairPin == null &&
|
|
|
|
|
step.dynamic == null &&
|
|
|
|
|
!step.isHold
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-04 15:25:02 +03:00
|
|
|
private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) {
|
|
|
|
|
var dcDone = sharedScreenModel.getDcDone()
|
|
|
|
|
val dsDone = sharedScreenModel.getDsDone()
|
|
|
|
|
|
2026-03-10 13:59:52 +03:00
|
|
|
navigationJob?.cancel()
|
|
|
|
|
navigationJob = playerScope.launch(Dispatchers.Default) {
|
|
|
|
|
sharedScreenModel.activeIndex.collect { currentIndex ->
|
2026-03-04 15:25:02 +03:00
|
|
|
|
2026-03-10 13:59:52 +03:00
|
|
|
val availableIndices = navigationSteps.map { it.gridIndex }
|
2026-03-13 14:51:45 +03:00
|
|
|
// println("bpm:$targetBpm _ ${sequencer?.tempoInBPM}| Index en mémoire : $availableIndices")
|
|
|
|
|
// println("i $currentIndex ")
|
2026-03-04 15:25:02 +03:00
|
|
|
|
|
|
|
|
if (sequencer?.isRunning == true) {
|
2026-03-10 13:59:52 +03:00
|
|
|
if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1) {
|
|
|
|
|
forceTempo(targetBpm.toDouble())
|
|
|
|
|
}
|
|
|
|
|
if (currentIndex < 0) return@collect
|
2026-03-04 15:25:02 +03:00
|
|
|
|
|
|
|
|
val step = navigationSteps.find {
|
2026-03-10 13:59:52 +03:00
|
|
|
it.gridIndex == currentIndex &&
|
2026-03-04 15:25:02 +03:00
|
|
|
!it.alreadyDone
|
|
|
|
|
}
|
2026-03-10 13:59:52 +03:00
|
|
|
|
2026-03-04 15:25:02 +03:00
|
|
|
if (step != null) {
|
2026-03-11 14:47:30 +03:00
|
|
|
when {
|
2026-03-12 12:11:01 +03:00
|
|
|
// Point d'orgue
|
2026-03-11 14:47:30 +03:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
synthetizer?.channels?.forEach { it?.controlChange(64, 127) }
|
|
|
|
|
val previousFactor = sequencer?.tempoFactor ?: 1.0f
|
|
|
|
|
sequencer?.tempoFactor = 0.0001f
|
|
|
|
|
|
|
|
|
|
delay(holdDuration)
|
|
|
|
|
|
|
|
|
|
sequencer?.tempoFactor = previousFactor
|
|
|
|
|
synthetizer?.channels?.forEach { it?.controlChange(64, 0) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 12:11:01 +03:00
|
|
|
//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
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 14:47:30 +03:00
|
|
|
//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")
|
2026-03-04 15:25:02 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-10 13:59:52 +03:00
|
|
|
private var syncJob: Job? = null
|
|
|
|
|
|
|
|
|
|
private fun startSyncLoop(sharedScreenModel: SharedScreenModel) {
|
|
|
|
|
syncJob?.cancel()
|
|
|
|
|
syncJob = playerScope.launch(Dispatchers.Default) {
|
|
|
|
|
while (isActive) {
|
|
|
|
|
if (sequencer?.isRunning == true) {
|
2026-03-10 16:09:54 +03:00
|
|
|
val resolution = sequencer?.sequence?.resolution?.toLong() ?: 480L
|
|
|
|
|
val gridIndex = (sequencer!!.tickPosition / resolution).toInt()
|
|
|
|
|
sharedScreenModel.updateActiveIndexByIndex(gridIndex)
|
2026-03-10 13:59:52 +03:00
|
|
|
}
|
|
|
|
|
delay(20)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-10 16:09:54 +03:00
|
|
|
actual fun seekToGrid(gridIndex: Int) {
|
2026-03-10 13:59:52 +03:00
|
|
|
val resolution = sequencer?.sequence?.resolution?.toDouble() ?: 480.0
|
2026-03-10 16:09:54 +03:00
|
|
|
val tick = (gridIndex.toDouble() * resolution).toLong()
|
|
|
|
|
sequencer?.tickPosition = tick
|
2026-03-10 13:59:52 +03:00
|
|
|
|
2026-03-10 16:09:54 +03:00
|
|
|
applyBpm()
|
2026-03-11 14:47:30 +03:00
|
|
|
// Voir les velocity avant
|
|
|
|
|
val lastDynamic = navigationSteps
|
|
|
|
|
.filter { it.dynamic != null && it.gridIndex <= gridIndex }
|
|
|
|
|
.maxByOrNull { it.gridIndex }
|
|
|
|
|
?.dynamic ?: Dynamic.MF
|
|
|
|
|
currentDynamicFactor = lastDynamic.factor
|
|
|
|
|
applyVoiceStates()
|
2026-03-10 13:59:52 +03:00
|
|
|
}
|
2026-03-04 15:25:02 +03:00
|
|
|
|
2026-01-18 16:22:42 +01:00
|
|
|
actual fun play(){
|
2026-03-10 13:59:52 +03:00
|
|
|
if (sequencer?.isOpen == true){
|
|
|
|
|
applyBpm()
|
|
|
|
|
sequencer?.start()
|
|
|
|
|
println("La sequence vient d etre lancé ${sequencer?.isRunning}")
|
|
|
|
|
// sequencer!!.open()
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
actual fun pause(){
|
|
|
|
|
sequencer?.stop()
|
|
|
|
|
}
|
2026-03-02 10:08:52 +03:00
|
|
|
actual fun requestSync(sharedScreenModel: SharedScreenModel) {
|
|
|
|
|
val seq = sequencer?.sequence
|
|
|
|
|
if (seq != null) {
|
|
|
|
|
syncTuoWithMidi(seq, sharedScreenModel)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 15:25:02 +03:00
|
|
|
actual fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel) {
|
2026-03-10 13:59:52 +03:00
|
|
|
this.boundModel = sharedScreenModel
|
2026-03-04 15:25:02 +03:00
|
|
|
prepareNavigation(sharedScreenModel)
|
|
|
|
|
startNavigationMonitor(sharedScreenModel)
|
2026-03-10 13:59:52 +03:00
|
|
|
startSyncLoop(sharedScreenModel)
|
2026-03-04 15:25:02 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-02 10:08:52 +03:00
|
|
|
fun syncTuoWithMidi(sequence: Sequence, sharedScreenModel: SharedScreenModel) {
|
|
|
|
|
val timestamps = mutableListOf<Long>()
|
|
|
|
|
val resolution = sequence.resolution.toDouble()
|
2026-03-10 13:59:52 +03:00
|
|
|
val bpm = targetBpm/*sequencer?.tempoInBPM?.toDouble() ?: 120.0*/
|
2026-03-02 10:08:52 +03:00
|
|
|
val usPerTick = (60_000_000.0 / bpm) / resolution
|
|
|
|
|
|
|
|
|
|
// détecter par 60 ticks càd par un temp (noir)
|
|
|
|
|
val totalTicks = sequence.tickLength
|
|
|
|
|
val step = 60L
|
|
|
|
|
for (tick in 0..totalTicks step step) {
|
|
|
|
|
val microsecond = (tick * usPerTick).toLong()
|
|
|
|
|
timestamps.add(microsecond)
|
2026-03-10 16:09:54 +03:00
|
|
|
// println("Note detectée au grille: ${tick/60} -> Temps: ${microsecond / 1000} ms")
|
2026-03-02 10:08:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Détection de tous les note y compris les /2 /4 temps
|
|
|
|
|
/*val processedTicks = mutableSetOf<Long>()
|
|
|
|
|
|
|
|
|
|
for (track in sequence.tracks) {
|
|
|
|
|
for (i in 0 until track.size()) {
|
|
|
|
|
val event = track.get(i)
|
|
|
|
|
val message = event.message
|
|
|
|
|
if (message is ShortMessage *//*&& message.command == ShortMessage.NOTE_ON *//*&& message.data2 > 0) {
|
|
|
|
|
if (!processedTicks.contains(event.tick)) {
|
|
|
|
|
val microsecond = (event.tick * usPerTick).toLong()
|
|
|
|
|
timestamps.add(microsecond)
|
|
|
|
|
processedTicks.add(event.tick)
|
|
|
|
|
println("Note detectée au Tick: ${event.tick} -> Temps: ${microsecond / 1000} ms")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}*/
|
|
|
|
|
val sortedList = timestamps.sorted()
|
|
|
|
|
sharedScreenModel.updateTimestamps(sortedList)
|
2026-03-10 16:09:54 +03:00
|
|
|
// println("SUR tempo $targetBpm Synchronization terminer et mis à jour")
|
2026-03-02 10:08:52 +03:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 16:22:42 +01:00
|
|
|
actual fun stop(){
|
2026-03-04 15:25:02 +03:00
|
|
|
// sequencer?.stop()
|
2026-01-18 16:22:42 +01:00
|
|
|
sequencer?.microsecondPosition = 0
|
2026-03-04 15:25:02 +03:00
|
|
|
navigationJob?.cancel()
|
|
|
|
|
resetNavigationFlags()
|
|
|
|
|
navigationSteps.clear()
|
2026-01-19 14:06:55 +03:00
|
|
|
clearLoop()
|
2026-03-11 14:47:30 +03:00
|
|
|
currentDynamicFactor = Dynamic.MF.factor
|
2026-03-04 15:25:02 +03:00
|
|
|
// release()
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
|
|
|
|
actual fun getDuration(): Long {
|
|
|
|
|
return (sequencer?.microsecondLength ?: 0L) / 1000
|
|
|
|
|
}
|
|
|
|
|
actual fun getCurrentPosition(): Long {
|
|
|
|
|
return (sequencer?.microsecondPosition ?: 0L) / 1000
|
|
|
|
|
}
|
|
|
|
|
actual fun seekTo(position: Long) {
|
|
|
|
|
sequencer?.microsecondPosition = position * 1000
|
2026-03-10 13:59:52 +03:00
|
|
|
applyBpm()
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
2026-03-04 15:25:02 +03:00
|
|
|
actual fun release() {
|
2026-01-18 16:22:42 +01:00
|
|
|
sequencer?.close()
|
2026-01-19 14:06:55 +03:00
|
|
|
synthetizer?.close()
|
2026-01-28 16:06:53 +03:00
|
|
|
playerScope.cancel()
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
|
|
|
|
actual fun setVolume(level: Float) {
|
|
|
|
|
try {
|
|
|
|
|
this.currentGlobalVolume = level
|
2026-03-11 14:47:30 +03:00
|
|
|
applyVoiceStates()
|
|
|
|
|
/*val volumeInt = (level * 127).toInt().coerceIn(0, 127)
|
2026-01-18 16:22:42 +01:00
|
|
|
|
|
|
|
|
synthetizer?.channels?.forEachIndexed { index, channel ->
|
|
|
|
|
if (index < 4) {
|
2026-02-18 10:44:24 +03:00
|
|
|
val vol = if (voiceVolumes[index] != 0f) volumeInt else 0
|
2026-01-18 16:22:42 +01:00
|
|
|
channel?.controlChange(7, vol)
|
|
|
|
|
} else {
|
|
|
|
|
channel?.controlChange(7, volumeInt)
|
|
|
|
|
}
|
2026-03-11 14:47:30 +03:00
|
|
|
}*/
|
2026-01-18 16:22:42 +01:00
|
|
|
val mixer = AudioSystem.getMixer(null)
|
|
|
|
|
val lines = mixer.sourceLines
|
|
|
|
|
for (line in lines) {
|
|
|
|
|
if(line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
|
|
|
|
|
val gainControl = line.getControl(FloatControl.Type.MASTER_GAIN) as FloatControl
|
|
|
|
|
|
|
|
|
|
val dB = (Math.log10(level.toDouble().coerceAtLeast(0.0001)) * 20).toFloat()
|
|
|
|
|
gainControl.value = dB.coerceIn(gainControl.minimum, gainControl.maximum)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception){
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
}
|
2026-03-10 13:59:52 +03:00
|
|
|
// println("la volume $level")
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actual fun setPointA() {
|
|
|
|
|
pointA = sequencer?.tickPosition ?: 0L
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actual fun setPointB() {
|
|
|
|
|
pointB = sequencer?.tickPosition ?: 0L
|
|
|
|
|
if (pointB > pointA && pointA != -1L) {
|
|
|
|
|
isLoopingAB = true
|
|
|
|
|
startABMonitor()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actual fun clearLoop() {
|
|
|
|
|
isLoopingAB = false
|
|
|
|
|
pointA = -1L
|
|
|
|
|
pointB = -1L
|
|
|
|
|
}
|
|
|
|
|
private fun startABMonitor() {
|
2026-01-28 16:06:53 +03:00
|
|
|
abJob?.cancel()
|
|
|
|
|
abJob = playerScope.launch {
|
2026-01-18 16:22:42 +01:00
|
|
|
while(isLoopingAB) {
|
|
|
|
|
val currentTick = sequencer?.tickPosition?: 0L
|
|
|
|
|
if (currentTick >= pointB) {
|
|
|
|
|
sequencer?.tickPosition = pointA
|
|
|
|
|
}
|
|
|
|
|
delay(50)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB)
|
|
|
|
|
|
|
|
|
|
actual fun toggleVoice(index: Int) {
|
2026-02-18 10:44:24 +03:00
|
|
|
saveVoicesVolumes()
|
2026-01-18 16:22:42 +01:00
|
|
|
applyVoiceStates()
|
|
|
|
|
}
|
2026-02-17 16:23:53 +03:00
|
|
|
actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) {
|
|
|
|
|
if (voiceIndex in 0..3) {
|
|
|
|
|
voiceVolumes[voiceIndex] = newVolume
|
2026-02-18 10:44:24 +03:00
|
|
|
saveVoicesVolumes()
|
2026-02-17 16:23:53 +03:00
|
|
|
applyVoiceStates()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 16:22:42 +01:00
|
|
|
|
|
|
|
|
private fun applyVoiceStates() {
|
|
|
|
|
try {
|
|
|
|
|
synthetizer?.channels?.let { channels ->
|
2026-02-18 10:44:24 +03:00
|
|
|
for (i in 0 until 4) {
|
|
|
|
|
if (i < channels.size) {
|
2026-03-11 14:47:30 +03:00
|
|
|
val userVol = voiceVolumes[i] / 127f
|
|
|
|
|
val globalVol = currentGlobalVolume
|
|
|
|
|
|
|
|
|
|
val finalVol = (127f * userVol * globalVol * currentDynamicFactor).toInt().coerceIn(0, 127)
|
|
|
|
|
|
2026-03-12 12:11:01 +03:00
|
|
|
// println("Voice[$i] user=${voiceVolumes[i]} global=$currentGlobalVolume dynFactor=$currentDynamicFactor → finalVol=$finalVol")
|
2026-03-11 14:47:30 +03:00
|
|
|
|
|
|
|
|
channels[i].controlChange(7, finalVol)
|
|
|
|
|
channels[i].controlChange(11, finalVol)
|
|
|
|
|
|
|
|
|
|
if (finalVol == 0) channels[i].controlChange(123, 0)
|
|
|
|
|
/*val targetVolume = voiceVolumes[i].toInt()
|
2026-02-18 10:44:24 +03:00
|
|
|
|
|
|
|
|
channels[i].controlChange(7, targetVolume)
|
|
|
|
|
channels[i].controlChange(11, targetVolume)
|
|
|
|
|
|
|
|
|
|
if (targetVolume == 0) {
|
|
|
|
|
channels[i].controlChange(123, 0)
|
2026-03-11 14:47:30 +03:00
|
|
|
}*/
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
2026-03-10 13:59:52 +03:00
|
|
|
// println("SATB $i Volumes: ${voiceVolumes[i]}")
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) { e.printStackTrace() }
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 10:44:24 +03:00
|
|
|
actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
|
2026-01-18 16:22:42 +01:00
|
|
|
|
|
|
|
|
actual fun changeInstru(noInstru: Int) {
|
|
|
|
|
val pgm = noInstru
|
|
|
|
|
synthetizer?.channels?.let {
|
|
|
|
|
channels ->
|
|
|
|
|
for (i in 0 until 4) {
|
|
|
|
|
channels[i].programChange(pgm)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
actual fun getCurrentBPM(): Float {
|
2026-03-10 09:33:21 +03:00
|
|
|
return targetBpm
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 09:33:21 +03:00
|
|
|
actual fun setTempo(bpm: Float){
|
|
|
|
|
this.targetBpm = bpm
|
|
|
|
|
sequencer?.tempoInBPM = bpm
|
|
|
|
|
boundModel?.let { modele ->
|
2026-03-10 16:09:54 +03:00
|
|
|
val seq = sequencer?.sequence
|
|
|
|
|
if (seq != null) {
|
|
|
|
|
syncTuoWithMidi(seq, modele)
|
|
|
|
|
}
|
2026-03-10 09:33:21 +03:00
|
|
|
prepareNavigation(modele)
|
|
|
|
|
}
|
|
|
|
|
println("Tempo réglé à : $bpm BPM")
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
2026-01-19 14:53:39 +03:00
|
|
|
|
2026-02-18 10:44:24 +03:00
|
|
|
private fun saveVoicesVolumes() {
|
|
|
|
|
val data = voiceVolumes.joinToString(",")
|
|
|
|
|
prefs.put("voices_volumes", data)
|
2026-01-19 14:53:39 +03:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 10:44:24 +03:00
|
|
|
private fun loadVoiceVolumes() {
|
|
|
|
|
val data = prefs.get("voices_volumes", "127,127,127,127")
|
|
|
|
|
val volumesArray = data.split(",")
|
2026-01-19 14:53:39 +03:00
|
|
|
|
2026-02-18 10:44:24 +03:00
|
|
|
if (volumesArray.size == 4) {
|
|
|
|
|
for (i in 0 until 4) {
|
|
|
|
|
voiceVolumes[i] = volumesArray[i].toFloatOrNull() ?: 127f
|
|
|
|
|
}
|
2026-01-19 14:53:39 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-19 14:06:55 +03:00
|
|
|
}
|