From e32bda0f2c2a689b4121b72c17a4e0a4973fd227 Mon Sep 17 00:00:00 2001 From: "hasinarak3@gmail.com" Date: Tue, 17 Mar 2026 11:29:44 +0300 Subject: [PATCH] Implement all fun markers for Android using midiDriver (sonivox EAS lib) --- composeApp/build.gradle.kts | 10 +- .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 669 ++++++++++++++---- 2 files changed, 541 insertions(+), 138 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 5fef4f0..9b8e06b 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import java.util.Properties plugins { alias(libs.plugins.kotlinMultiplatform) @@ -13,13 +14,13 @@ plugins { } kotlin { - linuxX64 { + /*linuxX64 { binaries { executable { } } - } + }*/ androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { @@ -51,10 +52,7 @@ kotlin { implementation(libs.koin.android) // Koin Android-specific extensions implementation(libs.koin.androidx.compose) implementation("com.google.zxing:core:3.5.4") - implementation("androidx.camera:camera-view:1.5.2") - implementation("androidx.camera:camera-core:1.5.2") - implementation("androidx.camera:camera-camera2:1.5.2") - implementation("androidx.camera:camera-lifecycle:1.5.2") + implementation("com.github.billthefarmer:mididriver:1.25") } commonMain.dependencies { // implementation(compose.components.resources) 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 65b4450..e765220 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -1,170 +1,575 @@ package mg.dot.feufaro.midi -import android.media.MediaPlayer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import SharedScreenModel +import kotlinx.coroutines.* +import org.billthefarmer.mididriver.MidiDriver import java.io.File -import java.io.FileInputStream +import java.io.RandomAccessFile -private var androidMediaPlayer: MediaPlayer?= null +actual class FMediaPlayer actual constructor( + private val filename: String, + private val onFinished: () -> Unit +) { + private data class MidiEvent( + val tickAbsolute: Long, + val type: Int, + val channel: Int, + val data1: Int, + val data2: Int, + val metaType: Int = -1, + val metaData: ByteArray = ByteArray(0) + ) -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 data class MidiSequence( + val resolution: Int, + val events: List, + val totalTicks: Long + ) + + private object MidiParser { + fun parse(file: File): MidiSequence { + val raf = RandomAccessFile(file, "r") + val events = mutableListOf() + val header = ByteArray(4); raf.readFully(header) + require(String(header) == "MThd") { "Not a MIDI file" } + raf.readInt() + val nTracks = run { raf.readShort(); raf.readShort().toInt() and 0xFFFF } + val division = raf.readShort().toInt() and 0xFFFF + val resolution = division and 0x7FFF + for (t in 0 until nTracks) { + val trkHeader = ByteArray(4); raf.readFully(trkHeader) + if (String(trkHeader) != "MTrk") break + val trkLen = raf.readInt() + val trkData = ByteArray(trkLen); raf.readFully(trkData) + parseTrack(trkData, events) + } + raf.close() + events.sortBy { it.tickAbsolute } + val totalTicks = events.maxOfOrNull { it.tickAbsolute } ?: 0L + return MidiSequence(resolution, events, totalTicks) + } + + private fun parseTrack(data: ByteArray, out: MutableList) { + var pos = 0; var tick = 0L; var runningStatus = 0 + fun readByte() = (data[pos++].toInt() and 0xFF) + fun readVarLen(): Long { + var value = 0L; var b: Int + do { b = readByte(); value = (value shl 7) or (b and 0x7F).toLong() } while (b and 0x80 != 0) + return value + } + while (pos < data.size) { + tick += readVarLen() + var status = readByte() + if (status and 0x80 == 0) { + pos--; status = runningStatus + } + else if (status and 0xF0 != 0xF0) runningStatus = status + + val type = status and 0xF0; + val ch = status and 0x0F + when { + status == 0xFF -> { + val mt=readByte(); + val len=readVarLen().toInt(); + val md=ByteArray(len){data[pos++]}; + out.add(MidiEvent(tick,0xFF,0,0,0,mt,md)) + } + status == 0xF0 || status == 0xF7 -> repeat(readVarLen().toInt()) { + pos++ + } + type == 0x80 -> { + val d1=readByte(); + val d2=readByte(); + out.add(MidiEvent(tick,0x80,ch,d1,d2)) + } + type == 0x90 -> { + val d1=readByte(); + val d2=readByte(); + out.add(MidiEvent(tick,if(d2==0) 0x80 else 0x90,ch,d1,d2)) + } + type == 0xA0 -> { + readByte(); readByte() + } + type == 0xB0 -> { + val d1=readByte(); + val d2=readByte(); + out.add(MidiEvent(tick,0xB0,ch,d1,d2)) + } + type == 0xC0 -> { + val d1=readByte(); + out.add(MidiEvent(tick,0xC0,ch,d1,0)) + } + type == 0xD0 -> readByte() + type == 0xE0 -> { + readByte(); readByte() + } + } + } + } + } + + private val midiDriver: MidiDriver = MidiDriver.getInstance() + private var sequence: MidiSequence? = null + private var resolution: Int = 480 + private var usPerTick: Double = 500_000.0 / 480.0 + + @Volatile private var isRunning = false + @Volatile private var isHolding = false + private var currentTickPos: Long = 0L + private var lastEventNano: Long = System.nanoTime() + private var targetBpm: Float = 120f + + private val voiceVolumes = FloatArray(4) { 127f } + private var currentGlobalVolume: Float = 0.8f + private var currentDynamicFactor: Float = Dynamic.MF.factor private var pointA: Long = -1L private var pointB: Long = -1L + private var isLoopingAB = false private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - private var abJob: Job? = null + private var playJob: Job? = null + private var navigationJob: Job? = null + private var syncJob: Job? = null + private var dynamicJob: Job? = null + private var boundModel: SharedScreenModel? = null - private var isLoopingAB: Boolean = false + private data class NavigationStep( + val marker: String, + val gridIndex: Int, + val targetGrid: Int = 0, + val isHold: Boolean = false, + var alreadyDone: Boolean = false, + var beatInDC: Int = 1, + val dynamic: Dynamic? = null, + val hairPin: Char? = null, + val hairPinEndGrid: Int = -1, + val hairPinFromFactor: Float = 1.0f, + val hairPinToFactor: Float = 1.0f, + val isFarany: Boolean = false, + var faranyActive: Boolean = false, + ) + private val navigationSteps = mutableListOf() init { - playerScope.launch { + midiDriver.start() + val file = File(filename) + if (file.exists()) { try { - val file = File(midiFileName) - if (file.exists()) { - val fis = FileInputStream(file) - mediaPlayer?.setDataSource(fis.fd) + sequence = MidiParser.parse(file) + resolution = sequence!!.resolution + usPerTick = (60_000_000.0 / targetBpm) / resolution + } catch (e: Exception) { e.printStackTrace() } + } + } - mediaPlayer?.setOnCompletionListener { - onFinished() - } - mediaPlayer?.prepareAsync() - fis.close() + private fun send(vararg bytes: Int) { + midiDriver.write(ByteArray(bytes.size) { bytes[it].toByte() }) + } + private fun controlChange(ch: Int, cc: Int, v: Int) = send(0xB0 or ch, cc, v) + private fun allNotesOff() { + for (ch in 0 until 4) { controlChange(ch, 123, 0); controlChange(ch, 64, 0) } + } + + private fun applyVoiceStates() { + for (i in 0 until 4) { + val vol = (127f * (voiceVolumes[i]/127f) * currentGlobalVolume * currentDynamicFactor) + .toInt().coerceIn(0, 127) + controlChange(i, 7, vol); controlChange(i, 11, vol) + if (vol == 0) controlChange(i, 123, 0) + } + } + private fun applyDynamic(dynamic: Dynamic) { + applyDynamicSmooth(if (dynamic == Dynamic.MF) 1.0f else dynamic.factor, 300L) + } + private fun applyDynamicSmooth(targetFactor: Float, durationMs: Long = 300L) { + dynamicJob?.cancel() + dynamicJob = playerScope.launch { + val start = currentDynamicFactor; val steps = 30; val stepMs = durationMs / steps + for (i in 1..steps) { + val p = i.toFloat()/steps; val s = p*p*(3f-2f*p) + currentDynamicFactor = start + (targetFactor - start) * s + applyVoiceStates(); delay(stepMs) + } + currentDynamicFactor = targetFactor; applyVoiceStates() + } + } + private fun applyCrescendo(fromFactor: Float, toFactor: Float, durationMs: Long) { + dynamicJob?.cancel() + dynamicJob = playerScope.launch { + val steps=40; val stepMs=(durationMs/steps).coerceAtLeast(10L) + for (i in 1..steps) { + val p=i.toFloat()/steps; val s=p*p*(3f-2f*p) + currentDynamicFactor = fromFactor + (toFactor-fromFactor)*s + applyVoiceStates(); delay(stepMs) + } + currentDynamicFactor = toFactor; applyVoiceStates() + } + } + private fun extractDynamic(marker: String) = Dynamic.entries.firstOrNull { + it.label == marker.trim().removeSuffix("=").trim() + } + fun factorToDynamic(factor: Float) = + Dynamic.entries.minByOrNull { kotlin.math.abs(it.factor - factor) } ?: Dynamic.MF + fun nextDynamic(currentFactor: Float, symbol: Char): Float { + val idx = Dynamic.entries.indexOf(factorToDynamic(currentFactor)) + return if (symbol == '<') Dynamic.entries.getOrElse(idx+1){Dynamic.FFF}.factor + else Dynamic.entries.getOrElse(idx-1){Dynamic.PPP}.factor + } + + private fun startPlaybackLoop() { + val seq = sequence ?: return + isRunning = true + playJob?.cancel() + playJob = playerScope.launch(Dispatchers.Default) { + val events = seq.events + var idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos } + .takeIf { it >= 0 } ?: events.size + var clockNano = System.nanoTime() + var clockTick = currentTickPos + + while (isActive && isRunning) { + if (isHolding) { + delay(10) + clockNano = System.nanoTime() + clockTick = currentTickPos + continue } - } catch (e: Exception) { - e.printStackTrace() + + if (idx >= events.size) { + val dc = getPendingDcStep() + if (dc != null) { + dc.alreadyDone = true + allNotesOff() + seekToGrid(dc.targetGrid) + clockNano = System.nanoTime(); clockTick = currentTickPos + idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos } + .takeIf { it >= 0 } ?: events.size + continue + } else { + isRunning = false; allNotesOff(); onFinished(); break + } + } + + val ev = events[idx] + val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000) + .toLong()) - System.nanoTime() + if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L)) + + currentTickPos = ev.tickAbsolute + lastEventNano = System.nanoTime() + + // A-B loop + if (isLoopingAB && pointA >= 0 && pointB > pointA && + ticksToMs(currentTickPos) >= pointB) { + allNotesOff(); currentTickPos = msToTicks(pointA) + clockNano = System.nanoTime(); clockTick = currentTickPos + idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos } + .takeIf { it >= 0 } ?: events.size + continue + } + + when (ev.type) { + 0x80 -> send(0x80 or ev.channel, ev.data1, ev.data2) + 0x90 -> send(0x90 or ev.channel, ev.data1, + (ev.data2 * currentDynamicFactor).toInt().coerceIn(0, 127)) + 0xB0 -> if (ev.data1 != 7 && ev.data1 != 11) + send(0xB0 or ev.channel, ev.data1, ev.data2) + 0xC0 -> send(0xC0 or ev.channel, ev.data1) + 0xFF -> { /* tempo ignoré */ } + } + idx++ + } + } + } + + actual fun seekToGrid(gridIndex: Int) { + currentTickPos = gridIndex.toLong() * resolution + lastEventNano = System.nanoTime() // ← recaler l'horloge d'interpolation + val lastDyn = navigationSteps + .filter { it.dynamic != null && it.gridIndex <= gridIndex } + .maxByOrNull { it.gridIndex }?.dynamic ?: Dynamic.MF + currentDynamicFactor = if (lastDyn == Dynamic.MF) 1.0f else lastDyn.factor + applyVoiceStates() + } + private fun ticksToMs(ticks: Long) = (ticks * usPerTick / 1000.0).toLong() + private fun msToTicks(ms: Long) = (ms * 1000.0 / usPerTick).toLong() + + private fun getPendingDcStep(): NavigationStep? = + navigationSteps.firstOrNull { + !it.alreadyDone && + it.hairPin == null && + it.dynamic == null && + !it.isHold && + !it.isFarany + } + + private fun resetNavigationFlags() { + navigationSteps.forEach { it.alreadyDone = false } + } + + private fun prepareNavigation(sharedScreenModel: SharedScreenModel) { + val metadataList = sharedScreenModel.getFullMarkers() + navigationSteps.clear() + var lastSegno=0; var lastFactor=Dynamic.MF.factor + + metadataList.forEach { (_, gridIndex, _, _, marker, _, _, note) -> + val ci = gridIndex ?: 0 + val dsR = Regex("""D\.?S\.?"""); val dcR = Regex("""D\.?C\.?""") + val dsG = Regex("""D\.?S\.?_GROUP_PART"""); val dcG = Regex("""D\.?C\.?_GROUP_PART""") + val last_grid = sharedScreenModel.getTotalGridCount() + val hairPins = sharedScreenModel.getHairPins() + + when { + marker.contains("$") -> lastSegno = ci + + marker.contains("\uD834\uDD10") -> { + val beat = if(note.contains('•')) 2 else 1 + navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat)) + } + + dsG.matches(marker.trim()) -> { + val target = if(lastSegno>0) lastSegno else 0 + val indx = if((last_grid-ci)<=0) ci-1 else ci + navigationSteps.add(NavigationStep(marker, indx, targetGrid=target)) + } + + dsR.matches(marker.trim()) || marker == "DSFin" -> { + navigationSteps.add(NavigationStep(marker, ci, + targetGrid = if(lastSegno>0) lastSegno else 0)) + } + + dcG.matches(marker.trim()) -> { + val indx = if((last_grid-ci)<=0) ci-1 else ci + navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) + println("DC_GROUP créé à $indx → 0") + } + + dcR.matches(marker.trim()) && !marker.contains("DC_GROUP_PART") -> { + val indx = if((last_grid-ci)<=0) ci else ci + navigationSteps.add(NavigationStep(marker, indx, targetGrid=0)) + println("DC créé à $indx → 0") + } + + // ── Farany ──────────────────────────────── + marker.trim().equals("Farany_GROUP_PART", ignoreCase = true) -> { + val hasDcAfter = metadataList.any { (_, gi, _, _, mk, _, _, _) -> + (gi ?: 0) > ci && + (mk.contains(Regex("""D\.?C\.?""")) || mk.contains("DC_GROUP_PART")) + } + if (hasDcAfter) { + navigationSteps.add(NavigationStep( + marker = marker, + gridIndex = ci, + targetGrid = last_grid, + isFarany = true, + faranyActive = false + )) + println("Farany mémorisé à $ci") + } else { + println("Farany ignoré à $ci") + } + } + + extractDynamic(marker) != null && marker.trim() != "=" -> { + val dyn = extractDynamic(marker) ?: return@forEach + lastFactor = if(dyn==Dynamic.MF) 1.0f else dyn.factor + navigationSteps.add(NavigationStep(marker=marker, gridIndex=ci, dynamic=dyn)) + } + + marker.trim()=="<" || marker.trim()==">" || + marker.trim().contains("cres", ignoreCase=true) || + marker.trim().contains("dim", ignoreCase=true) -> { + val sym = when { + marker.trim()=="<" -> '<'; marker.trim()==">" -> '>' + marker.trim().contains("cres",ignoreCase=true) -> '<'; else -> '>' + } + val pair = hairPins.find{it.startGrid==ci} ?: return@forEach + val explicitAfter = metadataList + .filter{(_,gi,_,_,mk,_,_,_)->(gi?:0)>=pair.endGrid && extractDynamic(mk)!=null} + .minByOrNull{it.gridIndex?:0}?.let{m->extractDynamic(m.marker)} + val from = lastFactor + val to = when { + explicitAfter!=null && sym=='<' && explicitAfter.factor>from -> explicitAfter.factor + explicitAfter!=null && sym=='>' && explicitAfter.factor explicitAfter.factor + else -> nextDynamic(from, sym) + } + lastFactor = to + navigationSteps.add(NavigationStep( + marker=marker.trim(), gridIndex=ci, hairPin=sym, + hairPinEndGrid=pair.endGrid, + hairPinFromFactor=from, hairPinToFactor=to + )) + } + } + } + } + + private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) { + navigationJob?.cancel() + navigationJob = playerScope.launch(Dispatchers.Default) { + sharedScreenModel.activeIndex.collect { currentIndex -> + if (!isRunning || currentIndex < 0) return@collect + + val step = navigationSteps.find { + it.gridIndex == currentIndex && !it.alreadyDone + } ?: return@collect + + when { + // ── Farany ──────────────────────────── + step.isFarany -> { + if (step.faranyActive) { + println("Farany activé → STOP à grille $currentIndex") + step.alreadyDone = true + isHolding = false + isRunning = false + playJob?.cancel() + allNotesOff() + onFinished() + } else { + println("Farany 1er passage — DC pas encore vu → on continue ✅") + } + } + + // ── Point d'orgue ───────────────────── + step.isHold -> { + val beatMs = (60_000 / targetBpm).toLong() + val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2 + println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms") + for (ch in 0 until 4) controlChange(ch, 64, 127) + isHolding = true + delay(holdDuration) + isHolding = false + for (ch in 0 until 4) controlChange(ch, 64, 0) + step.alreadyDone = true + } + + // ── Soufflet ────────────────────────── + step.hairPin != null -> { + step.alreadyDone = true + val dist = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1) + val beatMs = (60_000L / targetBpm).toLong() + applyCrescendo(step.hairPinFromFactor, step.hairPinToFactor, + (dist * beatMs).coerceIn(200L, 5000L)) + } + + // ── Dynamique ───────────────────────── + step.dynamic != null -> { + step.alreadyDone = true + applyDynamic(step.dynamic) + } + + // ── DC / DS ─────────────────────────── + else -> { + step.alreadyDone = true + val beatMs = (60_000 / targetBpm).toLong() + println("avant de sauter bpm=$targetBpm") + + for (ch in 0 until 4) controlChange(ch, 64, 127) + isHolding = true + delay(beatMs) + allNotesOff() + + seekToGrid(step.targetGrid) + isHolding = false + for (ch in 0 until 4) controlChange(ch, 64, 0) + + navigationSteps + .filter { it.isFarany && step.gridIndex > it.gridIndex } + .forEach { + it.faranyActive = true + it.alreadyDone = false + println("DC grille ${step.gridIndex} > Farany grille ${it.gridIndex} → Farany activé 🔴") + } + + startPlaybackLoop() + println("DC/DS → grille ${step.targetGrid}") + } + } + } + } + } + + private fun startSyncLoop(sharedScreenModel: SharedScreenModel) { + syncJob?.cancel() + syncJob = playerScope.launch(Dispatchers.Default) { + while (isActive) { + if (isRunning && !isHolding) { + val elapsedNano = System.nanoTime() - lastEventNano + val extraTicks = (elapsedNano / (usPerTick * 1000)).toLong() + val interpolated = (currentTickPos + extraTicks) / resolution + val rawGrid = interpolated.toInt().coerceAtLeast(0) + sharedScreenModel.updateActiveIndexByIndex(rawGrid) + } + delay(16) } } } actual fun play() { - mediaPlayer?.let { mp -> - if (!mp.isPlaying) { - // Ici, le player est prêt sans avoir bloqué l'UI - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - try { - val params = mp.playbackParams ?: android.media.PlaybackParams() - params.speed = 1.0f - mp.playbackParams = params - } catch (e: Exception) { - e.printStackTrace() - } - } - mp.start() - } - } + if (sequence == null) return + applyVoiceStates() + startPlaybackLoop() + println("Android MIDI play — tick=$currentTickPos bpm=$targetBpm") } actual fun pause() { - mediaPlayer?.pause() + isRunning = false; isHolding = false + playJob?.cancel(); allNotesOff() } actual fun stop() { - try { - val file = File(midiFileName) - if (file.exists() && - (mediaPlayer?.isPlaying == true)) { // Vérifie l'état avant d'agir - mediaPlayer?.stop() - mediaPlayer?.reset() - mediaPlayer?.prepare() - mediaPlayer?.seekTo(0) - } - clearLoop() - } catch(e: IllegalStateException) { - // - } + isRunning = false; isHolding = false + playJob?.cancel(); navigationJob?.cancel() + allNotesOff(); currentTickPos = 0L + resetNavigationFlags(); navigationSteps.clear(); clearLoop() + currentDynamicFactor = Dynamic.MF.factor } - - actual fun getDuration(): Long { - return mediaPlayer?.duration?.toLong() ?: 0L + actual fun release() { + stop(); midiDriver.stop(); playerScope.cancel() } - - actual fun getCurrentPosition(): Long { - return mediaPlayer?.currentPosition?.toLong() ?: 0L - } - actual fun seekTo(position: Long) { - mediaPlayer?.seekTo(position.toInt()) + currentTickPos = msToTicks(position) + lastEventNano = System.nanoTime() + applyVoiceStates() } -actual fun setVolume(level: Float) { - mediaPlayer?.setVolume(level, level) + actual fun getDuration(): Long = sequence?.let { ticksToMs(it.totalTicks) } ?: 0L + actual fun getCurrentPosition(): Long = ticksToMs(currentTickPos) + actual fun setVolume(level: Float) { + currentGlobalVolume = level + midiDriver.setVolume((level*100).toInt().coerceIn(0,100)) + applyVoiceStates() } - - actual fun setPointA() { - pointA = mediaPlayer?.currentPosition?.toLong() ?: 0L + actual fun setTempo(bpm: Float) { + targetBpm = bpm; usPerTick = (60_000_000.0/bpm)/resolution + boundModel?.let { prepareNavigation(it) } + println("Tempo → $bpm BPM") } - + actual fun getCurrentBPM(): Float = targetBpm + actual fun requestSync(sharedScreenModel: SharedScreenModel) { + val seq = sequence ?: return + val totalGrids = (seq.totalTicks/resolution).toInt() + val timestamps = (0..totalGrids).map { ticksToMs(it.toLong()*resolution)*1000L } + sharedScreenModel.updateTimestamps(timestamps) + println("requestSync Android — $totalGrids grilles") + } + actual fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel) { + this.boundModel = sharedScreenModel + prepareNavigation(sharedScreenModel) + startNavigationMonitor(sharedScreenModel) + startSyncLoop(sharedScreenModel) + } + actual fun setPointA() { pointA = getCurrentPosition() } actual fun setPointB() { - pointB = mediaPlayer?.currentPosition?.toLong() ?: 0L - if (pointB > pointA && pointA != -1L) { - isLoopingAB = true - startABMonitor() - } + pointB = getCurrentPosition() + if (pointB > pointA && pointA != -1L) isLoopingAB = true } - - actual fun clearLoop() { - isLoopingAB = false - pointA = -1L - pointB = -1L - abJob?.cancel() - } - - private fun startABMonitor() { - abJob?.cancel() - abJob = playerScope.launch { - while (isLoopingAB) { - val currentPos = mediaPlayer?.currentPosition?.toLong() ?: 0L - if (currentPos >= pointB) { - mediaPlayer?.seekTo(pointA.toInt()) - } - delay(50) - } - } - } - + actual fun clearLoop() { isLoopingAB = false; pointA = -1L; pointB = -1L } actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB) - actual fun toggleVoice(index: Int) { -// voiceStates[index] = !voiceStates[index] - println("Toggle voice $index (Limitation: Nécessite SoundFont/Fluidsynth sur Android)") - } - -// actual fun getVoiceStates(): List = voiceStates - actual fun getVoiceVolumes(): List = MutableList(4) { 127f } - - actual fun changeInstru(noInstru: Int) { - } - - actual fun setTempo(factor: Float) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - mediaPlayer?.let { - val params = it.playbackParams - params.speed = factor - it.playbackParams = params - } - } - } - - actual fun getCurrentBPM(): Float { - return 120f - } - + actual fun toggleVoice(index: Int) { applyVoiceStates() } actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) { - if (voiceIndex in 0..3) { - //TODO: implements split voices & change volume per voices - } + if (voiceIndex in 0..3) { voiceVolumes[voiceIndex] = newVolume; applyVoiceStates() } } - - fun release() { - mediaPlayer?.release() - mediaPlayer = null - playerScope.coroutineContext.cancel() + actual fun getVoiceVolumes(): List = voiceVolumes.toList() + actual fun changeInstru(noInstru: Int) { + for (ch in 0 until 4) send(0xC0 or ch, noInstru) } } \ No newline at end of file