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:
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user