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 package mg.dot.feufaro.midi
import SharedScreenModel
import mg.dot.feufaro.FileRepository import mg.dot.feufaro.FileRepository
expect class FMediaPlayer(filename: String, onFinished: () -> Unit) { expect class FMediaPlayer(filename: String, onFinished: () -> Unit) {
@ -20,6 +21,7 @@ expect class FMediaPlayer(filename: String, onFinished: () -> Unit) {
fun setTempo(factor: Float) fun setTempo(factor: Float)
fun getCurrentBPM(): Float fun getCurrentBPM(): Float
fun updateVoiceVolume(voiceIndex: Int, newVolume: 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.style.BaselineShift
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import SharedScreenModel import SharedScreenModel
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.text.TextMeasurer import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import mg.dot.feufaro.viewmodel.MidiMarkers
val FEUFAROO_TRIOLET_COLOR = Color.DarkGray val FEUFAROO_TRIOLET_COLOR = Color.DarkGray
val FEUFAROO_KEY_CHANGE_COLOR = Color.Blue 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 col = if (tuo.getNum() % 2 == 0) Color(0xff, 0xfa, 0xf7) else Color(0xfb, 0xf3, 0xff)
val currentDensity = LocalDensity.current 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( Column(
modifier = Modifier 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) { if (TimeUnitObject._hasMarker) {
val lineHeight = 20.sp val lineHeight = 20.sp
@ -571,14 +579,21 @@ fun LazyVerticalGridTUO(
val currentPos by sharedScreenModel.currentPos.collectAsState() val currentPos by sharedScreenModel.currentPos.collectAsState()
val duration by sharedScreenModel.duration.collectAsState() val duration by sharedScreenModel.duration.collectAsState()
val isPlay by sharedScreenModel.isPlay.collectAsState() val isPlay by sharedScreenModel.isPlay.collectAsState()
val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState()
val displayedList = tuoList.drop(1) val displayedList = tuoList.drop(1)
val nbTotalDesRow = (displayedList.size-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) ((currentPos / duration) * nbTotalDesRow).toInt().coerceIn(0, nbTotalDesRow - 1)
} else { } else {
-1 -1
} }*/
val measures = tuoList.drop(1).chunked(gridColumnCount) val measures = tuoList.drop(1).chunked(gridColumnCount)
val metadataList = mutableListOf<MidiMarkers>()
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
){ ){
@ -681,14 +696,32 @@ fun LazyVerticalGridTUO(
measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
val isActive = (globalIndex == activeRowIndex) 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) Box(modifier = Modifier.weight(1f)
.combinedClickable( .combinedClickable(
onClick = { 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") // println("tempNum ${oneTUO.numBlock} mesasindex ${measureIndex} & indexinM $indexInMeasure gridCount $gridColumnCount")
} , } ,
onDoubleClick = { 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( 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow 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.data.getDrawerItems
import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.midi.FMediaPlayer import mg.dot.feufaro.midi.FMediaPlayer
import mg.dot.feufaro.viewmodel.MidiMarkers
class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel { class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel {
private val _nextLabel = MutableStateFlow<String>("Next ...") private val _nextLabel = MutableStateFlow<String>("Next ...")
@ -122,13 +124,57 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
private val _canUpdPositionFromPartition = MutableStateFlow(false) private val _canUpdPositionFromPartition = MutableStateFlow(false)
val canUpdPositionFromPartition = _canUpdPositionFromPartition.asStateFlow() 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) { fun loadNewSong(newMidiFile: String) {
_mediaPlayer?.stop() _mediaPlayer?.stop()
_mediaPlayer = null
_isPos.value = true _isPos.value = true
_isPlay.value = false _isPlay.value = false
_currentPos.value = 0f _currentPos.value = 0f
_mediaPlayer = null
try { try {
val midiFileName = fileRepository.getFileName(newMidiFile) val midiFileName = fileRepository.getFileName(newMidiFile)
println("Opening xx129 $midiFileName") println("Opening xx129 $midiFileName")
@ -137,8 +183,12 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
// _isPlay.value = false // _isPlay.value = false
_currentPos.value = 0f _currentPos.value = 0f
seekTo(0f) seekTo(0f)
_mediaPlayer?.stop()
println("fin de lecture du Midi $newMidiFile") println("fin de lecture du Midi $newMidiFile")
}) })
// sync
_mediaPlayer?.requestSync(this)
finalizeMarkers()
} catch(e: Exception) { } catch(e: Exception) {
println("Erreur d'ouverture de mediaPlayer / ") println("Erreur d'ouverture de mediaPlayer / ")
} }
@ -245,6 +295,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
_stanza.value = 0 _stanza.value = 0
} }
loadNewSong("whawyd3.mid")
} }
fun setSongKey(theSongKey: String) { fun setSongKey(theSongKey: String) {

View file

@ -1,11 +1,14 @@
package mg.dot.feufaro.midi package mg.dot.feufaro.midi
import SharedScreenModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import mg.dot.feufaro.getConfigDirectoryPath import mg.dot.feufaro.getConfigDirectoryPath
import java.io.File import java.io.File
import java.util.prefs.Preferences import java.util.prefs.Preferences
import javax.sound.midi.MidiSystem import javax.sound.midi.MidiSystem
import javax.sound.midi.Sequence
import javax.sound.midi.Sequencer import javax.sound.midi.Sequencer
import javax.sound.midi.ShortMessage
import javax.sound.midi.Synthesizer import javax.sound.midi.Synthesizer
import javax.sound.sampled.AudioSystem import javax.sound.sampled.AudioSystem
import javax.sound.sampled.FloatControl import javax.sound.sampled.FloatControl
@ -48,8 +51,9 @@ actual class FMediaPlayer actual constructor(
} }
val file = File(filename) val file = File(filename)
if (file.exists()){ if (file.exists()) {
sequencer?.sequence = MidiSystem.getSequence(file) val mySequence = MidiSystem.getSequence(file)
sequencer?.sequence = mySequence
loadVoiceVolumes() loadVoiceVolumes()
applyVoiceStates() applyVoiceStates()
sequencer?.addMetaEventListener { meta -> sequencer?.addMetaEventListener { meta ->
@ -65,14 +69,58 @@ actual class FMediaPlayer actual constructor(
} }
actual fun play(){ actual fun play(){
if (sequencer!!.isOpen){ if (!sequencer!!.isOpen){
sequencer!!.open()
}
sequencer?.start() sequencer?.start()
println("La sequence vient d etre lancé ${sequencer?.isRunning}") println("La sequence vient d etre lancé ${sequencer?.isRunning}")
} }
}
actual fun pause(){ actual fun pause(){
sequencer?.stop() 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(){ actual fun stop(){
sequencer?.stop() sequencer?.stop()
sequencer?.microsecondPosition = 0 sequencer?.microsecondPosition = 0