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.
|
// (android.media.midi) and AAudio low-latency audio we rely on.
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 3
|
versionCode = 4
|
||||||
versionName = "0.3.0"
|
versionName = "0.3.1"
|
||||||
|
|
||||||
// We provide our own instrumentation runner if/when tests are added.
|
// We provide our own instrumentation runner if/when tests are added.
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
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.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
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
|
// Volume + ADSR as compact vertical faders (one row). The audition keyboard
|
||||||
// keyboard below gets room for full-size keys.
|
// is pinned separately at the bottom of the editor (see [SoundFontKeyboard]).
|
||||||
Text("Volume / envelope:", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp)
|
Text("Volume / envelope:", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp)
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
VerticalSlider(vm, slot, rev, "volume", "VOL", 0f, 1f, 0.8f, Modifier.weight(1f))
|
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, "sustain", "SUS", 0f, 1f, 1f, Modifier.weight(1f))
|
||||||
VerticalSlider(vm, slot, rev, "release", "REL", 0f, 3f, 0.20f, 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 ->
|
importError?.let { msg ->
|
||||||
@@ -225,13 +210,40 @@ private fun VerticalSlider(
|
|||||||
private fun formatSliderValue(v: Float): String =
|
private fun formatSliderValue(v: Float): String =
|
||||||
if (v == v.toInt().toFloat()) v.toInt().toString() else String.format("%.2f", v)
|
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
|
/** 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
|
* 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
|
* instrument. [modifier] carries the weight so the two octaves share the pinned
|
||||||
* PianoOctave needs a bounded height to lay its keys out. */
|
* keyboard's height (PianoOctave needs a bounded height to lay its keys out). */
|
||||||
@Composable
|
@Composable
|
||||||
private fun AuditionOctave(vm: AppViewModel, slotIndex: Int, startMidi: Int) {
|
private fun AuditionOctave(vm: AppViewModel, slotIndex: Int, startMidi: Int, modifier: Modifier) {
|
||||||
PianoOctave(Modifier.fillMaxWidth().height(150.dp)) { pc, isBlack, keyMod ->
|
PianoOctave(modifier.fillMaxWidth()) { pc, isBlack, keyMod ->
|
||||||
val midi = (startMidi + pc).coerceIn(Pitch.LOWEST, Pitch.HIGHEST)
|
val midi = (startMidi + pc).coerceIn(Pitch.LOWEST, Pitch.HIGHEST)
|
||||||
val liveMidi = rememberUpdatedState(midi)
|
val liveMidi = rememberUpdatedState(midi)
|
||||||
PianoKey(
|
PianoKey(
|
||||||
|
|||||||
@@ -259,9 +259,21 @@ private fun ParamEditor(vm: AppViewModel, slot: ToolboxSlot, onClose: () -> Unit
|
|||||||
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so title/params refresh
|
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so title/params refresh
|
||||||
val type = slot.type ?: return
|
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))) {
|
Box(Modifier.fillMaxSize().background(c.background.copy(alpha = 0.96f))) {
|
||||||
Column(Modifier.fillMaxSize().padding(12.dp).verticalScroll(rememberScrollState()),
|
Column(Modifier.fillMaxSize()) {
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
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) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(slot.name, color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 15.sp)
|
Text(slot.name, color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 15.sp)
|
||||||
Text("CLOSE ✕", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 13.sp,
|
Text("CLOSE ✕", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 13.sp,
|
||||||
@@ -287,6 +299,12 @@ private fun ParamEditor(vm: AppViewModel, slot: ToolboxSlot, onClose: () -> Unit
|
|||||||
else -> ParamGrid(vm, slot, type.params)
|
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