Synchronize TUO with midi sequence per beat with grid index
This commit is contained in:
parent
e6af7bd39e
commit
8e15cfc667
4 changed files with 145 additions and 11 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -49,7 +52,8 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
val file = File(filename)
|
||||
if (file.exists()) {
|
||||
sequencer?.sequence = MidiSystem.getSequence(file)
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue