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 SharedScreenModel
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import mg.dot.feufaro.provideSettings
|
||||||
import org.billthefarmer.mididriver.MidiDriver
|
import org.billthefarmer.mididriver.MidiDriver
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
@ -76,7 +77,16 @@ actual class FMediaPlayer actual constructor(
|
||||||
val mt=readByte();
|
val mt=readByte();
|
||||||
val len=readVarLen().toInt();
|
val len=readVarLen().toInt();
|
||||||
val md=ByteArray(len){data[pos++]};
|
val md=ByteArray(len){data[pos++]};
|
||||||
out.add(MidiEvent(tick,0xFF,0,0,0,mt,md))
|
|
||||||
|
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()) {
|
status == 0xF0 || status == 0xF7 -> repeat(readVarLen().toInt()) {
|
||||||
pos++
|
pos++
|
||||||
|
|
@ -164,16 +174,25 @@ actual class FMediaPlayer actual constructor(
|
||||||
val targetTempoMultiplier: Float = 0.6f
|
val targetTempoMultiplier: Float = 0.6f
|
||||||
)
|
)
|
||||||
private val navigationSteps = mutableListOf<NavigationStep>()
|
private val navigationSteps = mutableListOf<NavigationStep>()
|
||||||
|
private val prefs: Settings = provideSettings()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
midiDriver.start()
|
midiDriver.start()
|
||||||
loadVoiceVolumes()
|
loadVoiceVolumes()
|
||||||
loadSavedInstrumentsToPlayer(this)
|
|
||||||
val file = File(filename)
|
val file = File(filename)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
try {
|
try {
|
||||||
sequence = MidiParser.parse(file)
|
sequence = MidiParser.parse(file)
|
||||||
resolution = sequence!!.resolution
|
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
|
usPerTick = (60_000_000.0 / targetBpm) / resolution
|
||||||
} catch (e: Exception) { e.printStackTrace() }
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
}
|
}
|
||||||
|
|
@ -277,28 +296,37 @@ actual class FMediaPlayer actual constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlaybackLoop() {
|
private fun startPlaybackLoop() {
|
||||||
val seq = sequence ?: return
|
val seq = sequence ?: run {
|
||||||
|
return
|
||||||
|
}
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
needsClockSync = true
|
||||||
|
|
||||||
playJob?.cancel()
|
playJob?.cancel()
|
||||||
playJob = playerScope.launch(Dispatchers.Default) {
|
playJob = playerScope.launch(Dispatchers.Default) {
|
||||||
val events = seq.events
|
val events = seq.events
|
||||||
var idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
|
||||||
.takeIf { it >= 0 } ?: events.size
|
|
||||||
var clockNano = System.nanoTime()
|
var clockNano = System.nanoTime()
|
||||||
var clockTick = currentTickPos
|
var clockTick = currentTickPos
|
||||||
|
var idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||||
|
.takeIf { it >= 0 } ?: events.size
|
||||||
|
|
||||||
while (isActive && isRunning) {
|
while (isActive && isRunning) {
|
||||||
if (needsClockSync) {
|
if (needsClockSync) {
|
||||||
clockNano = System.nanoTime()
|
clockNano = System.nanoTime()
|
||||||
clockTick = currentTickPos
|
clockTick = currentTickPos
|
||||||
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
|
||||||
|
val targetIdx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||||
.takeIf { it >= 0 } ?: events.size
|
.takeIf { it >= 0 } ?: events.size
|
||||||
|
if (targetIdx > idx) {
|
||||||
|
idx = targetIdx
|
||||||
|
}
|
||||||
|
|
||||||
needsClockSync = false
|
needsClockSync = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHolding) {
|
if (isHolding) {
|
||||||
yield()
|
yield()
|
||||||
// delay(10)
|
|
||||||
clockNano = System.nanoTime()
|
clockNano = System.nanoTime()
|
||||||
clockTick = currentTickPos
|
clockTick = currentTickPos
|
||||||
continue
|
continue
|
||||||
|
|
@ -309,7 +337,8 @@ actual class FMediaPlayer actual constructor(
|
||||||
if (dc != null) {
|
if (dc != null) {
|
||||||
allNotesOff()
|
allNotesOff()
|
||||||
seekToGrid(dc.targetGrid)
|
seekToGrid(dc.targetGrid)
|
||||||
clockNano = System.nanoTime(); clockTick = currentTickPos
|
clockNano = System.nanoTime()
|
||||||
|
clockTick = currentTickPos
|
||||||
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||||
.takeIf { it >= 0 } ?: events.size
|
.takeIf { it >= 0 } ?: events.size
|
||||||
continue
|
continue
|
||||||
|
|
@ -322,16 +351,11 @@ actual class FMediaPlayer actual constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val ev = events[idx]
|
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 targetNano = clockNano + ((ev.tickAbsolute - clockTick) * usPerTick * 1000).toLong()
|
||||||
val now = System.nanoTime()
|
val now = System.nanoTime()
|
||||||
val waitNano = targetNano - now
|
val waitNano = targetNano - now
|
||||||
|
|
||||||
// if (waitNano > 0) delay((waitNano / 1_000_000).coerceAtLeast(0L))
|
if (waitNano > 0 && waitNano < 3_000_000_000L) {
|
||||||
if (waitNano > 0) {
|
|
||||||
// Thread.sleep est beaucoup plus précis que delay() pour les micro-délais MIDI
|
|
||||||
val ms = waitNano / 1_000_000
|
val ms = waitNano / 1_000_000
|
||||||
val ns = (waitNano % 1_000_000).toInt()
|
val ns = (waitNano % 1_000_000).toInt()
|
||||||
try {
|
try {
|
||||||
|
|
@ -339,9 +363,11 @@ actual class FMediaPlayer actual constructor(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
yield()
|
yield()
|
||||||
}
|
}
|
||||||
}
|
} else if (waitNano < -100_000_000L) {
|
||||||
if (needsClockSync) {
|
clockNano = System.nanoTime()
|
||||||
continue
|
clockTick = ev.tickAbsolute
|
||||||
|
} else {
|
||||||
|
yield()
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTickPos = ev.tickAbsolute
|
currentTickPos = ev.tickAbsolute
|
||||||
|
|
@ -350,8 +376,10 @@ actual class FMediaPlayer actual constructor(
|
||||||
// A-B loop
|
// A-B loop
|
||||||
if (isLoopingAB && pointA >= 0 && pointB > pointA &&
|
if (isLoopingAB && pointA >= 0 && pointB > pointA &&
|
||||||
ticksToMs(currentTickPos) >= pointB) {
|
ticksToMs(currentTickPos) >= pointB) {
|
||||||
allNotesOff(); currentTickPos = msToTicks(pointA)
|
allNotesOff()
|
||||||
clockNano = System.nanoTime(); clockTick = currentTickPos
|
currentTickPos = msToTicks(pointA)
|
||||||
|
clockNano = System.nanoTime()
|
||||||
|
clockTick = currentTickPos
|
||||||
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
idx = events.indexOfFirst { it.tickAbsolute >= currentTickPos }
|
||||||
.takeIf { it >= 0 } ?: events.size
|
.takeIf { it >= 0 } ?: events.size
|
||||||
continue
|
continue
|
||||||
|
|
@ -364,9 +392,21 @@ actual class FMediaPlayer actual constructor(
|
||||||
0xB0 -> if (ev.data1 != 7 && ev.data1 != 11)
|
0xB0 -> if (ev.data1 != 7 && ev.data1 != 11)
|
||||||
send(0xB0 or ev.channel, ev.data1, ev.data2)
|
send(0xB0 or ev.channel, ev.data1, ev.data2)
|
||||||
0xC0 -> send(0xC0 or ev.channel, ev.data1)
|
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()) {
|
if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
|
||||||
boundModel?.updateActiveIndex(currentGrid)
|
boundModel?.updateActiveIndex(currentGrid)
|
||||||
}
|
}
|
||||||
|
|
@ -704,7 +744,10 @@ actual class FMediaPlayer actual constructor(
|
||||||
}
|
}
|
||||||
actual fun play() {
|
actual fun play() {
|
||||||
if (sequence == null) return
|
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()
|
playCountIn()
|
||||||
} else {
|
} else {
|
||||||
applyVoiceStates()
|
applyVoiceStates()
|
||||||
|
|
@ -778,6 +821,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
applyVoiceStates()
|
applyVoiceStates()
|
||||||
|
needsClockSync = true
|
||||||
startPlaybackLoop()
|
startPlaybackLoop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -812,9 +856,11 @@ actual class FMediaPlayer actual constructor(
|
||||||
applyVoiceStates()
|
applyVoiceStates()
|
||||||
}
|
}
|
||||||
actual fun setTempo(bpm: Float) {
|
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) }
|
boundModel?.let { prepareNavigation(it) }
|
||||||
println("Tempo → $bpm BPM")
|
needsClockSync = true
|
||||||
}
|
}
|
||||||
actual fun getCurrentBPM(): Float = targetBpm
|
actual fun getCurrentBPM(): Float = targetBpm
|
||||||
actual fun requestSync(sharedScreenModel: SharedScreenModel) {
|
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)
|
for (ch in 0 until 4) send(0xC0 or ch, noInstru)
|
||||||
}
|
}
|
||||||
actual fun getAvalaibleInstruments(): List<MidiInstrument> {
|
actual fun getAvalaibleInstruments(): List<MidiInstrument> {
|
||||||
val gmInstruments = listOf(
|
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")
|
||||||
"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(
|
MidiInstrument(
|
||||||
|
icon = icon,
|
||||||
program = index,
|
program = index,
|
||||||
name = name
|
name = name
|
||||||
)
|
)
|
||||||
|
|
@ -885,9 +933,4 @@ actual class FMediaPlayer actual constructor(
|
||||||
val defaultProgram = 0
|
val defaultProgram = 0
|
||||||
return settings.getInt("voice_instrument", defaultProgram)
|
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
|
package mg.dot.feufaro.midi
|
||||||
|
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
|
import mg.dot.feufaro.provideSettings
|
||||||
import java.util.prefs.Preferences
|
import java.util.prefs.Preferences
|
||||||
|
|
||||||
enum class Dynamic(val velocity: Int, val label: String, val defaultFactor: Float) {
|
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),
|
MF (80, "mf", 1.00f),
|
||||||
F (96, "f", 1.15f),
|
F (96, "f", 1.15f),
|
||||||
FF (112, "ff", 1.35f),
|
FF (112, "ff", 1.35f),
|
||||||
FFF(126, "fff", 1.60f);
|
FFF(127, "fff", 1.60f);
|
||||||
private val prefs = Preferences.userRoot().node("mg.dot.feufaro")
|
private val prefs: Settings = provideSettings()
|
||||||
var savedFactor: Float = prefs.getFloat("dynamic_${name.lowercase()}", defaultFactor)
|
var savedFactor: Float = prefs.getFloat("dynamic_${name.lowercase()}", defaultFactor)
|
||||||
set(value) {
|
set(value) {
|
||||||
field = 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
|
get() = if (isGloballyEnabled) savedFactor else 1.00f
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val globalPrefs = Preferences.userRoot().node("mg.dot.feufaro")
|
private val prefs: Settings = provideSettings()
|
||||||
var isGloballyEnabled: Boolean = globalPrefs.getBoolean("dynamics_enabled", true)
|
var isGloballyEnabled: Boolean = prefs.getBoolean("dynamics_enabled", true)
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
globalPrefs.putBoolean("dynamics_enabled", value)
|
prefs.putBoolean("dynamics_enabled", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromVelocity(v: Int): Dynamic =
|
fun fromVelocity(v: Int): Dynamic =
|
||||||
|
|
@ -35,5 +37,9 @@ enum class Dynamic(val velocity: Int, val label: String, val defaultFactor: Floa
|
||||||
fun resetToDefaults() {
|
fun resetToDefaults() {
|
||||||
entries.forEach { it.savedFactor = it.defaultFactor }
|
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 duration: Int = 0,
|
||||||
var markers: List<String> = listOf(),
|
var markers: List<String> = listOf(),
|
||||||
var tick : Int = 0,
|
var tick : Int = 0,
|
||||||
var metaBytes: String = "",
|
var metaBytes: ByteArray = byteArrayOf(),
|
||||||
var metaByteSize: Int = 0,
|
var metaByteSize: Int = 0,
|
||||||
var metaType : Int = -1
|
var metaType : Int = -1
|
||||||
) {
|
) {
|
||||||
|
|
@ -22,8 +22,11 @@ data class MidiPitch (
|
||||||
var curVoiceNumber: Int = 0
|
var curVoiceNumber: Int = 0
|
||||||
var nextTick : MutableList<Int> = mutableListOf()
|
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) {
|
fun setMeta(type: Int, tickMeta: Int, nb: Int, vararg data: Int) {
|
||||||
metaBytes = ""+ data0.toChar() + data1.toChar() + data2.toChar() + data3.toChar() + data4.toChar()
|
val bytes = ByteArray(nb) { i ->
|
||||||
|
(data.getOrElse(i) { 0 } and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
metaBytes = bytes
|
||||||
tick = tickMeta
|
tick = tickMeta
|
||||||
metaByteSize = nb
|
metaByteSize = nb
|
||||||
metaType = type
|
metaType = type
|
||||||
|
|
@ -55,30 +58,33 @@ data class MidiPitch (
|
||||||
nextTick.add(0)
|
nextTick.add(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun initKey(theKey: String) {
|
fun initKey(theKey: String, isMinor: Boolean = false) {
|
||||||
key = theKey
|
key = theKey
|
||||||
val armature = Transpose.toArmature(key)
|
val armature = Transpose.toArmature(theKey)
|
||||||
|
|
||||||
val tickMeta = nextTick.getOrNull(0) ?: 0
|
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 {
|
fun initMeasure(theMeasure: String): Boolean {
|
||||||
val numerator = theMeasure.replace(Regex("/.*"), "").toIntOrNull()
|
val parts = theMeasure.split("/")
|
||||||
val denominator = theMeasure.replace(Regex("^[^/]*/(\\d+).*$"), "$1").toIntOrNull() ?: 4
|
val numerator = parts.getOrNull(0)?.toIntOrNull() ?: return false
|
||||||
val data1 = when (denominator) {
|
val denominator = parts.getOrNull(1)?.toIntOrNull() ?: 4
|
||||||
|
|
||||||
|
val denomPower = when (denominator) {
|
||||||
1 -> 0
|
1 -> 0
|
||||||
2 -> 1
|
2 -> 1
|
||||||
4 -> 2
|
4 -> 2
|
||||||
8 -> 3
|
8 -> 3
|
||||||
16 -> 4
|
16 -> 4
|
||||||
else -> 5
|
32 -> 5
|
||||||
|
else -> 2
|
||||||
}
|
}
|
||||||
val blankDuration = (4 * (numerator ?: 4) - ParseULine.blankDuration) * 15
|
val tickMeta = nextTick.getOrNull(0) ?: 0
|
||||||
if (numerator != null) {
|
setMeta(0x58, tickMeta, 4, numerator, denomPower, 24, 8)
|
||||||
setMeta(0x58, blankDuration, 4, numerator, data1, 60, 20)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
tick = 0
|
tick = 0
|
||||||
nextTick.clear()
|
nextTick.clear()
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,4 @@ expect class FMediaPlayer(filename: String, sharedScreenModel: SharedScreenModel
|
||||||
fun getVoiceInstrument(): Int
|
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) {
|
fun addSequence(channel: Int, pitch: Int, currentTick: Long, type: Int = 80, finalVelocity: Int = 100) {
|
||||||
val myTrack = tracks[0]
|
val myTrack = tracks[0]
|
||||||
|
if (type == 0xC0) {
|
||||||
|
myTrack.addProgramChange(channel, pitch, currentTick)
|
||||||
|
} else {
|
||||||
myTrack.addNote(channel, pitch, currentTick, type, finalVelocity)
|
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) {
|
fun MutableList<Byte>.addNote(channel: Int, pitch: Int, currentTick: Long, type: Int = 80, finalVelocity: Int = 100) {
|
||||||
// Calcul du Delta-Time
|
// Calcul du Delta-Time
|
||||||
val delta = currentTick - lastTick
|
val delta = currentTick - lastTick
|
||||||
|
|
@ -127,4 +146,20 @@ class MidiSequence(val resolution: Int = 60) {
|
||||||
outTrack.addAll(data.toList().take(nbData))
|
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
|
tick = 0
|
||||||
pitches.forEach {
|
pitches.forEach {
|
||||||
if (it.metaType > 0) {
|
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 != "") {
|
} else if (it.pitch != "") {
|
||||||
addNote(it.voiceNumber, it.pitch.toInt(), 100, it.tick.toLong())
|
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
|
package mg.dot.feufaro.solfa
|
||||||
|
|
||||||
import SharedScreenModel
|
import SharedScreenModel
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import mg.dot.feufaro.FileRepository
|
import mg.dot.feufaro.FileRepository
|
||||||
import mg.dot.feufaro.SaveSettings
|
import mg.dot.feufaro.SaveSettings
|
||||||
import mg.dot.feufaro.getGlobalTemplate
|
import mg.dot.feufaro.getGlobalTemplate
|
||||||
|
import mg.dot.feufaro.getPlatform
|
||||||
import mg.dot.feufaro.launchFilePicker
|
import mg.dot.feufaro.launchFilePicker
|
||||||
import mg.dot.feufaro.midi.MidiPitch
|
import mg.dot.feufaro.midi.MidiPitch
|
||||||
import mg.dot.feufaro.midi.MidiWriterKotlin
|
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.viewmodel.PartitionMetadata
|
||||||
import mg.dot.feufaro.transformLyricsInput
|
import mg.dot.feufaro.transformLyricsInput
|
||||||
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.prefs.Preferences
|
||||||
|
import kotlin.getValue
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
|
//@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)
|
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) {
|
fun justBuild(sourceFile: String, sourceContent: String) {
|
||||||
currentFile = sourceFile
|
currentFile = sourceFile
|
||||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
|
@ -1987,12 +2013,17 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
val z = midiPitch.copy()
|
val z = midiPitch.copy()
|
||||||
if (z.tick != 0) {
|
if (z.tick != 0) {
|
||||||
val tickMeta = midiPitch.tick
|
val tickMeta = midiPitch.tick
|
||||||
val numerator = midiPitch.metaBytes.toByteArray()[0].toInt()
|
val numerator = (midiPitch.metaBytes.getOrNull(0)?.toInt() ?: 4) and 0xFF
|
||||||
val denominatorPower = midiPitch.metaBytes.toByteArray()[1].toInt()
|
val denominatorPower = (midiPitch.metaBytes.getOrNull(1)?.toInt() ?: 2) and 0xFF
|
||||||
val denominator = 1 shl denominatorPower
|
val denominator = 1 shl denominatorPower
|
||||||
z.tick = 0
|
z.tick = 0
|
||||||
val newNumerator = tickMeta / 15 / denominator
|
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)
|
pitches.add(z)
|
||||||
}
|
}
|
||||||
|
|
@ -2005,7 +2036,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
} else if (typeBlock == "meta") {
|
} else if (typeBlock == "meta") {
|
||||||
pitches.add(midiPitch.copy())
|
pitches.add(midiPitch.copy())
|
||||||
}
|
}
|
||||||
midiPitch.metaBytes = ""
|
midiPitch.metaBytes = byteArrayOf()
|
||||||
midiPitch.metaType = -1
|
midiPitch.metaType = -1
|
||||||
midiPitch.duration = 0
|
midiPitch.duration = 0
|
||||||
lastNoteString = ""
|
lastNoteString = ""
|
||||||
|
|
@ -2015,7 +2046,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
|
|
||||||
midiPitch.currentVoiceNumber(voiceNumber)
|
midiPitch.currentVoiceNumber(voiceNumber)
|
||||||
if (voiceNumber == 1) {
|
if (voiceNumber == 1) {
|
||||||
midiPitch.initKey(meta["C"] ?: "C")
|
midiPitch.initKey(meta["C"] ?: "C", meta["C"]?.endsWith("m") ?: false)
|
||||||
pushMidi("meta")
|
pushMidi("meta")
|
||||||
if (midiPitch.initMeasure(meta["m"] ?: "4/4")) {
|
if (midiPitch.initMeasure(meta["m"] ?: "4/4")) {
|
||||||
pushMidi("measure")
|
pushMidi("measure")
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class Transpose {
|
||||||
val octaveSigns = listOf("₄", "₃", "₂", "₁", "", "¹", "²", "³", "⁴")
|
val octaveSigns = listOf("₄", "₃", "₂", "₁", "", "¹", "²", "³", "⁴")
|
||||||
val noteToNumber = listOf("d", "di", "r", "ri", "m", "f", "fi", "s", "si", "l", "ta", "t")
|
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 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 {
|
fun transposeText(text: String, interval: Int): String {
|
||||||
if (text.isEmpty()) return text
|
if (text.isEmpty()) return text
|
||||||
|
|
||||||
|
|
@ -100,9 +100,51 @@ class Transpose {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toArmature(note: String): Int {
|
fun toArmature(note: String): Int {
|
||||||
val index = keyToNumber.indexOf(note)
|
val cleanKey = note.trim()
|
||||||
return keyToArmature.getOrNull(index) ?: 0
|
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> {
|
fun transposeMidi(note: String, fromKey: String) : Pair<String, Int> {
|
||||||
return Pair(note, 4)
|
return Pair(note, 4)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ fun MainScreenWithDrawer(
|
||||||
if (showSettingsDialog) {
|
if (showSettingsDialog) {
|
||||||
Settings(
|
Settings(
|
||||||
sharedScreenModel = sharedScreenModel,
|
sharedScreenModel = sharedScreenModel,
|
||||||
|
solfaScreenModel,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showSettingsDialog = false
|
showSettingsDialog = false
|
||||||
},
|
},
|
||||||
|
|
@ -448,8 +449,10 @@ fun MainScreenWithDrawer(
|
||||||
}, actions = {
|
}, actions = {
|
||||||
var tempInterval by remember(fileContent) { mutableStateOf(0) }
|
var tempInterval by remember(fileContent) { mutableStateOf(0) }
|
||||||
var isEyeVisible by remember { mutableStateOf(false) }
|
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 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 rawKeyIndex = (songKeyIndex + tempInterval) % 12
|
||||||
val tempUiKey = keysOrder[if (rawKeyIndex < 0) rawKeyIndex + 12 else rawKeyIndex]
|
val tempUiKey = keysOrder[if (rawKeyIndex < 0) rawKeyIndex + 12 else rawKeyIndex]
|
||||||
val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState()
|
val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState()
|
||||||
|
|
@ -465,7 +468,7 @@ fun MainScreenWithDrawer(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = tempUiKey,
|
text = if(isMinor) "${tempUiKey}m" else tempUiKey,
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
fontWeight = FontWeight.Black,
|
fontWeight = FontWeight.Black,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
@ -477,7 +480,7 @@ fun MainScreenWithDrawer(
|
||||||
) {
|
) {
|
||||||
isEyeVisible = !isEyeVisible
|
isEyeVisible = !isEyeVisible
|
||||||
}
|
}
|
||||||
.width(45.dp)
|
.width(65.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -809,7 +812,8 @@ fun MainScreenWithDrawer(
|
||||||
},
|
},
|
||||||
onVoiceVolumeChange = { index, volume ->
|
onVoiceVolumeChange = { index, volume ->
|
||||||
player?.updateVoiceVolume(index, volume)
|
player?.updateVoiceVolume(index, volume)
|
||||||
}
|
},
|
||||||
|
sharedScreenModel = sharedScreenModel
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text("Sélectionner un morceau")
|
Text("Sélectionner un morceau")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package mg.dot.feufaro.ui
|
package mg.dot.feufaro.ui
|
||||||
|
|
||||||
|
import SharedScreenModel
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.tween
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
import feufaro.composeapp.generated.resources.Res
|
import feufaro.composeapp.generated.resources.Res
|
||||||
import feufaro.composeapp.generated.resources.ic_mixer_satb
|
import feufaro.composeapp.generated.resources.ic_mixer_satb
|
||||||
import feufaro.composeapp.generated.resources.ic_organ
|
import feufaro.composeapp.generated.resources.ic_organ
|
||||||
|
|
@ -40,6 +42,7 @@ import feufaro.composeapp.generated.resources.mixer_fader
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import mg.dot.feufaro.getPlatform
|
import mg.dot.feufaro.getPlatform
|
||||||
import mg.dot.feufaro.midi.FMediaPlayer
|
import mg.dot.feufaro.midi.FMediaPlayer
|
||||||
|
import mg.dot.feufaro.provideSettings
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -55,6 +58,7 @@ fun MidiControlPanel(
|
||||||
onVolumeChange: (Float) -> Unit,
|
onVolumeChange: (Float) -> Unit,
|
||||||
onVoiceVolumeChange: (voiceIndex: Int, newVolume: Float) -> Unit,
|
onVoiceVolumeChange: (voiceIndex: Int, newVolume: Float) -> Unit,
|
||||||
mediaPlayer: FMediaPlayer,
|
mediaPlayer: FMediaPlayer,
|
||||||
|
sharedScreenModel: SharedScreenModel,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val momo = duration.toInt() - currentPos.toInt()
|
val momo = duration.toInt() - currentPos.toInt()
|
||||||
|
|
@ -71,8 +75,14 @@ fun MidiControlPanel(
|
||||||
}
|
}
|
||||||
val labels = listOf("S", "A", "T", "B")
|
val labels = listOf("S", "A", "T", "B")
|
||||||
listOf("Soprano", "Alto", "Ténor", "Basse")
|
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 currentBpm by remember { mutableStateOf(mediaPlayer.getCurrentBPM()) }
|
||||||
|
|
||||||
var isPianoSelected by remember { mutableStateOf(true) }
|
var isPianoSelected by remember { mutableStateOf(true) }
|
||||||
|
|
@ -82,6 +92,7 @@ fun MidiControlPanel(
|
||||||
val platform = getPlatform().name
|
val platform = getPlatform().name
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val prefs: Settings = provideSettings()
|
||||||
LaunchedEffect(currentPos, duration, solfaScrollState.viewportSize, solfaScrollState.maxValue) {
|
LaunchedEffect(currentPos, duration, solfaScrollState.viewportSize, solfaScrollState.maxValue) {
|
||||||
if (duration > 0f && solfaScrollState.maxValue > 0) {
|
if (duration > 0f && solfaScrollState.maxValue > 0) {
|
||||||
val progress = (currentPos / duration).coerceIn(0f, 1f)
|
val progress = (currentPos / duration).coerceIn(0f, 1f)
|
||||||
|
|
@ -99,9 +110,9 @@ fun MidiControlPanel(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(tempo) {
|
/*LaunchedEffect(tempo) {
|
||||||
currentBpm = mediaPlayer.getCurrentBPM()
|
currentBpm = mediaPlayer.getCurrentBPM()
|
||||||
}
|
}*/
|
||||||
fun updateTempoToBpm(newBpm: Int) {
|
fun updateTempoToBpm(newBpm: Int) {
|
||||||
tempo = newBpm.toFloat()
|
tempo = newBpm.toFloat()
|
||||||
mediaPlayer?.setTempo(tempo)
|
mediaPlayer?.setTempo(tempo)
|
||||||
|
|
@ -319,7 +330,7 @@ fun MidiControlPanel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tempo <= 160) { // limite 160BPM
|
if (tempo <= 320) {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.background(Color(0XFF2C3130)),
|
modifier = Modifier.background(Color(0XFF2C3130)),
|
||||||
onClick = { updateTempoByBpmStep(10) }) {
|
onClick = { updateTempoByBpmStep(10) }) {
|
||||||
|
|
@ -436,11 +447,12 @@ fun MidiControlPanel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val selectedInstru = prefs.getInt("voice_instrument", 1)
|
||||||
val instrumentButton = @Composable {
|
val instrumentButton = @Composable {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
isPianoSelected = !isPianoSelected
|
isPianoSelected = !isPianoSelected
|
||||||
mediaPlayer?.changeInstru(if (isPianoSelected) 1 else 20)
|
mediaPlayer?.changeInstru(if (isPianoSelected) selectedInstru else 16)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (isPianoSelected) {
|
if (isPianoSelected) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
import feufaro.composeapp.generated.resources.Emmentaler
|
import feufaro.composeapp.generated.resources.Emmentaler
|
||||||
import feufaro.composeapp.generated.resources.Res
|
import feufaro.composeapp.generated.resources.Res
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -36,18 +37,22 @@ import mg.dot.feufaro.DisplayConfigManager
|
||||||
import mg.dot.feufaro.getPlatform
|
import mg.dot.feufaro.getPlatform
|
||||||
import mg.dot.feufaro.midi.Dynamic
|
import mg.dot.feufaro.midi.Dynamic
|
||||||
import mg.dot.feufaro.midi.MidiInstrument
|
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.Font
|
||||||
import org.jetbrains.compose.resources.FontResource
|
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Settings(
|
fun Settings(
|
||||||
sharedScreenModel: SharedScreenModel,
|
sharedScreenModel: SharedScreenModel,
|
||||||
|
solfaScreenModel: SolfaScreenModel,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
expandedDropdownN: Boolean,
|
expandedDropdownN: Boolean,
|
||||||
isAndroid: Boolean
|
isAndroid: Boolean
|
||||||
) {
|
) {
|
||||||
|
val prefs: Settings = provideSettings()
|
||||||
|
var hasCountIn by remember { mutableStateOf(prefs.getBoolean("isCountIn", true)) }
|
||||||
var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) }
|
var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) }
|
||||||
var refreshTrigger by remember { mutableStateOf(0) }
|
var refreshTrigger by remember { mutableStateOf(0) }
|
||||||
val isFullScreenEnabled by sharedScreenModel.isFullScreen.collectAsState()
|
val isFullScreenEnabled by sharedScreenModel.isFullScreen.collectAsState()
|
||||||
|
|
@ -86,7 +91,7 @@ fun Settings(
|
||||||
mutableStateOf(player?.getVoiceInstrument() ?: 1)
|
mutableStateOf(player?.getVoiceInstrument() ?: 1)
|
||||||
}
|
}
|
||||||
val selectedInstrument = instruments.find { it.program == globalInstrumentProgram }
|
val selectedInstrument = instruments.find { it.program == globalInstrumentProgram }
|
||||||
?: MidiInstrument(globalInstrumentProgram, "Instrument $globalInstrumentProgram")
|
?: MidiInstrument(program = globalInstrumentProgram, name = "Instrument $globalInstrumentProgram")
|
||||||
|
|
||||||
val menuScrollState = rememberScrollState()
|
val menuScrollState = rememberScrollState()
|
||||||
var expandedDropdown by remember { mutableStateOf(false) }
|
var expandedDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
@ -368,17 +373,28 @@ fun Settings(
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(top = 8.dp),
|
Column(modifier = Modifier.padding(top = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth()
|
||||||
horizontalArrangement = Arrangement.End,
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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(
|
Switch(
|
||||||
checked = isDynamicEnabled,
|
checked = isDynamicEnabled,
|
||||||
onCheckedChange = { enabled ->
|
onCheckedChange = { enabled ->
|
||||||
|
|
@ -387,6 +403,11 @@ fun Settings(
|
||||||
refreshTrigger++
|
refreshTrigger++
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
if (isDynamicEnabled) "Désactiver" else "Activer",
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -606,7 +627,7 @@ fun Settings(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = "${selectedInstrument.name}",
|
value = "${selectedInstrument.icon} - ${selectedInstrument.name}",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropdown) },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropdown) },
|
||||||
|
|
@ -642,13 +663,13 @@ fun Settings(
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
globalInstrumentProgram = instrument.program
|
globalInstrumentProgram = instrument.program
|
||||||
|
sharedScreenModel.saveVoiceInstrument(instrument.program)
|
||||||
player?.changeInstru(instrument.program)
|
solfaScreenModel.regenMidi(120.0f)
|
||||||
player?.saveVoiceInstrument(instrument.program)
|
sharedScreenModel.loadNewSong("whawyd3.mid")
|
||||||
expandedDropdown = false
|
expandedDropdown = false
|
||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Text("🎹")
|
Text(instrument.icon)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (isSelected) {
|
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.State
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -22,6 +24,7 @@ import mg.dot.feufaro.data.GridTUOData
|
||||||
import mg.dot.feufaro.data.getCombinedList
|
import mg.dot.feufaro.data.getCombinedList
|
||||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||||
import mg.dot.feufaro.midi.FMediaPlayer
|
import mg.dot.feufaro.midi.FMediaPlayer
|
||||||
|
import mg.dot.feufaro.provideSettings
|
||||||
import mg.dot.feufaro.solfa.TUOEditState
|
import mg.dot.feufaro.solfa.TUOEditState
|
||||||
import mg.dot.feufaro.solfa.getAllMarker
|
import mg.dot.feufaro.solfa.getAllMarker
|
||||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||||
|
|
@ -72,6 +75,17 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
private val _drawerItems = MutableStateFlow<List<DrawerItem>>(emptyList())
|
private val _drawerItems = MutableStateFlow<List<DrawerItem>>(emptyList())
|
||||||
val drawerItems: StateFlow<List<DrawerItem>> = _drawerItems.asStateFlow()
|
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
|
val internalItems: StateFlow<List<DrawerItem>> = _drawerItems
|
||||||
.map { list -> list.filter { it.path.startsWith("assets://") } }
|
.map { list -> list.filter { it.path.startsWith("assets://") } }
|
||||||
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
||||||
|
|
@ -845,4 +859,11 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
return _playlist.value[playlistIndex]
|
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)
|
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
|
package mg.dot.feufaro.midi
|
||||||
|
|
||||||
import SharedScreenModel
|
import SharedScreenModel
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import mg.dot.feufaro.getConfigDirectoryPath
|
import mg.dot.feufaro.provideSettings
|
||||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
import mg.dot.feufaro.solfa.Transpose
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.prefs.Preferences
|
import javax.sound.midi.*
|
||||||
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.sampled.AudioSystem
|
import javax.sound.sampled.AudioSystem
|
||||||
import javax.sound.sampled.FloatControl
|
import javax.sound.sampled.FloatControl
|
||||||
|
|
||||||
|
|
@ -29,7 +23,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val prefs = Preferences.userRoot().node("mg.dot.feufaro")
|
private val prefs: Settings = provideSettings()
|
||||||
private var synthetizer = MidiSystem.getSynthesizer() as Synthesizer?
|
private var synthetizer = MidiSystem.getSynthesizer() as Synthesizer?
|
||||||
|
|
||||||
private var pointA: Long = -1L
|
private var pointA: Long = -1L
|
||||||
|
|
@ -41,8 +35,8 @@ actual class FMediaPlayer actual constructor(
|
||||||
private var currentDynamicVelocity: Int = Dynamic.MF.velocity
|
private var currentDynamicVelocity: Int = Dynamic.MF.velocity
|
||||||
private var currentDynamicFactor: Float = Dynamic.MF.factor
|
private var currentDynamicFactor: Float = Dynamic.MF.factor
|
||||||
|
|
||||||
private var currentTempo: Float = 1.0f
|
|
||||||
private var targetBpm: Float = 120f
|
private var targetBpm: Float = 120f
|
||||||
|
private var initialBpm: Float = 120f
|
||||||
private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val playerScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
private var abJob: Job? = null
|
private var abJob: Job? = null
|
||||||
|
|
||||||
|
|
@ -69,7 +63,9 @@ actual class FMediaPlayer actual constructor(
|
||||||
val isTempoChange: Boolean = false,
|
val isTempoChange: Boolean = false,
|
||||||
val tempoType: String = "",
|
val tempoType: String = "",
|
||||||
val endGridForTempo: Int = -1,
|
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>()
|
private val navigationSteps = mutableListOf<NavigationStep>()
|
||||||
|
|
@ -254,6 +250,8 @@ actual class FMediaPlayer actual constructor(
|
||||||
var lastFactor = Dynamic.MF.factor
|
var lastFactor = Dynamic.MF.factor
|
||||||
val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
|
val ritRegex = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
|
||||||
val rallRegex = Regex("""rall\.?|rallent\.?|rallentando""", 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 ->
|
metadataList.forEach { midiMarker ->
|
||||||
val currentIndex = midiMarker.gridIndex ?: 0
|
val currentIndex = midiMarker.gridIndex ?: 0
|
||||||
|
|
@ -278,6 +276,37 @@ actual class FMediaPlayer actual constructor(
|
||||||
val hasFin = finReg.containsMatchIn(mTrim) || finReg.containsMatchIn(nextTrim)
|
val hasFin = finReg.containsMatchIn(mTrim) || finReg.containsMatchIn(nextTrim)
|
||||||
val isDcFin = hasDc && hasFin
|
val isDcFin = hasDc && hasFin
|
||||||
val isDsFin = hasDs && 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 {
|
when {
|
||||||
//DSFin
|
//DSFin
|
||||||
isDsFin || isDcFin -> {
|
isDsFin || isDcFin -> {
|
||||||
|
|
@ -529,11 +558,46 @@ actual class FMediaPlayer actual constructor(
|
||||||
val currentSteps = navigationSteps.filter {
|
val currentSteps = navigationSteps.filter {
|
||||||
it.gridIndex == currentIndex && !it.alreadyDone
|
it.gridIndex == currentIndex && !it.alreadyDone
|
||||||
}
|
}
|
||||||
currentSteps.forEach { step ->
|
for (step in currentSteps) {
|
||||||
if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) {
|
if (Math.abs(sequencer!!.tempoInBPM - targetBpm) > 0.1 && !isInTempoChange) {
|
||||||
forceTempo(targetBpm.toDouble())
|
forceTempo(targetBpm.toDouble())
|
||||||
}
|
}
|
||||||
when {
|
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 -> {
|
step.isFin -> {
|
||||||
if(step.finActive) {
|
if(step.finActive) {
|
||||||
step.alreadyDone = true
|
step.alreadyDone = true
|
||||||
|
|
@ -658,13 +722,24 @@ actual class FMediaPlayer actual constructor(
|
||||||
val tick = (gridIndex.toDouble() * resolution).toLong()
|
val tick = (gridIndex.toDouble() * resolution).toLong()
|
||||||
sequencer?.tickPosition = tick
|
sequencer?.tickPosition = tick
|
||||||
|
|
||||||
|
val initialOrPreviousBpm = navigationSteps
|
||||||
|
.filter { it.newBpm != null && it.gridIndex <= gridIndex }
|
||||||
|
.maxByOrNull { it.gridIndex }?.newBpm ?: 120f
|
||||||
|
|
||||||
|
targetBpm = initialOrPreviousBpm
|
||||||
applyBpm()
|
applyBpm()
|
||||||
|
sharedScreenModel.setBpmFlow(initialOrPreviousBpm)
|
||||||
// Voir les velocity avant
|
// Voir les velocity avant
|
||||||
val lastDynamic = navigationSteps
|
val lastDynamic = navigationSteps
|
||||||
.filter { it.dynamic != null && it.gridIndex <= gridIndex }
|
.filter { it.dynamic != null && it.gridIndex <= gridIndex }
|
||||||
.maxByOrNull { it.gridIndex }
|
.maxByOrNull { it.gridIndex }
|
||||||
?.dynamic ?: Dynamic.MF
|
?.dynamic ?: Dynamic.MF
|
||||||
currentDynamicFactor = lastDynamic.factor
|
currentDynamicFactor = lastDynamic.factor
|
||||||
|
navigationSteps.forEach { step ->
|
||||||
|
if (step.gridIndex > gridIndex) {
|
||||||
|
step.alreadyDone = false
|
||||||
|
}
|
||||||
|
}
|
||||||
applyVoiceStates()
|
applyVoiceStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -676,6 +751,13 @@ actual class FMediaPlayer actual constructor(
|
||||||
if (sequencer?.isOpen == true) {
|
if (sequencer?.isOpen == true) {
|
||||||
val currentTick = sequencer?.tickPosition ?: 0L
|
val currentTick = sequencer?.tickPosition ?: 0L
|
||||||
if (currentTick == 0L) {
|
if (currentTick == 0L) {
|
||||||
|
resetNavigationFlags()
|
||||||
|
targetBpm = initialBpm
|
||||||
|
forceTempo(initialBpm.toDouble())
|
||||||
|
boundModel?.setBpmFlow(initialBpm)
|
||||||
|
}
|
||||||
|
val countInEnable = prefs.getBoolean("isCountIn", true)
|
||||||
|
if (currentTick == 0L && countInEnable) {
|
||||||
playCountIn()
|
playCountIn()
|
||||||
} else {
|
} else {
|
||||||
startMidiPlayback()
|
startMidiPlayback()
|
||||||
|
|
@ -805,6 +887,9 @@ actual class FMediaPlayer actual constructor(
|
||||||
navigationJob?.cancel()
|
navigationJob?.cancel()
|
||||||
resetNavigationFlags()
|
resetNavigationFlags()
|
||||||
navigationSteps.clear()
|
navigationSteps.clear()
|
||||||
|
targetBpm = initialBpm
|
||||||
|
sequencer?.tempoInBPM = initialBpm
|
||||||
|
boundModel?.setBpmFlow(initialBpm)
|
||||||
clearLoop()
|
clearLoop()
|
||||||
currentDynamicFactor = Dynamic.MF.factor
|
currentDynamicFactor = Dynamic.MF.factor
|
||||||
resetTempoToNormal()
|
resetTempoToNormal()
|
||||||
|
|
@ -929,12 +1014,14 @@ actual class FMediaPlayer actual constructor(
|
||||||
|
|
||||||
actual fun setTempo(bpm: Float){
|
actual fun setTempo(bpm: Float){
|
||||||
this.targetBpm = bpm
|
this.targetBpm = bpm
|
||||||
|
this.initialBpm = bpm
|
||||||
sequencer?.tempoInBPM = bpm
|
sequencer?.tempoInBPM = bpm
|
||||||
boundModel?.let { modele ->
|
boundModel?.let { modele ->
|
||||||
val seq = sequencer?.sequence
|
val seq = sequencer?.sequence
|
||||||
if (seq != null) {
|
if (seq != null) {
|
||||||
syncTuoWithMidi(seq, modele)
|
syncTuoWithMidi(seq, modele)
|
||||||
}
|
}
|
||||||
|
modele.setBpmFlow(bpm)
|
||||||
prepareNavigation(modele)
|
prepareNavigation(modele)
|
||||||
}
|
}
|
||||||
println("Tempo réglé à : $bpm BPM")
|
println("Tempo réglé à : $bpm BPM")
|
||||||
|
|
@ -947,18 +1034,18 @@ actual class FMediaPlayer actual constructor(
|
||||||
}
|
}
|
||||||
synthesizer.availableInstruments.map { instrument ->
|
synthesizer.availableInstruments.map { instrument ->
|
||||||
MidiInstrument(
|
MidiInstrument(
|
||||||
|
icon = "\uD83C\uDFB9",
|
||||||
program = instrument.patch.program,
|
program = instrument.patch.program,
|
||||||
name = instrument.name.trim()
|
name = instrument.name.trim()
|
||||||
)
|
)
|
||||||
}.sortedBy { it.program }
|
}.sortedBy { it.program }
|
||||||
} catch (e: Exception) {
|
} 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) {
|
actual fun saveVoiceInstrument(program: Int) {
|
||||||
prefs.putInt("voice_instrument", program)
|
prefs.putInt("voice_instrument", program)
|
||||||
try { prefs.flush() } catch (e: Exception) { e.printStackTrace() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getVoiceInstrument(): Int {
|
actual fun getVoiceInstrument(): Int {
|
||||||
|
|
@ -976,11 +1063,11 @@ actual class FMediaPlayer actual constructor(
|
||||||
|
|
||||||
private fun saveVoicesVolumes() {
|
private fun saveVoicesVolumes() {
|
||||||
val data = voiceVolumes.joinToString(",")
|
val data = voiceVolumes.joinToString(",")
|
||||||
prefs.put("voices_volumes", data)
|
prefs.putString("voices_volumes", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadVoiceVolumes() {
|
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(",")
|
val volumesArray = data.split(",")
|
||||||
|
|
||||||
if (volumesArray.size == 4) {
|
if (volumesArray.size == 4) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue