diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index e763790..6e48484 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -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 = 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 { + 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) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index e73a593..421e9dd 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -26,4 +26,9 @@ expect class FMediaPlayer(filename: String, sharedScreenModel: SharedScreenModel fun requestSync(sharedScreenModel: SharedScreenModel) fun syncNavigationMonitor(sharedScreenModel: SharedScreenModel) fun release() -} \ No newline at end of file + fun getAvalaibleInstruments(): List + fun saveVoiceInstrument(program: Int) + fun getVoiceInstrument(): Int +} + +data class MidiInstrument(val program: Int, val name: String) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index 4de5e70..3e2a413 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt index a0b827e..5b26958 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt @@ -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) + ) + } + } + } + } + } } } } diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 7a8a912..6fb7a6c 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -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 { + 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(",")