1058 lines
No EOL
47 KiB
Kotlin
1058 lines
No EOL
47 KiB
Kotlin
package mg.dot.feufaro.midi
|
|
|
|
import SharedScreenModel
|
|
import com.russhwolf.settings.Settings
|
|
import kotlinx.coroutines.*
|
|
import mg.dot.feufaro.provideSettings
|
|
import mg.dot.feufaro.solfa.Transpose
|
|
import org.billthefarmer.mididriver.MidiDriver
|
|
import org.koin.core.component.KoinComponent
|
|
import org.koin.core.component.inject
|
|
import java.io.File
|
|
import java.io.RandomAccessFile
|
|
|
|
actual class FMediaPlayer actual constructor(
|
|
private val filename: String,
|
|
private val sharedScreenModel: SharedScreenModel,
|
|
private val onFinished: () -> Unit
|
|
): KoinComponent {
|
|
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)
|
|
)
|
|
|
|
private data class MidiSequence(
|
|
val resolution: Int,
|
|
val events: List<MidiEvent>,
|
|
val totalTicks: Long
|
|
)
|
|
|
|
private object MidiParser {
|
|
fun parse(file: File): MidiSequence {
|
|
val raf = RandomAccessFile(file, "r")
|
|
val events = mutableListOf<MidiEvent>()
|
|
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<MidiEvent>) {
|
|
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++]};
|
|
|
|
if (mt == 0x51 && md.size == 3) {
|
|
val usPerQuarter = ((md[0].toInt() and 0xFF) shl 16) or
|
|
((md[1].toInt() and 0xFF) shl 8) or
|
|
(md[2].toInt() and 0xFF)
|
|
val bpm = 60_000_000f / usPerQuarter
|
|
out.add(MidiEvent(tick, 0xFF, 0, 0, 0, mt, md))
|
|
} else {
|
|
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
|
|
@Volatile private var usPerTick: Double = (60_000_000.0 / 120.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 var initialBpm: Float = 120f
|
|
|
|
@Volatile private var currentPlaybackBpm: Float = 120f
|
|
@Volatile private var isInTempoChange: Boolean = false
|
|
@Volatile private var needsClockSync = false
|
|
private var tempoChangeJob: Job? = null
|
|
private val voiceVolumes = FloatArray(4) { 127f }
|
|
private var currentGlobalVolume: Float = 0.8f
|
|
private var currentDynamicFactor: Float = Dynamic.MF.factor
|
|
private var transpositionSemitones: Int = 0
|
|
|
|
private var pointA: Long = -1L
|
|
private var pointB: Long = -1L
|
|
private var isLoopingAB = false
|
|
|
|
private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
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 val settings: Settings by inject()
|
|
|
|
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,
|
|
// rit & rall
|
|
val isTempoChange: Boolean = false,
|
|
val tempoType: String = "",
|
|
val endGridForTempo: Int = -1,
|
|
val targetTempoMultiplier: Float = 0.6f,
|
|
val newBpm: Float? = null,
|
|
val newKey: String? = null
|
|
)
|
|
private val navigationSteps = mutableListOf<NavigationStep>()
|
|
private val prefs: Settings = provideSettings()
|
|
|
|
|
|
@Volatile private var userCustomTempo: Boolean = false
|
|
init {
|
|
midiDriver.start()
|
|
loadVoiceVolumes()
|
|
loadSavedInstrumentsToPlayer(this)
|
|
val file = File(filename)
|
|
if (file.exists()) {
|
|
try {
|
|
sequence = MidiParser.parse(file)
|
|
resolution = sequence!!.resolution
|
|
val tempoEvent = sequence!!.events.firstOrNull { it.type == 0xFF && it.metaType == 0x51 }
|
|
if (tempoEvent != null && tempoEvent.metaData.size == 3) {
|
|
val md = tempoEvent.metaData
|
|
val usPerQuarter = ((md[0].toInt() and 0xFF) shl 16) or
|
|
((md[1].toInt() and 0xFF) shl 8) or
|
|
(md[2].toInt() and 0xFF)
|
|
val parsedBpm = 60_000_000f / usPerQuarter
|
|
targetBpm = parsedBpm
|
|
initialBpm = parsedBpm
|
|
}
|
|
|
|
usPerTick = (60_000_000.0 / targetBpm) / resolution
|
|
} catch (e: Exception) { e.printStackTrace() }
|
|
}
|
|
}
|
|
|
|
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 applyTempoChange(
|
|
startBpm: Float,
|
|
endBpm: Float,
|
|
durationMs: Long,
|
|
tempoType: String
|
|
) {
|
|
tempoChangeJob?.cancel()
|
|
isInTempoChange = true
|
|
tempoChangeJob = playerScope.launch {
|
|
val steps = 50
|
|
val stepMs = (durationMs / steps).coerceAtLeast(20L)
|
|
val startTime = System.currentTimeMillis()
|
|
|
|
for (i in 1..steps) {
|
|
val elapsed = System.currentTimeMillis() - startTime
|
|
val progress = (elapsed.toFloat() / durationMs).coerceIn(0f, 1f)
|
|
val smooth = progress * progress * (3f - 2f * progress) // ease-in-out
|
|
currentPlaybackBpm = startBpm + (endBpm - startBpm) * smooth
|
|
usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution
|
|
needsClockSync = true
|
|
delay(stepMs)
|
|
if (!isActive) break
|
|
}
|
|
|
|
currentPlaybackBpm = endBpm
|
|
usPerTick = (60_000_000.0 / currentPlaybackBpm) / resolution
|
|
isInTempoChange = false
|
|
println("$tempoType terminé → $endBpm BPM")
|
|
}
|
|
}
|
|
|
|
private fun resetTempoToNormal() {
|
|
if (kotlin.math.abs(currentPlaybackBpm - targetBpm) > 0.5f) {
|
|
applyTempoChange(
|
|
startBpm = currentPlaybackBpm,
|
|
endBpm = targetBpm,
|
|
durationMs = 800L,
|
|
tempoType = "Retour tempo"
|
|
)
|
|
}
|
|
}
|
|
private fun resetTempoToInitial() {
|
|
tempoChangeJob?.cancel()
|
|
isInTempoChange = false
|
|
//targetBpm = initialBpm
|
|
userCustomTempo = false
|
|
usPerTick = (60_000_000.0 / targetBpm.toDouble()) / resolution.toDouble()
|
|
boundModel?.setBpmFlow(targetBpm)
|
|
}
|
|
|
|
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 ?: run {
|
|
return
|
|
}
|
|
isRunning = true
|
|
needsClockSync = true
|
|
|
|
playJob?.cancel()
|
|
playJob = playerScope.launch(Dispatchers.Default) {
|
|
val events = seq.events
|
|
|
|
var clockNano = System.nanoTime()
|
|
var clockTick = currentTickPos
|
|
var idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
|
.takeIf { it >= 0 } ?: events.size
|
|
|
|
while (isActive && isRunning) {
|
|
if (needsClockSync) {
|
|
clockNano = System.nanoTime()
|
|
clockTick = currentTickPos
|
|
|
|
val targetIdx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
|
.takeIf { it >= 0 } ?: events.size
|
|
if (targetIdx > idx) {
|
|
idx = targetIdx
|
|
}
|
|
|
|
needsClockSync = false
|
|
}
|
|
|
|
if (isHolding) {
|
|
yield()
|
|
clockNano = System.nanoTime()
|
|
clockTick = currentTickPos
|
|
continue
|
|
}
|
|
|
|
if (idx >= events.size) {
|
|
val dc = getPendingDcStep()
|
|
if (dc != null) {
|
|
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()
|
|
resetTempoToNormal()
|
|
onFinished()
|
|
break
|
|
}
|
|
}
|
|
|
|
val ev = events[idx]
|
|
val targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong()
|
|
val now = System.nanoTime()
|
|
val waitNano = targetNano - now
|
|
|
|
if (waitNano > 0 && waitNano < 3_000_000_000L) {
|
|
val ms = waitNano / 1_000_000
|
|
val ns = (waitNano % 1_000_000).toInt()
|
|
try {
|
|
Thread.sleep(ms, ns)
|
|
} catch (e: Exception) {
|
|
yield()
|
|
}
|
|
} else if (waitNano < -100_000_000L) {
|
|
clockNano = System.nanoTime()
|
|
clockTick = ev.tickAbsolute
|
|
} else {
|
|
yield()
|
|
}
|
|
if (!isRunning || !isActive) break
|
|
|
|
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 -> {
|
|
val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127)
|
|
send(0x80 or ev.channel, transposedNote, ev.data2)
|
|
}
|
|
0x90 -> {
|
|
val transposedNote = (ev.data1 + transpositionSemitones).coerceIn(0, 127)
|
|
send(0x90 or ev.channel, transposedNote,
|
|
(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 -> {
|
|
if (ev.metaType == 0x51 && ev.metaData.size == 3) {
|
|
val md = ev.metaData
|
|
val usPerQuarter = ((md[0].toInt() and 0xFF) shl 16) or
|
|
((md[1].toInt() and 0xFF) shl 8) or
|
|
(md[2].toInt() and 0xFF)
|
|
currentPlaybackBpm = targetBpm
|
|
usPerTick = (60_000_000.0 / targetBpm) / resolution
|
|
clockNano = System.nanoTime()
|
|
clockTick = ev.tickAbsolute
|
|
}
|
|
}
|
|
}
|
|
val currentGrid = (currentTickPos / resolution).toLong()
|
|
if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
|
|
boundModel?.updateActiveIndexByIndex(currentGrid.toInt())
|
|
}
|
|
//println("C= ${currentGrid.toInt()} et shrd ${boundModel?.activeIndex?.value}")
|
|
idx++
|
|
}
|
|
}
|
|
}
|
|
|
|
actual fun seekToGrid(gridIndex: Int) {
|
|
currentTickPos = gridIndex.toLong() * resolution
|
|
lastEventNano = System.nanoTime()
|
|
|
|
val initialOrPreviousBpm = if (userCustomTempo) {
|
|
targetBpm
|
|
} else {
|
|
navigationSteps
|
|
.filter { it.newBpm != null && it.gridIndex <= gridIndex }
|
|
.maxByOrNull { it.gridIndex }?.newBpm ?: initialBpm
|
|
}
|
|
|
|
targetBpm = initialOrPreviousBpm
|
|
sharedScreenModel.setBpmFlow(initialOrPreviousBpm)
|
|
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
|
|
navigationSteps.forEach { step ->
|
|
if (step.gridIndex > gridIndex) {
|
|
step.alreadyDone = false
|
|
if (step.isFarany) {
|
|
step.faranyActive = false
|
|
}
|
|
}
|
|
}
|
|
applyVoiceStates()
|
|
|
|
needsClockSync = true
|
|
}
|
|
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
|
|
it.faranyActive = false
|
|
}
|
|
}
|
|
|
|
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
|
|
val metadataList = sharedScreenModel.getFullMarkers()
|
|
navigationSteps.clear()
|
|
var lastSegno=0;
|
|
var lastFactor=Dynamic.MF.factor
|
|
val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
|
|
val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
|
|
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 finReg = Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE)
|
|
|
|
val modulaRegex = Regex("""D(ô|o)?\s*dia\s*(C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B)""", RegexOption.IGNORE_CASE)
|
|
val tempoRegex = Regex("""^♩\s*=\s*(\d+)""", RegexOption.IGNORE_CASE)
|
|
metadataList.forEach { midiMarker ->
|
|
val ci = midiMarker.gridIndex ?: 0
|
|
val last_grid = sharedScreenModel.getTotalGridCount()
|
|
val hairPins = sharedScreenModel.getHairPins()
|
|
val markerList = midiMarker.marker
|
|
|
|
var i = 0
|
|
while(i < markerList.size) {
|
|
val marker = markerList[i]
|
|
val mTrim = marker.trim()
|
|
val nextMarker = markerList.getOrNull(i + 1)
|
|
val nextTrim = nextMarker?.trim() ?: ""
|
|
|
|
val hasDs = dsR.containsMatchIn(mTrim)
|
|
val hasDc = dcR.containsMatchIn(mTrim)
|
|
val hasFin = finReg.containsMatchIn(mTrim) || finReg.containsMatchIn(nextTrim)
|
|
|
|
val isDsFin = hasDs && hasFin
|
|
val isDcFin = hasDc && hasFin
|
|
|
|
// Changement de Tempo ♩=
|
|
val tempoMatch = tempoRegex.find(mTrim)
|
|
if (tempoMatch != null) {
|
|
val bpmVal = tempoMatch.groupValues[1].toFloatOrNull()
|
|
if (bpmVal != null) {
|
|
navigationSteps.add(
|
|
NavigationStep(
|
|
marker = mTrim,
|
|
gridIndex = ci,
|
|
newBpm = bpmVal
|
|
)
|
|
)
|
|
println("Changement de BPM mémorisé à la grille $ci -> $bpmVal BPM")
|
|
}
|
|
}
|
|
|
|
// Modulation Do dia ..
|
|
val modMatch = modulaRegex.find(mTrim)
|
|
if (modMatch != null) {
|
|
val targetKey = modMatch.groupValues[2]
|
|
navigationSteps.add(
|
|
NavigationStep(
|
|
marker = mTrim,
|
|
gridIndex = ci,
|
|
newKey = targetKey
|
|
)
|
|
)
|
|
println("Modulation mémorisée à la grille $ci -> $targetKey")
|
|
}
|
|
|
|
when {
|
|
isDsFin || isDcFin -> {
|
|
val target = if (lastSegno > 0) lastSegno else 0
|
|
navigationSteps.add(NavigationStep(
|
|
marker = "$mTrim$nextTrim",
|
|
gridIndex = ci,
|
|
targetGrid = if (isDsFin) target else 0
|
|
))
|
|
i += 2
|
|
continue
|
|
}
|
|
|
|
marker.contains("$") -> lastSegno = ci
|
|
|
|
marker.contains("\uD834\uDD10") -> {
|
|
val beat = if(midiMarker.note.contains('•')) 2 else 1
|
|
navigationSteps.add(NavigationStep(marker, ci, isHold=true, beatInDC=beat))
|
|
}
|
|
ritRegex.containsMatchIn(marker) -> {
|
|
val endGrid = if(midiMarker.lastCallerMarker == 0) last_grid else midiMarker.lastCallerMarker
|
|
navigationSteps.add(
|
|
NavigationStep(
|
|
marker = marker,
|
|
gridIndex = ci,
|
|
isTempoChange = true,
|
|
tempoType = "rit",
|
|
endGridForTempo = endGrid,
|
|
targetTempoMultiplier = 0.55f
|
|
)
|
|
)
|
|
}
|
|
rallRegex.containsMatchIn(marker) -> {
|
|
val endGrid = last_grid
|
|
navigationSteps.add(
|
|
NavigationStep(
|
|
marker = marker,
|
|
gridIndex = ci,
|
|
isTempoChange = true,
|
|
tempoType = "rall",
|
|
endGridForTempo = endGrid,
|
|
targetTempoMultiplier = 0.50f
|
|
)
|
|
)
|
|
}
|
|
|
|
dsR.matches(mTrim) -> {
|
|
navigationSteps.add(NavigationStep(marker, ci,
|
|
targetGrid = if(lastSegno>0) lastSegno else 0))
|
|
}
|
|
dcR.matches(mTrim) -> {
|
|
val indx = if((last_grid-ci)<=0) ci else ci
|
|
navigationSteps.add(NavigationStep(marker, indx, targetGrid=0))
|
|
println("DC créé à $indx → 0")
|
|
}
|
|
|
|
// ── Farany ────────────────────────────────
|
|
finReg.containsMatchIn(mTrim) -> {
|
|
val hasDcOrDsAfter = metadataList.any { m ->
|
|
(m.gridIndex ?: 0) > ci && m.marker.any { mk ->
|
|
dcR.containsMatchIn(mk) || dsR.containsMatchIn(mk)
|
|
}
|
|
}
|
|
if (hasDcOrDsAfter) {
|
|
navigationSteps.add(
|
|
NavigationStep(
|
|
marker,
|
|
ci,
|
|
targetGrid = last_grid,
|
|
isFarany = true, faranyActive = false)
|
|
)
|
|
} else {
|
|
val verse = sharedScreenModel.stanza.value
|
|
val verses = sharedScreenModel.nbStanzas.value
|
|
println("Farany ignoré à $ci i $verse == $verses")
|
|
navigationSteps.add(
|
|
NavigationStep(
|
|
marker = "${marker}_STZ",
|
|
gridIndex = ci,
|
|
isFarany = true, faranyActive = (verse != verses)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
extractDynamic(marker) != null && mTrim != "=" -> {
|
|
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))
|
|
}
|
|
|
|
mTrim=="<" || mTrim==">" ||
|
|
mTrim.contains("cres", ignoreCase=true) ||
|
|
mTrim.contains("dim", ignoreCase=true) -> {
|
|
val sym = when {
|
|
mTrim=="<" -> '<'; mTrim==">" -> '>'
|
|
mTrim.contains("cres",ignoreCase=true) -> '<'; else -> '>'
|
|
}
|
|
val pair = hairPins.find{it.startGrid==ci} ?: return@forEach
|
|
val explicitAfter = metadataList
|
|
.filter{ m ->(m.gridIndex ?:0) >= pair.endGrid }
|
|
.mapNotNull { m ->
|
|
val dynamic = m.marker.firstNotNullOfOrNull { mk -> extractDynamic(mk) }
|
|
if (dynamic != null) (m.gridIndex ?: 0) to dynamic else null
|
|
}
|
|
.minByOrNull { it.first }
|
|
?.second
|
|
|
|
val from = lastFactor
|
|
val to = when {
|
|
explicitAfter!=null && sym=='<' && explicitAfter.factor>from -> explicitAfter.factor
|
|
explicitAfter!=null && sym=='>' && explicitAfter.factor<from -> explicitAfter.factor
|
|
else -> nextDynamic(from, sym)
|
|
}
|
|
lastFactor = to
|
|
navigationSteps.add(NavigationStep(
|
|
marker=mTrim, gridIndex=ci, hairPin=sym,
|
|
hairPinEndGrid=pair.endGrid,
|
|
hairPinFromFactor=from, hairPinToFactor=to
|
|
))
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun startNavigationMonitor(sharedScreenModel: SharedScreenModel) {
|
|
var lastProcessedIndex = -1
|
|
var isJumping = false
|
|
var pendingFin = false
|
|
navigationJob?.cancel()
|
|
|
|
navigationJob = playerScope.launch(Dispatchers.Default) {
|
|
sharedScreenModel.activeIndex.collect { currentIndex ->
|
|
//if (currentIndex <= lastProcessedIndex && lastProcessedIndex != -1) return@collect
|
|
|
|
lastProcessedIndex = currentIndex
|
|
|
|
if (!isRunning || currentIndex < 0) return@collect
|
|
val currentSteps = navigationSteps.filter {
|
|
it.gridIndex == currentIndex && !it.alreadyDone
|
|
}.sortedBy { step ->
|
|
when {
|
|
step.isHold -> 0
|
|
step.isTempoChange -> 1
|
|
step.isFarany -> 1
|
|
step.dynamic != null || step.hairPin != null -> 1
|
|
else -> 3
|
|
}
|
|
}
|
|
for(step in currentSteps) {
|
|
when {
|
|
//♩
|
|
step.newBpm != null -> {
|
|
step.alreadyDone = true
|
|
if (step.gridIndex == 0 && userCustomTempo) {
|
|
|
|
}
|
|
else {
|
|
println("Exécution changement de tempo : ${step.newBpm} BPM à la grille $currentIndex")
|
|
targetBpm = step.newBpm
|
|
sharedScreenModel.setBpmFlow(step.newBpm)
|
|
}
|
|
}
|
|
|
|
// Modulation
|
|
step.newKey != null -> {
|
|
step.alreadyDone = true
|
|
val baseKeyIdx = Transpose.keyToNumber.indexOf("C").coerceAtLeast(0)
|
|
val targetKeyIdx = Transpose.keyToNumber.indexOf(step.newKey).coerceAtLeast(0)
|
|
transpositionSemitones = targetKeyIdx - baseKeyIdx
|
|
}
|
|
|
|
// ── Point d'orgue ─────────────────────
|
|
step.isHold -> {
|
|
val beatMs = (60_000 / targetBpm).toLong()
|
|
val holdDuration = if(step.beatInDC==2) beatMs/2 else beatMs*2
|
|
|
|
isHolding = true
|
|
for (ch in 0 until 4) controlChange(ch, 64, 127)
|
|
delay(holdDuration)
|
|
for (ch in 0 until 4) controlChange(ch, 64, 0)
|
|
isHolding = false
|
|
println("POINT D'ORGUE grille $currentIndex | ${holdDuration}ms")
|
|
}
|
|
// Rit. / Rall. , ...
|
|
step.isTempoChange -> {
|
|
step.alreadyDone = true
|
|
val startBpm = currentPlaybackBpm
|
|
val endBpm = targetBpm * step.targetTempoMultiplier
|
|
val gridDistance = (step.endGridForTempo - step.gridIndex).coerceAtLeast(1)
|
|
val beatDurationMs = (60_000L / targetBpm).toLong()
|
|
val durationMs = (gridDistance * beatDurationMs).coerceIn(1000L, 8000L)
|
|
|
|
println("${step.tempoType.uppercase()} : Grille ${step.gridIndex} → ${step.endGridForTempo}")
|
|
println(" $startBpm BPM → $endBpm BPM sur ${durationMs}ms")
|
|
|
|
applyTempoChange(
|
|
startBpm = startBpm,
|
|
endBpm = endBpm,
|
|
durationMs = durationMs,
|
|
tempoType = step.tempoType
|
|
)
|
|
}
|
|
|
|
// ── 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)
|
|
}
|
|
|
|
// ── Farany ─────────────────────────
|
|
step.isFarany -> {
|
|
println("C Farany ok $step")
|
|
if (pendingFin || step.faranyActive) {
|
|
step.alreadyDone = true
|
|
isRunning = false
|
|
allNotesOff()
|
|
playJob?.cancel()
|
|
seekToGrid(0)
|
|
boundModel?.updateActiveIndexByIndex(0)
|
|
println("Farany activé → STOP à grille $currentIndex")
|
|
onFinished()
|
|
return@collect
|
|
} else {
|
|
println("Farany 1er passage — DC pas encore vu")
|
|
pendingFin = true
|
|
step.alreadyDone = true
|
|
continue
|
|
}
|
|
}
|
|
// ── DC / DS ───────────────────────────
|
|
else -> {
|
|
step.alreadyDone = true
|
|
val beatMs = (60_000 / targetBpm).toLong()
|
|
println("avant de sauter bpm=$targetBpm")
|
|
|
|
//lastProcessedIndex = -1
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun startSyncLoop(sharedScreenModel: SharedScreenModel) {
|
|
syncJob?.cancel()
|
|
syncJob = playerScope.launch(Dispatchers.Default) {
|
|
var lastSentGrid = -1
|
|
while (isActive) {
|
|
if (isRunning && !isHolding) {
|
|
val elapsedNano = System.nanoTime() - lastEventNano
|
|
val extraTicks = (elapsedNano / (usPerTick * 1000)).toLong().coerceAtLeast(0L)
|
|
val interpolated = (currentTickPos + extraTicks) / resolution
|
|
val rawGrid = interpolated.toInt().coerceAtLeast(0)
|
|
//sharedScreenModel.updateActiveIndexByIndex(rawGrid)
|
|
if (interpolated.toInt() != lastSentGrid) {
|
|
lastSentGrid = interpolated.toInt()
|
|
sharedScreenModel.updateActiveIndexByIndex(interpolated.toInt())
|
|
}
|
|
}
|
|
delay(16)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var countInJob: Job? = null
|
|
|
|
private fun getCountInBeats(measureStr: String): Int {
|
|
val firstNumeratorChar = measureStr.substringBefore('/').trim()
|
|
return firstNumeratorChar.toIntOrNull() ?: 4
|
|
}
|
|
actual fun play() {
|
|
if (sequence == null) return
|
|
usPerTick = (60_000_000.0 / targetBpm.toDouble()) / resolution.toDouble()
|
|
currentPlaybackBpm = targetBpm
|
|
val countInEnable = prefs.getBoolean("isCountIn", true)
|
|
if (currentTickPos == 0L && countInEnable) {
|
|
playCountIn()
|
|
} else {
|
|
applyVoiceStates()
|
|
startPlaybackLoop()
|
|
println("Android MIDI play — tick=$currentTickPos bpm=$targetBpm")
|
|
}
|
|
}
|
|
|
|
private fun playCountIn() {
|
|
countInJob?.cancel()
|
|
countInJob = playerScope.launch(Dispatchers.Default) {
|
|
val bpm = targetBpm
|
|
val beatDurationMs = (60_000L / bpm).toLong()
|
|
|
|
val noteEvent = 42 // 39:Hand clap
|
|
val velocity = 100
|
|
val originalMeasure = sharedScreenModel.measureInt.value
|
|
val nbSilent = sharedScreenModel.silentDurationBefore.value
|
|
|
|
val adjustedMeasure = when {
|
|
originalMeasure == 6 -> 3
|
|
originalMeasure % 2 != 0 && originalMeasure > 3 -> 3
|
|
originalMeasure % 2 == 0 && originalMeasure > 4 -> 4
|
|
else -> originalMeasure
|
|
}
|
|
val anacrouseBeats = nbSilent / 4
|
|
val noteOnEvent = byteArrayOf(0x99.toByte(), noteEvent.toByte(), velocity.toByte())
|
|
val noteOffEvent = byteArrayOf(0x89.toByte(), noteEvent.toByte(), 0.toByte())
|
|
|
|
val noteOnSnare = byteArrayOf(0x99.toByte(), 38.toByte(), velocity.toByte())
|
|
val noteOffSnare = byteArrayOf(0x89.toByte(), 38.toByte(), 0.toByte())
|
|
|
|
val isEvenMeasure = adjustedMeasure in setOf(2, 4, 8)
|
|
if (isEvenMeasure) {
|
|
for (i in 1..2) {
|
|
midiDriver.write(noteOnEvent)
|
|
delay(100)
|
|
midiDriver.write(noteOffEvent)
|
|
|
|
val slowDuration = beatDurationMs * 2
|
|
delay((slowDuration - 100).coerceAtLeast(0L))
|
|
}
|
|
|
|
val remainingBeats = if (anacrouseBeats == 0) 4 else anacrouseBeats
|
|
for (i in 1..remainingBeats) {
|
|
val onBytes = if (i == remainingBeats) noteOnSnare else noteOnEvent
|
|
val offBytes = if (i == remainingBeats) noteOffSnare else noteOffEvent
|
|
|
|
midiDriver.write(onBytes)
|
|
delay(100)
|
|
midiDriver.write(offBytes)
|
|
|
|
delay((beatDurationMs - 100).coerceAtLeast(0L))
|
|
}
|
|
|
|
} else {
|
|
val totalBeats = if (anacrouseBeats > 5) anacrouseBeats else adjustedMeasure + anacrouseBeats
|
|
// println("Android dcompte total : $totalBeats bips ($adjustedMeasure de préparation + $anacrouseBeats d'anacrouse) à $bpm BPM")
|
|
|
|
for (i in 1..totalBeats) {
|
|
val onBytes = if (i == adjustedMeasure) noteOnSnare else noteOnEvent
|
|
val offBytes = if (i == adjustedMeasure) noteOffSnare else noteOffEvent
|
|
|
|
midiDriver.write(onBytes)
|
|
delay(100)
|
|
midiDriver.write(offBytes)
|
|
|
|
delay((beatDurationMs - 100).coerceAtLeast(0L))
|
|
}
|
|
}
|
|
|
|
withContext(Dispatchers.Main) {
|
|
applyVoiceStates()
|
|
needsClockSync = true
|
|
startPlaybackLoop()
|
|
}
|
|
}
|
|
}
|
|
actual fun pause() {
|
|
countInJob?.cancel()
|
|
isRunning = false; isHolding = false
|
|
playJob?.cancel(); allNotesOff()
|
|
}
|
|
actual fun stop() {
|
|
countInJob?.cancel()
|
|
isRunning = false; isHolding = false
|
|
playJob?.cancel(); navigationJob?.cancel()
|
|
allNotesOff(); currentTickPos = 0L
|
|
resetNavigationFlags(); navigationSteps.clear(); clearLoop()
|
|
currentDynamicFactor = Dynamic.MF.factor
|
|
//resetTempoToNormal()
|
|
currentPlaybackBpm = targetBpm
|
|
usPerTick = (60_000_000.0 / targetBpm.toDouble()) / resolution.toDouble()
|
|
resetTempoToInitial()
|
|
}
|
|
actual fun release() {
|
|
stop(); midiDriver.stop(); playerScope.cancel()
|
|
}
|
|
actual fun seekTo(position: Long) {
|
|
currentTickPos = msToTicks(position)
|
|
lastEventNano = System.nanoTime()
|
|
applyVoiceStates()
|
|
}
|
|
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 setTempo(bpm: Float) {
|
|
targetBpm = bpm
|
|
currentPlaybackBpm = bpm
|
|
userCustomTempo = true
|
|
usPerTick = (60_000_000.0 / bpm.toDouble()) / resolution.toDouble()
|
|
boundModel?.setBpmFlow(bpm)
|
|
boundModel?.let { prepareNavigation(it) }
|
|
needsClockSync = true
|
|
}
|
|
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 = getCurrentPosition()
|
|
if (pointB > pointA && pointA != -1L) isLoopingAB = true
|
|
}
|
|
actual fun clearLoop() { isLoopingAB = false; pointA = -1L; pointB = -1L }
|
|
actual fun getLoopState() = Triple(pointA, pointB, isLoopingAB)
|
|
actual fun toggleVoice(index: Int) { applyVoiceStates() }
|
|
private fun saveVoicesVolumes() {
|
|
val data = voiceVolumes.joinToString(",")
|
|
settings.putString("voices_volumes", data)
|
|
}
|
|
|
|
private fun loadVoiceVolumes() {
|
|
val data = settings.getString("voices_volumes", "127,127,127,127")
|
|
val volumesArray = data.split(",")
|
|
|
|
if (volumesArray.size == 4) {
|
|
for (i in 0 until 4) {
|
|
voiceVolumes[i] = volumesArray[i].toFloatOrNull() ?: 127f
|
|
}
|
|
}
|
|
}
|
|
actual fun updateVoiceVolume(voiceIndex: Int, newVolume: Float) {
|
|
if (voiceIndex in 0..3) {
|
|
voiceVolumes[voiceIndex] = newVolume;
|
|
saveVoicesVolumes()
|
|
applyVoiceStates()
|
|
}
|
|
}
|
|
actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
|
|
actual fun changeInstru(noInstru: Int) {
|
|
for (ch in 0 until 4) send(0xC0 or ch, noInstru)
|
|
}
|
|
actual fun getAvalaibleInstruments(): List<MidiInstrument> {
|
|
val gmInstruments = listOf("🎹 Acoustic Grand Piano", "🎹 Bright Acoustic Piano", "🎹 Electric Grand Piano", "🎹 Honky-tonk Piano", "🎹 Electric Piano 1", "🎹 Electric Piano 2", "🎹 Harpsichord", "🎹 Clavi", "🔔 Celesta", "🔔 Glockenspiel", "🔔 Music Box", "🔔 Vibraphone", "🔔 Marimba", "🔔 Xylophone", "🔔 Tubular Bells", "🔔 Dulcimer", "🎹 Drawbar Organ", "🎹 Percussive Organ", "🎹 Rock Organ", "🎹 Church Organ", "🎹 Reed Organ", "🎹 Accordion", "🎹 Harmonica", "🎹 Tango Accordion", "🎸 Acoustic Guitar (nylon)", "🎸 Acoustic Guitar (steel)", "🎸 Electric Guitar (jazz)", "🎸 Electric Guitar (clean)", "🎸 Electric Guitar (muted)", "🎸 Overdriven Guitar", "🎸 Distortion Guitar", "🎸 Guitar harmonics", "🎸 Acoustic Bass", "🎸 Electric Bass (finger)", "🎸 Electric Bass (pick)", "🎸 Fretless Bass", "🎸 Slap Bass 1", "🎸 Slap Bass 2", "🎸 Synth Bass 1", "🎸 Synth Bass 2", "🎻 Violin", "🎻 Viola", "🎻 Cello", "🎻 Contrabass", "🎻 Tremolo Strings", "🎻 Pizzicato Strings", "🎻 Orchestral Harp", "🎻 Timpani", "🧑🤝🧑 String Ensemble 1", "🧑🤝🧑 String Ensemble 2", "🧑🤝🧑 SynthStrings 1", "🧑🤝🧑 SynthStrings 2", "🧑🤝🧑 Choir Aahs", "🧑🤝🧑 Voice Oohs", "🧑🤝🧑 Synth Voice", "🧑🤝🧑 Orchestra Hit", "🎺 Trumpet", "🎺 Trombone", "🎺 Tuba", "🎺 Muted Trumpet", "🎺 French Horn", "🎺 Brass Section", "🎺 SynthBrass 1", "🎺 SynthBrass 2", "🎷 Soprano Sax", "🎷 Alto Sax", "🎷 Tenor Sax", "🎷 Baritone Sax", "🍾 Oboe", "🍾 English Horn", "🍾 Bassoon", "🍾 Clarinet", "🍾 Piccolo", "🍾 Flute", "🍾 Recorder", "🍾 Pan Flute", "🍾 Blown Bottle", "🍾 Shakuhachi", "🍾 Whistle", "🍾 Ocarina", "🕹️ Lead 1 (square)", "🕹️ Lead 2 (sawtooth)", "🕹️ Lead 3 (calliope)", "🕹️ Lead 4 (chiff)", "🕹️ Lead 5 (charang)", "🕹️ Lead 6 (voice)", "🕹️ Lead 7 (fifths)", "🕹️ Lead 8 (bass+lead)", "🔮 Pad 1 (new age)", "🔮 Pad 2 (warm)", "🔮 Pad 3 (polysynth)", "🔮 Pad 4 (choir)", "🔮 Pad 5 (bowed)", "🔮 Pad 6 (metallic)", "🔮 Pad 7 (halo)", "🔮 Pad 8 (sweep)", "⚡ FX 1 (rain)", "⚡ FX 2 (soundtrack)", "⚡ FX 3 (crystal)", "⚡ FX 4 (atmosphere)", "⚡ FX 5 (brightness)", "⚡ FX 6 (goblins)", "⚡ FX 7 (echoes)", "⚡ FX 8 (sci-fi)", "🍛 Sitar", "🍛 Banjo", "🍛 Shamisen", "🍛 Koto", "🍛 Kalimba", "🍛 Bag pipe", "🍛 Fiddle", "🍛 Shanai", "🥁 Tinkle Bell", "🥁 Agogo", "🥁 Steel Drums", "🥁 Woodblock", "🥁 Taiko Drum", "🥁 Melodic Tom", "🥁 Synth Drum", "🥁 Reverse Cymbal", "🚁 Guitar Fret Noise", "🚁 Breath Noise", "🚁 Seashore", "🚁 Bird Tweet", "🚁 Telephone Ring", "🚁 Helicopter", "🚁 Applause", "🚁 Gunshot")
|
|
|
|
return gmInstruments.mapIndexed { index, text ->
|
|
val icon = text.substringBefore(' ')
|
|
val name = text.substringAfter(' ')
|
|
|
|
MidiInstrument(
|
|
icon = icon,
|
|
program = index,
|
|
name = name
|
|
)
|
|
}
|
|
}
|
|
|
|
actual fun saveVoiceInstrument(program: Int) {
|
|
settings.putInt("voice_instrument", program)
|
|
}
|
|
|
|
actual fun getVoiceInstrument(): Int {
|
|
val defaultProgram = 0
|
|
return settings.getInt("voice_instrument", defaultProgram)
|
|
}
|
|
|
|
fun loadSavedInstrumentsToPlayer(mediaPlayer: FMediaPlayer) {
|
|
val savedProgram = getVoiceInstrument()
|
|
mediaPlayer.changeInstru(savedProgram)
|
|
}
|
|
} |