Add instrument selection menu on Settings
This commit is contained in:
parent
90c7b40d84
commit
5f8e0e03a7
5 changed files with 210 additions and 6 deletions
|
|
@ -168,6 +168,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
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 {
|
||||||
|
|
@ -779,6 +780,36 @@ actual class FMediaPlayer actual constructor(
|
||||||
}
|
}
|
||||||
actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
|
actual fun getVoiceVolumes(): List<Float> = voiceVolumes.toList()
|
||||||
actual fun changeInstru(noInstru: Int) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,4 +26,9 @@ expect class FMediaPlayer(filename: String, sharedScreenModel: SharedScreenModel
|
||||||
fun requestSync(sharedScreenModel: SharedScreenModel)
|
fun requestSync(sharedScreenModel: SharedScreenModel)
|
||||||
fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel)
|
fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel)
|
||||||
fun release()
|
fun release()
|
||||||
}
|
fun getAvalaibleInstruments(): List<MidiInstrument>
|
||||||
|
fun saveVoiceInstrument(program: Int)
|
||||||
|
fun getVoiceInstrument(): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MidiInstrument(val program: Int, val name: String)
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
|
@ -225,12 +226,15 @@ fun MainScreenWithDrawer(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expandedDropdown by remember { mutableStateOf(false) }
|
||||||
if (showSettingsDialog) {
|
if (showSettingsDialog) {
|
||||||
Settings(
|
Settings(
|
||||||
sharedScreenModel = sharedScreenModel,
|
sharedScreenModel = sharedScreenModel,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showSettingsDialog = false
|
showSettingsDialog = false
|
||||||
}
|
},
|
||||||
|
expandedDropdown
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
|
val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
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.DarkMode
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
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.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.graphics.Color
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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 feufaro.composeapp.generated.resources.Emmentaler
|
||||||
|
import feufaro.composeapp.generated.resources.Res
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import mg.dot.feufaro.DisplayConfigManager
|
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 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,
|
||||||
onDismissRequest: () -> Unit
|
onDismissRequest: () -> Unit,
|
||||||
|
expandedDropdownN: Boolean
|
||||||
) {
|
) {
|
||||||
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) }
|
||||||
|
|
@ -62,7 +70,33 @@ fun Settings(
|
||||||
mutableStateOf(currentDisplayConfig.fontSize)
|
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(
|
AlertDialog(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -417,9 +451,11 @@ fun Settings(
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = dynamicItem.label.lowercase(),
|
text = dynamicItem.label.lowercase(),
|
||||||
fontSize = 12.sp,
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontFamily = emmentaler
|
||||||
|
),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = if (isDynamicEnabled) Color.White else Color.Gray,
|
color = if (isDynamicEnabled) MaterialTheme.colorScheme.onSecondary else Color.Gray,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.width(30.dp)
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
sequencer?.tempoFactor = 1.0f
|
sequencer?.tempoFactor = 1.0f
|
||||||
loadVoiceVolumes()
|
loadVoiceVolumes()
|
||||||
applyVoiceStates()
|
applyVoiceStates()
|
||||||
|
loadSavedInstrumentsToPlayer(this)
|
||||||
sequencer?.addMetaEventListener { meta ->
|
sequencer?.addMetaEventListener { meta ->
|
||||||
if(meta.type == 47){
|
if(meta.type == 47){
|
||||||
val totalGrids = (sequencer!!.sequence.tickLength / sequencer!!.sequence.resolution).toInt()
|
val totalGrids = (sequencer!!.sequence.tickLength / sequencer!!.sequence.resolution).toInt()
|
||||||
|
|
@ -881,6 +882,40 @@ actual class FMediaPlayer actual constructor(
|
||||||
}
|
}
|
||||||
println("Tempo réglé à : $bpm BPM")
|
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() {
|
private fun saveVoicesVolumes() {
|
||||||
val data = voiceVolumes.joinToString(",")
|
val data = voiceVolumes.joinToString(",")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue