Settings: countIn control; key&time signature on midifile; MidiPlayer: support change tempo , modulation . Support minor key
This commit is contained in:
parent
340f72855f
commit
b38bfebfe6
18 changed files with 854 additions and 115 deletions
|
|
@ -0,0 +1,11 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
private object AndroidSettingsResolver : KoinComponent {
|
||||
fun resolve(): Settings = get()
|
||||
}
|
||||
|
||||
actual fun provideSettings(): Settings = AndroidSettingsResolver.resolve()
|
||||
|
|
@ -3,6 +3,7 @@ package mg.dot.feufaro.midi
|
|||
import SharedScreenModel
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.*
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import org.billthefarmer.mididriver.MidiDriver
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
|
@ -76,7 +77,16 @@ actual class FMediaPlayer actual constructor(
|
|||
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++
|
||||
|
|
@ -164,16 +174,25 @@ actual class FMediaPlayer actual constructor(
|
|||
val targetTempoMultiplier: Float = 0.6f
|
||||
)
|
||||
private val navigationSteps = mutableListOf<NavigationStep>()
|
||||
private val prefs: Settings = provideSettings()
|
||||
|
||||
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)
|
||||
targetBpm = 60_000_000f / usPerQuarter
|
||||
}
|
||||
|
||||
usPerTick = (60_000_000.0 / targetBpm) / resolution
|
||||
} catch (e: Exception) { e.printStackTrace() }
|
||||
}
|
||||
|
|
@ -277,28 +296,37 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
|
||||
private fun startPlaybackLoop() {
|
||||
val seq = sequence ?: return
|
||||
val seq = sequence ?: run {
|
||||
return
|
||||
}
|
||||
isRunning = true
|
||||
needsClockSync = 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
|
||||
var idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||
.takeIf { it >= 0 } ?: events.size
|
||||
|
||||
while (isActive && isRunning) {
|
||||
if (needsClockSync) {
|
||||
clockNano = System.nanoTime()
|
||||
clockTick = currentTickPos
|
||||
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||
|
||||
val targetIdx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||
.takeIf { it >= 0 } ?: events.size
|
||||
if (targetIdx > idx) {
|
||||
idx = targetIdx
|
||||
}
|
||||
|
||||
needsClockSync = false
|
||||
}
|
||||
|
||||
if (isHolding) {
|
||||
yield()
|
||||
// delay(10)
|
||||
clockNano = System.nanoTime()
|
||||
clockTick = currentTickPos
|
||||
continue
|
||||
|
|
@ -309,7 +337,8 @@ actual class FMediaPlayer actual constructor(
|
|||
if (dc != null) {
|
||||
allNotesOff()
|
||||
seekToGrid(dc.targetGrid)
|
||||
clockNano = System.nanoTime(); clockTick = currentTickPos
|
||||
clockNano = System.nanoTime()
|
||||
clockTick = currentTickPos
|
||||
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||
.takeIf { it >= 0 } ?: events.size
|
||||
continue
|
||||
|
|
@ -322,16 +351,11 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
|
||||
val ev = events[idx]
|
||||
// val waitNano = (clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000)
|
||||
// .toLong()) - System.nanoTime()
|
||||
|
||||
val targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong()
|
||||
val now = System.nanoTime()
|
||||
val waitNano = targetNano - now
|
||||
|
||||
// if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L))
|
||||
if (waitNano > 0) {
|
||||
// Thread.sleep est beaucoup plus précis que delay() pour les micro-délais MIDI
|
||||
if (waitNano > 0 && waitNano < 3_000_000_000L) {
|
||||
val ms = waitNano / 1_000_000
|
||||
val ns = (waitNano % 1_000_000).toInt()
|
||||
try {
|
||||
|
|
@ -339,9 +363,11 @@ actual class FMediaPlayer actual constructor(
|
|||
} catch (e: Exception) {
|
||||
yield()
|
||||
}
|
||||
}
|
||||
if (needsClockSync) {
|
||||
continue
|
||||
} else if (waitNano < -100_000_000L) {
|
||||
clockNano = System.nanoTime()
|
||||
clockTick = ev.tickAbsolute
|
||||
} else {
|
||||
yield()
|
||||
}
|
||||
|
||||
currentTickPos = ev.tickAbsolute
|
||||
|
|
@ -350,8 +376,10 @@ actual class FMediaPlayer actual constructor(
|
|||
// A-B loop
|
||||
if (isLoopingAB && pointA >= 0 && pointB > pointA &&
|
||||
ticksToMs(currentTickPos) >= pointB) {
|
||||
allNotesOff(); currentTickPos = msToTicks(pointA)
|
||||
clockNano = System.nanoTime(); clockTick = currentTickPos
|
||||
allNotesOff()
|
||||
currentTickPos = msToTicks(pointA)
|
||||
clockNano = System.nanoTime()
|
||||
clockTick = currentTickPos
|
||||
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||
.takeIf { it >= 0 } ?: events.size
|
||||
continue
|
||||
|
|
@ -364,9 +392,21 @@ actual class FMediaPlayer actual constructor(
|
|||
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é */ }
|
||||
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/* ev.tickAbsolute*/ / resolution).toLong()
|
||||
}
|
||||
}
|
||||
|
||||
val currentGrid = (currentTickPos / resolution).toLong()
|
||||
if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
|
||||
boundModel?.updateActiveIndex(currentGrid)
|
||||
}
|
||||
|
|
@ -704,7 +744,10 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
actual fun play() {
|
||||
if (sequence == null) return
|
||||
if(currentTickPos == 0L) {
|
||||
usPerTick = (60_000_000.0 / targetBpm.toDouble()) / resolution.toDouble()
|
||||
currentPlaybackBpm = targetBpm
|
||||
val countInEnable = prefs.getBoolean("isCountIn", true)
|
||||
if (currentTickPos == 0L && countInEnable) {
|
||||
playCountIn()
|
||||
} else {
|
||||
applyVoiceStates()
|
||||
|
|
@ -778,6 +821,7 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
withContext(Dispatchers.Main) {
|
||||
applyVoiceStates()
|
||||
needsClockSync = true
|
||||
startPlaybackLoop()
|
||||
}
|
||||
}
|
||||
|
|
@ -812,9 +856,11 @@ actual class FMediaPlayer actual constructor(
|
|||
applyVoiceStates()
|
||||
}
|
||||
actual fun setTempo(bpm: Float) {
|
||||
targetBpm = bpm; usPerTick = (60_000_000.0/bpm)/resolution
|
||||
targetBpm = bpm
|
||||
currentPlaybackBpm = bpm
|
||||
usPerTick = (60_000_000.0 / bpm.toDouble()) / resolution.toDouble()
|
||||
boundModel?.let { prepareNavigation(it) }
|
||||
println("Tempo → $bpm BPM")
|
||||
needsClockSync = true
|
||||
}
|
||||
actual fun getCurrentBPM(): Float = targetBpm
|
||||
actual fun requestSync(sharedScreenModel: SharedScreenModel) {
|
||||
|
|
@ -865,12 +911,14 @@ actual class FMediaPlayer actual constructor(
|
|||
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"
|
||||
)
|
||||
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(' ')
|
||||
|
||||
return gmInstruments.mapIndexed { index, name ->
|
||||
MidiInstrument(
|
||||
icon = icon,
|
||||
program = index,
|
||||
name = name
|
||||
)
|
||||
|
|
@ -885,9 +933,4 @@ actual class FMediaPlayer actual constructor(
|
|||
val defaultProgram = 0
|
||||
return settings.getInt("voice_instrument", defaultProgram)
|
||||
}
|
||||
|
||||
fun loadSavedInstrumentsToPlayer(mediaPlayer: FMediaPlayer) {
|
||||
val savedProgram = getVoiceInstrument()
|
||||
mediaPlayer.changeInstru(savedProgram)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
|
||||
expect fun provideSettings(): Settings
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package mg.dot.feufaro.midi
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import java.util.prefs.Preferences
|
||||
|
||||
enum class Dynamic(val velocity: Int, val label: String, val defaultFactor: Float) {
|
||||
|
|
@ -10,8 +12,8 @@ enum class Dynamic(val velocity: Int, val label: String, val defaultFactor: Floa
|
|||
MF (80, "mf", 1.00f),
|
||||
F (96, "f", 1.15f),
|
||||
FF (112, "ff", 1.35f),
|
||||
FFF(126, "fff", 1.60f);
|
||||
private val prefs = Preferences.userRoot().node("mg.dot.feufaro")
|
||||
FFF(127, "fff", 1.60f);
|
||||
private val prefs: Settings = provideSettings()
|
||||
var savedFactor: Float = prefs.getFloat("dynamic_${name.lowercase()}", defaultFactor)
|
||||
set(value) {
|
||||
field = value
|
||||
|
|
@ -22,11 +24,11 @@ enum class Dynamic(val velocity: Int, val label: String, val defaultFactor: Floa
|
|||
get() = if (isGloballyEnabled) savedFactor else 1.00f
|
||||
|
||||
companion object {
|
||||
private val globalPrefs = Preferences.userRoot().node("mg.dot.feufaro")
|
||||
var isGloballyEnabled: Boolean = globalPrefs.getBoolean("dynamics_enabled", true)
|
||||
private val prefs: Settings = provideSettings()
|
||||
var isGloballyEnabled: Boolean = prefs.getBoolean("dynamics_enabled", true)
|
||||
set(value) {
|
||||
field = value
|
||||
globalPrefs.putBoolean("dynamics_enabled", value)
|
||||
prefs.putBoolean("dynamics_enabled", value)
|
||||
}
|
||||
|
||||
fun fromVelocity(v: Int): Dynamic =
|
||||
|
|
@ -35,5 +37,9 @@ enum class Dynamic(val velocity: Int, val label: String, val defaultFactor: Floa
|
|||
fun resetToDefaults() {
|
||||
entries.forEach { it.savedFactor = it.defaultFactor }
|
||||
}
|
||||
fun fromLabel(label: String): Dynamic? {
|
||||
val cleanedLabel = label.trim().lowercase()
|
||||
return entries.find { it.label == cleanedLabel }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ data class MidiPitch (
|
|||
var duration: Int = 0,
|
||||
var markers: List<String> = listOf(),
|
||||
var tick : Int = 0,
|
||||
var metaBytes: String = "",
|
||||
var metaBytes: ByteArray = byteArrayOf(),
|
||||
var metaByteSize: Int = 0,
|
||||
var metaType : Int = -1
|
||||
) {
|
||||
|
|
@ -22,8 +22,11 @@ data class MidiPitch (
|
|||
var curVoiceNumber: Int = 0
|
||||
var nextTick : MutableList<Int> = mutableListOf()
|
||||
}
|
||||
fun setMeta(type: Int, tickMeta: Int, nb: Int, data0: Int, data1: Int = 0, data2: Int = 0, data3: Int = 0, data4: Int = 0) {
|
||||
metaBytes = ""+ data0.toChar() + data1.toChar() + data2.toChar() + data3.toChar() + data4.toChar()
|
||||
fun setMeta(type: Int, tickMeta: Int, nb: Int, vararg data: Int) {
|
||||
val bytes = ByteArray(nb) { i ->
|
||||
(data.getOrElse(i) { 0 } and 0xFF).toByte()
|
||||
}
|
||||
metaBytes = bytes
|
||||
tick = tickMeta
|
||||
metaByteSize = nb
|
||||
metaType = type
|
||||
|
|
@ -55,30 +58,33 @@ data class MidiPitch (
|
|||
nextTick.add(0)
|
||||
}
|
||||
}
|
||||
fun initKey(theKey: String) {
|
||||
fun initKey(theKey: String, isMinor: Boolean = false) {
|
||||
key = theKey
|
||||
val armature = Transpose.toArmature(key)
|
||||
val armature = Transpose.toArmature(theKey)
|
||||
|
||||
val tickMeta = nextTick.getOrNull(0) ?: 0
|
||||
setMeta(0x59, tickMeta, 2, armature)
|
||||
val mode = if (isMinor) 1 else 0
|
||||
setMeta(0x59, tickMeta, 2, armature, mode)
|
||||
}
|
||||
fun initMeasure(theMeasure: String): Boolean {
|
||||
val numerator = theMeasure.replace(Regex("/.*"), "").toIntOrNull()
|
||||
val denominator = theMeasure.replace(Regex("^[^/]*/(\\d+).*$"), "$1").toIntOrNull() ?: 4
|
||||
val data1 = when (denominator) {
|
||||
val parts = theMeasure.split("/")
|
||||
val numerator = parts.getOrNull(0)?.toIntOrNull() ?: return false
|
||||
val denominator = parts.getOrNull(1)?.toIntOrNull() ?: 4
|
||||
|
||||
val denomPower = when (denominator) {
|
||||
1 -> 0
|
||||
2 -> 1
|
||||
4 -> 2
|
||||
8 -> 3
|
||||
16 -> 4
|
||||
else -> 5
|
||||
32 -> 5
|
||||
else -> 2
|
||||
}
|
||||
val blankDuration = (4 * (numerator ?: 4) - ParseULine.blankDuration) * 15
|
||||
if (numerator != null) {
|
||||
setMeta(0x58, blankDuration, 4, numerator, data1, 60, 20)
|
||||
val tickMeta = nextTick.getOrNull(0) ?: 0
|
||||
setMeta(0x58, tickMeta, 4, numerator, denomPower, 24, 8)
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
fun reset() {
|
||||
tick = 0
|
||||
nextTick.clear()
|
||||
|
|
|
|||
|
|
@ -31,4 +31,4 @@ expect class FMediaPlayer(filename: String, sharedScreenModel: SharedScreenModel
|
|||
fun getVoiceInstrument(): Int
|
||||
}
|
||||
|
||||
data class MidiInstrument(val program: Int, val name: String)
|
||||
data class MidiInstrument(val icon: String = "\uD83C\uDFB9", val program: Int, val name: String)
|
||||
|
|
@ -36,8 +36,27 @@ class MidiSequence(val resolution: Int = 60) {
|
|||
|
||||
fun addSequence(channel: Int, pitch: Int, currentTick: Long, type: Int = 80, finalVelocity: Int = 100) {
|
||||
val myTrack = tracks[0]
|
||||
if (type == 0xC0) {
|
||||
myTrack.addProgramChange(channel, pitch, currentTick)
|
||||
} else {
|
||||
myTrack.addNote(channel, pitch, currentTick, type, finalVelocity)
|
||||
}
|
||||
}
|
||||
fun MutableList<Byte>.addProgramChange(channel: Int, program: Int, currentTick: Long) {
|
||||
val delta = currentTick - lastTick
|
||||
lastTick = currentTick
|
||||
val status = 0xC0 or (channel and 0x0F)
|
||||
|
||||
this.addMidiEvent(delta, status, program, null)
|
||||
}
|
||||
fun addControlChange(channel: Int, controller: Int, value: Int, currentTick: Long) {
|
||||
val myTrack = if (tracks.isEmpty()) createTrack() else tracks[0]
|
||||
val delta = (currentTick - lastTick).coerceAtLeast(0L)
|
||||
lastTick = currentTick
|
||||
|
||||
val status = 0xB0 or (channel and 0x0F)
|
||||
myTrack.addMidiEvent(delta, status, controller, value)
|
||||
}
|
||||
fun MutableList<Byte>.addNote(channel: Int, pitch: Int, currentTick: Long, type: Int = 80, finalVelocity: Int = 100) {
|
||||
// Calcul du Delta-Time
|
||||
val delta = currentTick - lastTick
|
||||
|
|
@ -127,4 +146,20 @@ class MidiSequence(val resolution: Int = 60) {
|
|||
outTrack.addAll(data.toList().take(nbData))
|
||||
|
||||
}
|
||||
fun addMetaFe(type: Int, tick: Int, nbData: Int, metaByte: ByteArray) {
|
||||
if (tracks.isEmpty()) {
|
||||
tracks.add(mutableListOf<Byte>(0))
|
||||
}
|
||||
val outTrack = tracks[0]
|
||||
// Un Meta Message commence toujours par FF, puis le type, puis la longueur
|
||||
outTrack.addAll(0L.toVLQList())
|
||||
outTrack.add(0xFF.toByte()) // Status byte
|
||||
outTrack.add(type.toByte()) // Meta-event type
|
||||
outTrack.addAll(nbData.toLong().toVLQList())
|
||||
// Ajout des données
|
||||
for (i in 0 until nbData) {
|
||||
val byteVal = metaByte.getOrElse(i) { 0 }
|
||||
outTrack.add(byteVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ class MidiWriterKotlin constructor(private val fileRepository: FileRepository)
|
|||
tick = 0
|
||||
pitches.forEach {
|
||||
if (it.metaType > 0) {
|
||||
addMetaMessage(it.metaType, it.tick, it.metaByteSize, it.metaBytes)
|
||||
val metaDataString = String(it.metaBytes, Charsets.ISO_8859_1)
|
||||
addMetaMessage(it.metaType, it.tick, it.metaByteSize, metaDataString)
|
||||
} else if (it.pitch != "") {
|
||||
addNote(it.voiceNumber, it.pitch.toInt(), 100, it.tick.toLong())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,340 @@
|
|||
package mg.dot.feufaro.midi
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mg.dot.feufaro.FileRepository
|
||||
import mg.dot.feufaro.solfa.Transpose
|
||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||
|
||||
class MidiWriterforEXport constructor(private val fileRepository: FileRepository) {
|
||||
private val sequence = MidiSequence(480)
|
||||
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
|
||||
fun addNote( voiceNumber: Int, note: Int, velocity: Int, tick: Long) {
|
||||
val channel = (voiceNumber -1).coerceIn(0, 3)
|
||||
var finalNote = note
|
||||
|
||||
if (voiceNumber == 3 || voiceNumber == 4) {
|
||||
finalNote -= 12
|
||||
}
|
||||
if (lastPitch.size > voiceNumber && lastPitch[voiceNumber] > 0) {
|
||||
sequence.addSequence(channel, lastPitch[voiceNumber], tick)
|
||||
}
|
||||
var finalVelocity = velocity
|
||||
var midiNote = finalNote
|
||||
if (finalNote <= 0) {
|
||||
midiNote = 40
|
||||
finalVelocity = 0
|
||||
}
|
||||
sequence.addSequence(channel, midiNote, tick, 90, finalVelocity)
|
||||
|
||||
while(lastPitch.size <= voiceNumber) {
|
||||
lastPitch.add(0)
|
||||
}
|
||||
lastPitch[voiceNumber] = midiNote
|
||||
}
|
||||
fun save(filePath: String) {
|
||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||
parseScope.launch {
|
||||
sequence.write()
|
||||
fileRepository.saveFile(filePath, sequence.out.toByteArray())
|
||||
//val fout = fileRepository.getOutputStream(filePath)
|
||||
}
|
||||
}
|
||||
fun addMetaMessage(type: Int, tick: Int, nbData: Int, metaBytes: ByteArray) {
|
||||
sequence.addMeta(type, tick, nbData, metaBytes.toString())
|
||||
}
|
||||
|
||||
fun setTempoMeta(bpm: Float, tickPosition: Int = 0) {
|
||||
val mpqn = (60_000_000f / bpm.coerceAtLeast(1f)).toInt()
|
||||
val bytes = byteArrayOf(
|
||||
(mpqn shr 16 and 0xFF).toByte(),
|
||||
(mpqn shr 8 and 0xFF).toByte(),
|
||||
(mpqn and 0xFF).toByte()
|
||||
)
|
||||
addMetaMessage(0x51, tickPosition, 3, bytes)
|
||||
}
|
||||
fun setProgramChange(voiceNumber: Int, instrumentProgram: Int, tickPosition: Long = 0) {
|
||||
val channel = (voiceNumber - 1).coerceIn(0, 3)
|
||||
val program = instrumentProgram.coerceIn(0, 127)
|
||||
sequence.addSequence(channel, program, tickPosition, 0xC0, 0)
|
||||
}
|
||||
|
||||
fun addControlChange(voiceNumber: Int, controller: Int, value: Int, tickPosition: Long) {
|
||||
val channel = (voiceNumber - 1).coerceIn(0, 3)
|
||||
val ctrl = controller.coerceIn(0, 127)
|
||||
val valByte = value.coerceIn(0, 127)
|
||||
sequence.addControlChange(channel, ctrl, valByte, tickPosition)
|
||||
}
|
||||
|
||||
fun process(
|
||||
pitches: List<MidiPitch>,
|
||||
markers: List<MidiMarkers> = emptyList(),
|
||||
initialBpm: Float = 120.0f,
|
||||
voiceInstruments: Map<Int, Int> = mapOf(1 to 0, 2 to 0, 3 to 0, 4 to 0),
|
||||
initialKey: String = "C"
|
||||
) {
|
||||
val lastTick = 0
|
||||
nextTick.clear()
|
||||
// addMetaMessage(0x59, 4, 2, 2,0)
|
||||
tick = 0
|
||||
setTempoMeta(initialBpm, 0)
|
||||
voiceInstruments.forEach { (voiceIndex, programNumber) ->
|
||||
setProgramChange(voiceNumber = voiceIndex, instrumentProgram = programNumber, tickPosition = 0L)
|
||||
}
|
||||
|
||||
val nuanceMap = mutableMapOf<Long, Int>()
|
||||
val fermataTicks = mutableSetOf<Long>()
|
||||
val textMetaMap = mutableMapOf<Long, MutableList<String>>()
|
||||
val keyChangeMap = mutableMapOf<Long, String>()
|
||||
val tempoChangeMap = mutableMapOf<Long, Float>()
|
||||
val hairpinMap = mutableMapOf<Long, Pair<String, Long>>()
|
||||
var pendingHairpinType: String? = null
|
||||
var pendingHairpinStartTick: Long? = null
|
||||
|
||||
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)
|
||||
|
||||
markers.forEach { midiMarker ->
|
||||
val gridIdx = (midiMarker.gridIndex ?: 0) * 480L
|
||||
midiMarker.marker.forEach { text ->
|
||||
val cleanText = text.trim()
|
||||
val vel = getVelocityFromNuance(cleanText, -1)
|
||||
val tempoMatch = tempoRegex.find(cleanText)
|
||||
val modMatch = modulaRegex.find(cleanText)
|
||||
/* Les nuances */
|
||||
if (vel != -1) {
|
||||
nuanceMap[gridIdx] = vel
|
||||
}
|
||||
|
||||
/* tempo */
|
||||
if (tempoMatch != null) {
|
||||
val bpmValue = tempoMatch.groupValues[1].toFloatOrNull()
|
||||
if (bpmValue != null) tempoChangeMap[gridIdx] = bpmValue
|
||||
}
|
||||
if (text.contains("𝄐")) {
|
||||
fermataTicks.add(gridIdx)
|
||||
}
|
||||
|
||||
/* Modulation */
|
||||
if (modMatch != null) {
|
||||
val targetKey = modMatch.groupValues[2]
|
||||
keyChangeMap[gridIdx] = targetKey
|
||||
}
|
||||
|
||||
/* Hairpins */
|
||||
val isCres = cleanText == "<" || cleanText.lowercase().startsWith("cres")
|
||||
val isDecres = cleanText == ">" || cleanText.lowercase().startsWith("decresc")
|
||||
|
||||
if (isCres || isDecres) {
|
||||
if (pendingHairpinStartTick != null && pendingHairpinType != null) {
|
||||
val defaultEnd = pendingHairpinStartTick!! + (2 * 480L)
|
||||
hairpinMap[pendingHairpinStartTick!!] = Pair(pendingHairpinType!!, defaultEnd)
|
||||
}
|
||||
|
||||
pendingHairpinType = if (isCres) "<" else ">"
|
||||
pendingHairpinStartTick = gridIdx
|
||||
} else if (cleanText == "=" && pendingHairpinStartTick != null && pendingHairpinType != null) {
|
||||
hairpinMap[pendingHairpinStartTick!!] = Pair(pendingHairpinType!!, gridIdx)
|
||||
pendingHairpinType = null
|
||||
pendingHairpinStartTick = null
|
||||
}
|
||||
|
||||
if (!isCres && !isDecres && cleanText != "<" && cleanText != ">" && cleanText != "=" && !cleanText.contains("𝄐") && vel == -1 && tempoMatch == null && modMatch == null) {
|
||||
textMetaMap.getOrPut(gridIdx) { mutableListOf() }.add(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val processedFermataTicks = mutableSetOf<Long>()
|
||||
val pendingSustainOff = mutableListOf<Pair<Long, Int>>()
|
||||
|
||||
|
||||
|
||||
var timeOffset = 0L
|
||||
val holdDuration = 480L *3 // 480L:2noire *2:3noir *4:ronde
|
||||
|
||||
if (pendingHairpinStartTick != null && pendingHairpinType != null) {
|
||||
val defaultEnd = pendingHairpinStartTick!! + (2 * 480L)
|
||||
hairpinMap[pendingHairpinStartTick!!] = Pair(pendingHairpinType!!, defaultEnd)
|
||||
}
|
||||
var currentKey = initialKey
|
||||
var baseVelocity = Dynamic.MF.velocity
|
||||
var baseExpression = 64 // Expression MIDI standard (0 à 127)
|
||||
val stepExpressionDelta = 10
|
||||
val baseKeyIdx = Transpose.keyToNumber.indexOf(initialKey).coerceAtLeast(0)
|
||||
|
||||
val stepDelta = 20
|
||||
val processedHairpins = mutableSetOf<Long>()
|
||||
|
||||
val pitchesByTick = pitches.groupBy { (it.tick) * 8L }.toSortedMap()
|
||||
pitchesByTick.forEach { (rawTick, eventsAtTick) ->
|
||||
|
||||
/* Fermata CC*/
|
||||
val adjustedTickForRelease = rawTick + timeOffset
|
||||
val iterator = pendingSustainOff.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val (offTick, voiceNum) = iterator.next()
|
||||
if (offTick <= adjustedTickForRelease) {
|
||||
addControlChange(voiceNumber = voiceNum, controller = 64, value = 0, tickPosition = offTick)
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
var isFermataAtThisTick = false
|
||||
if (fermataTicks.contains(rawTick) && !processedFermataTicks.contains(rawTick)) {
|
||||
processedFermataTicks.add(rawTick)
|
||||
isFermataAtThisTick = true
|
||||
|
||||
val fermataStartTick = rawTick + timeOffset
|
||||
val offTickTarget = fermataStartTick + holdDuration
|
||||
|
||||
(1..4).forEach { v ->
|
||||
addControlChange(voiceNumber = v, controller = 64, value = 127, tickPosition = fermataStartTick)
|
||||
pendingSustainOff.add(Pair(offTickTarget, v))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Nuances */
|
||||
if (nuanceMap.containsKey(rawTick)) {
|
||||
baseVelocity = nuanceMap[rawTick] ?: Dynamic.MF.velocity
|
||||
}
|
||||
|
||||
val adjustedTick = rawTick/* + timeOffset*/
|
||||
|
||||
|
||||
|
||||
// hairpin
|
||||
hairpinMap.forEach { (startTick, pair) ->
|
||||
val (type, endTick) = pair
|
||||
if (rawTick >= endTick && !processedHairpins.contains(startTick)) {
|
||||
if (type == "<") {
|
||||
baseExpression = (baseExpression + stepDelta).coerceIn(0, 127)
|
||||
} else if (type == ">") {
|
||||
baseExpression = (baseExpression - stepDelta).coerceIn(0, 127)
|
||||
}
|
||||
processedHairpins.add(startTick)
|
||||
}
|
||||
}
|
||||
var currentExpression = baseExpression
|
||||
|
||||
val activeHairpin = hairpinMap.entries.firstOrNull { (start, pair) ->
|
||||
rawTick >= start && rawTick < pair.second
|
||||
}
|
||||
|
||||
if (activeHairpin != null) {
|
||||
val startTick = activeHairpin.key
|
||||
val (type, endTick) = activeHairpin.value
|
||||
|
||||
if (endTick > startTick) {
|
||||
val progress = (rawTick - startTick).toDouble() / (endTick - startTick).toDouble()
|
||||
|
||||
if (type == "<") {
|
||||
currentExpression = (baseExpression + (stepExpressionDelta * progress)).toInt().coerceIn(0, 127)
|
||||
} else if (type == ">") {
|
||||
currentExpression = (baseExpression - (stepExpressionDelta * progress)).toInt().coerceIn(0, 127)
|
||||
}
|
||||
|
||||
(1..4).forEach { voiceNum ->
|
||||
addControlChange(voiceNumber = voiceNum, controller = 11, value = currentExpression, tickPosition = rawTick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Tempo Meta 0x51
|
||||
val pendingTempoTicks = tempoChangeMap.keys.filter { it <= rawTick }.sorted()
|
||||
pendingTempoTicks.forEach { tempoRawTick ->
|
||||
val newBpm = tempoChangeMap[tempoRawTick] ?: initialBpm
|
||||
val tempoAdjustedTick = tempoRawTick/* + timeOffset*/
|
||||
|
||||
setTempoMeta(newBpm, tempoAdjustedTick.toInt())
|
||||
tempoChangeMap.remove(tempoRawTick)
|
||||
}
|
||||
|
||||
// Modulation Meta 0x59
|
||||
val pendingKeyTicks = keyChangeMap.keys.filter { it <= rawTick }.sorted()
|
||||
pendingKeyTicks.forEach { keyRawTick ->
|
||||
val targetKey = keyChangeMap[keyRawTick] ?: initialKey
|
||||
currentKey = targetKey
|
||||
|
||||
val keyAdjustedTick = keyRawTick/* + timeOffset*/
|
||||
val keyIdx = Transpose.keyToNumber.indexOf(targetKey)
|
||||
|
||||
if (keyIdx != -1) {
|
||||
val sf = Transpose.keyToArmature[keyIdx]
|
||||
val keyBytes = byteArrayOf(sf.toByte(), 0.toByte()) // 0 = Majeur
|
||||
|
||||
addMetaMessage(0x59, keyAdjustedTick.toInt(), 2, keyBytes)
|
||||
}
|
||||
keyChangeMap.remove(keyRawTick)
|
||||
}
|
||||
|
||||
|
||||
/* Tetxe; marker */
|
||||
val pendingTextTicks = textMetaMap.keys.filter { it <= rawTick }.sorted()
|
||||
pendingTextTicks.forEach { textRawTick ->
|
||||
val textAdjustedTick = textRawTick/* + timeOffset*/
|
||||
|
||||
textMetaMap[textRawTick]?.forEach { annotationText ->
|
||||
addTextMeta(text = annotationText, tickPosition = textAdjustedTick, metaType = 0x06)
|
||||
}
|
||||
textMetaMap.remove(textRawTick)
|
||||
}
|
||||
|
||||
eventsAtTick.forEach { noteEvent ->
|
||||
// Note + transpose
|
||||
val currentKeyIdx = Transpose.keyToNumber.indexOf(currentKey).coerceAtLeast(0)
|
||||
val transpositionSemitones = currentKeyIdx - baseKeyIdx
|
||||
|
||||
if (noteEvent.metaType > 0) {
|
||||
addMetaMessage(noteEvent.metaType, adjustedTick.toInt(), noteEvent.metaByteSize, noteEvent.metaBytes)
|
||||
} else if (noteEvent.pitch != "") {
|
||||
val finalTransposedPitch = noteEvent.pitch.toInt() + transpositionSemitones
|
||||
addNote(noteEvent.voiceNumber, finalTransposedPitch, baseVelocity, adjustedTick)
|
||||
}
|
||||
}
|
||||
|
||||
if (fermataTicks.contains(rawTick)) {
|
||||
timeOffset += holdDuration
|
||||
}
|
||||
}
|
||||
|
||||
tempoChangeMap.forEach { (tempoRawTick, bpm) ->
|
||||
val tempoAdjustedTick = tempoRawTick/* + timeOffset*/
|
||||
setTempoMeta(bpm, tempoAdjustedTick.toInt())
|
||||
}
|
||||
tempoChangeMap.clear()
|
||||
|
||||
|
||||
pendingSustainOff.forEach { (offTick, voiceNum) ->
|
||||
addControlChange(voiceNumber = voiceNum, controller = 64, value = 0, tickPosition = offTick)
|
||||
}
|
||||
pendingSustainOff.clear()
|
||||
}
|
||||
|
||||
fun getVelocityFromNuance(markerText: String, defaultVelocity: Int = -1): Int {
|
||||
val dynamic = Dynamic.fromLabel(markerText)
|
||||
if (dynamic != null) {
|
||||
return dynamic.velocity
|
||||
}
|
||||
return -1
|
||||
}
|
||||
/**
|
||||
* (Type 0x01 = Text, 0x06 = Marker, 0x05 = Lyric).
|
||||
*/
|
||||
fun addTextMeta(text: String, tickPosition: Long, metaType: Int = 0x01) {
|
||||
val bytes = text.toByteArray(Charsets.UTF_8)
|
||||
addMetaMessage(
|
||||
type = metaType,
|
||||
tick = tickPosition.toInt(),
|
||||
nbData = bytes.size,
|
||||
metaBytes = bytes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,23 @@
|
|||
package mg.dot.feufaro.solfa
|
||||
|
||||
import SharedScreenModel
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.*
|
||||
import mg.dot.feufaro.FileRepository
|
||||
import mg.dot.feufaro.SaveSettings
|
||||
import mg.dot.feufaro.getGlobalTemplate
|
||||
import mg.dot.feufaro.getPlatform
|
||||
import mg.dot.feufaro.launchFilePicker
|
||||
import mg.dot.feufaro.midi.MidiPitch
|
||||
import mg.dot.feufaro.midi.MidiWriterKotlin
|
||||
import mg.dot.feufaro.midi.MidiWriterforEXport
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import mg.dot.feufaro.viewmodel.PartitionMetadata
|
||||
import mg.dot.feufaro.transformLyricsInput
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.util.prefs.Preferences
|
||||
import kotlin.getValue
|
||||
import kotlin.math.min
|
||||
|
||||
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
|
||||
|
|
@ -251,6 +258,25 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
sharedScreenModel.setStanza(1)
|
||||
}
|
||||
}
|
||||
|
||||
private val prefs: Settings = provideSettings()
|
||||
|
||||
fun generateMidiFile(bpm: Float) {
|
||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||
parseScope.launch {
|
||||
val pitchesSorted = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
|
||||
val midiWriter = MidiWriterforEXport(fileRepository)
|
||||
|
||||
val instruments = mapOf(
|
||||
1 to prefs.getInt("voice_instrument", 0),
|
||||
2 to prefs.getInt("voice_instrument", 0),
|
||||
3 to prefs.getInt("voice_instrument", 0),
|
||||
4 to prefs.getInt("voice_instrument", 0),
|
||||
)
|
||||
midiWriter.process(pitchesSorted, sharedScreenModel.getFullMarkers(), initialBpm = bpm, instruments, initialKey = sharedScreenModel.songKey.value)
|
||||
midiWriter.save("whawyd3.mid")
|
||||
}
|
||||
}
|
||||
fun justBuild(sourceFile: String, sourceContent: String) {
|
||||
currentFile = sourceFile
|
||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||
|
|
@ -1987,12 +2013,17 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
val z = midiPitch.copy()
|
||||
if (z.tick != 0) {
|
||||
val tickMeta = midiPitch.tick
|
||||
val numerator = midiPitch.metaBytes.toByteArray()[0].toInt()
|
||||
val denominatorPower = midiPitch.metaBytes.toByteArray()[1].toInt()
|
||||
val numerator = (midiPitch.metaBytes.getOrNull(0)?.toInt() ?: 4) and 0xFF
|
||||
val denominatorPower = (midiPitch.metaBytes.getOrNull(1)?.toInt() ?: 2) and 0xFF
|
||||
val denominator = 1 shl denominatorPower
|
||||
z.tick = 0
|
||||
val newNumerator = tickMeta / 15 / denominator
|
||||
z.metaBytes = "" + newNumerator.toChar() + denominatorPower.toChar() + 60.toChar() + 20.toChar()
|
||||
z.metaBytes = byteArrayOf(
|
||||
newNumerator.toByte(),
|
||||
denominatorPower.toByte(),
|
||||
60.toByte(),
|
||||
20.toByte()
|
||||
)
|
||||
|
||||
pitches.add(z)
|
||||
}
|
||||
|
|
@ -2005,7 +2036,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
} else if (typeBlock == "meta") {
|
||||
pitches.add(midiPitch.copy())
|
||||
}
|
||||
midiPitch.metaBytes = ""
|
||||
midiPitch.metaBytes = byteArrayOf()
|
||||
midiPitch.metaType = -1
|
||||
midiPitch.duration = 0
|
||||
lastNoteString = ""
|
||||
|
|
@ -2015,7 +2046,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
|
||||
midiPitch.currentVoiceNumber(voiceNumber)
|
||||
if (voiceNumber == 1) {
|
||||
midiPitch.initKey(meta["C"] ?: "C")
|
||||
midiPitch.initKey(meta["C"] ?: "C", meta["C"]?.endsWith("m") ?: false)
|
||||
pushMidi("meta")
|
||||
if (midiPitch.initMeasure(meta["m"] ?: "4/4")) {
|
||||
pushMidi("measure")
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class Transpose {
|
|||
val octaveSigns = listOf("₄", "₃", "₂", "₁", "", "¹", "²", "³", "⁴")
|
||||
val noteToNumber = listOf("d", "di", "r", "ri", "m", "f", "fi", "s", "si", "l", "ta", "t")
|
||||
val keyToNumber = listOf("C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" )
|
||||
val keyToArmature = listOf(0, -7, 2, -3, 4, -1, -6, 1, -4, 3, -2, 5 )
|
||||
val keyToArmature = listOf(0, -5, 2, -3, 4, -1, -6, 1, -4, 3, -2, 5 )
|
||||
fun transposeText(text: String, interval: Int): String {
|
||||
if (text.isEmpty()) return text
|
||||
|
||||
|
|
@ -100,9 +100,51 @@ class Transpose {
|
|||
}
|
||||
}
|
||||
fun toArmature(note: String): Int {
|
||||
val index = keyToNumber.indexOf(note)
|
||||
return keyToArmature.getOrNull(index) ?: 0
|
||||
val cleanKey = note.trim()
|
||||
val isMinor = cleanKey.endsWith("m", ignoreCase = true) && !cleanKey.lowercase().endsWith("dim")
|
||||
val rootNote = if (isMinor) cleanKey.dropLast(1) else cleanKey
|
||||
if (isMinor) {
|
||||
val relativeMajor = when (rootNote.uppercase()) {
|
||||
"AB" -> "CB"
|
||||
"EB" -> "GB"
|
||||
"BB" -> "DB"
|
||||
"F" -> "AB"
|
||||
"C" -> "EB"
|
||||
"G" -> "BB"
|
||||
"D" -> "F"
|
||||
"A" -> "C"
|
||||
"E" -> "G"
|
||||
"B" -> "D"
|
||||
"F#" -> "A"
|
||||
"C#" -> "E"
|
||||
"G#" -> "B"
|
||||
"D#" -> "F#"
|
||||
"A#" -> "C#"
|
||||
else -> rootNote
|
||||
}
|
||||
return toArmature(relativeMajor)
|
||||
}
|
||||
|
||||
return when (rootNote.uppercase()) {
|
||||
"C" -> 0
|
||||
"G" -> 1
|
||||
"D" -> 2
|
||||
"A" -> 3
|
||||
"E" -> 4
|
||||
"B" -> 5
|
||||
"F#" -> 6
|
||||
"C#" -> 7
|
||||
"F" -> -1
|
||||
"BB" -> -2
|
||||
"EB" -> -3
|
||||
"AB" -> -4
|
||||
"DB" -> -5
|
||||
"GB" -> -6
|
||||
"CB" -> -7
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun transposeMidi(note: String, fromKey: String) : Pair<String, Int> {
|
||||
return Pair(note, 4)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ fun MainScreenWithDrawer(
|
|||
if (showSettingsDialog) {
|
||||
Settings(
|
||||
sharedScreenModel = sharedScreenModel,
|
||||
solfaScreenModel,
|
||||
onDismissRequest = {
|
||||
showSettingsDialog = false
|
||||
},
|
||||
|
|
@ -448,8 +449,10 @@ fun MainScreenWithDrawer(
|
|||
}, actions = {
|
||||
var tempInterval by remember(fileContent) { mutableStateOf(0) }
|
||||
var isEyeVisible by remember { mutableStateOf(false) }
|
||||
val isMinor = songKey.endsWith("m", ignoreCase = true)
|
||||
val cleanSongKey = if (isMinor) songKey.dropLast(1) else songKey
|
||||
val keysOrder = Transpose.keyToNumber
|
||||
val songKeyIndex = keysOrder.indexOf(songKey).takeIf { it != -1 } ?: 0
|
||||
val songKeyIndex = keysOrder.indexOf(cleanSongKey).takeIf { it != -1 } ?: 0
|
||||
val rawKeyIndex = (songKeyIndex + tempInterval) % 12
|
||||
val tempUiKey = keysOrder[if (rawKeyIndex < 0) rawKeyIndex + 12 else rawKeyIndex]
|
||||
val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState()
|
||||
|
|
@ -465,7 +468,7 @@ fun MainScreenWithDrawer(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = tempUiKey,
|
||||
text = if(isMinor) "${tempUiKey}m" else tempUiKey,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
fontWeight = FontWeight.Black,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -477,7 +480,7 @@ fun MainScreenWithDrawer(
|
|||
) {
|
||||
isEyeVisible = !isEyeVisible
|
||||
}
|
||||
.width(45.dp)
|
||||
.width(65.dp)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -809,7 +812,8 @@ fun MainScreenWithDrawer(
|
|||
},
|
||||
onVoiceVolumeChange = { index, volume ->
|
||||
player?.updateVoiceVolume(index, volume)
|
||||
}
|
||||
},
|
||||
sharedScreenModel = sharedScreenModel
|
||||
)
|
||||
} else {
|
||||
Text("Sélectionner un morceau")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package mg.dot.feufaro.ui
|
||||
|
||||
import SharedScreenModel
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
|
|
@ -32,6 +33,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.russhwolf.settings.Settings
|
||||
import feufaro.composeapp.generated.resources.Res
|
||||
import feufaro.composeapp.generated.resources.ic_mixer_satb
|
||||
import feufaro.composeapp.generated.resources.ic_organ
|
||||
|
|
@ -40,6 +42,7 @@ import feufaro.composeapp.generated.resources.mixer_fader
|
|||
import kotlinx.coroutines.delay
|
||||
import mg.dot.feufaro.getPlatform
|
||||
import mg.dot.feufaro.midi.FMediaPlayer
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -55,6 +58,7 @@ fun MidiControlPanel(
|
|||
onVolumeChange: (Float) -> Unit,
|
||||
onVoiceVolumeChange: (voiceIndex: Int, newVolume: Float) -> Unit,
|
||||
mediaPlayer: FMediaPlayer,
|
||||
sharedScreenModel: SharedScreenModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val momo = duration.toInt() - currentPos.toInt()
|
||||
|
|
@ -71,8 +75,14 @@ fun MidiControlPanel(
|
|||
}
|
||||
val labels = listOf("S", "A", "T", "B")
|
||||
listOf("Soprano", "Alto", "Ténor", "Basse")
|
||||
val activeBpm by sharedScreenModel.getBpmFlow().collectAsState(initial = 120f)
|
||||
var tempo by remember { mutableStateOf(120f) }
|
||||
|
||||
var tempo by remember { mutableStateOf(120.toFloat()) }
|
||||
LaunchedEffect(activeBpm) {
|
||||
if (activeBpm > 0f) {
|
||||
tempo = activeBpm
|
||||
}
|
||||
}
|
||||
var currentBpm by remember { mutableStateOf(mediaPlayer.getCurrentBPM()) }
|
||||
|
||||
var isPianoSelected by remember { mutableStateOf(true) }
|
||||
|
|
@ -82,6 +92,7 @@ fun MidiControlPanel(
|
|||
val platform = getPlatform().name
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val prefs: Settings = provideSettings()
|
||||
LaunchedEffect(currentPos, duration, solfaScrollState.viewportSize, solfaScrollState.maxValue) {
|
||||
if (duration > 0f && solfaScrollState.maxValue > 0) {
|
||||
val progress = (currentPos / duration).coerceIn(0f, 1f)
|
||||
|
|
@ -99,9 +110,9 @@ fun MidiControlPanel(
|
|||
)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(tempo) {
|
||||
/*LaunchedEffect(tempo) {
|
||||
currentBpm = mediaPlayer.getCurrentBPM()
|
||||
}
|
||||
}*/
|
||||
fun updateTempoToBpm(newBpm: Int) {
|
||||
tempo = newBpm.toFloat()
|
||||
mediaPlayer?.setTempo(tempo)
|
||||
|
|
@ -319,7 +330,7 @@ fun MidiControlPanel(
|
|||
}
|
||||
}
|
||||
|
||||
if (tempo <= 160) { // limite 160BPM
|
||||
if (tempo <= 320) {
|
||||
IconButton(
|
||||
modifier = Modifier.background(Color(0XFF2C3130)),
|
||||
onClick = { updateTempoByBpmStep(10) }) {
|
||||
|
|
@ -436,11 +447,12 @@ fun MidiControlPanel(
|
|||
}
|
||||
}
|
||||
|
||||
val selectedInstru = prefs.getInt("voice_instrument", 1)
|
||||
val instrumentButton = @Composable {
|
||||
IconButton(
|
||||
onClick = {
|
||||
isPianoSelected = !isPianoSelected
|
||||
mediaPlayer?.changeInstru(if (isPianoSelected) 1 else 20)
|
||||
mediaPlayer?.changeInstru(if (isPianoSelected) selectedInstru else 16)
|
||||
}
|
||||
) {
|
||||
if (isPianoSelected) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.russhwolf.settings.Settings
|
||||
import feufaro.composeapp.generated.resources.Emmentaler
|
||||
import feufaro.composeapp.generated.resources.Res
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -36,18 +37,22 @@ import mg.dot.feufaro.DisplayConfigManager
|
|||
import mg.dot.feufaro.getPlatform
|
||||
import mg.dot.feufaro.midi.Dynamic
|
||||
import mg.dot.feufaro.midi.MidiInstrument
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||
import org.jetbrains.compose.resources.Font
|
||||
import org.jetbrains.compose.resources.FontResource
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun Settings(
|
||||
sharedScreenModel: SharedScreenModel,
|
||||
solfaScreenModel: SolfaScreenModel,
|
||||
onDismissRequest: () -> Unit,
|
||||
expandedDropdownN: Boolean,
|
||||
isAndroid: Boolean
|
||||
) {
|
||||
val prefs: Settings = provideSettings()
|
||||
var hasCountIn by remember { mutableStateOf(prefs.getBoolean("isCountIn", true)) }
|
||||
var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) }
|
||||
var refreshTrigger by remember { mutableStateOf(0) }
|
||||
val isFullScreenEnabled by sharedScreenModel.isFullScreen.collectAsState()
|
||||
|
|
@ -86,7 +91,7 @@ fun Settings(
|
|||
mutableStateOf(player?.getVoiceInstrument() ?: 1)
|
||||
}
|
||||
val selectedInstrument = instruments.find { it.program == globalInstrumentProgram }
|
||||
?: MidiInstrument(globalInstrumentProgram, "Instrument $globalInstrumentProgram")
|
||||
?: MidiInstrument(program = globalInstrumentProgram, name = "Instrument $globalInstrumentProgram")
|
||||
|
||||
val menuScrollState = rememberScrollState()
|
||||
var expandedDropdown by remember { mutableStateOf(false) }
|
||||
|
|
@ -368,17 +373,28 @@ fun Settings(
|
|||
) {
|
||||
Column(modifier = Modifier.padding(top = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Réglage de la nuance",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
Text(if(isDynamicEnabled) "Désactiver" else "Activer", fontWeight = FontWeight.Bold, fontSize = 15.sp)
|
||||
Switch(
|
||||
checked = isDynamicEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
|
|
@ -387,6 +403,11 @@ fun Settings(
|
|||
refreshTrigger++
|
||||
}
|
||||
)
|
||||
Text(
|
||||
if (isDynamicEnabled) "Désactiver" else "Activer",
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
|
|
@ -606,7 +627,7 @@ fun Settings(
|
|||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = "${selectedInstrument.name}",
|
||||
value = "${selectedInstrument.icon} - ${selectedInstrument.name}",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropdown) },
|
||||
|
|
@ -642,13 +663,13 @@ fun Settings(
|
|||
},
|
||||
onClick = {
|
||||
globalInstrumentProgram = instrument.program
|
||||
|
||||
player?.changeInstru(instrument.program)
|
||||
player?.saveVoiceInstrument(instrument.program)
|
||||
sharedScreenModel.saveVoiceInstrument(instrument.program)
|
||||
solfaScreenModel.regenMidi(120.0f)
|
||||
sharedScreenModel.loadNewSong("whawyd3.mid")
|
||||
expandedDropdown = false
|
||||
},
|
||||
leadingIcon = {
|
||||
Text("🎹")
|
||||
Text(instrument.icon)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (isSelected) {
|
||||
|
|
@ -672,6 +693,67 @@ fun Settings(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Règlage du player",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Décompte avant la lecture ",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
if(!isAndroid) {
|
||||
Text(
|
||||
if (hasCountIn) "Désactiver" else "Activer",
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = hasCountIn,
|
||||
onCheckedChange = { enabled ->
|
||||
hasCountIn = enabled
|
||||
prefs.putBoolean("isCountIn", enabled)
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline
|
||||
),
|
||||
modifier = Modifier.size(45.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -22,6 +24,7 @@ import mg.dot.feufaro.data.GridTUOData
|
|||
import mg.dot.feufaro.data.getCombinedList
|
||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||
import mg.dot.feufaro.midi.FMediaPlayer
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import mg.dot.feufaro.solfa.TUOEditState
|
||||
import mg.dot.feufaro.solfa.getAllMarker
|
||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||
|
|
@ -72,6 +75,17 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
private val _drawerItems = MutableStateFlow<List<DrawerItem>>(emptyList())
|
||||
val drawerItems: StateFlow<List<DrawerItem>> = _drawerItems.asStateFlow()
|
||||
|
||||
private val prefs: Settings = provideSettings()
|
||||
|
||||
private val _currentBpmFlow = MutableStateFlow(120f)
|
||||
val currentBpmFlow: StateFlow<Float> = _currentBpmFlow.asStateFlow()
|
||||
|
||||
fun setBpmFlow(newBpm: Float) {
|
||||
_currentBpmFlow.value = newBpm
|
||||
}
|
||||
fun getBpmFlow(): StateFlow<Float> {
|
||||
return currentBpmFlow
|
||||
}
|
||||
val internalItems: StateFlow<List<DrawerItem>> = _drawerItems
|
||||
.map { list -> list.filter { it.path.startsWith("assets://") } }
|
||||
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
|
@ -845,4 +859,11 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
return _playlist.value[playlistIndex]
|
||||
}
|
||||
|
||||
fun getVoiceInstrument(): Int {
|
||||
val defaultProgram = 0
|
||||
return prefs.getInt("voice_instrument", defaultProgram)
|
||||
}
|
||||
fun saveVoiceInstrument(program: Int) {
|
||||
prefs.putInt("voice_instrument", program)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,4 +55,7 @@ class SolfaScreenModel(
|
|||
solfa.createNewSolfa(metadata)
|
||||
}
|
||||
}
|
||||
fun regenMidi(bpm: Float) {
|
||||
solfa.generateMidiFile(bpm)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
import com.russhwolf.settings.PreferencesSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import java.util.prefs.Preferences
|
||||
|
||||
actual fun provideSettings(): Settings {
|
||||
val javaPrefs = Preferences.userRoot().node("mg.dot.feufaro")
|
||||
return PreferencesSettings(javaPrefs)
|
||||
}
|
||||
|
|
@ -1,18 +1,12 @@
|
|||
package mg.dot.feufaro.midi
|
||||
|
||||
import SharedScreenModel
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.*
|
||||
import mg.dot.feufaro.getConfigDirectoryPath
|
||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import mg.dot.feufaro.solfa.Transpose
|
||||
import java.io.File
|
||||
import java.util.prefs.Preferences
|
||||
import javax.sound.midi.MetaMessage
|
||||
import javax.sound.midi.MidiSystem
|
||||
import javax.sound.midi.Sequence
|
||||
import javax.sound.midi.Sequencer
|
||||
import javax.sound.midi.ShortMessage
|
||||
import javax.sound.midi.Synthesizer
|
||||
import javax.sound.midi.Track
|
||||
import javax.sound.midi.*
|
||||
import javax.sound.sampled.AudioSystem
|
||||
import javax.sound.sampled.FloatControl
|
||||
|
||||
|
|
@ -29,7 +23,7 @@ actual class FMediaPlayer actual constructor(
|
|||
null
|
||||
}
|
||||
|
||||
private val prefs = Preferences.userRoot().node("mg.dot.feufaro")
|
||||
private val prefs: Settings = provideSettings()
|
||||
private var synthetizer = MidiSystem.getSynthesizer() as Synthesizer?
|
||||
|
||||
private var pointA: Long = -1L
|
||||
|
|
@ -41,8 +35,8 @@ actual class FMediaPlayer actual constructor(
|
|||
private var currentDynamicVelocity: Int = Dynamic.MF.velocity
|
||||
private var currentDynamicFactor: Float = Dynamic.MF.factor
|
||||
|
||||
private var currentTempo: Float = 1.0f
|
||||
private var targetBpm: Float = 120f
|
||||
private var initialBpm: Float = 120f
|
||||
private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
private var abJob: Job? = null
|
||||
|
||||
|
|
@ -69,7 +63,9 @@ actual class FMediaPlayer actual constructor(
|
|||
val isTempoChange: Boolean = false,
|
||||
val tempoType: String = "",
|
||||
val endGridForTempo: Int = -1,
|
||||
val targetTempoMultiplier: Float = 0.6f
|
||||
val targetTempoMultiplier: Float = 0.6f,
|
||||
val newBpm: Float? = null,
|
||||
val newKey: String? = null
|
||||
|
||||
)
|
||||
private val navigationSteps = mutableListOf<NavigationStep>()
|
||||
|
|
@ -254,6 +250,8 @@ actual class FMediaPlayer actual constructor(
|
|||
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 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 currentIndex = midiMarker.gridIndex ?: 0
|
||||
|
|
@ -278,6 +276,37 @@ actual class FMediaPlayer actual constructor(
|
|||
val hasFin = finReg.containsMatchIn(mTrim) || finReg.containsMatchIn(nextTrim)
|
||||
val isDcFin = hasDc && hasFin
|
||||
val isDsFin = hasDs && 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 = currentIndex,
|
||||
newBpm = bpmVal
|
||||
)
|
||||
)
|
||||
println("Changement de BPM mémorisé à la grille $currentIndex -> $bpmVal BPM")
|
||||
}
|
||||
}
|
||||
|
||||
// ModulationDo dia C
|
||||
val modMatch = modulaRegex.find(mTrim)
|
||||
if (modMatch != null) {
|
||||
val targetKey = modMatch.groupValues[2]
|
||||
navigationSteps.add(
|
||||
NavigationStep(
|
||||
marker = mTrim,
|
||||
gridIndex = currentIndex,
|
||||
newKey = targetKey
|
||||
)
|
||||
)
|
||||
println("Modulation mémorisée à la grille $currentIndex -> $targetKey")
|
||||
}
|
||||
|
||||
when {
|
||||
//DSFin
|
||||
isDsFin || isDcFin -> {
|
||||
|
|
@ -529,11 +558,46 @@ actual class FMediaPlayer actual constructor(
|
|||
val currentSteps = navigationSteps.filter {
|
||||
it.gridIndex == currentIndex && !it.alreadyDone
|
||||
}
|
||||
currentSteps.forEach { step ->
|
||||
for (step in currentSteps) {
|
||||
if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) {
|
||||
forceTempo(targetBpm.toDouble())
|
||||
}
|
||||
when {
|
||||
//♩
|
||||
step.newBpm != null -> {
|
||||
step.alreadyDone = true
|
||||
println("Exécution changement de tempo : ${step.newBpm} BPM à la grille $currentIndex")
|
||||
targetBpm = step.newBpm
|
||||
forceTempo(targetBpm.toDouble())
|
||||
sharedScreenModel.setBpmFlow(step.newBpm)
|
||||
}
|
||||
|
||||
// Modulation
|
||||
step.newKey != null -> {
|
||||
step.alreadyDone = true
|
||||
//println("Exécution modulation vers tonalité ${step.newKey} à la grille $currentIndex")
|
||||
val baseKeyIdx = Transpose.keyToNumber.indexOf("C").coerceAtLeast(0)
|
||||
val targetKeyIdx = Transpose.keyToNumber.indexOf(step.newKey).coerceAtLeast(0)
|
||||
val semitonesShift = targetKeyIdx - baseKeyIdx
|
||||
|
||||
val pitchBendValue = (8192 + (semitonesShift * 2048)).coerceIn(0, 16383)
|
||||
|
||||
try {
|
||||
val synth = synthetizer
|
||||
if (synth != null) {
|
||||
val channels = synth.channels
|
||||
if (channels != null) {
|
||||
val count = if (channels.size < 4) channels.size else 4
|
||||
for (i in 0 until count) {
|
||||
val channel: MidiChannel? = channels[i]
|
||||
channel?.setPitchBend(pitchBendValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
step.isFin -> {
|
||||
if(step.finActive) {
|
||||
step.alreadyDone = true
|
||||
|
|
@ -658,13 +722,24 @@ actual class FMediaPlayer actual constructor(
|
|||
val tick = (gridIndex.toDouble() * resolution).toLong()
|
||||
sequencer?.tickPosition = tick
|
||||
|
||||
val initialOrPreviousBpm = navigationSteps
|
||||
.filter { it.newBpm != null && it.gridIndex <= gridIndex }
|
||||
.maxByOrNull { it.gridIndex }?.newBpm ?: 120f
|
||||
|
||||
targetBpm = initialOrPreviousBpm
|
||||
applyBpm()
|
||||
sharedScreenModel.setBpmFlow(initialOrPreviousBpm)
|
||||
// Voir les velocity avant
|
||||
val lastDynamic = navigationSteps
|
||||
.filter { it.dynamic != null && it.gridIndex <= gridIndex }
|
||||
.maxByOrNull { it.gridIndex }
|
||||
?.dynamic ?: Dynamic.MF
|
||||
currentDynamicFactor = lastDynamic.factor
|
||||
navigationSteps.forEach { step ->
|
||||
if (step.gridIndex > gridIndex) {
|
||||
step.alreadyDone = false
|
||||
}
|
||||
}
|
||||
applyVoiceStates()
|
||||
}
|
||||
|
||||
|
|
@ -676,6 +751,13 @@ actual class FMediaPlayer actual constructor(
|
|||
if (sequencer?.isOpen == true) {
|
||||
val currentTick = sequencer?.tickPosition ?: 0L
|
||||
if (currentTick == 0L) {
|
||||
resetNavigationFlags()
|
||||
targetBpm = initialBpm
|
||||
forceTempo(initialBpm.toDouble())
|
||||
boundModel?.setBpmFlow(initialBpm)
|
||||
}
|
||||
val countInEnable = prefs.getBoolean("isCountIn", true)
|
||||
if (currentTick == 0L && countInEnable) {
|
||||
playCountIn()
|
||||
} else {
|
||||
startMidiPlayback()
|
||||
|
|
@ -805,6 +887,9 @@ actual class FMediaPlayer actual constructor(
|
|||
navigationJob?.cancel()
|
||||
resetNavigationFlags()
|
||||
navigationSteps.clear()
|
||||
targetBpm = initialBpm
|
||||
sequencer?.tempoInBPM = initialBpm
|
||||
boundModel?.setBpmFlow(initialBpm)
|
||||
clearLoop()
|
||||
currentDynamicFactor = Dynamic.MF.factor
|
||||
resetTempoToNormal()
|
||||
|
|
@ -929,12 +1014,14 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
actual fun setTempo(bpm: Float){
|
||||
this.targetBpm = bpm
|
||||
this.initialBpm = bpm
|
||||
sequencer?.tempoInBPM = bpm
|
||||
boundModel?.let { modele ->
|
||||
val seq = sequencer?.sequence
|
||||
if (seq != null) {
|
||||
syncTuoWithMidi(seq, modele)
|
||||
}
|
||||
modele.setBpmFlow(bpm)
|
||||
prepareNavigation(modele)
|
||||
}
|
||||
println("Tempo réglé à : $bpm BPM")
|
||||
|
|
@ -947,18 +1034,18 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
synthesizer.availableInstruments.map { instrument ->
|
||||
MidiInstrument(
|
||||
icon = "\uD83C\uDFB9",
|
||||
program = instrument.patch.program,
|
||||
name = instrument.name.trim()
|
||||
)
|
||||
}.sortedBy { it.program }
|
||||
} catch (e: Exception) {
|
||||
listOf(MidiInstrument(0, "Acoustic Grand Piano"), MidiInstrument(19, "Church Organ"))
|
||||
listOf(MidiInstrument("🎹", 0, "Acoustic Grand Piano"), MidiInstrument("🎹", 19, "Church Organ"))
|
||||
}
|
||||
}
|
||||
|
||||
actual fun saveVoiceInstrument(program: Int) {
|
||||
prefs.putInt("voice_instrument", program)
|
||||
try { prefs.flush() } catch (e: Exception) { e.printStackTrace() }
|
||||
}
|
||||
|
||||
actual fun getVoiceInstrument(): Int {
|
||||
|
|
@ -976,11 +1063,11 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
private fun saveVoicesVolumes() {
|
||||
val data = voiceVolumes.joinToString(",")
|
||||
prefs.put("voices_volumes", data)
|
||||
prefs.putString("voices_volumes", data)
|
||||
}
|
||||
|
||||
private fun loadVoiceVolumes() {
|
||||
val data = prefs.get("voices_volumes", "127,127,127,127")
|
||||
val data = prefs.getString("voices_volumes", "127,127,127,127")
|
||||
val volumesArray = data.split(",")
|
||||
|
||||
if (volumesArray.size == 4) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue