diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt new file mode 100644 index 0000000..4b013ec --- /dev/null +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -0,0 +1,129 @@ +package mg.dot.feufaro.midi + +import java.io.File +import android.media.MediaPlayer +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private var androidMediaPlayer: MediaPlayer?= null + +actual class MediaPlayer actual constructor(filename: String, onFinished: () -> Unit) { + private val player = androidMediaPlayer?.apply { + setDataSource(filename) + prepare() + setOnCompletionListener { + seekTo(0) + onFinished() + } +// val file = File(android.app.Instrumentation().context.filesDir, filename) +// if(file.exists()){ +// androidMediaPlayer?.setDataSource(file.path) +// androidMediaPlayer?.prepare() +// androidMediaPlayer?.setOnCompletionListener { +// onFinished() +// } +// } else { +// println("Fichier midi non trouvée") +// } + } + private val voiceStates = mutableListOf(true, true, true, true) + private var currentGlobalVolume: Float = 0.8f + + private var pointA: Long = -1L + private var pointB: Long = -1L + private var isLoopingAB: Boolean = false + actual fun play() { +// player?.start() + } + actual fun pause() { +// player?.pause() + } + actual fun stop() { +// player?.stop() + } + actual fun getDuration(): Long { +// player!!.duration.toLong() ?: 0L + return 4.toLong() + } + actual fun getCurrentPosition(): Long { +// player!!.currentPosition.toLong() + return 4.toLong() + } + actual fun seekTo(position: Long) { +// player!!.seekTo(position.toInt()) + } + + actual fun setVolume(level: Float){ +// currentGlobalVolume =level +// player?.setVolume(level, level) + } + + actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB) + + actual fun toggleVoice(index: Int): Unit{ + voiceStates[index] = !voiceStates[index] + } + + actual fun getVoiceStates(): List = voiceStates + + actual fun changeInstru(noInstru: Int): Unit{ + + } + + actual fun setPointA(): Unit{ +// pointA = getCurrentPosition() + } + + actual fun setPointB(): Unit{ + pointB = getCurrentPosition() + if(pointB > pointA) { + isLoopingAB = true + startABMonitor() + } + } + + actual fun clearLoop(): Unit{ + isLoopingAB = false + } + private fun startABMonitor(){ + GlobalScope.launch { + while(isLoopingAB){ +// if ((player?.currentPosition ?: 0) >= pointB){ +// player?.seekTo(pointA?.toInt()) +// } + delay(50) + } + } + } + actual fun setTempo(factor: Float){ + + } + + actual fun getCurrentBPM(): Float { + return 120f + } +} +/* +actual fun MidiPlayer(filename: String, onFinished: () -> Unit) { + StopMidi() + val file = File(android.app.Instrumentation().context.filesDir, filename) + if(file.exists()){ + androidMediaPlayer?.setDataSource(file.path) + androidMediaPlayer?.prepare() + androidMediaPlayer?.setOnCompletionListener { + onFinished() + } + androidMediaPlayer?.start() + } +} + +actual fun StopMidi() { + androidMediaPlayer?.let { + if (it.isPlaying) { + it.stop() + it.release() + } + } + androidMediaPlayer = null +}*/ diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt new file mode 100644 index 0000000..07374db --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -0,0 +1,27 @@ +package mg.dot.feufaro.midi + +import mg.dot.feufaro.FileRepository + +expect class MediaPlayer(filename: String, onFinished: () -> Unit) { + fun play() + fun pause() + fun stop() + fun getDuration(): Long + fun getLoopState(): Triple + fun toggleVoice(index: Int) + fun getVoiceStates(): List + fun changeInstru(noInstru: Int) + fun getCurrentPosition(): Long + fun seekTo(position: Long) + fun setVolume(level: Float) + fun setPointA() + fun setPointB() + fun clearLoop() + fun setTempo(factor: Float) + fun getCurrentBPM(): Float +} + +/* +expect fun MidiPlayer(filename: String, onFinished: () -> Unit) + +expect fun StopMidi()*/ diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 361ee70..b509243 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.foundation.Canvas +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged @@ -35,6 +36,7 @@ import androidx.compose.ui.unit.sp import mg.dot.feufaro.data.GridTUOData import kotlin.math.min import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -302,9 +304,9 @@ fun TimeUnitComposable( } AutoResizingText( text = text, - minFontSize = 8.sp, + minFontSize = 18.sp, maxFontSize = 18.sp, - maxLines = 1, + maxLines = 2, fontStyle = fontStyle, fontWeight = fontWeight, modifier = Modifier.fillMaxWidth() @@ -419,9 +421,9 @@ fun TimeUnitComposable( } AutoResizingText( text = tuo.lyricsAsMultiString(stanzaNumber), - minFontSize = 8.sp, + minFontSize = 16.sp, maxFontSize = 16.sp, - maxLines = 1, + maxLines = 2, modifier = Modifier.fillMaxWidth() ) } @@ -431,6 +433,7 @@ fun TimeUnitComposable( fun bestTUOWidth(items: List): Dp { val textMeasurer = rememberTextMeasurer() val density = LocalDensity.current + var maxWidth by remember { mutableStateOf(0.dp) } LaunchedEffect(items) { maxWidth = 0.dp @@ -494,7 +497,8 @@ fun AutoResizingText( fontWeight = fontWeight, // Le modificateur drawWithContent est utilisé pour retarder le dessin // jusqu'à ce que la taille de police finale soit déterminée. - modifier = Modifier.fillMaxWidth() // Le modifier du Text interne peut être ajusté + softWrap = false, + modifier = Modifier.fillMaxWidth() .drawWithContent { if (readyToDraw) { drawContent() @@ -557,7 +561,7 @@ fun LazyVerticalGridTUO( .fillMaxWidth(flowRowSize) .combinedClickable( onClick = { - println("Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex") + println("Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex FL $flowRowSize GC $gridColumnCount") } , onDoubleClick = { println("Double-Clicked: ${oneTUO.numBlock} / relative index: $relativeIndex") diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index fdf2787..c952700 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -2,12 +2,21 @@ package mg.dot.feufaro.ui import SharedScreenModel import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -18,16 +27,22 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.platform.* import kotlinx.coroutines.* import mg.dot.feufaro.data.DrawerItem import mg.dot.feufaro.data.getDrawerItems import mg.dot.feufaro.getPlatform +import mg.dot.feufaro.midi.MediaPlayer +//import mg.dot.feufaro.midi.MidiPlayer +//import mg.dot.feufaro.midi.StopMidi import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.viewmodel.SolfaScreenModel +import java.io.File @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -55,42 +70,348 @@ fun MainScreenWithDrawer( var isSearchActive by remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } + var isExpanded by remember { mutableStateOf(false) } + + + +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 midiFile = "whawyd3.mid" +var refreshTrigeer by remember { mutableStateOf(0)} + +var volumelevel by remember { mutableStateOf(0.8f) } + +val mediaPlayer = remember(refreshTrigeer) { + MediaPlayer(filename = midiFile, onFinished = { + isPos = true + isPlay = false + currentPos = 0f +// isPlayMid = true + println("fin de lecture du whawyd3.mid") + + }).apply { setVolume(volumelevel) } +} + +LaunchedEffect(isPlay, isPos, mediaPlayer) { + if (isPlay && !isPos) { + val d = mediaPlayer.getDuration().toFloat() + if (d > 0) duration = d + while (isPlay && !isPos) { + if(!isDragging) { + val p = mediaPlayer.getCurrentPosition().toFloat() + if (p >= 0) currentPos = p + } + delay(100) + } + } +} + LaunchedEffect(isSearchActive) { if (isSearchActive) { focusRequester.requestFocus() } } - ModalNavigationDrawer( - drawerState = drawerState, - drawerContent = { - SimpleDrawerContent( - items, solfaScreenModel, sharedScreenModel, currentActivePath, drawerState, scope, - onScannerButtonClick = { - scope.launch { drawerState.close() } - onScannerButtonClick() - } + ModalNavigationDrawer(drawerState = drawerState, drawerContent = { + SimpleDrawerContent( + items, + solfaScreenModel, + sharedScreenModel, + currentActivePath, + drawerState, + scope, + onScannerButtonClick = { + scope.launch { drawerState.close() } + onScannerButtonClick() + }, + onSongSelected = { newSong -> + mediaPlayer?.stop() + isPos = true + isPlay = false + currentPos = 0f + refreshTrigeer++ + } ) - }, - content = { - Scaffold( - contentWindowInsets = WindowInsets(0, 0, 0, 0), - topBar = { - TopAppBar( - modifier = Modifier - .height(55.dp) - .windowInsetsPadding(WindowInsets.statusBars), - title = { - Column ( - modifier= Modifier - .fillMaxSize() - .verticalScroll(scrollState) + }, content = { + Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { + TopAppBar( + modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = { + Column( + modifier = Modifier.fillMaxSize().verticalScroll(scrollState) + ) { + Text( + songTitle, + modifier = Modifier.weight(1f, fill = true), + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + ) + } + }, navigationIcon = { + IconButton(onClick = { + scope.launch { drawerState.open() } + }) { + Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") + } + }, actions = {/* Text( + text = measure, + modifier = Modifier.weight(1f, fill = false), + fontSize = 20.sp, + maxLines = 2, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.width(8.dp))*/ + Text( + text = songKey, + fontSize = 25.sp, + fontWeight = FontWeight.Black, + ) + Spacer(Modifier.width(8.dp)) + + }, colors = TopAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + scrolledContainerColor = MaterialTheme.colorScheme.onPrimary, + ) + ) + }, floatingActionButton = { + Row( + modifier = Modifier.fillMaxWidth() //horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(7.dp) + ) { + AnimatedVisibility( + visible = isExpanded and !isPlayMid, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + FloatingActionButton( + onClick = { + sharedScreenModel.toggleQRCodeVisibility() + isExpanded = false + }, modifier = Modifier.alpha(0.45f) + ) { + Icon( + imageVector = Icons.Filled.QrCode, + contentDescription = "share qr", + tint = Color.Blue + ) + } + } + + + AnimatedVisibility( + visible = isExpanded and !isPlayMid, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + FloatingActionButton( + onClick = {}, modifier = Modifier.alpha(0.45f) + ) { + Icon( + imageVector = Icons.Filled.Print, + contentDescription = "Imprimer", + tint = Color.Blue + ) + } + } + + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + FloatingActionButton( + onClick = { + isPlayMid = !isPlayMid + if(mediaPlayer.getCurrentPosition() != 0L) { + mediaPlayer?.seekTo(0) + mediaPlayer?.stop() + } + }, modifier = Modifier.alpha(0.45f) + ) { + Icon( + imageVector = if(isPlayMid) Icons.Filled.StopCircle else Icons.Filled.PlayCircle, + contentDescription = "Jouer", + tint = Color.Blue + ) + } + } + if (!isPlayMid) { + FloatingActionButton( + onClick = { + isExpanded = !isExpanded + refreshTrigeer++ + }, modifier = Modifier.alpha(0.45f) + ) { + Icon( + imageVector = if (isExpanded) Icons.Filled.Close else Icons.Filled.Menu, + contentDescription = "MenuFermer", + tint = Color.Blue + ) + } + } + AnimatedVisibility( + visible = isPlayMid, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + Box( + modifier = Modifier.fillMaxWidth(0.9f) + ) { + MidiControlPanel( + isPause = isPos, + currentPos = currentPos, + volume = volumelevel, + duration = duration, + onPlayPauseClick = { + if(isPlay){ + mediaPlayer?.pause() + isPlay = false + isPos = true + } else { + if(currentPos == 0f) { + mediaPlayer?.seekTo(0) + } + 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 + }*/ + + println("je clique pause = $isPos play = $isPlay") +// 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") + } + ) + /*Row( + modifier = Modifier.align(Alignment.Center).padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { + Slider( + value = currentPos, + onValueChange = { newPos -> + currentPos = newPos + mediaPlayer?.seekTo(newPos.toLong()) + }, + valueRange = 0f..duration + ) + } + Button( + onClick = { + isPos = !isPos + if(isPlay) mediaPlayer?.pause() else mediaPlayer?.play() + if(isPos) mediaPlayer?.pause() else mediaPlayer?.play() + } + ) { + Text(if (isPos) "Pause" else "play Midi") + }*/ + } + } + } + } + + }) { paddingValues -> + + Box( + modifier = Modifier.fillMaxSize().padding(paddingValues).windowInsetsPadding(WindowInsets.ime) + ) { + content(PaddingValues(0.dp)) + if (sharedScreenModel.isQRCodeVisible.value) { + QRDisplay(sharedScreenModel = sharedScreenModel) + } else { + if (filteredSongs.isNotEmpty()) { + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight().align(Alignment.TopCenter) + .background(MaterialTheme.colorScheme.surface).border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + shape = MaterialTheme.shapes.medium + ) + ) { + if (filteredSongs.isNotEmpty()) { + LazyColumn(Modifier.fillMaxSize()) { + itemsIndexed(filteredSongs) { index, item -> + ListItem( + headlineContent = { Text(item.title) }, + //supportingContent = { Text(item.contentTitle, maxLines = 1) }, + modifier = Modifier.clickable { + sharedScreenModel.updateSearchTxt("") + sharedScreenModel.reset() + solfaScreenModel.loadFromFile(item.path) + isSearchActive = false + }) + HorizontalDivider() + } + } + } else { + content(paddingValues) + } + } + } + + + Box( + modifier = Modifier.fillMaxSize().fillMaxWidth(0.75f).align(Alignment.Center) + .padding(paddingValues) + ) { + Row( + modifier = Modifier.align(Alignment.TopEnd).padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column( + + ) { + AnimatedContent( - targetState = isSearchActive, - label = "Search Transition" - ) { - targetIsActive -> - if(targetIsActive) { + targetState = isSearchActive, label = "Search Transition" + ) { targetIsActive -> + if (targetIsActive) { TextField( value = textInput, onValueChange = { newValue -> @@ -98,276 +419,76 @@ fun MainScreenWithDrawer( sharedScreenModel.updateSearchTxt(newValue) }, placeholder = { - Text("Rechercher...") + Text("... ...") }, - leadingIcon = { + /*leadingIcon = { Icon( imageVector = Icons.Default.Search, contentDescription = "Icône de recherche", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.primary ) - }, + },*/ textStyle = MaterialTheme.typography.titleLarge.copy( fontSize = 17.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ), - modifier = Modifier - .focusRequester(focusRequester) - .fillMaxWidth() - ) - } else { - Text(songTitle, - modifier = Modifier.weight(1f, fill = true), - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, + modifier = Modifier.focusRequester(focusRequester).fillMaxWidth(0.45f) + .border( + width = 2.dp, + color = Color.Gray, + shape = RoundedCornerShape(8.dp) + ) + .alpha(0.6f) ) } } } - }, - navigationIcon = { - IconButton(onClick = { - scope.launch { drawerState.open() } - }) { - Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") - } - }, - actions = { - if (isSearchActive) { - IconButton(onClick = { - textInput = "" - isSearchActive = false - }) { - Icon(Icons.Default.Close, contentDescription = "Annuler la recherche") - } - } else { - Text( - text = measure, - modifier = Modifier.weight(1f, fill = false), - fontSize = 20.sp, - maxLines = 2, - softWrap = false, - overflow = TextOverflow.Ellipsis - ) - Spacer(Modifier.width(8.dp)) - Text( - text = songKey, - fontSize = 25.sp, - fontWeight = FontWeight.Black, - ) - Spacer(Modifier.width(8.dp)) - IconButton(onClick = { - isSearchActive = true - }) { - Icon(Icons.Default.Search, contentDescription = "Rechercher") - } - } + Column( - }, - colors = TopAppBarColors( - containerColor = MaterialTheme.colorScheme.primary, - titleContentColor = MaterialTheme.colorScheme.onPrimary, - actionIconContentColor = MaterialTheme.colorScheme.onPrimary, - navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, - scrolledContainerColor = MaterialTheme.colorScheme.onPrimary, - ) - ) - }, - floatingActionButton = { - FloatingActionButton ( - onClick = { - sharedScreenModel.toggleQRCodeVisibility() - }, - modifier = Modifier.alpha(0.5f) - ) { - Icon( - imageVector = Icons.Filled.Share, - contentDescription = "Partager", - tint = Color.Blue - ) - } - } - ) { - paddingValues -> - - Box(modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .windowInsetsPadding(WindowInsets.ime) - ){ - content(PaddingValues(0.dp)) - if (sharedScreenModel.isQRCodeVisible.value) { - QRDisplay(sharedScreenModel = sharedScreenModel) - } else { - if (filteredSongs.isNotEmpty()) { - Column( - modifier = Modifier - .fillMaxWidth(0.65f) - .heightIn(max = 350.dp) - .align(Alignment.TopCenter) - .background(MaterialTheme.colorScheme.surface) - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape = MaterialTheme.shapes.medium) - ){ - if (filteredSongs.isNotEmpty()) { - LazyColumn(Modifier.fillMaxSize()) { - itemsIndexed(filteredSongs){ index, item -> - ListItem( - headlineContent = { Text(item.title) }, - supportingContent = { Text(item.path, maxLines = 1) }, - modifier = Modifier.clickable { - sharedScreenModel.updateSearchTxt("") - sharedScreenModel.reset() - solfaScreenModel.loadFromFile(item.path) - isSearchActive = false - } - ) - HorizontalDivider() - } - } - } else if (songTitle.trim().isNotEmpty() && songTitle != sharedScreenModel.songTitle.value) { - Text( - text = "Aucune chanson trouvée pour \"$songTitle\"", - modifier = Modifier.padding(16.dp) + ) { + IconButton( + onClick = { + isSearchActive = !isSearchActive + sharedScreenModel.updateSearchTxt("") + textInput = "" + }, modifier = Modifier.size(56.dp).alpha(0.45f).background( + color = Color.Blue, shape = CircleShape + ) + ) { + Icon( + if(isSearchActive) Icons.Default.Close else Icons.Default.Search, + contentDescription = "la recherche", + tint = Color.White ) - } else { - content(paddingValues) } } + } } + + } } } - ) + }) } +/* -@Composable -fun SimpleDrawerContent( - items: List, - solfaScreenModel: SolfaScreenModel, - sharedScreenModel: SharedScreenModel, - activePath: String = Solfa.currentFile, - drawerState: DrawerState, - scope: CoroutineScope, - onScannerButtonClick: () -> Unit -) { - var searchTxt by remember { mutableStateOf("") } +var isPlay by mutableStateOf(false) + private set - val context = getPlatform() - - - ModalDrawerSheet( - modifier = Modifier.width(300.dp) - ) { - Box ( - modifier = Modifier.fillMaxSize() - ) - { - val lazyListState = rememberLazyListState() - ScrollableDrawerContent( - lazyListState = lazyListState, - modifier = Modifier.fillMaxSize() - ) { - LazyColumn( - state = lazyListState, - ){ - stickyHeader { - Surface( - color = MaterialTheme.colorScheme.surfaceContainerHigh, - modifier = Modifier.fillParentMaxWidth() - ) { - Text( - "Liste des solfa disponibles", - style = MaterialTheme.typography.titleLarge - ) - } - } - itemsIndexed(items){ index, item -> - val isSelected = item.path == activePath - val title = item.title - var isFfpm = false - var isEws = false - var isFF = false - - if (title.startsWith("ffpm")) { - isFfpm = true - } else if(title.startsWith("ews")) { - isEws = true - } else { - isFF = true - } - NavigationDrawerItem( - label = { - Text(item.title) - }, - icon = { - val isIcon = when { - isFfpm -> Icons.Filled.MenuBook - isEws -> Icons.Filled.MusicNote - isFF -> Icons.Filled.Book - else -> Icons.Filled.Menu - } - Icon( - isIcon, - contentDescription = "", - tint = Color.Blue - ) - }, - badge = { - Icon( - Icons.Filled.Menu, - contentDescription = "" - ) - }, - selected = if(isSelected){ true } else { false }, - onClick = { - scope.launch { - drawerState.close() - } - sharedScreenModel.reset() - solfaScreenModel.loadFromFile(item.path) - } - ) - } - } - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row ( - - ) { - Spacer(modifier = Modifier.width(10.dp)) - Button( - onClick = { - onScannerButtonClick() - } - ) { - Text("Scanner") - } - Spacer(modifier = Modifier.width(10.dp)) - Button( - onClick = { - scope.launch { - drawerState.close() - } - solfaScreenModel.loadCustomFile() - } - ) { - Text("Importer") - } - } - } - } - } +fun playMidi() { + if(!isPlay) { + isPlay = true + MidiPlayer("whawyd3.mid") { + isPlay = false } } -} \ No newline at end of file +} +fun stopMidi() { + if(isPlay) { + StopMidi() + isPlay = false + } +}*/ diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt new file mode 100644 index 0000000..ce9c760 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt @@ -0,0 +1,410 @@ +package mg.dot.feufaro.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Church +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.ClearAll +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material.icons.filled.Loop +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.Piano +import androidx.compose.material.icons.filled.PianoOff +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.SettingsVoice +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.Tonality +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.VolumeOff +import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledIconToggleButton +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.produceState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import mg.dot.feufaro.midi.MediaPlayer +import javax.swing.Icon + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MidiControlPanel( + isPause: Boolean, + currentPos: Float, + volume: Float, + duration: Float, + onPlayPauseClick: () -> Unit, + onSeek: (Float) -> Unit, + onVolumeChange: (Float) -> Unit, + mediaPlayer: MediaPlayer, + modifier: Modifier = Modifier +) { + val momo = duration.toInt() - currentPos.toInt() + + val loopState by produceState(Triple(-1L, -1L, false), mediaPlayer) { + while(true) { + value = mediaPlayer.getLoopState() + delay(200) + } + } + + val voiceStates = mediaPlayer.getVoiceStates() + val labels = listOf("S","A","T","B") + val fullLabels = listOf("Soprano","Alto","Ténor","Basse") + + var tempo by remember { mutableStateOf(1.0f) } + var currentBpm by remember { mutableStateOf(mediaPlayer.getCurrentBPM()) } + val basseBpm = 120f + var bpmInput by remember { mutableStateOf((basseBpm * tempo).toInt().toString()) } + + var isPianoSelected by remember { mutableStateOf(true) } + + var showBPMTools by remember { mutableStateOf(false) } + var showInstruTools by remember { mutableStateOf(false) } + var showSATBTools by remember { mutableStateOf(false) } + var showVolumeTools by remember { mutableStateOf(false) } + + LaunchedEffect(tempo) { + currentBpm = mediaPlayer.getCurrentBPM() + } + Column ( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row ( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("${currentPos.toInt() / 1000}s") + Slider( + value = currentPos, + onValueChange = onSeek, + valueRange = 0f..(if (duration > 0) duration else 1f), + modifier = Modifier.weight(1f), + colors = SliderDefaults.colors( + thumbColor = Color.Red, + activeTrackColor = Color.Green, + inactiveTrackColor = Color.Gray, + inactiveTickColor = Color(0xffb0BEC5), + disabledThumbColor = Color(0xff78909C), + disabledActiveTickColor = Color(0xff757575), + disabledActiveTrackColor = Color(0xffBDBDBD), + disabledInactiveTickColor = Color(0xff616161), + disabledInactiveTrackColor = Color(0xffBCAAA4), + ), + thumb = { + Box ( + modifier = Modifier + .size(15.dp) + .background(Color.Gray, CircleShape) + ) + }, + track = { sliderState -> + SliderDefaults.Track( + sliderState = sliderState, + modifier = Modifier.height(5.dp) + ) + } + ) + Text("${momo / 1000}s") + } + + + Row( + verticalAlignment = Alignment.CenterVertically + ){ + + + + + IconButton( + onClick = onPlayPauseClick, + modifier = Modifier.size(48.dp) + ) { + Icon( + imageVector = if (isPause) Icons.Filled.PlayArrow else Icons.Filled.Pause, + contentDescription = "Pla", + tint = MaterialTheme.colorScheme.primary + ) + } + + } + AnimatedVisibility( + visible = showSATBTools, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + Row { + Column ( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.padding(vertical = 8.dp), + horizontalArrangement = Arrangement. spacedBy(4.dp) + ) { + labels.forEachIndexed { index, label -> + FilterChip( + selected = voiceStates[index], + onClick = { mediaPlayer.toggleVoice(index) }, + label = { Text(label) }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, + selectedLabelColor = MaterialTheme.colorScheme.primary + ) + ) + } + } + } + } + } + AnimatedVisibility( + visible = showInstruTools, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + Row ( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + FilledIconToggleButton( + checked = isPianoSelected, + onCheckedChange = { + isPianoSelected = true; + mediaPlayer.changeInstru(1) + } + ){ + Icon(imageVector = Icons.Filled.Piano, contentDescription = "Piano") + } + FilledIconToggleButton( + checked = !isPianoSelected, + onCheckedChange = { + isPianoSelected = false; + mediaPlayer.changeInstru(20) + }){ + Icon(imageVector = Icons.Filled.PianoOff, contentDescription = "Church Organ") + } + } + } + AnimatedVisibility( + visible = showBPMTools, + enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, + exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ){ + Column( + horizontalAlignment = Alignment.Start + ) { + OutlinedTextField( + value = bpmInput, onValueChange = { newValue -> + if (newValue.all { it.isDigit() } && newValue.length <= 3) { + bpmInput = newValue + } + }, label = { Text("BPM") }, singleLine = true, keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, imeAction = ImeAction.Done + ), keyboardActions = KeyboardActions( + onDone = { + val newBpm = bpmInput.toFloatOrNull() ?: 0f + val newFactor = newBpm / basseBpm + tempo = newFactor + mediaPlayer.setTempo(newFactor) + bpmInput = (basseBpm * newFactor).toInt().toString() + }), modifier = Modifier.width(65.dp)) + } + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Slider(value = tempo, onValueChange = { + tempo = it + mediaPlayer?.setTempo(it) + currentBpm = mediaPlayer.getCurrentBPM() + bpmInput = (basseBpm * it).toInt().toString() + }, valueRange = 0.25f..1.5f, modifier = Modifier.width(200.dp), thumb = { + Box( + modifier = Modifier.size(15.dp).background(Color.Magenta, CircleShape) + ) + }, track = { sliderState -> + SliderDefaults.Track( + sliderState = sliderState, modifier = Modifier.height(5.dp) + ) + }) + } + Column( + horizontalAlignment = Alignment.End + ) { + IconButton(onClick = { + tempo = 1.0f + mediaPlayer?.setTempo(1.0f) + currentBpm = mediaPlayer.getCurrentBPM() + }) { + Icon(Icons.Default.Refresh, contentDescription = "Reset") + } + } + } + } + Row( + verticalAlignment = Alignment.CenterVertically + ){ + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button( + onClick = { + if (loopState.first == -1L) { + mediaPlayer.setPointA() + } else if(!loopState.third) { + mediaPlayer.setPointB() + } + }, + colors = ButtonDefaults.buttonColors( + containerColor = if (loopState.third) Color.Green else MaterialTheme.colorScheme.secondary + ) + ) { + when { + loopState.first == -1L -> Text("A?") + !loopState.third -> Text("B?") + else -> Icon( + imageVector = Icons.Default.Loop, + contentDescription = "boucle" + ) + } + } + } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if(loopState.first != -1L) { + IconButton( + onClick = { + mediaPlayer.clearLoop() + } + ) { + Icon(Icons.Default.Clear, contentDescription = "Actualiser", ) + } + } + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + IconButton( + onClick = { + showBPMTools = !showBPMTools + }) { + Icon(imageVector = Icons.Default.MusicNote, contentDescription = "Tempo") + } + AnimatedVisibility(visible = showBPMTools){ + Box( + modifier = Modifier + .width(20.dp) + .height(3.dp) + .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(2.dp)) + )} + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + IconButton( + onClick = { + showInstruTools = !showInstruTools + }) { + Icon(imageVector = Icons.Default.Tune, contentDescription = "Instru") + } + AnimatedVisibility(visible = showInstruTools){ + Box( + modifier = Modifier + .width(20.dp) + .height(3.dp) + .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(2.dp)) + )} + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + IconButton( + onClick = { + showSATBTools = !showSATBTools + }) { + Icon(imageVector = Icons.Default.SettingsVoice, contentDescription = "SATB") + } + AnimatedVisibility(visible = showSATBTools){ + Box( + modifier = Modifier + .width(20.dp) + .height(3.dp) + .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(2.dp)) + )} + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = if (volume > 0) Icons.Filled.VolumeUp else Icons.Filled.VolumeOff, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Slider( + value = volume, + onValueChange = onVolumeChange, + valueRange = 0f..1f, + modifier = Modifier.width(100.dp), + thumb = { + Box( + modifier = Modifier.size(15.dp).background(Color.Blue, CircleShape) + ) + }, + track = { sliderState -> + SliderDefaults.Track( + sliderState = sliderState, modifier = Modifier.height(5.dp) + ) + }) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt new file mode 100644 index 0000000..234e07e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -0,0 +1,173 @@ +package mg.dot.feufaro.ui + +import SharedScreenModel +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Book +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.MenuBook +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material3.Button +import androidx.compose.material3.DrawerState +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mg.dot.feufaro.data.DrawerItem +import mg.dot.feufaro.getPlatform +import mg.dot.feufaro.solfa.Solfa +import mg.dot.feufaro.viewmodel.SolfaScreenModel + +@Composable +fun SimpleDrawerContent( + items: List, + solfaScreenModel: SolfaScreenModel, + sharedScreenModel: SharedScreenModel, + activePath: String = Solfa.currentFile, + drawerState: DrawerState, + scope: CoroutineScope, + onScannerButtonClick: () -> Unit, + onSongSelected: (String) -> Unit, +) { + var searchTxt by remember { mutableStateOf("") } + + val context = getPlatform() + +val midi = "whawyd3.mid" + ModalDrawerSheet( + modifier = Modifier.width(300.dp) + ) { + Box ( + modifier = Modifier.fillMaxSize() + ) + { + val lazyListState = rememberLazyListState() + ScrollableDrawerContent( + lazyListState = lazyListState, + modifier = Modifier.fillMaxSize() + ) { + LazyColumn( + state = lazyListState, + ){ + stickyHeader { + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier.fillParentMaxWidth() + ) { + Text( + "Liste des solfa disponibles", + style = MaterialTheme.typography.titleLarge + ) + } + } + itemsIndexed(items){ index, item -> + val isSelected = item.path == activePath + val title = item.title + var isFfpm = false + var isEws = false + var isFF = false + + if (title.startsWith("ffpm")) { + isFfpm = true + } else if(title.startsWith("ews")) { + isEws = true + } else { + isFF = true + } + NavigationDrawerItem( + label = { + Text(item.title) + }, + icon = { + val isIcon = when { + isFfpm -> Icons.Filled.MenuBook + isEws -> Icons.Filled.MusicNote + isFF -> Icons.Filled.Book + else -> Icons.Filled.Menu + } + Icon( + isIcon, + contentDescription = "", + tint = Color.Blue + ) + }, + badge = { + Icon( + Icons.Filled.Menu, + contentDescription = "" + ) + }, + selected = if(isSelected){ true } else { false }, + onClick = { + scope.launch { + drawerState.close() + } + sharedScreenModel.reset() + solfaScreenModel.loadFromFile(item.path) + onSongSelected(midi) + } + ) + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row ( + + ) { + Spacer(modifier = Modifier.width(10.dp)) + Button( + onClick = { + onScannerButtonClick() + } + ) { + Text("Scanner") + } + Spacer(modifier = Modifier.width(10.dp)) + Button( + onClick = { + scope.launch { + drawerState.close() + } + solfaScreenModel.loadCustomFile() + } + ) { + Text("Importer") + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt new file mode 100644 index 0000000..20a3c95 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -0,0 +1,240 @@ +package mg.dot.feufaro.midi + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import mg.dot.feufaro.FileRepository +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.* + +//private var sequencer: javax.sound.midi.Sequencer?= null +actual class MediaPlayer actual constructor( + private val filename: String, + private val onFinished: () -> Unit +) { + 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 pointA: Long = -1L + private var pointB: Long = -1L + private var isLoopingAB: Boolean = false + + private val voiceStates = mutableListOf(true, true, true, true) + + private var currentGlobalVolume: Float = 0.8f + + private var currentTempo: Float = 1.0f + + init { + sequencer?.open() + synthetizer?.open() + + 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() + } + } + } + } + actual fun play(){ + if (sequencer!!.isOpen){ + sequencer?.start() + println("La sequence vient d etre lancé ${sequencer?.isRunning}") + } + } + actual fun pause(){ + sequencer?.stop() + } + actual fun stop(){ + sequencer?.stop() + sequencer?.microsecondPosition = 0 +// disableLoop() + } + actual fun getDuration(): Long { + return (sequencer?.microsecondLength ?: 0L) / 1000 + } + actual fun getCurrentPosition(): Long { + return (sequencer?.microsecondPosition ?: 0L) / 1000 + } + actual fun seekTo(position: Long) { + sequencer?.microsecondPosition = position * 1000 + } + fun release() { + sequencer?.close() +// synthetizer?.close() + } + actual fun setVolume(level: Float) { + try { + this.currentGlobalVolume = level + val volumeInt = (level * 127).toInt().coerceIn(0, 127) + + synthetizer?.channels?.forEachIndexed { index, channel -> + if (index < 4) { + val vol = if (voiceStates[index]) volumeInt else 0 + channel?.controlChange(7, vol) + } else { + channel?.controlChange(7, volumeInt) + } + } + /*val logVolume = if (level > 0 ){ + (Math.log10(level.toDouble() * 9 + 1) / Math.log10(10.0)).toFloat() + } else { + 0f + } + val volumeInt = (logVolume * 127).toInt().coerceIn(0, 127) + + synthetizer?.channels?.forEach { it?.controlChange(7, volumeInt) } + synthetizer?.channels?.forEach { it?.controlChange(11, volumeInt) } +*/ + val mixer = AudioSystem.getMixer(null) + val lines = mixer.sourceLines + for (line in lines) { + if(line.isControlSupported(FloatControl.Type.MASTER_GAIN)) { + val gainControl = line.getControl(FloatControl.Type.MASTER_GAIN) as FloatControl + + val dB = (Math.log10(level.toDouble().coerceAtLeast(0.0001)) * 20).toFloat() + gainControl.value = dB.coerceIn(gainControl.minimum, gainControl.maximum) + } + } + +// val msg = ShortMessage() +// val receiver = sequencer?.receiver +// for (i in 0 until 16) { +// val msg = ShortMessage() +// msg.setMessage(ShortMessage.CONTROL_CHANGE, i, 7, volumeInt) +// receiver?.send(msg, -1) +// } + + } catch (e: Exception){ + e.printStackTrace() + } + println("la volume $level") + } + + actual fun setPointA() { + pointA = sequencer?.tickPosition ?: 0L + } + + actual fun setPointB() { + pointB = sequencer?.tickPosition ?: 0L + if (pointB > pointA && pointA != -1L) { + isLoopingAB = true + startABMonitor() + } + } + + actual fun clearLoop() { + isLoopingAB = false + pointA = -1L + pointB = -1L + } + private fun startABMonitor() { + GlobalScope.launch { + while(isLoopingAB) { + val currentTick = sequencer?.tickPosition?: 0L + if (currentTick >= pointB) { + sequencer?.tickPosition = pointA + } + delay(50) + } + } + } + actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB) + + actual fun toggleVoice(index: Int) { + voiceStates[index] = !voiceStates[index] + applyVoiceStates() + } + + private fun applyVoiceStates() { + try { + synthetizer?.channels?.let { channels -> + for (i in 0 until 4) { + if (i < channels.size) { + val isVoiceActive = voiceStates[i] + val volume = if (voiceStates[i]) 127 else 0 + channels[i].controlChange(7, volume) + channels[i].controlChange(11, volume) + if(!isVoiceActive) { + channels[i].controlChange(123, 0) + } + } + } + println("STAB màj: $voiceStates") + } + } catch (e: Exception) { e.printStackTrace() } + } + + actual fun getVoiceStates(): List = voiceStates + + actual fun changeInstru(noInstru: Int) { + val pgm = noInstru + synthetizer?.channels?.let { + channels -> + for (i in 0 until 4) { + channels[i].programChange(pgm) + } + } + } + actual fun getCurrentBPM(): Float { +// return sequencer?.tempoInBPM?.toInt() ?: 0 + val currentFactor = sequencer?.tempoFactor ?: 1.0f +// val currentBPM = sequencer?.tempoInBPM ?: 120.0f + val currentBPM = (120f * currentFactor) +// println("202: ${sequencer?.tempoInBPM} ${sequencer?.tempoFactor} ${sequencer?.tempoInMPQ}") + return currentBPM + } + + actual fun setTempo(factor: Float){ + currentTempo = factor + sequencer?.tempoFactor = factor + } + fun getTempo(): Float = currentTempo +} + + +/* + +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() + } + } + start() + } + } +} + +actual fun StopMidi() { + if(sequencer?.isRunning == true){ + sequencer?.stop() + sequencer?.close() + } +}*/ diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt index 0a7eac2..2fa5a25 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt @@ -17,31 +17,31 @@ actual class MidiWriterKotlin actual constructor(private val fileRepository: Fil private val lastPitch : MutableList = mutableListOf() private val useChord : Boolean = true actual fun addNote( voiceNumber: Int, note: Int, velocity: Int, tick: Long) { - var channel: Int = voiceNumber - 1 - if (useChord) { - channel = channel / 2 - } - var note = note + val channel = (voiceNumber -1).coerceIn(0, 3) + var finalNote = note + if (voiceNumber == 3 || voiceNumber == 4) { - note -= 12 + finalNote -= 12 } if (lastPitch.size > voiceNumber && lastPitch[voiceNumber] > 0) { - noteOff.setMessage(ShortMessage.NOTE_OFF, channel, lastPitch[voiceNumber], 0) - val n2 = noteOff.clone() as MidiMessage - track.add(MidiEvent(n2, tick)) + val offMsg = ShortMessage() + offMsg.setMessage(ShortMessage.NOTE_OFF, channel, lastPitch[voiceNumber], 0) + track.add(MidiEvent(offMsg, tick)) } - var velocity = velocity - if (note <= 0) { - note = 40 - velocity = 0 + var finalVelocity = velocity + var midiNote = finalNote + if (finalNote <= 0) { + midiNote = 40 + finalVelocity = 0 } - noteOn.setMessage(ShortMessage.NOTE_ON, channel, note, velocity) - val n1: MidiMessage = noteOn.clone() as MidiMessage - track.add(MidiEvent(n1, tick)) + val onMsg = ShortMessage() + onMsg.setMessage(ShortMessage.NOTE_ON, channel, midiNote, finalVelocity) + track.add(MidiEvent(onMsg, tick)) + while(lastPitch.size <= voiceNumber) { lastPitch.add(0) } - lastPitch[voiceNumber] = note + lastPitch[voiceNumber] = midiNote } actual fun save(filePath: String) { val parseScope = CoroutineScope(Dispatchers.Default)