From e8be53a41ec86529f65f4a9464a6f4eeaaa10bae Mon Sep 17 00:00:00 2001 From: "hasinarak3@gmail.com" Date: Mon, 2 Mar 2026 10:08:52 +0300 Subject: [PATCH] Synchronize TUO with midi sequence per beat with grid index --- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 2 + .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 43 ++++++++++++-- .../mg/dot/feufaro/viewmodel/MidiMetadata.kt | 11 ++++ .../feufaro/viewmodel/SharedScreenModel.kt | 53 ++++++++++++++++- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 58 +++++++++++++++++-- 5 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt 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 12694e8..f06c593 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -1,5 +1,6 @@ package mg.dot.feufaro.midi +import SharedScreenModel import mg.dot.feufaro.FileRepository expect class FMediaPlayer(filename: String, onFinished: () -> Unit) { @@ -20,6 +21,7 @@ expect class FMediaPlayer(filename: String, onFinished: () -> Unit) { fun setTempo(factor: Float) fun getCurrentBPM(): Float fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) + fun requestSync(sharedScreenModel: SharedScreenModel) } /* 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 5dc1885..a3820c8 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -40,10 +40,13 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.withStyle import SharedScreenModel +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.text.TextMeasurer import androidx.compose.ui.text.style.TextAlign +import mg.dot.feufaro.viewmodel.MidiMarkers val FEUFAROO_TRIOLET_COLOR = Color.DarkGray val FEUFAROO_KEY_CHANGE_COLOR = Color.Blue @@ -252,9 +255,14 @@ fun TimeUnitComposable( ) { val col = if (tuo.getNum() % 2 == 0) Color(0xff, 0xfa, 0xf7) else Color(0xfb, 0xf3, 0xff) val currentDensity = LocalDensity.current + + val animatedColor by animateColorAsState( + targetValue = if (gridActive) Color.Cyan.copy(alpha = 0.5f) else col, + animationSpec = tween(durationMillis = 100) // Très court pour rester réactif + ) Column( modifier = Modifier - .background(if(gridActive) Color.Cyan.copy(alpha = 0.5f) else col) + .background(/*if(gridActive) Color.Cyan.copy(alpha = 0.5f) else col*/animatedColor) ) { if (TimeUnitObject._hasMarker) { val lineHeight = 20.sp @@ -571,14 +579,21 @@ fun LazyVerticalGridTUO( val currentPos by sharedScreenModel.currentPos.collectAsState() val duration by sharedScreenModel.duration.collectAsState() val isPlay by sharedScreenModel.isPlay.collectAsState() + val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() val displayedList = tuoList.drop(1) val nbTotalDesRow = (displayedList.size-1) - val activeRowIndex = if (duration > 0f) { + val activeRowIndex = remember(currentPos, tuoTimestamps) { + val currentPosMicros = (currentPos * 1000).toLong() + val index = tuoTimestamps.indexOfLast { it <= currentPosMicros } + index.coerceIn(-1, nbTotalDesRow) + } + /*val activeRowIndex = if (duration > 0f) { ((currentPos / duration) * nbTotalDesRow).toInt().coerceIn(0, nbTotalDesRow - 1) } else { -1 - } + }*/ val measures = tuoList.drop(1).chunked(gridColumnCount) + val metadataList = mutableListOf() Column( modifier = Modifier.fillMaxWidth() ){ @@ -681,14 +696,32 @@ fun LazyVerticalGridTUO( measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure val isActive = (globalIndex == activeRowIndex) + + val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } + + if(oneTUO.pTemplate.markerToString() != "") { + metadataList.add( + MidiMarkers( + myTimestamp, + oneTUO.pTemplate.template, + oneTUO.pTemplate.lastCalledMarker, + oneTUO.pTemplate.markerToString() + ) + ) + } Box(modifier = Modifier.weight(1f) .combinedClickable( onClick = { - sharedScreenModel.updatePositionFromPartition(globalIndex, nbTotalDesRow) + val targetMicros = tuoTimestamps.getOrNull(globalIndex) ?: 0L + sharedScreenModel.seekToTimestamp(targetMicros) +// sharedScreenModel.updatePositionFromPartition(globalIndex, nbTotalDesRow) // println("tempNum ${oneTUO.numBlock} mesasindex ${measureIndex} & indexinM $indexInMeasure gridCount $gridColumnCount") } , onDoubleClick = { - println("Double-Clicked: ${oneTUO.numBlock} / relative index: ") + val m = sharedScreenModel.getFullMarkers() + m.forEach { (timestamp, template, lastCallerMarker, marker) -> + println("Allmarker : $marker in $timestamp ms") + } } )) { TimeUnitComposable( diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt new file mode 100644 index 0000000..a63027d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/MidiMetadata.kt @@ -0,0 +1,11 @@ +package mg.dot.feufaro.viewmodel + +data class MidiMarkers( + val timestamp: Long, + 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 946ca5b..b776614 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -6,6 +6,7 @@ import cafe.adriel.voyager.core.model.ScreenModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -17,6 +18,7 @@ import mg.dot.feufaro.data.DrawerItem import mg.dot.feufaro.data.getDrawerItems import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.midi.FMediaPlayer +import mg.dot.feufaro.viewmodel.MidiMarkers class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel { private val _nextLabel = MutableStateFlow("Next ...") @@ -122,13 +124,57 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode private val _canUpdPositionFromPartition = MutableStateFlow(false) val canUpdPositionFromPartition = _canUpdPositionFromPartition.asStateFlow() + private val _tuoTimestamps = MutableStateFlow>(emptyList()) + val tuoTimestamps: StateFlow> = _tuoTimestamps + + fun updateTimestamps(list: List) { + _tuoTimestamps.value = list + } + + fun seekToTimestamp(micros: Long) { + _mediaPlayer?.seekTo(micros / 1000) + } + + private val _midiMarkersList = MutableStateFlow>(emptyList()) + val midiMarkersList: StateFlow> = _midiMarkersList + + fun updateMidiData(newList: List) { + _midiMarkersList.value = newList + } + + fun getFullMarkers(): List { + return _midiMarkersList.value + } + + fun finalizeMarkers() { + val timestamps = _tuoTimestamps.value + val tuos = _tuoList.value.drop(1) + val newMtdList = mutableListOf() + + tuos.forEachIndexed { index, tuo -> + val markerText = tuo.pTemplate.markerToString() + if (markerText.isNotEmpty()) { + val ts = timestamps.getOrNull(index) ?: 0L + newMtdList.add( + MidiMarkers( + timestamp = ts, + template = tuo.pTemplate.template, + lastCallerMarker = tuo.pTemplate.lastCalledMarker, + marker = markerText + ) + ) + } + } + updateMidiData(newMtdList) + println("Markers loaded >> ok") + } fun loadNewSong(newMidiFile: String) { _mediaPlayer?.stop() + _mediaPlayer = null _isPos.value = true _isPlay.value = false _currentPos.value = 0f - _mediaPlayer = null try { val midiFileName = fileRepository.getFileName(newMidiFile) println("Opening xx129 $midiFileName") @@ -137,8 +183,12 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode // _isPlay.value = false _currentPos.value = 0f seekTo(0f) + _mediaPlayer?.stop() println("fin de lecture du Midi $newMidiFile") }) +// sync + _mediaPlayer?.requestSync(this) + finalizeMarkers() } catch(e: Exception) { println("Erreur d'ouverture de mediaPlayer / ") } @@ -245,6 +295,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode } catch (e: NumberFormatException) { _stanza.value = 0 } + loadNewSong("whawyd3.mid") } fun setSongKey(theSongKey: String) { 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 b4a9a12..e96944e 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -1,11 +1,14 @@ package mg.dot.feufaro.midi +import SharedScreenModel import kotlinx.coroutines.* import mg.dot.feufaro.getConfigDirectoryPath import java.io.File import java.util.prefs.Preferences 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.sampled.AudioSystem import javax.sound.sampled.FloatControl @@ -48,8 +51,9 @@ actual class FMediaPlayer actual constructor( } val file = File(filename) - if (file.exists()){ - sequencer?.sequence = MidiSystem.getSequence(file) + if (file.exists()) { + val mySequence = MidiSystem.getSequence(file) + sequencer?.sequence = mySequence loadVoiceVolumes() applyVoiceStates() sequencer?.addMetaEventListener { meta -> @@ -65,14 +69,58 @@ actual class FMediaPlayer actual constructor( } actual fun play(){ - if (sequencer!!.isOpen){ - sequencer?.start() - println("La sequence vient d etre lancé ${sequencer?.isRunning}") + if (!sequencer!!.isOpen){ + sequencer!!.open() } + sequencer?.start() + println("La sequence vient d etre lancé ${sequencer?.isRunning}") } actual fun pause(){ sequencer?.stop() } + actual fun requestSync(sharedScreenModel: SharedScreenModel) { + val seq = sequencer?.sequence + if (seq != null) { + syncTuoWithMidi(seq, sharedScreenModel) + } + } + + fun syncTuoWithMidi(sequence: Sequence, sharedScreenModel: SharedScreenModel) { + val timestamps = mutableListOf() + 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() + + 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) + } + actual fun stop(){ sequencer?.stop() sequencer?.microsecondPosition = 0