Synchronize TUO with midi sequence per beat with grid index

This commit is contained in:
hasinarak3@gmail.com 2026-03-02 10:08:52 +03:00
parent e6af7bd39e
commit e8be53a41e
5 changed files with 156 additions and 11 deletions

View file

@ -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)
}
/*

View file

@ -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<MidiMarkers>()
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(

View file

@ -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
)

View file

@ -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<String>("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<List<Long>>(emptyList())
val tuoTimestamps: StateFlow<List<Long>> = _tuoTimestamps
fun updateTimestamps(list: List<Long>) {
_tuoTimestamps.value = list
}
fun seekToTimestamp(micros: Long) {
_mediaPlayer?.seekTo(micros / 1000)
}
private val _midiMarkersList = MutableStateFlow<List<MidiMarkers>>(emptyList())
val midiMarkersList: StateFlow<List<MidiMarkers>> = _midiMarkersList
fun updateMidiData(newList: List<MidiMarkers>) {
_midiMarkersList.value = newList
}
fun getFullMarkers(): List<MidiMarkers> {
return _midiMarkersList.value
}
fun finalizeMarkers() {
val timestamps = _tuoTimestamps.value
val tuos = _tuoList.value.drop(1)
val newMtdList = mutableListOf<MidiMarkers>()
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) {

View file

@ -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){
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<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)
}
actual fun stop(){
sequencer?.stop()
sequencer?.microsecondPosition = 0