MIDI generation on android devices, take 1
This commit is contained in:
parent
08f45dfc08
commit
1eb0a25f0f
18 changed files with 404 additions and 114 deletions
|
|
@ -7,6 +7,7 @@ import org.koin.android.ext.koin.androidContext
|
|||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.logger.Level
|
||||
import mg.dot.feufaro.di.androidModule
|
||||
import mg.dot.feufaro.di.platformModule
|
||||
|
||||
class AndroidApp: Application() {
|
||||
override fun onCreate() {
|
||||
|
|
@ -17,7 +18,7 @@ class AndroidApp: Application() {
|
|||
// Fournit le Context Android à Koin pour l'injection
|
||||
androidContext(this@AndroidApp)
|
||||
// Incluez votre module Koin commun (et d'autres modules Android spécifiques si vous en avez)
|
||||
modules(commonModule, androidModule)
|
||||
modules(commonModule, androidModule, platformModule)
|
||||
}
|
||||
|
||||
// Optionnel : Vous pouvez ajouter un log ou un petit test ici pour vérifier que Koin démarre bien
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
import android.content.Context
|
||||
import feufaro.composeapp.generated.resources.Res
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
class AndroidFileRepository(private val context: Context) : FileRepository {
|
||||
|
||||
override suspend fun getOutputStream(filePath: String): OutputStream {
|
||||
// On utilise getExternalFilesDir pour le debug (accessible via ADB/Vim)
|
||||
val folder = context.getExternalFilesDir(null)
|
||||
val outputFile = File(folder, filePath)
|
||||
|
||||
// On s'assure que le dossier existe
|
||||
outputFile.parentFile?.mkdirs()
|
||||
|
||||
return FileOutputStream(outputFile)
|
||||
}
|
||||
|
||||
override suspend fun saveFile(filePath: String, data: ByteArray) {
|
||||
val folder = context.getExternalFilesDir(null)
|
||||
println("Save to xx27 $folder $filePath")
|
||||
File(folder, filePath).writeBytes(data)
|
||||
}
|
||||
override suspend fun readFileLines(filePath: String): List<String> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
when {
|
||||
filePath.startsWith("assets://") -> {
|
||||
readAssetFileLines(filePath)
|
||||
}
|
||||
|
||||
else -> {
|
||||
File(filePath).readLines()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw IOException("Failed to read file or asset '$filePath'")
|
||||
}
|
||||
}
|
||||
override suspend fun readFileContent(filePath: String): String = withContext(Dispatchers.IO) {
|
||||
val lines = readFileLines(filePath)
|
||||
lines.joinToString("\n") { it }
|
||||
}
|
||||
private suspend fun readAssetFileLines(assetFileName: String): List<String> {
|
||||
return try {
|
||||
Res.readBytes("files/"+assetFileName.removePrefix("assets://")).decodeToString().split("\n")
|
||||
} catch (e: IOException) {
|
||||
println("Could not read /"+assetFileName.removePrefix("assets://"))
|
||||
throw IOException("Could not read asset file: $assetFileName", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFileName(shortName: String): String {
|
||||
val folder = context.getExternalFilesDir(null)
|
||||
return "$folder/$shortName"
|
||||
}
|
||||
// Dans androidMain / AndroidFileRepository.kt
|
||||
/*override suspend fun readFileBytes(filePath: String): ByteArray = withContext(Dispatchers.IO) {
|
||||
val folder = context.getExternalFilesDir(null)
|
||||
val file = File(folder, filePath)
|
||||
if (!file.exists()) throw IOException("File not found: ${file.absolutePath}")
|
||||
file.readBytes()
|
||||
}*/
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package mg.dot.feufaro.di
|
||||
|
||||
import android.content.Context
|
||||
import mg.dot.feufaro.AndroidFileRepository
|
||||
import mg.dot.feufaro.config.AppConfig
|
||||
import org.koin.core.module.dsl.singleOf // Import for Koin DSL
|
||||
import org.koin.core.module.dsl.bind // Import for Koin DSL
|
||||
|
|
@ -19,3 +20,8 @@ val androidModule = module {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// androidMain/kotlin/mg/dot/feufaro/Koin.android.kt
|
||||
actual val platformModule = module {
|
||||
single<FileRepository> { AndroidFileRepository(get()) } // get() récupère androidContext
|
||||
}
|
||||
|
|
@ -13,9 +13,10 @@ import java.io.FileInputStream
|
|||
|
||||
private var androidMediaPlayer: MediaPlayer?= null
|
||||
|
||||
actual class MediaPlayer actual constructor(filename: String, onFinished: () -> Unit) {
|
||||
private var mediaPlayer: MediaPlayer? = MediaPlayer()
|
||||
actual class FMediaPlayer actual constructor(val filename: String, onFinished: () -> Unit) {
|
||||
private var mediaPlayer: android.media.MediaPlayer? = android.media.MediaPlayer()
|
||||
private val voiceStates = mutableListOf(true, true, true, true)
|
||||
private val midiFileName = filename
|
||||
// private var currentGlobalVolume: Float = 0.8f
|
||||
|
||||
private var pointA: Long = -1L
|
||||
|
|
@ -27,27 +28,32 @@ actual class MediaPlayer actual constructor(filename: String, onFinished: () ->
|
|||
private var isLoopingAB: Boolean = false
|
||||
|
||||
init {
|
||||
playerScope.launch {
|
||||
try {
|
||||
val file = File(filename)
|
||||
val file = File(midiFileName)
|
||||
if (file.exists()) {
|
||||
val fis = FileInputStream(file)
|
||||
mediaPlayer?.setDataSource(fis.fd)
|
||||
mediaPlayer?.prepare()
|
||||
mediaPlayer?.setOnPreparedListener { mp ->
|
||||
// Ici, le player est prêt sans avoir bloqué l'UI
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
val params = mediaPlayer?.playbackParams ?: android.media.PlaybackParams()
|
||||
val params = mp.playbackParams ?: android.media.PlaybackParams()
|
||||
params.speed = 1.0f
|
||||
mediaPlayer?.playbackParams = params
|
||||
mp.playbackParams = params
|
||||
}
|
||||
fis.close()
|
||||
}
|
||||
mediaPlayer?.prepareAsync()
|
||||
|
||||
mediaPlayer?.setOnCompletionListener {
|
||||
onFinished()
|
||||
}
|
||||
fis.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual fun play() {
|
||||
mediaPlayer?.start()
|
||||
|
|
@ -56,10 +62,19 @@ actual class MediaPlayer actual constructor(filename: String, onFinished: () ->
|
|||
mediaPlayer?.pause()
|
||||
}
|
||||
actual fun stop() {
|
||||
try {
|
||||
val file = File(midiFileName)
|
||||
if (file.exists() &&
|
||||
(mediaPlayer?.isPlaying == true)) { // Vérifie l'état avant d'agir
|
||||
mediaPlayer?.stop()
|
||||
mediaPlayer?.reset()
|
||||
mediaPlayer?.prepare()
|
||||
mediaPlayer?.seekTo(0)
|
||||
}
|
||||
clearLoop()
|
||||
} catch(e: IllegalStateException) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
actual fun getDuration(): Long {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,64 @@
|
|||
package mg.dot.feufaro.midi
|
||||
|
||||
import android.media.midi.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mg.dot.feufaro.FileRepository
|
||||
|
||||
actual class MidiWriterKotlin actual constructor(private val fileRepository: FileRepository) {
|
||||
private val midiEvents = mutableListOf<String>()
|
||||
private val sequence = MidiSequence(60)
|
||||
private val track = sequence.createTrack()
|
||||
private var tick: Long = 0
|
||||
private var nextTick: MutableList<MidiPitch> = mutableListOf()
|
||||
private val lastPitch : MutableList<Int> = mutableListOf()
|
||||
private val useChord : Boolean = true
|
||||
actual fun addNote( voiceNumber: Int, note: Int, velocity: Int, tick: Long) {
|
||||
// TODO: Implémentation Android pour ajouter des notes
|
||||
midiEvents.add("Note: $note on tick $tick")
|
||||
}
|
||||
val channel = (voiceNumber -1).coerceIn(0, 3)
|
||||
var finalNote = note
|
||||
|
||||
if (voiceNumber == 3 || voiceNumber == 4) {
|
||||
finalNote -= 12
|
||||
}
|
||||
if (lastPitch.size > voiceNumber && lastPitch[voiceNumber] > 0) {
|
||||
sequence.addNote(channel, lastPitch[voiceNumber], tick)
|
||||
}
|
||||
var finalVelocity = velocity
|
||||
var midiNote = finalNote
|
||||
if (finalNote <= 0) {
|
||||
midiNote = 40
|
||||
finalVelocity = 0
|
||||
}
|
||||
sequence.addNote(channel, midiNote, tick, 90, finalVelocity)
|
||||
|
||||
while(lastPitch.size <= voiceNumber) {
|
||||
lastPitch.add(0)
|
||||
}
|
||||
lastPitch[voiceNumber] = midiNote
|
||||
}
|
||||
actual fun save(filePath: String) {
|
||||
// TODO: Implémentation Android pour écrire le fichier MIDI
|
||||
println("Sauvegarde MIDI sur Android (Non implémenté complètement)")
|
||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||
parseScope.launch {
|
||||
sequence.write()
|
||||
fileRepository.saveFile(filePath, sequence.out.toByteArray())
|
||||
//val fout = fileRepository.getOutputStream(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
actual fun addMetaMessage(type: Int, tick: Int, nbData: Int, metaByteString: String) {
|
||||
// TODO: Implémentation Android pour ajouter des Meta Messages
|
||||
/*val byteArray = metaByteString.toByteArray()
|
||||
val metaMessage = MetaMessage(type, byteArray, nbData)
|
||||
track.add(MidiEvent(metaMessage, tick.toLong()))*/
|
||||
}
|
||||
|
||||
actual fun process(pitches: List<MidiPitch>) {
|
||||
// TODO: Implémentation Android pour traiter les pitches
|
||||
val lastTick = 0
|
||||
nextTick.clear()
|
||||
// addMetaMessage(0x59, 4, 2, 2,0)
|
||||
tick = 0
|
||||
pitches.forEach {
|
||||
if (it.metaType > 0) {
|
||||
addMetaMessage(it.metaType, it.tick, it.metaByteSize, it.metaBytes)
|
||||
} else if (it.pitch != "") {
|
||||
addNote(it.voiceNumber, it.pitch.toInt(), 100, it.tick.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
import java.io.IOException // Java.io.IOException est généralement partagée sur JVM/Android
|
||||
import feufaro.composeapp.generated.resources.Res
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
// Définissez une expect interface. Elle spécifie le contrat de votre repository.
|
||||
// Utilisez 'expect interface' car l'implémentation (actual) variera selon la plateforme.
|
||||
interface FileRepository {
|
||||
|
|
@ -18,41 +14,10 @@ interface FileRepository {
|
|||
suspend fun readFileContent(filePath: String): String
|
||||
suspend fun getOutputStream(filePath: String): OutputStream
|
||||
//Lire le dernier dossier d'importation
|
||||
suspend fun saveFile(filePath: String, data: ByteArray)
|
||||
fun getFileName(shortName: String) : String
|
||||
// suspend fun readFileBytes(filePath: String): ByteArray
|
||||
}
|
||||
|
||||
// This is just a regular class that implements the common 'FileRepository' interface.
|
||||
// It is NOT an 'actual' declaration of 'FileRepository'.
|
||||
class CommonFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE commonMain 'FileRepository' interface
|
||||
override suspend fun getOutputStream(filePath: String): OutputStream {
|
||||
val outputFile = File(filePath)
|
||||
return FileOutputStream(outputFile)
|
||||
}
|
||||
override suspend fun readFileLines(filePath: String): List<String> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
when {
|
||||
filePath.startsWith("assets://") -> {
|
||||
readAssetFileLines(filePath)
|
||||
}
|
||||
|
||||
else -> {
|
||||
File(filePath).readLines()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw IOException("Failed to read file or asset '$filePath'")
|
||||
}
|
||||
}
|
||||
override suspend fun readFileContent(filePath: String): String = withContext(Dispatchers.IO) {
|
||||
val lines = readFileLines(filePath)
|
||||
lines.joinToString("\n") { it }
|
||||
}
|
||||
private suspend fun readAssetFileLines(assetFileName: String): List<String> {
|
||||
return try {
|
||||
Res.readBytes("files/"+assetFileName.removePrefix("assets://")).decodeToString().split("\n")
|
||||
} catch (e: IOException) {
|
||||
println("Could not read /"+assetFileName.removePrefix("assets://"))
|
||||
throw IOException("Could not read asset file: $assetFileName", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,24 +1,22 @@
|
|||
package mg.dot.feufaro.di
|
||||
|
||||
import SharedScreenModel
|
||||
import mg.dot.feufaro.CommonFileRepository
|
||||
import mg.dot.feufaro.FileRepository
|
||||
import mg.dot.feufaro.DisplayConfigManager // Importez DisplayConfigManager
|
||||
import mg.dot.feufaro.musicXML.MusicXML
|
||||
import mg.dot.feufaro.musicXML.SolfaXML
|
||||
import mg.dot.feufaro.solfa.Solfa
|
||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
|
||||
val commonModule = module {
|
||||
singleOf(::MusicXML)
|
||||
single<FileRepository> { CommonFileRepository() }
|
||||
single { DisplayConfigManager(fileRepository = get())}
|
||||
single { SharedScreenModel() }
|
||||
single { SharedScreenModel(fileRepository = get()) }
|
||||
|
||||
single { Solfa(get(), get()) }
|
||||
single { SolfaScreenModel(get()) }
|
||||
single { MusicXML(get()) }
|
||||
}
|
||||
|
||||
expect val platformModule: Module
|
||||
|
|
@ -2,7 +2,7 @@ package mg.dot.feufaro.midi
|
|||
|
||||
import mg.dot.feufaro.FileRepository
|
||||
|
||||
expect class MediaPlayer(filename: String, onFinished: () -> Unit) {
|
||||
expect class FMediaPlayer(filename: String, onFinished: () -> Unit) {
|
||||
fun play()
|
||||
fun pause()
|
||||
fun stop()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
package mg.dot.feufaro.midi
|
||||
|
||||
// Représente le fichier complet
|
||||
class MidiSequence(val resolution: Int = 60) {
|
||||
// Une liste de pistes, où chaque piste est une liste d'octets (ByteArray)
|
||||
val tracks = mutableListOf<MutableList<Byte>>()
|
||||
val out: MutableList<Byte> = mutableListOf<Byte>()
|
||||
private var lastTick = mutableListOf<Long>()
|
||||
|
||||
fun getDeltaAndSync(channel: Int, currentTick: Long): Long {
|
||||
// Si le canal demandé dépasse la taille actuelle, on remplit de 0L
|
||||
while (lastTick.size <= channel) {
|
||||
lastTick.add(0L)
|
||||
}
|
||||
|
||||
val delta = currentTick - lastTick[channel]
|
||||
lastTick[channel] = currentTick
|
||||
return delta
|
||||
}
|
||||
fun createTrack(): MutableList<Byte> {
|
||||
val newTrack = mutableListOf<Byte>()
|
||||
tracks.add(newTrack)
|
||||
return newTrack
|
||||
}
|
||||
|
||||
fun MutableList<Byte>.write16Bit(value: Int) {
|
||||
this.add((value shr 8 and 0xFF).toByte())
|
||||
this.add((value and 0xFF).toByte())
|
||||
}
|
||||
|
||||
fun MutableList<Byte>.write32Bit(value: Int) {
|
||||
this.add((value shr 24 and 0xFF).toByte())
|
||||
this.add((value shr 16 and 0xFF).toByte())
|
||||
this.add((value shr 8 and 0xFF).toByte())
|
||||
this.add((value and 0xFF).toByte())
|
||||
}
|
||||
|
||||
fun MutableList<Byte>.addMidiEvent(deltaTicks: Long, status: Int, data1: Int, data2: Int? = null) {
|
||||
this.addAll(deltaTicks.toVLQList())
|
||||
this.add(status.toByte())
|
||||
this.add(data1.toByte())
|
||||
if (data2 != null) {
|
||||
this.add(data2.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
fun addNote(channel: Int, pitch: Int, currentTick: Long, type: Int = 80, finalVelocity: Int = 80) {
|
||||
while (tracks.size <= channel) {
|
||||
tracks.add(mutableListOf<Byte>(0))
|
||||
}
|
||||
|
||||
val myTrack = tracks[channel]
|
||||
myTrack.addNote(channel, pitch, currentTick, type, finalVelocity)
|
||||
}
|
||||
fun MutableList<Byte>.addNote(channel: Int, pitch: Int, currentTick: Long, type: Int = 80, finalVelocity: Int = 80) {
|
||||
// Calcul du Delta-Time
|
||||
val delta = getDeltaAndSync(channel, currentTick)
|
||||
|
||||
val status = if (type == 80) (0x80 or channel) else (0x90 or channel)
|
||||
|
||||
// Ajout de l'événement (Velocity à 0 pour OFF, 100 pour ON par défaut)
|
||||
val velocity = if (type == 80) 0 else finalVelocity
|
||||
|
||||
this.addMidiEvent(delta, status, pitch, velocity)
|
||||
}
|
||||
fun Long.toVLQList(): List<Byte> {
|
||||
var value = this
|
||||
val buffer = mutableListOf<Byte>()
|
||||
|
||||
// On extrait les groupes de 7 bits en commençant par la fin
|
||||
// Le premier octet ajouté (le moins significatif) n'a pas le bit de poids fort à 1
|
||||
buffer.add((value and 0x7F).toByte())
|
||||
|
||||
while (value > 0x7F) {
|
||||
value = value shr 7
|
||||
// Pour les octets suivants, on force le bit 7 à 1 (| 0x80)
|
||||
buffer.add(0, ((value and 0x7F) or 0x80).toByte())
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
private fun writeFileHeader(): MutableList<Byte> {
|
||||
val trackCount: Int = tracks.size
|
||||
// 1. Signature "MThd" (4 octets)
|
||||
out.addAll("MThd".map { it.code.toByte() })
|
||||
|
||||
// 2. Taille du header (toujours 6 octets pour le format standard)
|
||||
out.write32Bit(6)
|
||||
|
||||
// 3. Format (16 bits)
|
||||
// 0 = piste unique, 1 = pistes multiples synchronisées
|
||||
out.write16Bit(1)
|
||||
|
||||
// 4. Nombre de pistes (16 bits)
|
||||
out.write16Bit(trackCount) // trackCount = 4 is wrong, why?
|
||||
|
||||
// 5. Division / Résolution (16 bits)
|
||||
// Ticks par noire (votre variable 'resolution')
|
||||
out.write16Bit(resolution)
|
||||
return out
|
||||
}
|
||||
private fun writeTrackChunk(trackData: MutableList<Byte>) {
|
||||
// 1. Signature "MTrk" (4 octets)
|
||||
out.addAll("MTrk".map { it.code.toByte() })
|
||||
|
||||
// 2. Marqueur de fin de piste obligatoire (si pas déjà présent)
|
||||
// Delta 0 (00), Meta (FF), Type Fin (2F), Longueur 0 (00)
|
||||
val endMarker = listOf<Byte>(0x00, 0xFF.toByte(), 0x2F, 0x00)
|
||||
|
||||
// 3. Calcul de la taille totale du bloc de données
|
||||
val totalSize = trackData.size + endMarker.size
|
||||
|
||||
// 4. Écriture de la taille (32 bits)
|
||||
out.write32Bit(totalSize)
|
||||
|
||||
// 5. Copie des données de la piste
|
||||
out.addAll(trackData)
|
||||
|
||||
// 6. Ajout du marqueur de fin
|
||||
out.addAll(endMarker)
|
||||
}
|
||||
fun write() {
|
||||
writeFileHeader()
|
||||
for (track in tracks) {
|
||||
writeTrackChunk( track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -223,7 +223,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
val pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
|
||||
val midiWriter = MidiWriterKotlin(fileRepository)
|
||||
midiWriter.process(pitches)
|
||||
midiWriter.save("${getConfigDirectoryPath()}/whawyd3.mid")
|
||||
midiWriter.save("whawyd3.mid")
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import mg.dot.feufaro.data.DrawerItem
|
|||
import mg.dot.feufaro.data.getDrawerItems
|
||||
import mg.dot.feufaro.getConfigDirectoryPath
|
||||
import mg.dot.feufaro.getPlatform
|
||||
import mg.dot.feufaro.midi.MediaPlayer
|
||||
import mg.dot.feufaro.solfa.Solfa
|
||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||
import java.io.File
|
||||
|
|
@ -101,7 +100,7 @@ LaunchedEffect(isPlay, isPos) {
|
|||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
sharedScreenModel.loadNewSong("${getConfigDirectoryPath()}$midiFile")
|
||||
sharedScreenModel.loadNewSong("$midiFile")
|
||||
}
|
||||
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
|
||||
SimpleDrawerContent(
|
||||
|
|
@ -116,7 +115,7 @@ LaunchedEffect(isPlay, isPos) {
|
|||
onScannerButtonClick()
|
||||
},
|
||||
onSongSelected = { newSong ->
|
||||
sharedScreenModel.loadNewSong("${getConfigDirectoryPath()}$midiFile")
|
||||
sharedScreenModel.loadNewSong("$midiFile")
|
||||
}
|
||||
)
|
||||
}, content = {
|
||||
|
|
@ -225,7 +224,7 @@ LaunchedEffect(isPlay, isPos) {
|
|||
onClick = {
|
||||
isExpanded = !isExpanded
|
||||
refreshTrigeer++
|
||||
sharedScreenModel.loadNewSong("${getConfigDirectoryPath()}$midiFile")
|
||||
sharedScreenModel.loadNewSong("$midiFile")
|
||||
}, modifier = Modifier.alpha(0.45f)
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import feufaro.composeapp.generated.resources.ic_mixer_satb
|
|||
import feufaro.composeapp.generated.resources.ic_organ
|
||||
import kotlinx.coroutines.delay
|
||||
import mg.dot.feufaro.getPlatform
|
||||
import mg.dot.feufaro.midi.MediaPlayer
|
||||
import mg.dot.feufaro.midi.FMediaPlayer
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -43,7 +43,7 @@ fun MidiControlPanel(
|
|||
onPlayPauseClick: () -> Unit,
|
||||
onSeek: (Float) -> Unit,
|
||||
onVolumeChange: (Float) -> Unit,
|
||||
mediaPlayer: MediaPlayer,
|
||||
mediaPlayer: FMediaPlayer,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val momo = duration.toInt() - currentPos.toInt()
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import mg.dot.feufaro.FileRepository
|
||||
import mg.dot.feufaro.data.DrawerItem
|
||||
import mg.dot.feufaro.data.getDrawerItems
|
||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||
import mg.dot.feufaro.midi.MediaPlayer
|
||||
import mg.dot.feufaro.midi.FMediaPlayer
|
||||
|
||||
class SharedScreenModel() : ScreenModel {
|
||||
class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel {
|
||||
private val _nextLabel = MutableStateFlow<String>("Next ...")
|
||||
val nextLabel: StateFlow<String> = _nextLabel.asStateFlow()
|
||||
|
||||
|
|
@ -95,8 +96,8 @@ class SharedScreenModel() : ScreenModel {
|
|||
_searchTitle.value = searchValue
|
||||
}
|
||||
|
||||
private var _mediaPlayer by mutableStateOf<MediaPlayer?>(null)
|
||||
val mediaPlayer: MediaPlayer? get() = _mediaPlayer
|
||||
private var _mediaPlayer by mutableStateOf<FMediaPlayer?>(null)
|
||||
val mediaPlayer: FMediaPlayer? get() = _mediaPlayer
|
||||
|
||||
private val _isPlay = MutableStateFlow(false)
|
||||
val isPlay = _isPlay.asStateFlow()
|
||||
|
|
@ -124,13 +125,20 @@ class SharedScreenModel() : ScreenModel {
|
|||
_isPos.value = true
|
||||
_isPlay.value = false
|
||||
_currentPos.value = 0f
|
||||
_mediaPlayer = MediaPlayer(filename = newMidiFile, onFinished = {
|
||||
_mediaPlayer = null
|
||||
try {
|
||||
val midiFileName = fileRepository.getFileName(newMidiFile)
|
||||
println("Opening xx129 $midiFileName")
|
||||
_mediaPlayer = FMediaPlayer(filename = midiFileName, onFinished = {
|
||||
// _isPos.value = true
|
||||
// _isPlay.value = false
|
||||
_currentPos.value = 0f
|
||||
seekTo(0f)
|
||||
println("fin de lecture du Midi $newMidiFile")
|
||||
})
|
||||
} catch(e: Exception) {
|
||||
println("Erreur d'ouverture de mediaPlayer / ")
|
||||
}
|
||||
println("New media Player crée $newMidiFile")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package mg.dot.feufaro.di
|
||||
|
||||
import feufaro.composeapp.generated.resources.Res
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import mg.dot.feufaro.FileRepository
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
class DesktopFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE commonMain 'FileRepository' interface
|
||||
override suspend fun getOutputStream(filePath: String): OutputStream {
|
||||
val outputFile = File(filePath)
|
||||
return FileOutputStream(outputFile)
|
||||
}
|
||||
override suspend fun readFileLines(filePath: String): List<String> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
when {
|
||||
filePath.startsWith("assets://") -> {
|
||||
readAssetFileLines(filePath)
|
||||
}
|
||||
|
||||
else -> {
|
||||
File(filePath).readLines()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw IOException("Failed to read file or asset '$filePath'")
|
||||
}
|
||||
}
|
||||
override suspend fun readFileContent(filePath: String): String = withContext(Dispatchers.IO) {
|
||||
val lines = readFileLines(filePath)
|
||||
lines.joinToString("\n") { it }
|
||||
}
|
||||
private suspend fun readAssetFileLines(assetFileName: String): List<String> {
|
||||
return try {
|
||||
Res.readBytes("files/"+assetFileName.removePrefix("assets://")).decodeToString().split("\n")
|
||||
} catch (e: IOException) {
|
||||
println("Could not read /"+assetFileName.removePrefix("assets://"))
|
||||
throw IOException("Could not read asset file: $assetFileName", e)
|
||||
}
|
||||
}
|
||||
override suspend fun saveFile(filePath: String, data: ByteArray) {
|
||||
val userHome = System.getProperty("user.home")
|
||||
val file = File("$userHome/$filePath")
|
||||
file.writeBytes(data) // Extension Kotlin très efficace
|
||||
}
|
||||
|
||||
override fun getFileName(shortName: String): String {
|
||||
val userHome = System.getProperty("user.home")
|
||||
return "$userHome/$shortName"
|
||||
}
|
||||
}
|
||||
|
|
@ -16,3 +16,7 @@ val desktopModule = module {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
actual val platformModule = module {
|
||||
single<FileRepository> { DesktopFileRepository() } // Une version simple sans Context
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import mg.dot.feufaro.di.commonModule
|
||||
import mg.dot.feufaro.di.desktopModule
|
||||
import mg.dot.feufaro.di.platformModule
|
||||
import org.koin.compose.KoinContext
|
||||
import org.koin.core.context.GlobalContext.startKoin
|
||||
import org.koin.core.context.KoinContext
|
||||
|
|
@ -27,7 +28,7 @@ import mg.dot.feufaro.WindowState as MyWindowState
|
|||
fun main() = application {
|
||||
startKoin {
|
||||
printLogger(Level.INFO) // Mettez Level.INFO pour voir les logs de Koin
|
||||
modules(commonModule, desktopModule) // Incluez seulement le module commun
|
||||
modules(commonModule, platformModule) // Incluez seulement le module commun
|
||||
}
|
||||
|
||||
// Testez l'injection
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import javax.sound.sampled.AudioFormat
|
|||
import javax.sound.sampled.AudioSystem
|
||||
import javax.sound.sampled.FloatControl
|
||||
|
||||
actual class MediaPlayer actual constructor(
|
||||
actual class FMediaPlayer actual constructor(
|
||||
private val filename: String,
|
||||
private val onFinished: () -> Unit
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ actual class MidiWriterKotlin actual constructor(private val fileRepository: Fil
|
|||
actual fun save(filePath: String) {
|
||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||
parseScope.launch {
|
||||
val out = fileRepository.getOutputStream(filePath)
|
||||
val midiFileName = fileRepository.getFileName(filePath)
|
||||
val out = fileRepository.getOutputStream(midiFileName)
|
||||
println("write to : $midiFileName ")
|
||||
MidiSystem.write(sequence, 1, out)
|
||||
out.close()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue