added MidiPlayer

This commit is contained in:
dotmg 2026-01-18 16:22:42 +01:00
parent 5ae8e292c3
commit 5733709853
8 changed files with 1400 additions and 296 deletions

View file

@ -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<Boolean> = 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
}*/

View file

@ -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<Long, Long, Boolean>
fun toggleVoice(index: Int)
fun getVoiceStates(): List<Boolean>
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()*/

View file

@ -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<TimeUnitObject>): 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")

View file

@ -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<DrawerItem>,
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
}
}
}
fun stopMidi() {
if(isPlay) {
StopMidi()
isPlay = false
}
}*/

View file

@ -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)
)
})
}
}
}
}

View file

@ -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<DrawerItem>,
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")
}
}
}
}
}
}
}
}

View file

@ -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<Boolean> = 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()
}
}*/

View file

@ -17,31 +17,31 @@ actual class MidiWriterKotlin actual constructor(private val fileRepository: Fil
private val lastPitch : MutableList<Int> = 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)