From fa603dc71e6b59dc0352f95755986008def021a3 Mon Sep 17 00:00:00 2001 From: "hasinarak3@gmail.com" Date: Wed, 4 Mar 2026 15:25:02 +0300 Subject: [PATCH] =?UTF-8?q?Implement=20DC=20(simple)=20&=20=F0=9D=84=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 3 + .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 10 +- .../mg/dot/feufaro/viewmodel/MidiMetadata.kt | 3 + .../feufaro/viewmodel/SharedScreenModel.kt | 61 ++++++- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 153 +++++++++++++++++- 5 files changed, 222 insertions(+), 8 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index f06c593..0c2fb72 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -2,6 +2,7 @@ package mg.dot.feufaro.midi import SharedScreenModel import mg.dot.feufaro.FileRepository +import mg.dot.feufaro.viewmodel.MidiMarkers expect class FMediaPlayer(filename: String, onFinished: () -> Unit) { fun play() @@ -22,6 +23,8 @@ expect class FMediaPlayer(filename: String, onFinished: () -> Unit) { fun getCurrentBPM(): Float fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) fun requestSync(sharedScreenModel: SharedScreenModel) + fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel) + fun release() } /* diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index a3820c8..e0f61bc 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -700,12 +700,16 @@ fun LazyVerticalGridTUO( val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } if(oneTUO.pTemplate.markerToString() != "") { + val nott = oneTUO.tuNotes.getOrNull(1) metadataList.add( MidiMarkers( myTimestamp, oneTUO.pTemplate.template, oneTUO.pTemplate.lastCalledMarker, - oneTUO.pTemplate.markerToString() + oneTUO.pTemplate.markerToString(), + oneTUO.prevTUO!!.pTemplate!!.template, + oneTUO.sep0, + oneTUO.tuNotes.getOrNull(1).toString() ) ) } @@ -719,8 +723,8 @@ fun LazyVerticalGridTUO( } , onDoubleClick = { val m = sharedScreenModel.getFullMarkers() - m.forEach { (timestamp, template, lastCallerMarker, marker) -> - println("Allmarker : $marker in $timestamp ms") + m.forEach { (timestamp, template, lastCallerMarker, marker, noteBefore, separat, note) -> + println("Allmarker : $marker in $timestamp ms & note= $note & nBefore =$noteBefore & separateur = $separat") } } )) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt index f998e5e..a63027d 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt @@ -5,4 +5,7 @@ data class MidiMarkers( val template: String, val lastCallerMarker: Int, val marker: String, + val noteBefore: String, + val separat: String, + val note: String ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index b776614..d9d7d8f 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -124,6 +124,12 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode private val _canUpdPositionFromPartition = MutableStateFlow(false) val canUpdPositionFromPartition = _canUpdPositionFromPartition.asStateFlow() + private val _dcDone = MutableStateFlow(false) + val dcDone = _dcDone.asStateFlow() + + private val _dsDone = MutableStateFlow(false) + val dsDone = _dsDone.asStateFlow() + private val _tuoTimestamps = MutableStateFlow>(emptyList()) val tuoTimestamps: StateFlow> = _tuoTimestamps @@ -146,6 +152,19 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode return _midiMarkersList.value } + fun setDcDone(state: Boolean) { + _dcDone.value = state + } + fun setDsDone(state: Boolean) { + _dsDone.value = state + } + fun getDcDone(): Boolean { + return _dcDone.value + } + fun getDsDone(): Boolean { + return _dsDone.value + } + fun finalizeMarkers() { val timestamps = _tuoTimestamps.value val tuos = _tuoList.value.drop(1) @@ -160,9 +179,40 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode timestamp = ts, template = tuo.pTemplate.template, lastCallerMarker = tuo.pTemplate.lastCalledMarker, - marker = markerText + marker = markerText, + noteBefore = tuo.prevTUO!!.pTemplate!!.template, + separat = tuo.sep0, + note = tuo.tuNotes.getOrNull(1).toString() ) ) + if(markerText.contains("DC")){ // marquer la fin de mesure après un D.C. + var internIn = index +1 + while (internIn < tuos.size) { + val currentTuo = tuos[internIn] + val currentNote = currentTuo.tuNotes.getOrNull(1)?.toString() ?: "" + val currentSep = currentTuo.sep0 ?: "" + + // On se position sur la note apres les tiret "-" ou sur / + if ((currentNote != "―" && currentNote.isNotBlank()) || (currentSep == "/")) { + val currentTs = timestamps.getOrNull(internIn) ?: 0L + println("Dernier '-' passé. Nouvelle note '$currentNote' trouvée à l'index $internIn (Position du saut)") + newMtdList.clear() + newMtdList.add( + MidiMarkers( + timestamp = currentTs, + template = currentTuo.pTemplate.template, + lastCallerMarker = currentTuo.pTemplate.lastCalledMarker, + marker = "DC_GROUP_PART", + noteBefore = currentTuo.prevTUO!!.pTemplate!!.template, + separat = currentTuo.sep0, + note = currentNote + ) + ) + break + } + internIn++ + } + } } } updateMidiData(newMtdList) @@ -171,10 +221,13 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode fun loadNewSong(newMidiFile: String) { _mediaPlayer?.stop() + _mediaPlayer?.release() _mediaPlayer = null _isPos.value = true _isPlay.value = false _currentPos.value = 0f + _dcDone.value = false + _dsDone.value = false try { val midiFileName = fileRepository.getFileName(newMidiFile) println("Opening xx129 $midiFileName") @@ -184,11 +237,15 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode _currentPos.value = 0f seekTo(0f) _mediaPlayer?.stop() + setDcDone(false) + setDsDone(false) println("fin de lecture du Midi $newMidiFile") + _mediaPlayer?.syncNavigationMonitor(this) }) -// sync +// synchro _mediaPlayer?.requestSync(this) finalizeMarkers() + _mediaPlayer?.syncNavigationMonitor(this) } catch(e: Exception) { println("Erreur d'ouverture de mediaPlayer / ") } diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index e96944e..30bd452 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -3,13 +3,16 @@ package mg.dot.feufaro.midi import SharedScreenModel import kotlinx.coroutines.* import mg.dot.feufaro.getConfigDirectoryPath +import mg.dot.feufaro.viewmodel.MidiMarkers import java.io.File import java.util.prefs.Preferences +import javax.sound.midi.MetaMessage import javax.sound.midi.MidiSystem import javax.sound.midi.Sequence import javax.sound.midi.Sequencer import javax.sound.midi.ShortMessage import javax.sound.midi.Synthesizer +import javax.sound.midi.Track import javax.sound.sampled.AudioSystem import javax.sound.sampled.FloatControl @@ -39,6 +42,17 @@ actual class FMediaPlayer actual constructor( private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private var abJob: Job? = null + 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() + init { try { sequencer?.open() @@ -68,6 +82,131 @@ actual class FMediaPlayer actual constructor( } } + 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) + } + } + } + actual fun play(){ if (!sequencer!!.isOpen){ sequencer!!.open() @@ -85,6 +224,11 @@ actual class FMediaPlayer actual constructor( } } + actual fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel) { + prepareNavigation(sharedScreenModel) + startNavigationMonitor(sharedScreenModel) + } + fun syncTuoWithMidi(sequence: Sequence, sharedScreenModel: SharedScreenModel) { val timestamps = mutableListOf() val resolution = sequence.resolution.toDouble() @@ -122,10 +266,13 @@ actual class FMediaPlayer actual constructor( } actual fun stop(){ - sequencer?.stop() + // sequencer?.stop() sequencer?.microsecondPosition = 0 + navigationJob?.cancel() + resetNavigationFlags() + navigationSteps.clear() clearLoop() - release() +// release() } actual fun getDuration(): Long { return (sequencer?.microsecondLength ?: 0L) / 1000 @@ -136,7 +283,7 @@ actual class FMediaPlayer actual constructor( actual fun seekTo(position: Long) { sequencer?.microsecondPosition = position * 1000 } - fun release() { + actual fun release() { sequencer?.close() synthetizer?.close() playerScope.cancel()