Pin the SF2/XI audition keyboard at the editor bottom (v0.3.1)

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 <noreply@anthropic.com>
This commit is contained in:
Reactorcoremeltdown
2026-07-17 20:45:41 +02:00
parent ba56b5ab9e
commit b5620b429d
3 changed files with 77 additions and 47 deletions

View File

@@ -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"

View File

@@ -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(

View File

@@ -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))
}
}
}