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

248 lines
7.6 KiB
Kotlin
Raw Normal View History

2026-01-18 16:22:42 +01:00
package mg.dot.feufaro.midi
import kotlinx.coroutines.*
2026-02-02 14:20:03 +03:00
import mg.dot.feufaro.getConfigDirectoryPath
2026-01-18 16:22:42 +01:00
import java.io.ByteArrayInputStream
import java.io.File
2026-01-19 14:53:39 +03:00
import java.util.prefs.Preferences
2026-01-18 16:22:42 +01:00
import javax.sound.midi.MidiSystem
import javax.sound.midi.Sequencer
2026-01-19 14:53:39 +03:00
import javax.sound.midi.Synthesizer
import javax.sound.sampled.AudioFormat
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 voiceStates = mutableListOf(true, true, true, true)
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
private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private var abJob: Job? = 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()){
sequencer?.sequence = MidiSystem.getSequence(file)
2026-01-19 14:53:39 +03:00
loadVoiceStates()
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-01-18 16:22:42 +01:00
actual fun play(){
if (sequencer!!.isOpen){
sequencer?.start()
println("La sequence vient d etre lancé ${sequencer?.isRunning}")
}
}
actual fun pause(){
sequencer?.stop()
}
actual fun stop(){
sequencer?.stop()
sequencer?.microsecondPosition = 0
clearLoop()
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
}
fun release() {
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 (voiceStates[index]) volumeInt else 0
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) {
voiceStates[index] = !voiceStates[index]
2026-01-19 14:53:39 +03:00
saveVoiceStates()
2026-01-18 16:22:42 +01:00
applyVoiceStates()
}
actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) {
if (voiceIndex in 0..3) {
voiceVolumes[voiceIndex] = newVolume
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 volume = voiceVolumes[i].toInt()
val isVoiceActive = voiceStates[i]
val currentVolume = voiceVolumes[i].toInt()
if (volume == 0) {
channels[i].controlChange(123, 0)
voiceStates[i] = false
} else {
channels[i].controlChange(7, currentVolume)
channels[i].controlChange(11, currentVolume)
voiceStates[i] = true
2026-01-18 16:22:42 +01:00
}
}
println("SATB màj: $voiceStates, Volumes: ${voiceVolumes[i]}")
2026-01-18 16:22:42 +01:00
}
}
2026-01-18 16:22:42 +01:00
} catch (e: Exception) { e.printStackTrace() }
}
actual fun getVoiceStates(): List<Boolean> = voiceStates
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 {
val currentFactor = sequencer?.tempoFactor ?: 1.0f
val currentBPM = (120f * currentFactor)
return currentBPM
}
actual fun setTempo(factor: Float){
currentTempo = factor
sequencer?.tempoFactor = factor
}
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 saveVoiceStates() {
val data = voiceStates.joinToString(",")
prefs.put("voice_states", data)
}
private fun loadVoiceStates() {
val defaultValue = "true,true,true,true"
val savedData = prefs.get("voice_states", defaultValue)
val states = savedData.split(",").map { it.toBoolean() }
for (i in 0 until 4) {
if (i < states.size) voiceStates[i] = states[i]
}
}
}