From 1eb0a25f0f9837b3d4bdbd875953d4977d23a667 Mon Sep 17 00:00:00 2001 From: dotmg Date: Mon, 16 Feb 2026 17:20:32 +0100 Subject: [PATCH] MIDI generation on android devices, take 1 --- .../kotlin/mg/dot/feufaro/AndroidApp.kt | 3 +- .../mg/dot/feufaro/AndroidFileRepository.kt | 70 ++++++++++ .../kotlin/mg/dot/feufaro/di/AndroidModule.kt | 6 + .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 59 +++++--- .../mg/dot/feufaro/midi/MidiWriterKotlin.kt | 61 +++++++-- .../kotlin/mg/dot/feufaro/FileRepository.kt | 41 +----- .../kotlin/mg/dot/feufaro/di/AppModule.kt | 12 +- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 2 +- .../mg/dot/feufaro/midi/MidiSequence.kt | 128 ++++++++++++++++++ .../kotlin/mg/dot/feufaro/solfa/Solfa.kt | 2 +- .../kotlin/mg/dot/feufaro/ui/DrawerUI.kt | 7 +- .../mg/dot/feufaro/ui/MidiControlPanel.kt | 4 +- .../feufaro/viewmodel/SharedScreenModel.kt | 26 ++-- .../dot/feufaro/di/DesktopFileRepository.kt | 54 ++++++++ .../kotlin/mg/dot/feufaro/di/DesktopModule.kt | 34 +++-- .../desktopMain/kotlin/mg/dot/feufaro/main.kt | 3 +- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 2 +- .../mg/dot/feufaro/midi/MidiWriterKotlin.kt | 4 +- 18 files changed, 404 insertions(+), 114 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiSequence.kt create mode 100644 composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidApp.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidApp.kt index 1e2eade..cb9cdb7 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidApp.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidApp.kt @@ -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 diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt new file mode 100644 index 0000000..27d603a --- /dev/null +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt @@ -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 = 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 { + 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() + }*/ + +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt index b1339a2..6a3d710 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt @@ -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 @@ -18,4 +19,9 @@ val androidModule = module { ) } +} + +// androidMain/kotlin/mg/dot/feufaro/Koin.android.kt +actual val platformModule = module { + single { AndroidFileRepository(get()) } // get() récupère androidContext } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index ff4839c..31dcb60 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -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,25 +28,30 @@ actual class MediaPlayer actual constructor(filename: String, onFinished: () -> 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() + playerScope.launch { + try { + val file = File(midiFileName) + if (file.exists()) { + val fis = FileInputStream(file) + mediaPlayer?.setDataSource(fis.fd) + 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 = mp.playbackParams ?: android.media.PlaybackParams() + params.speed = 1.0f + mp.playbackParams = params + } + } + mediaPlayer?.prepareAsync() - mediaPlayer?.setOnCompletionListener { - onFinished() + mediaPlayer?.setOnCompletionListener { + onFinished() + } + fis.close() } + } catch (e: Exception) { + e.printStackTrace() } - } catch (e: Exception) { - e.printStackTrace() } } @@ -56,10 +62,19 @@ actual class MediaPlayer actual constructor(filename: String, onFinished: () -> mediaPlayer?.pause() } actual fun stop() { - mediaPlayer?.stop() - mediaPlayer?.prepare() - mediaPlayer?.seekTo(0) - clearLoop() + 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 { diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt index b2ed4bb..cbdabe7 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt @@ -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() + private val sequence = MidiSequence(60) + private val track = sequence.createTrack() + private var tick: Long = 0 + private var nextTick: MutableList = mutableListOf() + private val lastPitch : MutableList = 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) { - // 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()) + } + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt index d9133d9..94e9757 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt @@ -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 = 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 { - 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) - } - } - -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt index 7c84898..6f81e6e 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt @@ -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 { CommonFileRepository() } single { DisplayConfigManager(fileRepository = get())} - single { SharedScreenModel() } + single { SharedScreenModel(fileRepository = get()) } single { Solfa(get(), get()) } single { SolfaScreenModel(get()) } single { MusicXML(get()) } -} \ No newline at end of file +} + +expect val platformModule: Module \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 07374db..12a1e12 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiSequence.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiSequence.kt new file mode 100644 index 0000000..8555fbc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiSequence.kt @@ -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>() + val out: MutableList = mutableListOf() + private var lastTick = mutableListOf() + + 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 { + val newTrack = mutableListOf() + tracks.add(newTrack) + return newTrack + } + + fun MutableList.write16Bit(value: Int) { + this.add((value shr 8 and 0xFF).toByte()) + this.add((value and 0xFF).toByte()) + } + + fun MutableList.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.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(0)) + } + + val myTrack = tracks[channel] + myTrack.addNote(channel, pitch, currentTick, type, finalVelocity) + } + fun MutableList.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 { + var value = this + val buffer = mutableListOf() + + // 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 { + 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) { + // 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(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) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt index 70e12a6..7cf0419 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt @@ -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") } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index d0d918e..2c05533 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt index ce8fb26..38d9a8d 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index eba4618..bf785d8 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -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("Next ...") val nextLabel: StateFlow = _nextLabel.asStateFlow() @@ -95,8 +96,8 @@ class SharedScreenModel() : ScreenModel { _searchTitle.value = searchValue } - private var _mediaPlayer by mutableStateOf(null) - val mediaPlayer: MediaPlayer? get() = _mediaPlayer + private var _mediaPlayer by mutableStateOf(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") - }) + _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") } diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt new file mode 100644 index 0000000..9717243 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt @@ -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 = 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 { + 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" + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt index 5b43312..51ef348 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt @@ -1,18 +1,22 @@ -package mg.dot.feufaro.di + package mg.dot.feufaro.di -import org.koin.core.module.dsl.singleOf // Import for Koin DSL -import org.koin.core.module.dsl.bind // Import for Koin DSL -import org.koin.dsl.module -import mg.dot.feufaro.FileRepository -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 + import org.koin.dsl.module + import mg.dot.feufaro.FileRepository + import mg.dot.feufaro.config.AppConfig -val desktopModule = module { - // When Koin is initialized on Desktop, it will use DesktopFileRepository - // as the implementation for the FileRepository interface. - single { - AppConfig( - transposeto = "C", - transposeasif = "C" - ) + val desktopModule = module { + // When Koin is initialized on Desktop, it will use DesktopFileRepository + // as the implementation for the FileRepository interface. + single { + AppConfig( + transposeto = "C", + transposeasif = "C" + ) + } } -} \ No newline at end of file + + actual val platformModule = module { + single { DesktopFileRepository() } // Une version simple sans Context + } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt index db79e68..16fb10b 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt @@ -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 diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 212b546..0b60395 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -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 ) { diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt index 2fa5a25..74c7302 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiWriterKotlin.kt @@ -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() }