added MidiPlayer
This commit is contained in:
parent
5ae8e292c3
commit
5733709853
8 changed files with 1400 additions and 296 deletions
|
|
@ -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
|
||||
}*/
|
||||
|
|
@ -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()*/
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,95 +70,93 @@ 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 = {
|
||||
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
|
||||
SimpleDrawerContent(
|
||||
items, solfaScreenModel, sharedScreenModel, currentActivePath, drawerState, scope,
|
||||
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 = {
|
||||
}, 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)
|
||||
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState)
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = isSearchActive,
|
||||
label = "Search Transition"
|
||||
) {
|
||||
targetIsActive ->
|
||||
if(targetIsActive) {
|
||||
TextField(
|
||||
value = textInput,
|
||||
onValueChange = { newValue ->
|
||||
textInput = newValue
|
||||
sharedScreenModel.updateSearchTxt(newValue)
|
||||
},
|
||||
placeholder = {
|
||||
Text("Rechercher...")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Icône de recherche",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
textStyle = MaterialTheme.typography.titleLarge.copy(
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
Text(songTitle,
|
||||
Text(
|
||||
songTitle,
|
||||
modifier = Modifier.weight(1f, fill = true),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
}, 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(
|
||||
}, actions = {/* Text(
|
||||
text = measure,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
fontSize = 20.sp,
|
||||
|
|
@ -151,22 +164,15 @@ fun MainScreenWithDrawer(
|
|||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
colors = TopAppBarColors(
|
||||
}, colors = TopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
|
|
@ -174,200 +180,315 @@ fun MainScreenWithDrawer(
|
|||
scrolledContainerColor = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton (
|
||||
}, 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()
|
||||
},
|
||||
modifier = Modifier.alpha(0.5f)
|
||||
isExpanded = false
|
||||
}, modifier = Modifier.alpha(0.45f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Share,
|
||||
contentDescription = "Partager",
|
||||
imageVector = Icons.Filled.QrCode,
|
||||
contentDescription = "share qr",
|
||||
tint = Color.Blue
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
paddingValues ->
|
||||
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.windowInsetsPadding(WindowInsets.ime)
|
||||
){
|
||||
|
||||
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(0.65f)
|
||||
.heightIn(max = 350.dp)
|
||||
.align(Alignment.TopCenter)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape = MaterialTheme.shapes.medium)
|
||||
){
|
||||
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 ->
|
||||
itemsIndexed(filteredSongs) { index, item ->
|
||||
ListItem(
|
||||
headlineContent = { Text(item.title) },
|
||||
supportingContent = { Text(item.path, maxLines = 1) },
|
||||
//supportingContent = { Text(item.contentTitle, 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)
|
||||
)
|
||||
} 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("") }
|
||||
|
||||
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
|
||||
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(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row (
|
||||
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
onScannerButtonClick()
|
||||
|
||||
AnimatedContent(
|
||||
targetState = isSearchActive, label = "Search Transition"
|
||||
) { targetIsActive ->
|
||||
if (targetIsActive) {
|
||||
TextField(
|
||||
value = textInput,
|
||||
onValueChange = { newValue ->
|
||||
textInput = newValue
|
||||
sharedScreenModel.updateSearchTxt(newValue)
|
||||
},
|
||||
placeholder = {
|
||||
Text("... ...")
|
||||
},
|
||||
/*leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Icône de recherche",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},*/
|
||||
textStyle = MaterialTheme.typography.titleLarge.copy(
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.focusRequester(focusRequester).fillMaxWidth(0.45f)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = Color.Gray,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.alpha(0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
|
||||
) {
|
||||
Text("Scanner")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Button(
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
solfaScreenModel.loadCustomFile()
|
||||
}
|
||||
isSearchActive = !isSearchActive
|
||||
sharedScreenModel.updateSearchTxt("")
|
||||
textInput = ""
|
||||
}, modifier = Modifier.size(56.dp).alpha(0.45f).background(
|
||||
color = Color.Blue, shape = CircleShape
|
||||
)
|
||||
) {
|
||||
Text("Importer")
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
if(isSearchActive) Icons.Default.Close else Icons.Default.Search,
|
||||
contentDescription = "la recherche",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
/*
|
||||
|
||||
var isPlay by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun playMidi() {
|
||||
if(!isPlay) {
|
||||
isPlay = true
|
||||
MidiPlayer("whawyd3.mid") {
|
||||
isPlay = false
|
||||
}
|
||||
}
|
||||
}
|
||||
fun stopMidi() {
|
||||
if(isPlay) {
|
||||
StopMidi()
|
||||
isPlay = false
|
||||
}
|
||||
}*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}*/
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue