feufaro/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt

723 lines
28 KiB
Kotlin
Raw Normal View History

2026-01-18 16:22:42 +01:00
package mg.dot.feufaro.midi
import SharedScreenModel
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
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
import javax.sound.sampled.AudioSystem
import javax.sound.sampled.FloatControl
2026-01-18 16:22:42 +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-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")
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
private val voiceVolumes = FloatArray(4) { 127f }
2026-01-18 16:22:42 +01:00
private var currentGlobalVolume: Float = 0.8f
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
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(
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
val dynamic: Dynamic?= null, // velocité
// 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>()
private var boundModel: SharedScreenModel? = null
2026-01-18 16:22:42 +01:00
init {
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)
if (file.exists()) {
val mySequence = MidiSystem.getSequence(file)
sequencer?.sequence = mySequence
stripTempoEvents(mySequence)
sequencer?.tempoFactor = 1.0f
loadVoiceVolumes()
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-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-18 16:22:42 +01: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 }
}
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)
}
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()
var lastSegno = 0
var lastFactor = Dynamic.MF.factor
metadataList.forEach { (timestamp, gridIndex, template, lastCallerMarker, marker, noteBefore, separat, note) ->
val currentIndex = gridIndex ?: 0
2026-03-04 15:25:02 +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
val last_grid = sharedScreenModel.getTotalGridCount()
val hairPins = sharedScreenModel.getHairPins()
2026-03-04 15:25:02 +03:00
when {
// segno
marker.contains("$") -> {
lastSegno = currentIndex
println("Cible ($) mémorisée au $lastSegno")
}
// 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
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
}
// DS
dsGPattern.matches(marker.trim())-> {
val target = if (lastSegno > 0) lastSegno else 0
var indx = if((last_grid - currentIndex) <= 0) {
currentIndex - 1
} 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
}
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")
}
// DC
dcGPattern.matches(marker.trim()) -> {
2026-03-13 14:51:45 +03:00
val indx = if((last_grid - currentIndex) <= 0) {
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")
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
}
// velocité
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,
gridIndex = currentIndex,
dynamic = dyn
)
)
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})")
}
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()
navigationJob?.cancel()
navigationJob = playerScope.launch(Dispatchers.Default) {
sharedScreenModel.activeIndex.collect { currentIndex ->
2026-03-04 15:25:02 +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) {
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 {
it.gridIndex == currentIndex &&
2026-03-04 15:25:02 +03:00
!it.alreadyDone
}
2026-03-04 15:25:02 +03:00
if (step != null) {
when {
// Point d'orgue
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) }
}
//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)
}
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
}
}
}
}
}
}
}
private var syncJob: Job? = null
private fun startSyncLoop(sharedScreenModel: SharedScreenModel) {
syncJob?.cancel()
syncJob = playerScope.launch(Dispatchers.Default) {
while (isActive) {
if (sequencer?.isRunning == true) {
val resolution = sequencer?.sequence?.resolution?.toLong() ?: 480L
val gridIndex = (sequencer!!.tickPosition / resolution).toInt()
sharedScreenModel.updateActiveIndexByIndex(gridIndex)
}
delay(20)
}
}
}
actual fun seekToGrid(gridIndex: Int) {
val resolution = sequencer?.sequence?.resolution?.toDouble() ?: 480.0
val tick = (gridIndex.toDouble() * resolution).toLong()
sequencer?.tickPosition = tick
applyBpm()
// 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-04 15:25:02 +03:00
2026-01-18 16:22:42 +01:00
actual fun play(){
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()
}
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) {
this.boundModel = sharedScreenModel
2026-03-04 15:25:02 +03:00
prepareNavigation(sharedScreenModel)
startNavigationMonitor(sharedScreenModel)
startSyncLoop(sharedScreenModel)
2026-03-04 15:25:02 +03:00
}
fun syncTuoWithMidi(sequence: Sequence, sharedScreenModel: SharedScreenModel) {
val timestamps = mutableListOf<Long>()
val resolution = sequence.resolution.toDouble()
val bpm = targetBpm/*sequencer?.tempoInBPM?.toDouble() ?: 120.0*/
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)
// println("Note detectée au grille: ${tick/60} -> Temps: ${microsecond / 1000} ms")
}
// 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)
// println("SUR tempo $targetBpm Synchronization terminer et mis à jour")
}
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()
clearLoop()
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
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()
synthetizer?.close()
playerScope.cancel()
2026-01-18 16:22:42 +01:00
}
actual fun setVolume(level: Float) {
try {
this.currentGlobalVolume = level
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) {
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-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()
}
// 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() {
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) {
saveVoicesVolumes()
2026-01-18 16:22:42 +01:00
applyVoiceStates()
}
actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) {
if (voiceIndex in 0..3) {
voiceVolumes[voiceIndex] = newVolume
saveVoicesVolumes()
applyVoiceStates()
}
}
2026-01-18 16:22:42 +01:00
private fun applyVoiceStates() {
try {
synthetizer?.channels?.let { channels ->
for (i in 0 until 4) {
if (i < channels.size) {
val userVol = voiceVolumes[i] / 127f
val globalVol = currentGlobalVolume
val finalVol = (127f * userVol * globalVol * currentDynamicFactor).toInt().coerceIn(0, 127)
// println("Voice[$i] user=${voiceVolumes[i]} global=$currentGlobalVolume dynFactor=$currentDynamicFactor → finalVol=$finalVol")
channels[i].controlChange(7, finalVol)
channels[i].controlChange(11, finalVol)
if (finalVol == 0) channels[i].controlChange(123, 0)
/*val targetVolume = voiceVolumes[i].toInt()
channels[i].controlChange(7, targetVolume)
channels[i].controlChange(11, targetVolume)
if (targetVolume == 0) {
channels[i].controlChange(123, 0)
}*/
2026-01-18 16:22:42 +01:00
}
// println("SATB $i Volumes: ${voiceVolumes[i]}")
2026-01-18 16:22:42 +01:00
}
}
} catch (e: Exception) { e.printStackTrace() }
}
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 ->
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
private fun saveVoicesVolumes() {
val data = voiceVolumes.joinToString(",")
prefs.put("voices_volumes", data)
2026-01-19 14:53:39 +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
if (volumesArray.size == 4) {
for (i in 0 until 4) {
voiceVolumes[i] = volumesArray[i].toFloatOrNull() ?: 127f
}
2026-01-19 14:53:39 +03:00
}
}
}