implement realtime solfa highlighting with sync

This commit is contained in:
hasina 2026-01-19 14:06:55 +03:00
parent c9fd0fe81b
commit 4e8866a4e7
5 changed files with 260 additions and 113 deletions

View file

@ -149,6 +149,7 @@ object ScreenSolfa : Screen {
LazyVerticalGridTUO( LazyVerticalGridTUO(
gridTUOData, gridTUOData,
gridWidthPx = gridWidthPx, gridWidthPx = gridWidthPx,
sharedScreenModel = sharedScreenModel,
onGridWidthMeasured = { width -> gridWidthPx = width } onGridWidthMeasured = { width -> gridWidthPx = width }
) )
FlowRow( FlowRow(

View file

@ -42,6 +42,9 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString 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 androidx.compose.runtime.collectAsState
import kotlinx.coroutines.delay
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
@ -228,13 +231,14 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co
fun TimeUnitComposable( fun TimeUnitComposable(
tuo: TimeUnitObject, tuo: TimeUnitObject,
stanzaNumber: Int, stanzaNumber: Int,
gridColumnCount: Int gridColumnCount: Int,
gridActive: Boolean
) { ) {
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
Column( Column(
modifier = Modifier modifier = Modifier
.background(col) .background(if(gridActive) Color.Cyan.copy(alpha = 0.5f) else col)
) { ) {
if (TimeUnitObject._hasMarker) { if (TimeUnitObject._hasMarker) {
val lineHeight = 20.sp val lineHeight = 20.sp
@ -511,6 +515,7 @@ fun AutoResizingText(
fun LazyVerticalGridTUO( fun LazyVerticalGridTUO(
viewModel: GridTUOData, viewModel: GridTUOData,
gridWidthPx: Int, gridWidthPx: Int,
sharedScreenModel: SharedScreenModel,
onGridWidthMeasured: (Int) -> Unit, onGridWidthMeasured: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -547,6 +552,16 @@ fun LazyVerticalGridTUO(
val currentStanza = viewModel.stanza val currentStanza = viewModel.stanza
val currentPos by sharedScreenModel.currentPos.collectAsState()
val duration by sharedScreenModel.duration.collectAsState()
val isPlay by sharedScreenModel.isPlay.collectAsState()
val displayedList = tuoList.drop(1)
val nbTotalDesRow = displayedList.size
val activeRowIndex = if (duration > 0f) {
((currentPos / duration) * nbTotalDesRow).toInt().coerceIn(0, nbTotalDesRow - 1)
} else {
-1
}
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -556,12 +571,16 @@ fun LazyVerticalGridTUO(
// state = lazyGridState // state = lazyGridState
) { ) {
tuoList.drop(n=1).forEachIndexed { relativeIndex, oneTUO -> tuoList.drop(n=1).forEachIndexed { relativeIndex, oneTUO ->
val isActive = (relativeIndex == activeRowIndex)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth(flowRowSize) .fillMaxWidth(flowRowSize)
.background(Color.Transparent)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
println("Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex FL $flowRowSize GC $gridColumnCount") sharedScreenModel.updatePositionFromPartition(relativeIndex, nbTotalDesRow)
println("590: relative $relativeIndex active? $isActive")
println("TimeUnitObj:566 Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex FL $flowRowSize totaRow $nbTotalDesRow")
} , } ,
onDoubleClick = { onDoubleClick = {
println("Double-Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex") println("Double-Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex")
@ -571,7 +590,8 @@ fun LazyVerticalGridTUO(
TimeUnitComposable( TimeUnitComposable(
tuo = oneTUO, tuo = oneTUO,
currentStanza, currentStanza,
gridColumnCount gridColumnCount,
gridActive = isActive
) )
} }
} }

View file

@ -74,18 +74,23 @@ fun MainScreenWithDrawer(
var isDragging by remember { mutableStateOf(false) } //var isDragging by remember { mutableStateOf(false) }
var isPlay by remember { mutableStateOf(false) } //var isPlay by remember { mutableStateOf(false) }
var isPos by remember { mutableStateOf(true) } //var isPos by remember { mutableStateOf(true) }
var isPlayMid by remember { mutableStateOf(false) } var isPlayMid by remember { mutableStateOf(false) }
var currentPos by remember { mutableStateOf(0f) } //var currentPos by remember { mutableStateOf(0f) }
var duration by remember { mutableStateOf(0f) } val isPlay by sharedScreenModel.isPlay.collectAsState()
val isPos by sharedScreenModel.isPos.collectAsState()
var isDragging = sharedScreenModel.isDragging
val currentPos by sharedScreenModel.currentPos.collectAsState()
val duration by sharedScreenModel.duration.collectAsState()
var midiFile = "whawyd3.mid" var midiFile = "whawyd3.mid"
var refreshTrigeer by remember { mutableStateOf(0)} var refreshTrigeer by remember { mutableStateOf(0)}
var volumelevel by remember { mutableStateOf(0.8f) } val volumelevel by sharedScreenModel.volumeLevel.collectAsState()
val mediaPlayer = remember(refreshTrigeer) { val player = sharedScreenModel.mediaPlayer
/*val mediaPlayer = remember(refreshTrigeer) {
MediaPlayer(filename = midiFile, onFinished = { MediaPlayer(filename = midiFile, onFinished = {
isPos = true isPos = true
isPlay = false isPlay = false
@ -94,9 +99,18 @@ val mediaPlayer = remember(refreshTrigeer) {
println("fin de lecture du whawyd3.mid") println("fin de lecture du whawyd3.mid")
}).apply { setVolume(volumelevel) } }).apply { setVolume(volumelevel) }
}*/
LaunchedEffect(isPlay, isPos) {
if (isPlay && !isPos) {
// while (isPlay && !isPos) {
while (true) {
sharedScreenModel.updateProgress()
delay(100)
}
}
} }
LaunchedEffect(isPlay, isPos, mediaPlayer) { /*LaunchedEffect(isPlay, isPos, mediaPlayer) {
if (isPlay && !isPos) { if (isPlay && !isPos) {
val d = mediaPlayer.getDuration().toFloat() val d = mediaPlayer.getDuration().toFloat()
if (d > 0) duration = d if (d > 0) duration = d
@ -108,13 +122,16 @@ LaunchedEffect(isPlay, isPos, mediaPlayer) {
delay(100) delay(100)
} }
} }
} }*/
LaunchedEffect(isSearchActive) { LaunchedEffect(isSearchActive) {
if (isSearchActive) { if (isSearchActive) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
} }
LaunchedEffect(Unit) {
sharedScreenModel.loadNewSong("whawyd3.mid")
}
ModalNavigationDrawer(drawerState = drawerState, drawerContent = { ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
SimpleDrawerContent( SimpleDrawerContent(
items, items,
@ -128,11 +145,12 @@ LaunchedEffect(isPlay, isPos, mediaPlayer) {
onScannerButtonClick() onScannerButtonClick()
}, },
onSongSelected = { newSong -> onSongSelected = { newSong ->
mediaPlayer?.stop() // mediaPlayer?.stop()
isPos = true // isPos = true
isPlay = false // isPlay = false
currentPos = 0f // currentPos = 0f
refreshTrigeer++ sharedScreenModel.loadNewSong("whawyd3.mid")
// refreshTrigeer++
} }
) )
}, content = { }, content = {
@ -232,10 +250,11 @@ LaunchedEffect(isPlay, isPos, mediaPlayer) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
isPlayMid = !isPlayMid isPlayMid = !isPlayMid
if(mediaPlayer.getCurrentPosition() != 0L) { // if(isPlayMid) sharedScreenModel.stopMidi()
mediaPlayer?.seekTo(0) // if(mediaPlayer.getCurrentPosition() != 0L) {
mediaPlayer?.stop() // mediaPlayer?.seekTo(0)
} // mediaPlayer?.stop()
// }
}, modifier = Modifier.alpha(0.45f) }, modifier = Modifier.alpha(0.45f)
) { ) {
Icon( Icon(
@ -250,6 +269,7 @@ LaunchedEffect(isPlay, isPos, mediaPlayer) {
onClick = { onClick = {
isExpanded = !isExpanded isExpanded = !isExpanded
refreshTrigeer++ refreshTrigeer++
sharedScreenModel.loadNewSong("whawyd3.mid")
}, modifier = Modifier.alpha(0.45f) }, modifier = Modifier.alpha(0.45f)
) { ) {
Icon( Icon(
@ -267,13 +287,14 @@ LaunchedEffect(isPlay, isPos, mediaPlayer) {
Box( Box(
modifier = Modifier.fillMaxWidth(0.9f) modifier = Modifier.fillMaxWidth(0.9f)
) { ) {
if(player != null) {
MidiControlPanel( MidiControlPanel(
isPause = isPos, isPause = isPos,
currentPos = currentPos, currentPos = currentPos,
volume = volumelevel, volume = volumelevel,
duration = duration, duration = duration,
onPlayPauseClick = { onPlayPauseClick = {
if(isPlay){ sharedScreenModel.togglePlayPause()/*if(isPlay){
mediaPlayer?.pause() mediaPlayer?.pause()
isPlay = false isPlay = false
isPos = true isPos = true
@ -284,8 +305,8 @@ LaunchedEffect(isPlay, isPos, mediaPlayer) {
mediaPlayer?.play() mediaPlayer?.play()
mediaPlayer?.setVolume(volumelevel) mediaPlayer?.setVolume(volumelevel)
isPlay = true isPlay = true
isPos = false isPos = false*/
} },
/* if(!isPlay) { /* if(!isPlay) {
if (currentPos == 0f) mediaPlayer.seekTo(0) if (currentPos == 0f) mediaPlayer.seekTo(0)
mediaPlayer?.play() mediaPlayer?.play()
@ -297,36 +318,38 @@ LaunchedEffect(isPlay, isPos, mediaPlayer) {
isPos = true isPos = true
}*/ }*/
println("je clique pause = $isPos play = $isPlay") // println("je clique pause = ${sharedScreenModel.isPlay} play = ${sharedScreenModel.isPos}")
// if(isPos) { // if(isPos) {
// mediaPlayer.play() // mediaPlayer.play()
// isPos = false // isPos = false
// } else { // } else {
// mediaPlayer.pause() // mediaPlayer.pause()
// isPos = true // isPos = true
// } // }
/*if (isPlayMid) { /*if (isPlayMid) {
// mediaPlayer.seekTo(0f.toLong()) // mediaPlayer.seekTo(0f.toLong())
mediaPlayer.play() mediaPlayer.play()
isPlayMid = false isPlayMid = false
}
}*/ }*/
}, onSeek = { newPos -> // currentPos = newPos
onSeek = { newPos -> sharedScreenModel.setDragging(true)
currentPos = newPos sharedScreenModel.seekTo(newPos)
isDragging = true
mediaPlayer.seekTo(newPos.toLong())
scope.launch { scope.launch {
delay(100) delay(100)
isDragging = false sharedScreenModel.setDragging(false)
} }
println("DrawerUI:335: mihetsika $newPos")
}, },
mediaPlayer = mediaPlayer, mediaPlayer = player,
onVolumeChange = { newVolume -> onVolumeChange = { newVolume -> // volumelevel = newVolume
volumelevel = newVolume sharedScreenModel.setVolume(newVolume)
mediaPlayer?.setVolume(newVolume)
println("Changement volume $newVolume -l $volumelevel") println("Changement volume $newVolume -l $volumelevel")
} },
) )
} else {
Text("Sélectionner un morceau")
}
/*Row( /*Row(
modifier = Modifier.align(Alignment.Center).padding(16.dp), modifier = Modifier.align(Alignment.Center).padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

View file

@ -1,6 +1,8 @@
// commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt // commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -11,8 +13,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import mg.dot.feufaro.data.DrawerItem import mg.dot.feufaro.data.DrawerItem
import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.data.getDrawerItems import mg.dot.feufaro.data.getDrawerItems
import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.midi.MediaPlayer
class SharedScreenModel() : ScreenModel { class SharedScreenModel() : ScreenModel {
private val _nextLabel = MutableStateFlow<String>("Next ...") private val _nextLabel = MutableStateFlow<String>("Next ...")
@ -90,7 +93,113 @@ class SharedScreenModel() : ScreenModel {
} }
fun updateSearchTxt(searchValue: String) { fun updateSearchTxt(searchValue: String) {
_searchTitle.value = searchValue _searchTitle.value = searchValue
} fun appendData(otherData: String) { }
private var _mediaPlayer by mutableStateOf<MediaPlayer?>(null)
val mediaPlayer: MediaPlayer? get() = _mediaPlayer
private val _isPlay = MutableStateFlow(false)
val isPlay = _isPlay.asStateFlow()
private val _isPos = MutableStateFlow(true)
val isPos = _isPos.asStateFlow()
private val _isDragging = MutableStateFlow(true)
val isDragging = _isDragging.asStateFlow()
private val _currentPos = MutableStateFlow(0f)
val currentPos = _currentPos.asStateFlow()
private val _duration = MutableStateFlow(0f)
val duration = _duration.asStateFlow()
private val _volumeLevel = MutableStateFlow(0.8f)
val volumeLevel = _volumeLevel.asStateFlow()
private val _isPlayMid = MutableStateFlow(false)
val isPlayMid = _isPlayMid.asStateFlow()
private var midiFile = "whawyd3.mid"
fun loadNewSong(newMidiFile: String) {
_mediaPlayer?.stop()
_isPos.value = true
_isPlay.value = false
_currentPos.value = 0f
_mediaPlayer = MediaPlayer(filename = newMidiFile, onFinished = {
_isPos.value = true
_isPlay.value = false
_currentPos.value = 0f
println("fin de lecture du Midi $newMidiFile")
})
println("New media Player crée $newMidiFile")
}
// val mediaPlayer =
fun togglePlayPause() {
_mediaPlayer?.let { player ->
if (_isPlay.value) {
_isPlay.value = false
_isPos.value = true
player.pause()
} else {
_isPlay.value = true
_isPos.value = false
player.play()
player.setVolume(_volumeLevel.value)
if(currentPos.value == 0f) {
player.seekTo(0)
}
}
println("128: Status de isPlay ${_isPlay.value} \nisPos ${_isPos.value} \ncurrentPos ${_currentPos.value} \n volume ${_volumeLevel.value}")
// _isPlay.value = !_isPlay.value
}
}
fun stopMidi() {
_mediaPlayer?.let { player ->
_isPlay.value = false
_isPos.value = true
player.pause()
}
}
fun seekTo(pos: Float) {
_currentPos.value = pos
_mediaPlayer?.let { player ->
player.seekTo(pos.toLong())
}
}
fun setDragging(dragState: Boolean) {
_isDragging.value = dragState
}
fun setVolume(level: Float) {
_volumeLevel.value = level
_mediaPlayer?.let { player ->
player.setVolume(level) }
}
fun updateProgress(){
_mediaPlayer?.let { player->
if (_isPlay.value) {
val p = player.getCurrentPosition().toFloat()
val d = player.getDuration().toFloat()
if (p >= 0) _currentPos.value = p
if ((d > 0) && _duration.value != d) _duration.value = d
}
}
}
var currentNoteIndex by mutableStateOf(0f)
fun updatePositionFromPartition(index: Int, totalRow: Int) {
val duration = _duration.value
if(totalRow > 0) {
currentNoteIndex = index.toFloat()
val newPos = (currentNoteIndex / totalRow.toFloat()) * duration
seekTo(newPos)
println("Shared:196 currentNoteIndex $currentNoteIndex, Index $index et curret = ${_currentPos.value}")
}
}
fun appendData(otherData: String) {
_nextLabel.value += otherData _nextLabel.value += otherData
} }

View file

@ -8,9 +8,11 @@ import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import javax.sound.midi.MidiSystem import javax.sound.midi.MidiSystem
import javax.sound.midi.Sequencer import javax.sound.midi.Sequencer
import javax.sound.midi.ShortMessage import javax.sound.midi.Synthesizer //import javax.sound.midi.ShortMessage
import javax.sound.midi.Synthesizer //import javax.sound.midi.Synthesizer
import javax.sound.sampled.* import javax.sound.sampled.AudioFormat
import javax.sound.sampled.AudioSystem
import javax.sound.sampled.FloatControl
//private var sequencer: javax.sound.midi.Sequencer?= null //private var sequencer: javax.sound.midi.Sequencer?= null
actual class MediaPlayer actual constructor( actual class MediaPlayer actual constructor(
@ -20,11 +22,12 @@ actual class MediaPlayer actual constructor(
private var sequencer: Sequencer? = try { private var sequencer: Sequencer? = try {
MidiSystem.getSequencer(false) MidiSystem.getSequencer(false)
} catch (e: Exception){ } catch (e: Exception){
println("Erreur impossible obtenir ${e.message}") println("Erreur impossible obtenir ${e.message}")
null null
} }
private var synthetizer: Synthesizer? = MidiSystem.getSynthesizer() private var synthetizer = MidiSystem.getSynthesizer() as Synthesizer?
private var pointA: Long = -1L private var pointA: Long = -1L
private var pointB: Long = -1L private var pointB: Long = -1L
@ -37,12 +40,15 @@ actual class MediaPlayer actual constructor(
private var currentTempo: Float = 1.0f private var currentTempo: Float = 1.0f
init { init {
try {
sequencer?.open() sequencer?.open()
synthetizer?.open() synthetizer?.open()
val transmitter = sequencer?.transmitter val transmitter = sequencer?.transmitter
val synthReceiver = synthetizer?.receiver val synthReceiver = synthetizer?.receiver
transmitter?.receiver = synthReceiver transmitter?.receiver = synthReceiver
} catch (e: Exception) {
e.printStackTrace()
}
val file = File(filename) val file = File(filename)
if (file.exists()){ if (file.exists()){
@ -55,6 +61,7 @@ actual class MediaPlayer actual constructor(
} }
} }
} }
actual fun play(){ actual fun play(){
if (sequencer!!.isOpen){ if (sequencer!!.isOpen){
sequencer?.start() sequencer?.start()
@ -67,7 +74,8 @@ actual class MediaPlayer actual constructor(
actual fun stop(){ actual fun stop(){
sequencer?.stop() sequencer?.stop()
sequencer?.microsecondPosition = 0 sequencer?.microsecondPosition = 0
// disableLoop() clearLoop()
release()
} }
actual fun getDuration(): Long { actual fun getDuration(): Long {
return (sequencer?.microsecondLength ?: 0L) / 1000 return (sequencer?.microsecondLength ?: 0L) / 1000
@ -80,7 +88,7 @@ actual class MediaPlayer actual constructor(
} }
fun release() { fun release() {
sequencer?.close() sequencer?.close()
// synthetizer?.close() synthetizer?.close()
} }
actual fun setVolume(level: Float) { actual fun setVolume(level: Float) {
try { try {
@ -209,32 +217,18 @@ actual class MediaPlayer actual constructor(
sequencer?.tempoFactor = factor sequencer?.tempoFactor = factor
} }
fun getTempo(): Float = currentTempo fun getTempo(): Float = currentTempo
} private val MS8PER_NOTE = 500L
fun seekToNote(index: Int) {
/* try {
sequencer?.let { sequencer ->
private var sequencer: javax.sound.midi.Sequencer?= null if (sequencer.isOpen) {
actual fun MidiPlayer(filename: String, onFinished: () -> Unit) { val targetPos = index * MS8PER_NOTE * 1000
val file = File(filename) sequencer.microsecondPosition = targetPos
if (file.exists()){
StopMidi()
sequencer = MidiSystem.getSequencer().apply {
open()
sequence = MidiSystem.getSequence(file)
addMetaEventListener { meta ->
if(meta.type == 47){
onFinished()
} }
} }
start() } catch (e: Exception) {
e.printStackTrace()
} }
} }
} }
actual fun StopMidi() {
if(sequencer?.isRunning == true){
sequencer?.stop()
sequencer?.close()
}
}*/