2026-01-18 16:22:42 +01:00
|
|
|
package mg.dot.feufaro.midi
|
|
|
|
|
|
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-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
|
2026-01-19 14:06:55 +03:00
|
|
|
import javax.sound.sampled.AudioFormat
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
private val voiceStates = mutableListOf(true, true, true, true)
|
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
|
|
|
|
|
|
|
|
|
|
private var currentTempo: Float = 1.0f
|
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
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
if (file.exists()){
|
|
|
|
|
sequencer?.sequence = MidiSystem.getSequence(file)
|
2026-01-19 14:53:39 +03:00
|
|
|
loadVoiceStates()
|
2026-01-19 14:06:55 +03:00
|
|
|
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-19 14:06:55 +03:00
|
|
|
}
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
2026-01-19 14:06:55 +03: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
|
2026-01-19 14:06:55 +03:00
|
|
|
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()
|
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
|
|
|
|
|
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() {
|
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) {
|
|
|
|
|
voiceStates[index] = !voiceStates[index]
|
2026-01-19 14:53:39 +03:00
|
|
|
saveVoiceStates()
|
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
|
|
|
|
|
applyVoiceStates()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 16:22:42 +01:00
|
|
|
|
|
|
|
|
private fun applyVoiceStates() {
|
|
|
|
|
try {
|
|
|
|
|
synthetizer?.channels?.let { channels ->
|
2026-02-17 16:23:53 +03:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
2026-02-17 16:23:53 +03:00
|
|
|
println("SATB màj: $voiceStates, Volumes: ${voiceVolumes[i]}")
|
2026-01-18 16:22:42 +01:00
|
|
|
}
|
2026-02-17 16:23:53 +03: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
|
2026-01-19 14:06:55 +03:00
|
|
|
private val MS8PER_NOTE = 500L
|
2026-01-18 16:22:42 +01:00
|
|
|
|
2026-01-19 14:06:55 +03: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
|
|
|
}
|
|
|
|
|
}
|
2026-01-19 14:06:55 +03: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]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 14:06:55 +03:00
|
|
|
}
|