From b5620b429d06686cfcb355b8657dcf1b489aae3d Mon Sep 17 00:00:00 2001 From: Reactorcoremeltdown Date: Fri, 17 Jul 2026 20:45:41 +0200 Subject: [PATCH] Pin the SF2/XI audition keyboard at the editor bottom (v0.3.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lay out and place the SF2/XI loader's audition keyboard like the toolbox and tracker keyboards: extract it into SoundFontKeyboard (an octave selector above two weighted octaves) and pin it at the bottom of the device editor, with the controls scrolling in a weighted region above (1.7 : 1, matching the toolbox tab) — so it's the same height and always visible instead of scrolling away. Patch bump to 0.3.1 (versionCode 4). Co-Authored-By: Claude Opus 4.8 --- app/build.gradle.kts | 4 +- .../sizzle/ui/toolbox/SoundFontEditor.kt | 56 +++++++++------- .../sizzle/ui/toolbox/ToolboxScreen.kt | 64 ++++++++++++------- 3 files changed, 77 insertions(+), 47 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c96586c..f1369d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { // (android.media.midi) and AAudio low-latency audio we rely on. minSdk = 26 targetSdk = 34 - versionCode = 3 - versionName = "0.3.0" + versionCode = 4 + versionName = "0.3.1" // We provide our own instrumentation runner if/when tests are added. testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/SoundFontEditor.kt b/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/SoundFontEditor.kt index 805fffb..acd90c9 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/SoundFontEditor.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/SoundFontEditor.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -133,8 +134,8 @@ fun SoundFontEditor(vm: AppViewModel, slot: ToolboxSlot) { } } - // Volume + ADSR as compact vertical faders (one row) so the audition - // keyboard below gets room for full-size keys. + // Volume + ADSR as compact vertical faders (one row). The audition keyboard + // is pinned separately at the bottom of the editor (see [SoundFontKeyboard]). Text("Volume / envelope:", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp) Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) { VerticalSlider(vm, slot, rev, "volume", "VOL", 0f, 1f, 0.8f, Modifier.weight(1f)) @@ -143,22 +144,6 @@ fun SoundFontEditor(vm: AppViewModel, slot: ToolboxSlot) { VerticalSlider(vm, slot, rev, "sustain", "SUS", 0f, 1f, 1f, Modifier.weight(1f)) VerticalSlider(vm, slot, rev, "release", "REL", 0f, 3f, 0.20f, Modifier.weight(1f)) } - - // Audition keyboard — same look and behaviour as the toolbox MIDI piano: - // an octave selector over two stacked octaves, hold a key to sound the - // loaded instrument. - var octave by remember { mutableIntStateOf(3) } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Text("OCT", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp) - RepeatButton("-") { octave = (octave - 1).coerceAtLeast(0) } - Text("$octave–${octave + 1}", color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 13.sp) - RepeatButton("+") { octave = (octave + 1).coerceAtMost(8) } - } - AuditionOctave(vm, slot.index, (octave + 2) * 12) // upper octave on top - AuditionOctave(vm, slot.index, (octave + 1) * 12) // lower octave below } importError?.let { msg -> @@ -225,13 +210,40 @@ private fun VerticalSlider( private fun formatSliderValue(v: Float): String = if (v == v.toInt().toFloat()) v.toInt().toString() else String.format("%.2f", v) +/** The SF2/XI audition keyboard, laid out and sized like the shared audition / + * punch-in keyboard ([space.rcmd.android.sizzle.ui.StackedKeyboard]): a small octave + * selector above two weighted octaves. It is pinned at the bottom of the editor (by + * [space.rcmd.android.sizzle.ui.toolbox] ParamEditor) rather than scrolling with the + * controls. Holding a key sounds THIS slot's loaded instrument. */ +@Composable +fun SoundFontKeyboard(vm: AppViewModel, slot: ToolboxSlot, modifier: Modifier) { + val c = LocalRetro.current + var octave by remember(slot.index) { mutableIntStateOf(3) } + Column( + modifier.fillMaxWidth().background(c.background).padding(6.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text("OCT", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp) + RepeatButton("-") { octave = (octave - 1).coerceAtLeast(0) } + Text("$octave–${octave + 1}", color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 13.sp) + RepeatButton("+") { octave = (octave + 1).coerceAtMost(8) } + } + AuditionOctave(vm, slot.index, (octave + 2) * 12, Modifier.weight(1f)) // upper octave on top + AuditionOctave(vm, slot.index, (octave + 1) * 12, Modifier.weight(1f)) // lower octave below + } +} + /** One octave of the SoundFont audition keyboard — a real piano octave (like the * toolbox piano and the cell-editor keyboard); hold a key to sound the slot's - * instrument. A fixed, full-size height is used because the editor scrolls, and - * PianoOctave needs a bounded height to lay its keys out. */ + * instrument. [modifier] carries the weight so the two octaves share the pinned + * keyboard's height (PianoOctave needs a bounded height to lay its keys out). */ @Composable -private fun AuditionOctave(vm: AppViewModel, slotIndex: Int, startMidi: Int) { - PianoOctave(Modifier.fillMaxWidth().height(150.dp)) { pc, isBlack, keyMod -> +private fun AuditionOctave(vm: AppViewModel, slotIndex: Int, startMidi: Int, modifier: Modifier) { + PianoOctave(modifier.fillMaxWidth()) { pc, isBlack, keyMod -> val midi = (startMidi + pc).coerceIn(Pitch.LOWEST, Pitch.HIGHEST) val liveMidi = rememberUpdatedState(midi) PianoKey( diff --git a/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt b/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt index ad0a486..1821c8f 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt @@ -259,32 +259,50 @@ private fun ParamEditor(vm: AppViewModel, slot: ToolboxSlot, onClose: () -> Unit @Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so title/params refresh val type = slot.type ?: return + // The SoundFont editor pins its audition keyboard at the bottom (same as the + // toolbox / tracker keyboards), so its controls scroll in a weighted region above + // the keyboard rather than the whole editor scrolling as one. + val pinnedKeyboard = type == ToolboxType.SOUNDFONT + Box(Modifier.fillMaxSize().background(c.background.copy(alpha = 0.96f))) { - Column(Modifier.fillMaxSize().padding(12.dp).verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text(slot.name, color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 15.sp) - Text("CLOSE ✕", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 13.sp, - modifier = Modifier.pointerInput(Unit) { detectTapGestures { onClose() } }) + Column(Modifier.fillMaxSize()) { + Column( + Modifier + .weight(if (pinnedKeyboard) 1.7f else 1f) + .fillMaxWidth() + .padding(12.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text(slot.name, color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 15.sp) + Text("CLOSE ✕", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 13.sp, + modifier = Modifier.pointerInput(Unit) { detectTapGestures { onClose() } }) + } + + // Preset browser: load / save / import / share for this device. + PresetBar(vm, slot) + + // The Sampler / SoundFont / Delay get bespoke editors; the reverb has so + // many controls it's paged (swipeable sections with dot pagination); the + // LFO adds a modulation-target picker; everything else uses the + // auto-generated parameter list, laid out two-per-row to fit more. + when (type) { + ToolboxType.SAMPLER -> SamplerEditor(vm, slot) + ToolboxType.SOUNDFONT -> SoundFontEditor(vm, slot) + ToolboxType.DELAY -> DelayEditor(vm, slot) + ToolboxType.AMBIENCE -> AmbiencePager(vm, slot) + ToolboxType.LFO -> { + ParamGrid(vm, slot, type.params) + LfoTargetPicker(vm, slot) + } + else -> ParamGrid(vm, slot, type.params) + } } - // Preset browser: load / save / import / share for this device. - PresetBar(vm, slot) - - // The Sampler / SoundFont / Delay get bespoke editors; the reverb has so - // many controls it's paged (swipeable sections with dot pagination); the - // LFO adds a modulation-target picker; everything else uses the - // auto-generated parameter list, laid out two-per-row to fit more. - when (type) { - ToolboxType.SAMPLER -> SamplerEditor(vm, slot) - ToolboxType.SOUNDFONT -> SoundFontEditor(vm, slot) - ToolboxType.DELAY -> DelayEditor(vm, slot) - ToolboxType.AMBIENCE -> AmbiencePager(vm, slot) - ToolboxType.LFO -> { - ParamGrid(vm, slot, type.params) - LfoTargetPicker(vm, slot) - } - else -> ParamGrid(vm, slot, type.params) + if (pinnedKeyboard) { + Box(Modifier.fillMaxWidth().height(2.dp).background(c.accent)) // fixed divider + SoundFontKeyboard(vm, slot, Modifier.weight(1f)) } } }