Compare commits

...

4 commits

5 changed files with 215 additions and 110 deletions

View file

@ -2,8 +2,19 @@ package mg.dot.feufaro
import android.content.Context
import org.koin.core.context.GlobalContext
import java.io.File
actual fun getConfigDirectoryPath(): String {
val context = GlobalContext.get().get<Context>()
return context.filesDir.absolutePath
val baseDir = GlobalContext.get().get<Context>().filesDir
val feufa2Folder = File(baseDir, "Feufaro")
if (!feufa2Folder.exists()) {
val created = feufa2Folder.mkdirs()
if (created) {
println("CP:15: Dossier créé avec succès : ${feufa2Folder.absolutePath}")
}
}
val path = feufa2Folder.absolutePath
return if (path.endsWith(File.separator)) path else path + File.separator
}

View file

@ -1,129 +1,142 @@
package mg.dot.feufaro.midi
import java.io.File
import android.media.MediaPlayer
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileInputStream
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 var mediaPlayer: MediaPlayer? = MediaPlayer()
private val voiceStates = mutableListOf(true, true, true, true)
private var currentGlobalVolume: Float = 0.8f
// private var currentGlobalVolume: Float = 0.8f
private var pointA: Long = -1L
private var pointB: Long = -1L
private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private var abJob: Job? = null
private var isLoopingAB: Boolean = false
init {
try {
val file = File(filename)
if (file.exists()) {
val fis = FileInputStream(file)
mediaPlayer?.setDataSource(fis.fd)
mediaPlayer?.prepare()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
val params = mediaPlayer?.playbackParams ?: android.media.PlaybackParams()
params.speed = 1.0f
mediaPlayer?.playbackParams = params
}
fis.close()
mediaPlayer?.setOnCompletionListener {
onFinished()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
actual fun play() {
// player?.start()
mediaPlayer?.start()
}
actual fun pause() {
// player?.pause()
mediaPlayer?.pause()
}
actual fun stop() {
// player?.stop()
mediaPlayer?.stop()
mediaPlayer?.prepare()
mediaPlayer?.seekTo(0)
clearLoop()
}
actual fun getDuration(): Long {
// player!!.duration.toLong() ?: 0L
return 4.toLong()
return mediaPlayer?.duration?.toLong() ?: 0L
}
actual fun getCurrentPosition(): Long {
// player!!.currentPosition.toLong()
return 4.toLong()
return mediaPlayer?.currentPosition?.toLong() ?: 0L
}
actual fun seekTo(position: Long) {
// player!!.seekTo(position.toInt())
mediaPlayer?.seekTo(position.toInt())
}
actual fun setVolume(level: Float) {
// currentGlobalVolume =level
// player?.setVolume(level, level)
mediaPlayer?.setVolume(level, level)
}
actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB)
actual fun toggleVoice(index: Int): Unit{
voiceStates[index] = !voiceStates[index]
actual fun setPointA() {
pointA = mediaPlayer?.currentPosition?.toLong() ?: 0L
}
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) {
actual fun setPointB() {
pointB = mediaPlayer?.currentPosition?.toLong() ?: 0L
if (pointB > pointA && pointA != -1L) {
isLoopingAB = true
startABMonitor()
}
}
actual fun clearLoop(): Unit{
actual fun clearLoop() {
isLoopingAB = false
pointA = -1L
pointB = -1L
abJob?.cancel()
}
private fun startABMonitor() {
GlobalScope.launch {
abJob?.cancel()
abJob = playerScope.launch {
while (isLoopingAB) {
// if ((player?.currentPosition ?: 0) >= pointB){
// player?.seekTo(pointA?.toInt())
// }
val currentPos = mediaPlayer?.currentPosition?.toLong() ?: 0L
if (currentPos >= pointB) {
mediaPlayer?.seekTo(pointA.toInt())
}
delay(50)
}
}
}
actual fun setTempo(factor: Float){
actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB)
actual fun toggleVoice(index: Int) {
voiceStates[index] = !voiceStates[index]
println("Toggle voice $index (Limitation: Nécessite SoundFont/Fluidsynth sur Android)")
}
actual fun getVoiceStates(): List<Boolean> = voiceStates
actual fun changeInstru(noInstru: Int) {
}
actual fun setTempo(factor: Float) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
mediaPlayer?.let {
val params = it.playbackParams
params.speed = factor
it.playbackParams = params
}
}
}
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()
fun release() {
mediaPlayer?.release()
mediaPlayer = null
playerScope.coroutineContext.cancel()
}
}
androidMediaPlayer = null
}*/

View file

@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntOffset
@ -64,6 +65,7 @@ object ScreenSolfa : Screen {
val gridTUOData = GridTUOData(measure, tuoList, stanza)
val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var viewportHeight by remember { mutableStateOf(0) }
var isScanning by remember { mutableStateOf(false) }
var qrCodeResult by remember { mutableStateOf("Aucun code scanné") }
@ -75,7 +77,8 @@ object ScreenSolfa : Screen {
qrCodeResult = qrCodeResult,
onScannerButtonClick = {
isScanning = true
}
},
solfaScrollState = scrollState
) { paddingValues ->
Box(
Modifier.fillMaxSize().padding(paddingValues)
@ -91,6 +94,9 @@ object ScreenSolfa : Screen {
showContextualMenu = true
}
)
}
.onGloballyPositioned { coordinates ->
viewportHeight = coordinates.size.height
},
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -192,7 +198,7 @@ object ScreenSolfa : Screen {
}
Row (
modifier = Modifier
.height(199.dp)
.height(75.dp)
) {
}
}

View file

@ -50,6 +50,7 @@ fun MainScreenWithDrawer(
sharedScreenModel: SharedScreenModel,
qrCodeResult: String,
onScannerButtonClick: () -> Unit,
solfaScrollState: ScrollState,
content: @Composable (PaddingValues) -> Unit,
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
@ -79,7 +80,7 @@ val isPos by sharedScreenModel.isPos.collectAsState()
var isDragging = sharedScreenModel.isDragging
val currentPos by sharedScreenModel.currentPos.collectAsState()
val duration by sharedScreenModel.duration.collectAsState()
val midiFile = "whawyd3.mid"
val midiFile = "whawyd3.mid" //What do we gain by all our hard work?
var refreshTrigeer by remember { mutableStateOf(0)}
val volumelevel by sharedScreenModel.volumeLevel.collectAsState()
@ -248,6 +249,7 @@ LaunchedEffect(isPlay, isPos) {
currentPos = currentPos,
volume = volumelevel,
duration = duration,
solfaScrollState,
onPlayPauseClick = {
sharedScreenModel.togglePlayPause()
},

View file

@ -1,6 +1,11 @@
package mg.dot.feufaro.ui
import androidx.compose.animation.*
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.ExperimentalAnimationSpecApi
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -34,6 +39,7 @@ fun MidiControlPanel(
currentPos: Float,
volume: Float,
duration: Float,
solfaScrollState: ScrollState,
onPlayPauseClick: () -> Unit,
onSeek: (Float) -> Unit,
onVolumeChange: (Float) -> Unit,
@ -61,6 +67,26 @@ fun MidiControlPanel(
var showSATBTools by remember { mutableStateOf(false) }
var expandedCtl by remember { mutableStateOf(false) }
val platform = getPlatform().name
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(currentPos, duration, solfaScrollState.viewportSize, solfaScrollState.maxValue) {
if (duration > 0f && solfaScrollState.maxValue > 0) {
val progress = (currentPos / duration).coerceIn(0f, 1f)
val viewportHeight = solfaScrollState.viewportSize.toFloat()
val totalContentHeight = solfaScrollState.maxValue + viewportHeight
val targetY = (progress * (totalContentHeight-200)) - (viewportHeight / 2)
val finalTarget = targetY.coerceIn(0f, solfaScrollState.maxValue.toFloat())
solfaScrollState.animateScrollTo(
value = finalTarget.toInt(),
animationSpec = tween (
durationMillis = 300,
easing = LinearEasing
)
)
}
}
LaunchedEffect(tempo) {
currentBpm = mediaPlayer.getCurrentBPM()
}
@ -79,7 +105,7 @@ fun MidiControlPanel(
tempo = newFactor
mediaPlayer?.setTempo(newFactor)
println("tempo : $tempo")
// println("tempo : $tempo")
}
Column(
modifier = Modifier.fillMaxWidth()
@ -91,8 +117,7 @@ fun MidiControlPanel(
) {
Column(
modifier = modifier
.fillMaxWidth(if (getPlatform().name.startsWith("Android")) 0.9f else 0.6f)
.padding(16.dp)
.fillMaxWidth(if (platform.startsWith("Android")) 1f else 0.6f)
.background(color = Color.Gray.copy(alpha = 0.5f), shape = RoundedCornerShape(size = 5.dp)),
horizontalAlignment = Alignment.CenterHorizontally
) {
@ -214,7 +239,7 @@ fun MidiControlPanel(
slideInHorizontally { -it } + fadeIn() togetherWith slideOutHorizontally { it } + fadeOut()
}
},
modifier = Modifier.padding(horizontal = 4.dp)
modifier = Modifier.padding(horizontal = 2.dp)
) { displayedBpm ->
val isCenter = displayedBpm == currentBpmInt
Text(
@ -225,7 +250,7 @@ fun MidiControlPanel(
modifier = Modifier
.clip(CircleShape)
.clickable(enabled = !isCenter) { updateTempoToBpm(displayedBpm) }
.padding(horizontal = 4.dp)
.padding(horizontal = 2.dp)
)
}
}
@ -333,6 +358,54 @@ fun MidiControlPanel(
}
Spacer(modifier = Modifier.weight(1f))
if(platform.startsWith("Android")) {
Column(
modifier = Modifier.wrapContentWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(
onClick = onPlayPauseClick,
modifier = Modifier.size(48.dp).background(MaterialTheme.colorScheme.primary, CircleShape)
) {
Icon(
imageVector = if (isPause) Icons.Filled.PlayArrow else Icons.Filled.Pause,
contentDescription = "Pla",
tint = Color.White
)
}
}
Spacer(modifier = Modifier.weight(1f))
Column {
IconButton(
onClick = {
isPianoSelected = !isPianoSelected
if (isPianoSelected) {
mediaPlayer?.changeInstru(1)
} else {
mediaPlayer?.changeInstru(20)
}
}
) {
if (isPianoSelected) {
Icon(
Icons.Default.Piano,
contentDescription = "Piano",
tint = Color.Black
)
} else {
Icon(
painter = painterResource(Res.drawable.ic_organ),
contentDescription = "Orgue",
tint = Color.Black,
modifier = Modifier.size(25.dp)
)
}
}
}
} else {
Column {
IconButton(
onClick = {
@ -363,7 +436,6 @@ fun MidiControlPanel(
}
Spacer(modifier = Modifier.weight(1f))
Column(
modifier = Modifier.wrapContentWidth(),
horizontalAlignment = Alignment.CenterHorizontally
@ -379,11 +451,12 @@ fun MidiControlPanel(
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
val platform = getPlatform()
if (platform.name.startsWith("Java")) {
if (platform.startsWith("Java")) {
ModernVolumeSlider(
volume, onVolumeChange = onVolumeChange
)