MIDI generation on android devices, take 1

This commit is contained in:
dotmg 2026-02-16 17:20:32 +01:00
parent 08f45dfc08
commit 1eb0a25f0f
18 changed files with 404 additions and 114 deletions

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(

View file

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

View file

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

View file

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

View file

@ -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> {
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> {
AppConfig(
transposeto = "C",
transposeasif = "C"
)
}
}
actual val platformModule = module {
single<FileRepository> { DesktopFileRepository() } // Une version simple sans Context
}
}

View file

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

View file

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

View file

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