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.android.ext.koin.androidLogger
import org.koin.core.logger.Level import org.koin.core.logger.Level
import mg.dot.feufaro.di.androidModule import mg.dot.feufaro.di.androidModule
import mg.dot.feufaro.di.platformModule
class AndroidApp: Application() { class AndroidApp: Application() {
override fun onCreate() { override fun onCreate() {
@ -17,7 +18,7 @@ class AndroidApp: Application() {
// Fournit le Context Android à Koin pour l'injection // Fournit le Context Android à Koin pour l'injection
androidContext(this@AndroidApp) androidContext(this@AndroidApp)
// Incluez votre module Koin commun (et d'autres modules Android spécifiques si vous en avez) // 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 // 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 package mg.dot.feufaro.di
import android.content.Context import android.content.Context
import mg.dot.feufaro.AndroidFileRepository
import mg.dot.feufaro.config.AppConfig import mg.dot.feufaro.config.AppConfig
import org.koin.core.module.dsl.singleOf // Import for Koin DSL 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.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 private var androidMediaPlayer: MediaPlayer?= null
actual class MediaPlayer actual constructor(filename: String, onFinished: () -> Unit) { actual class FMediaPlayer actual constructor(val filename: String, onFinished: () -> Unit) {
private var mediaPlayer: MediaPlayer? = MediaPlayer() private var mediaPlayer: android.media.MediaPlayer? = android.media.MediaPlayer()
private val voiceStates = mutableListOf(true, true, true, true) private val voiceStates = mutableListOf(true, true, true, true)
private val midiFileName = filename
// private var currentGlobalVolume: Float = 0.8f // private var currentGlobalVolume: Float = 0.8f
private var pointA: Long = -1L private var pointA: Long = -1L
@ -27,27 +28,32 @@ actual class MediaPlayer actual constructor(filename: String, onFinished: () ->
private var isLoopingAB: Boolean = false private var isLoopingAB: Boolean = false
init { init {
playerScope.launch {
try { try {
val file = File(filename) val file = File(midiFileName)
if (file.exists()) { if (file.exists()) {
val fis = FileInputStream(file) val fis = FileInputStream(file)
mediaPlayer?.setDataSource(fis.fd) 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) { 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 params.speed = 1.0f
mediaPlayer?.playbackParams = params mp.playbackParams = params
} }
fis.close() }
mediaPlayer?.prepareAsync()
mediaPlayer?.setOnCompletionListener { mediaPlayer?.setOnCompletionListener {
onFinished() onFinished()
} }
fis.close()
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
}
actual fun play() { actual fun play() {
mediaPlayer?.start() mediaPlayer?.start()
@ -56,10 +62,19 @@ actual class MediaPlayer actual constructor(filename: String, onFinished: () ->
mediaPlayer?.pause() mediaPlayer?.pause()
} }
actual fun stop() { actual fun stop() {
try {
val file = File(midiFileName)
if (file.exists() &&
(mediaPlayer?.isPlaying == true)) { // Vérifie l'état avant d'agir
mediaPlayer?.stop() mediaPlayer?.stop()
mediaPlayer?.reset()
mediaPlayer?.prepare() mediaPlayer?.prepare()
mediaPlayer?.seekTo(0) mediaPlayer?.seekTo(0)
}
clearLoop() clearLoop()
} catch(e: IllegalStateException) {
//
}
} }
actual fun getDuration(): Long { actual fun getDuration(): Long {

View file

@ -1,25 +1,64 @@
package mg.dot.feufaro.midi 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 import mg.dot.feufaro.FileRepository
actual class MidiWriterKotlin actual constructor(private val fileRepository: 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) { actual fun addNote( voiceNumber: Int, note: Int, velocity: Int, tick: Long) {
// TODO: Implémentation Android pour ajouter des notes val channel = (voiceNumber -1).coerceIn(0, 3)
midiEvents.add("Note: $note on tick $tick") 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) { actual fun save(filePath: String) {
// TODO: Implémentation Android pour écrire le fichier MIDI val parseScope = CoroutineScope(Dispatchers.Default)
println("Sauvegarde MIDI sur Android (Non implémenté complètement)") 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) { 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>) { 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 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 feufaro.composeapp.generated.resources.Res
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.io.OutputStream import java.io.OutputStream
import java.io.FileOutputStream
// Définissez une expect interface. Elle spécifie le contrat de votre repository. // 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. // Utilisez 'expect interface' car l'implémentation (actual) variera selon la plateforme.
interface FileRepository { interface FileRepository {
@ -18,41 +14,10 @@ interface FileRepository {
suspend fun readFileContent(filePath: String): String suspend fun readFileContent(filePath: String): String
suspend fun getOutputStream(filePath: String): OutputStream suspend fun getOutputStream(filePath: String): OutputStream
//Lire le dernier dossier d'importation //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. // This is just a regular class that implements the common 'FileRepository' interface.
// It is NOT an 'actual' declaration of 'FileRepository'. // 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 package mg.dot.feufaro.di
import SharedScreenModel import SharedScreenModel
import mg.dot.feufaro.CommonFileRepository
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.DisplayConfigManager // Importez DisplayConfigManager import mg.dot.feufaro.DisplayConfigManager // Importez DisplayConfigManager
import mg.dot.feufaro.musicXML.MusicXML import mg.dot.feufaro.musicXML.MusicXML
import mg.dot.feufaro.musicXML.SolfaXML
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
import org.koin.core.module.Module
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
val commonModule = module { val commonModule = module {
singleOf(::MusicXML) singleOf(::MusicXML)
single<FileRepository> { CommonFileRepository() }
single { DisplayConfigManager(fileRepository = get())} single { DisplayConfigManager(fileRepository = get())}
single { SharedScreenModel() } single { SharedScreenModel(fileRepository = get()) }
single { Solfa(get(), get()) } single { Solfa(get(), get()) }
single { SolfaScreenModel(get()) } single { SolfaScreenModel(get()) }
single { MusicXML(get()) } single { MusicXML(get()) }
} }
expect val platformModule: Module

View file

@ -2,7 +2,7 @@ package mg.dot.feufaro.midi
import mg.dot.feufaro.FileRepository import mg.dot.feufaro.FileRepository
expect class MediaPlayer(filename: String, onFinished: () -> Unit) { expect class FMediaPlayer(filename: String, onFinished: () -> Unit) {
fun play() fun play()
fun pause() fun pause()
fun stop() 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 pitches = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
val midiWriter = MidiWriterKotlin(fileRepository) val midiWriter = MidiWriterKotlin(fileRepository)
midiWriter.process(pitches) 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.data.getDrawerItems
import mg.dot.feufaro.getConfigDirectoryPath import mg.dot.feufaro.getConfigDirectoryPath
import mg.dot.feufaro.getPlatform import mg.dot.feufaro.getPlatform
import mg.dot.feufaro.midi.MediaPlayer
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
import java.io.File import java.io.File
@ -101,7 +100,7 @@ LaunchedEffect(isPlay, isPos) {
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
sharedScreenModel.loadNewSong("${getConfigDirectoryPath()}$midiFile") sharedScreenModel.loadNewSong("$midiFile")
} }
ModalNavigationDrawer(drawerState = drawerState, drawerContent = { ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
SimpleDrawerContent( SimpleDrawerContent(
@ -116,7 +115,7 @@ LaunchedEffect(isPlay, isPos) {
onScannerButtonClick() onScannerButtonClick()
}, },
onSongSelected = { newSong -> onSongSelected = { newSong ->
sharedScreenModel.loadNewSong("${getConfigDirectoryPath()}$midiFile") sharedScreenModel.loadNewSong("$midiFile")
} }
) )
}, content = { }, content = {
@ -225,7 +224,7 @@ LaunchedEffect(isPlay, isPos) {
onClick = { onClick = {
isExpanded = !isExpanded isExpanded = !isExpanded
refreshTrigeer++ refreshTrigeer++
sharedScreenModel.loadNewSong("${getConfigDirectoryPath()}$midiFile") sharedScreenModel.loadNewSong("$midiFile")
}, modifier = Modifier.alpha(0.45f) }, modifier = Modifier.alpha(0.45f)
) { ) {
Icon( Icon(

View file

@ -29,7 +29,7 @@ import feufaro.composeapp.generated.resources.ic_mixer_satb
import feufaro.composeapp.generated.resources.ic_organ import feufaro.composeapp.generated.resources.ic_organ
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import mg.dot.feufaro.getPlatform import mg.dot.feufaro.getPlatform
import mg.dot.feufaro.midi.MediaPlayer import mg.dot.feufaro.midi.FMediaPlayer
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -43,7 +43,7 @@ fun MidiControlPanel(
onPlayPauseClick: () -> Unit, onPlayPauseClick: () -> Unit,
onSeek: (Float) -> Unit, onSeek: (Float) -> Unit,
onVolumeChange: (Float) -> Unit, onVolumeChange: (Float) -> Unit,
mediaPlayer: MediaPlayer, mediaPlayer: FMediaPlayer,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val momo = duration.toInt() - currentPos.toInt() 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.asStateFlow
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.data.DrawerItem import mg.dot.feufaro.data.DrawerItem
import mg.dot.feufaro.data.getDrawerItems import mg.dot.feufaro.data.getDrawerItems
import mg.dot.feufaro.solfa.TimeUnitObject 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 ...") private val _nextLabel = MutableStateFlow<String>("Next ...")
val nextLabel: StateFlow<String> = _nextLabel.asStateFlow() val nextLabel: StateFlow<String> = _nextLabel.asStateFlow()
@ -95,8 +96,8 @@ class SharedScreenModel() : ScreenModel {
_searchTitle.value = searchValue _searchTitle.value = searchValue
} }
private var _mediaPlayer by mutableStateOf<MediaPlayer?>(null) private var _mediaPlayer by mutableStateOf<FMediaPlayer?>(null)
val mediaPlayer: MediaPlayer? get() = _mediaPlayer val mediaPlayer: FMediaPlayer? get() = _mediaPlayer
private val _isPlay = MutableStateFlow(false) private val _isPlay = MutableStateFlow(false)
val isPlay = _isPlay.asStateFlow() val isPlay = _isPlay.asStateFlow()
@ -124,13 +125,20 @@ class SharedScreenModel() : ScreenModel {
_isPos.value = true _isPos.value = true
_isPlay.value = false _isPlay.value = false
_currentPos.value = 0f _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 // _isPos.value = true
// _isPlay.value = false // _isPlay.value = false
_currentPos.value = 0f _currentPos.value = 0f
seekTo(0f) seekTo(0f)
println("fin de lecture du Midi $newMidiFile") println("fin de lecture du Midi $newMidiFile")
}) })
} catch(e: Exception) {
println("Erreur d'ouverture de mediaPlayer / ")
}
println("New media Player crée $newMidiFile") 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

@ -16,3 +16,7 @@ val desktopModule = module {
) )
} }
} }
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 kotlinx.coroutines.runBlocking
import mg.dot.feufaro.di.commonModule import mg.dot.feufaro.di.commonModule
import mg.dot.feufaro.di.desktopModule import mg.dot.feufaro.di.desktopModule
import mg.dot.feufaro.di.platformModule
import org.koin.compose.KoinContext import org.koin.compose.KoinContext
import org.koin.core.context.GlobalContext.startKoin import org.koin.core.context.GlobalContext.startKoin
import org.koin.core.context.KoinContext import org.koin.core.context.KoinContext
@ -27,7 +28,7 @@ import mg.dot.feufaro.WindowState as MyWindowState
fun main() = application { fun main() = application {
startKoin { startKoin {
printLogger(Level.INFO) // Mettez Level.INFO pour voir les logs de Koin 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 // Testez l'injection

View file

@ -20,7 +20,7 @@ import javax.sound.sampled.AudioFormat
import javax.sound.sampled.AudioSystem import javax.sound.sampled.AudioSystem
import javax.sound.sampled.FloatControl import javax.sound.sampled.FloatControl
actual class MediaPlayer actual constructor( actual class FMediaPlayer actual constructor(
private val filename: String, private val filename: String,
private val onFinished: () -> Unit private val onFinished: () -> Unit
) { ) {

View file

@ -46,7 +46,9 @@ actual class MidiWriterKotlin actual constructor(private val fileRepository: Fil
actual fun save(filePath: String) { actual fun save(filePath: String) {
val parseScope = CoroutineScope(Dispatchers.Default) val parseScope = CoroutineScope(Dispatchers.Default)
parseScope.launch { 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) MidiSystem.write(sequence, 1, out)
out.close() out.close()
} }