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(
gridTUOData,
gridWidthPx = gridWidthPx,
sharedScreenModel = sharedScreenModel,
onGridWidthMeasured = { width -> gridWidthPx = width }
)
FlowRow(

View file

@ -42,6 +42,9 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.BaselineShift
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_KEY_CHANGE_COLOR = Color.Blue
@ -228,13 +231,14 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co
fun TimeUnitComposable(
tuo: TimeUnitObject,
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 currentDensity = LocalDensity.current
Column(
modifier = Modifier
.background(col)
.background(if(gridActive) Color.Cyan.copy(alpha = 0.5f) else col)
) {
if (TimeUnitObject._hasMarker) {
val lineHeight = 20.sp
@ -511,6 +515,7 @@ fun AutoResizingText(
fun LazyVerticalGridTUO(
viewModel: GridTUOData,
gridWidthPx: Int,
sharedScreenModel: SharedScreenModel,
onGridWidthMeasured: (Int) -> Unit,
modifier: Modifier = Modifier
) {
@ -547,6 +552,16 @@ fun LazyVerticalGridTUO(
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(
modifier = Modifier.fillMaxWidth(),
@ -556,12 +571,16 @@ fun LazyVerticalGridTUO(
// state = lazyGridState
) {
tuoList.drop(n=1).forEachIndexed { relativeIndex, oneTUO ->
val isActive = (relativeIndex == activeRowIndex)
Box(
modifier = Modifier
.fillMaxWidth(flowRowSize)
.background(Color.Transparent)
.combinedClickable(
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 = {
println("Double-Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex")
@ -571,7 +590,8 @@ fun LazyVerticalGridTUO(
TimeUnitComposable(
tuo = oneTUO,
currentStanza,
gridColumnCount
gridColumnCount,
gridActive = isActive
)
}
}

View file

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

View file

@ -1,6 +1,8 @@
// commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.ScreenModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -11,8 +13,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import mg.dot.feufaro.data.DrawerItem
import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.data.getDrawerItems
import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.midi.MediaPlayer
class SharedScreenModel() : ScreenModel {
private val _nextLabel = MutableStateFlow<String>("Next ...")
@ -90,7 +93,113 @@ class SharedScreenModel() : ScreenModel {
}
fun updateSearchTxt(searchValue: String) {
_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
}

View file

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