Add instrument selection menu on Settings

This commit is contained in:
Hasinjato 2026-07-10 15:26:17 +03:00
parent 90c7b40d84
commit 5f8e0e03a7
5 changed files with 210 additions and 6 deletions

View file

@ -168,6 +168,7 @@ actual class FMediaPlayer actual constructor(
init {
midiDriver.start()
loadVoiceVolumes()
loadSavedInstrumentsToPlayer(this)
val file = File(filename)
if (file.exists()) {
try {
@ -779,6 +780,36 @@ actual class FMediaPlayer actual constructor(
}
actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
actual fun changeInstru(noInstru: Int) {
for (ch in 0 until 4) send(0xC0 or ch, noInstru)
val buffer = byteArrayOf(
(0xC0 or 0).toByte(),
noInstru.toByte()
)
midiDriver.write(buffer)
}
actual fun getAvalaibleInstruments(): List<MidiInstrument> {
val gmInstruments = listOf(
"Acoustic Grand Piano", "Bright Acoustic Piano", "Electric Grand Piano", "Honky-tonk Piano", "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavi", "Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba", "Xylophone", "Tubular Bells", "Dulcimer", "Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ", "Reed Organ", "Accordion", "Harmonica", "Tango Accordion", "Acoustic Guitar (nylon)", "Acoustic Guitar (steel)", "Electric Guitar (jazz)", "Electric Guitar (clean)", "Electric Guitar (muted)", "Overdriven Guitar", "Distortion Guitar", "Guitar harmonics", "Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2", "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings", "Orchestral Harp", "Timpani", "String Ensemble 1", "String Ensemble 2", "SynthStrings 1", "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit", "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", "Brass Section", "SynthBrass 1", "SynthBrass 2", "Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet", "Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Shakuhachi", "Whistle", "Ocarina", "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8 (bass+lead)", "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)", "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", "FX 8 (sci-fi)", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bag pipe", "Fiddle", "Shanai", "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", "Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", "Applause", "Gunshot"
)
return gmInstruments.mapIndexed { index, name ->
MidiInstrument(
program = index,
name = name
)
}
}
actual fun saveVoiceInstrument(program: Int) {
settings.putInt("voice_instrument", program)
}
actual fun getVoiceInstrument(): Int {
val defaultProgram = 0
return settings.getInt("voice_instrument", defaultProgram)
}
fun loadSavedInstrumentsToPlayer(mediaPlayer: FMediaPlayer) {
val savedProgram = getVoiceInstrument()
mediaPlayer.changeInstru(savedProgram)
}
}

View file

@ -26,4 +26,9 @@ expect class FMediaPlayer(filename: String, sharedScreenModel: SharedScreenModel
fun requestSync(sharedScreenModel: SharedScreenModel)
fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel)
fun release()
}
fun getAvalaibleInstruments(): List<MidiInstrument>
fun saveVoiceInstrument(program: Int)
fun getVoiceInstrument(): Int
}
data class MidiInstrument(val program: Int, val name: String)

View file

@ -19,6 +19,7 @@ import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -225,12 +226,15 @@ fun MainScreenWithDrawer(
}
)
}
var expandedDropdown by remember { mutableStateOf(false) }
if (showSettingsDialog) {
Settings(
sharedScreenModel = sharedScreenModel,
onDismissRequest = {
showSettingsDialog = false
}
},
expandedDropdown
)
}
val favoritePaths by sharedScreenModel.playlistItems.collectAsState()

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.LightMode
@ -21,22 +22,29 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import feufaro.composeapp.generated.resources.Emmentaler
import feufaro.composeapp.generated.resources.Res
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import mg.dot.feufaro.DisplayConfigManager
import mg.dot.feufaro.getPlatform
import mg.dot.feufaro.midi.Dynamic
import mg.dot.feufaro.midi.MidiInstrument
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,
onDismissRequest: () -> Unit
onDismissRequest: () -> Unit,
expandedDropdownN: Boolean
) {
var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) }
var refreshTrigger by remember { mutableStateOf(0) }
@ -62,7 +70,33 @@ fun Settings(
mutableStateOf(currentDisplayConfig.fontSize)
}
var emmentaler = FontFamily(Font(Res.font.Emmentaler))
val player = sharedScreenModel.mediaPlayer
val instruments = remember { player?.getAvalaibleInstruments() ?: emptyList() }
var globalInstrumentProgram by remember {
mutableStateOf(player?.getVoiceInstrument() ?: 1)
}
val selectedInstrument = instruments.find { it.program == globalInstrumentProgram }
?: MidiInstrument(globalInstrumentProgram, "Instrument $globalInstrumentProgram")
val menuScrollState = rememberScrollState()
var expandedDropdown by remember { mutableStateOf(false) }
LaunchedEffect(expandedDropdown, globalInstrumentProgram) {
if (expandedDropdown) {
val selectedIndex = instruments.indexOfFirst { it.program == globalInstrumentProgram }
if (selectedIndex != -1) {
val itemHeightPx = 16 * 3
kotlinx.coroutines.yield()
menuScrollState.scrollTo(selectedIndex * itemHeightPx)
}
}
}
AlertDialog(
modifier = Modifier
.fillMaxSize(),
onDismissRequest = onDismissRequest,
title = {
Text(
@ -417,9 +451,11 @@ fun Settings(
}
Text(
text = dynamicItem.label.lowercase(),
fontSize = 12.sp,
style = MaterialTheme.typography.displaySmall.copy(
fontFamily = emmentaler
),
fontWeight = FontWeight.Bold,
color = if (isDynamicEnabled) Color.White else Color.Gray,
color = if (isDynamicEnabled) MaterialTheme.colorScheme.onSecondary else Color.Gray,
textAlign = TextAlign.Center,
modifier = Modifier.width(30.dp)
)
@ -525,6 +561,99 @@ fun Settings(
}
}
}
HorizontalDivider(
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
modifier = Modifier.padding(vertical = 12.dp)
)
Text(
text = "Règlage des Instruments",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 8.dp)
)
val currentProgram = 0
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ExposedDropdownMenuBox(
expanded = expandedDropdown,
onExpandedChange = { expandedDropdown = !expandedDropdown },
modifier = Modifier.weight(1f)
) {
OutlinedTextField(
value = "${selectedInstrument.name}",
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropdown) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.menuAnchor(type = MenuAnchorType.PrimaryEditable, true).fillMaxWidth(),
textStyle = MaterialTheme.typography.bodyMedium,
shape = RoundedCornerShape(8.dp)
)
DropdownMenu(
expanded = expandedDropdown,
onDismissRequest = { expandedDropdown = false },
modifier = Modifier
.width(400.dp)
.heightIn(max = 250.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 250.dp)
.verticalScroll(menuScrollState)
) {
instruments.forEach { instrument ->
val isSelected = selectedInstrument.program == instrument.program
DropdownMenuItem(
modifier = Modifier.fillMaxWidth(),
text = {
Text(
"${instrument.program} - ${instrument.name}",
fontWeight = if(isSelected) FontWeight.Bold else FontWeight.Normal,
style = MaterialTheme.typography.bodyMedium
)
},
onClick = {
globalInstrumentProgram = instrument.program
player?.changeInstru(instrument.program)
player?.saveVoiceInstrument(instrument.program)
expandedDropdown = false
},
leadingIcon = {
Text("🎹")
},
trailingIcon = {
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
}
},
colors = MenuDefaults.itemColors(
textColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
leadingIconColor = MaterialTheme.colorScheme.primary,
trailingIconColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
)
}
}
}
}
}
}
}
}

View file

@ -94,6 +94,7 @@ actual class FMediaPlayer actual constructor(
sequencer?.tempoFactor = 1.0f
loadVoiceVolumes()
applyVoiceStates()
loadSavedInstrumentsToPlayer(this)
sequencer?.addMetaEventListener { meta ->
if(meta.type == 47){
val totalGrids = (sequencer!!.sequence.tickLength / sequencer!!.sequence.resolution).toInt()
@ -881,6 +882,40 @@ actual class FMediaPlayer actual constructor(
}
println("Tempo réglé à : $bpm BPM")
}
actual fun getAvalaibleInstruments(): List<MidiInstrument> {
return try {
val synthesizer = MidiSystem.getSynthesizer()
if (!synthesizer.isOpen) {
synthesizer.open()
}
synthesizer.availableInstruments.map { instrument ->
MidiInstrument(
program = instrument.patch.program,
name = instrument.name.trim()
)
}.sortedBy { it.program }
} catch (e: Exception) {
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 {
val defaultProgram = 0
return prefs.getInt("voice_instrument", defaultProgram)
}
fun loadSavedInstrumentsToPlayer(mediaPlayer: FMediaPlayer) {
for (i in 0 until 4) {
val savedProgram = getVoiceInstrument()
mediaPlayer.changeInstru(savedProgram)
}
}
private fun saveVoicesVolumes() {
val data = voiceVolumes.joinToString(",")