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.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
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
||||||
|
|
@ -18,4 +19,9 @@ 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
|
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,25 +28,30 @@ actual class MediaPlayer actual constructor(filename: String, onFinished: () ->
|
||||||
private var isLoopingAB: Boolean = false
|
private var isLoopingAB: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
playerScope.launch {
|
||||||
val file = File(filename)
|
try {
|
||||||
if (file.exists()) {
|
val file = File(midiFileName)
|
||||||
val fis = FileInputStream(file)
|
if (file.exists()) {
|
||||||
mediaPlayer?.setDataSource(fis.fd)
|
val fis = FileInputStream(file)
|
||||||
mediaPlayer?.prepare()
|
mediaPlayer?.setDataSource(fis.fd)
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
mediaPlayer?.setOnPreparedListener { mp ->
|
||||||
val params = mediaPlayer?.playbackParams ?: android.media.PlaybackParams()
|
// Ici, le player est prêt sans avoir bloqué l'UI
|
||||||
params.speed = 1.0f
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||||
mediaPlayer?.playbackParams = params
|
val params = mp.playbackParams ?: android.media.PlaybackParams()
|
||||||
}
|
params.speed = 1.0f
|
||||||
fis.close()
|
mp.playbackParams = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaPlayer?.prepareAsync()
|
||||||
|
|
||||||
mediaPlayer?.setOnCompletionListener {
|
mediaPlayer?.setOnCompletionListener {
|
||||||
onFinished()
|
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()
|
mediaPlayer?.pause()
|
||||||
}
|
}
|
||||||
actual fun stop() {
|
actual fun stop() {
|
||||||
mediaPlayer?.stop()
|
try {
|
||||||
mediaPlayer?.prepare()
|
val file = File(midiFileName)
|
||||||
mediaPlayer?.seekTo(0)
|
if (file.exists() &&
|
||||||
clearLoop()
|
(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 {
|
actual fun getDuration(): Long {
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 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")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.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
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import mg.dot.feufaro.FileRepository
|
import mg.dot.feufaro.FileRepository
|
||||||
import mg.dot.feufaro.config.AppConfig
|
import mg.dot.feufaro.config.AppConfig
|
||||||
|
|
||||||
val desktopModule = module {
|
val desktopModule = module {
|
||||||
// When Koin is initialized on Desktop, it will use DesktopFileRepository
|
// When Koin is initialized on Desktop, it will use DesktopFileRepository
|
||||||
// as the implementation for the FileRepository interface.
|
// as the implementation for the FileRepository interface.
|
||||||
single<AppConfig> {
|
single<AppConfig> {
|
||||||
AppConfig(
|
AppConfig(
|
||||||
transposeto = "C",
|
transposeto = "C",
|
||||||
transposeasif = "C"
|
transposeasif = "C"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue