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

439 lines
16 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-04 15:25:02 +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
import javax.sound.midi.ShortMessage
2026-01-19 14:53:39 +03:00
import javax.sound.midi.Synthesizer
2026-03-04 15:25:02 +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 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 triggerMs: Long, // DC | DS
val targetMs: Long, // farany, $ ,..
val isHold: Boolean = false, // point d'orgue
var alreadyDone: Boolean = false, // done
var beatInDC: Int = 1, // temps note sur le marker
var sustainNedd: Boolean = true
)
private val navigationSteps = mutableListOf<NavigationStep>()
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
loadVoiceVolumes()
applyVoiceStates()
sequencer?.addMetaEventListener { meta ->
if(meta.type == 47){
onFinished()
2026-01-18 16:22:42 +01: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
}
2026-03-04 15:25:02 +03:00
private fun resetNavigationFlags() {
navigationSteps.forEach { it.alreadyDone = false }
}
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
val metadataList = sharedScreenModel.getFullMarkers()
navigationSteps.clear()
var holdPos: Long = 0L
val currentBpm = sequencer?.tempoInBPM ?: 120f
val beatDurationMs = (60_000 / currentBpm).toLong()
metadataList.forEach { (timestamp, template, lastCallerMarker, marker, noteBefore, separat, note) ->
val timeMs = (timestamp / 1000)
when {
marker.contains("\uD834\uDD10") -> {
holdPos = timeMs
val beat = if(note.contains('•')) 2 else 1 // demi-ton sur .) ou non
navigationSteps.add(FMediaPlayer.NavigationStep(timeMs, -1L, isHold = true, beatInDC = beat))
println("Point d'orgue (\uD834\uDD10) mémorisée à $holdPos ms")
}
// Déclencheurs DC
marker == "DC_GROUP_PART" -> {
var needSustaine = false
var trigger = timeMs - (beatDurationMs / 2)
navigationSteps.add(NavigationStep(trigger, targetMs = 0L, sustainNedd = needSustaine))
println("Lien créé : $marker à $trigger ms vers cible 0 ms Avec Sustaine? $needSustaine")
}
/* Un Dc se place sur une note '-'*/
marker == "DC" -> {
var needSustaine = false
val trigger = timeMs
// Si le marker si place près de la fin de la partition
if((getDuration() - timeMs) <= beatDurationMs) {
println("Fin très proche")
needSustaine = true
}
navigationSteps.add(NavigationStep(trigger, targetMs = 0, sustainNedd = needSustaine))
println("Lien créé : $marker à $trigger ms vers le début")
}
}
}
}
private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) {
var dcDone = sharedScreenModel.getDcDone()
val dsDone = sharedScreenModel.getDsDone()
// la durée d'un temps (noire)
val currentBpm = sequencer?.tempoInBPM ?: 120f
val beatDurationMs = (60_000 / currentBpm).toLong()
val remainingTime = getDuration() - getCurrentPosition()
navigationJob?.cancel()
navigationJob = playerScope.launch {
while (isActive) {
if (sequencer?.isRunning == true) {
val currentPos = getCurrentPosition()
val duration = getDuration()
val measure = sharedScreenModel.measure.value
val firstVal = measure.substringBefore("/")
val mesureNum = firstVal.toIntOrNull() ?: 4
// On cherche si on est sur un point de saut
val step = navigationSteps.find {
currentPos >= it.triggerMs &&
currentPos < it.triggerMs + 800 &&
!it.alreadyDone
}
if (step != null) {
if (currentPos < step.triggerMs + 800) {
step.alreadyDone = true
val timeToFinish = duration - currentPos
val isCloseToEnd = timeToFinish < (beatDurationMs * 1.5).toLong()
// println("time to finish = $timeToFinish et isClo $isCloseToEnd")
// point d'orgue
if (step.isHold) {
synthetizer?.channels?.forEach { it?.controlChange(64, 127) }
sequencer?.tempoFactor = 0.0001f
if(step.beatInDC == 1){
delay(beatDurationMs * 2)
} else { //démi-ton sur le point d'orgue
delay(beatDurationMs)
}
println("Est-ce un demi-ton ? ${step.beatInDC} Le delay /2 est : ${(beatDurationMs / step.beatInDC) * 2} et pour 1 ${beatDurationMs * 2}")
synthetizer?.channels?.forEach { it?.controlChange(64, 0) }
sequencer?.tempoFactor = currentTempo
step.alreadyDone=false
} else {
step.alreadyDone = true
// Sustain
if(step.sustainNedd) {
synthetizer?.channels?.forEach { it?.controlChange(64, 127) }
sequencer?.tempoFactor = 0.0001f
delay(beatDurationMs * (mesureNum/2))
synthetizer?.channels?.forEach {
it?.controlChange(64, 0)
it?.controlChange(123, 0)
}
delay(50) // Silence de respiration
sequencer?.tempoFactor = currentTempo
setVolume(currentGlobalVolume)
}
seekTo(step.targetMs)
if (sequencer?.isRunning == false) sequencer?.start()
// delay(500)
}
}
delay(300)
}
}
delay(100)
}
}
}
2026-01-18 16:22:42 +01:00
actual fun play(){
if (!sequencer!!.isOpen){
sequencer!!.open()
2026-01-18 16:22:42 +01:00
}
sequencer?.start()
println("La sequence vient d etre lancé ${sequencer?.isRunning}")
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) {
prepareNavigation(sharedScreenModel)
startNavigationMonitor(sharedScreenModel)
}
fun syncTuoWithMidi(sequence: Sequence, sharedScreenModel: SharedScreenModel) {
val timestamps = mutableListOf<Long>()
val resolution = sequence.resolution.toDouble()
val bpm = 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 Tick: ${tick} -> 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)
}
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()
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-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
val volumeInt = (level * 127).toInt().coerceIn(0, 127)
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)
}
}
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")
}
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 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 ->
prepareNavigation(modele)
}
println("Tempo réglé à : $bpm BPM")
2026-01-18 16:22:42 +01:00
}
fun getTempo(): Float = currentTempo
private val MS8PER_NOTE = 500L
2026-01-18 16:22:42 +01:00
fun seekToNote(index: Int) {
try {
sequencer?.let { sequencer ->
if (sequencer.isOpen) {
val targetPos = index * MS8PER_NOTE * 1000
sequencer.microsecondPosition = targetPos
2026-01-18 16:22:42 +01:00
}
}
} catch (e: Exception) {
e.printStackTrace()
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
}
}
}