Stacked keyboards go chromatic when routed to a Sampler (v0.12.0)

A Sampler maps a pad to each MIDI note, so scale filtering left out-of-scale
pads greyed-out and unreachable. When the punch-in / audition keyboard's channel
routes to a bus with a Sampler instrument, bypass the scale filter and enable all
twelve keys; otherwise scale filtering is unchanged. Non-destructive - the
project's scale/root setting is untouched, and switching the channel back to a
synth/SF2 restores filtering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Reactorcoremeltdown
2026-07-18 23:59:36 +02:00
parent 8eee588780
commit abdcf77ce5
3 changed files with 24 additions and 4 deletions

View File

@@ -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 = 30 versionCode = 31
versionName = "0.11.2" versionName = "0.12.0"
// 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"

View File

@@ -221,6 +221,18 @@ class AppViewModel(
fun kbdNoteOn(midi: Int) { markNoteOn(midi); engine.busNoteOn(kbdChannel, midi, kbdVelocity) } fun kbdNoteOn(midi: Int) { markNoteOn(midi); engine.busNoteOn(kbdChannel, midi, kbdVelocity) }
fun kbdNoteOff(midi: Int) { markNoteOff(midi); engine.busNoteOff(kbdChannel, midi) } fun kbdNoteOff(midi: Int) { markNoteOff(midi); engine.busNoteOff(kbdChannel, midi) }
/** True if the on-screen keyboard's MIDI channel routes to any bus whose instrument
* is a Sampler. A Sampler maps a pad to each MIDI note, so scale filtering would
* hide pads — the stacked keyboards go fully chromatic in that case. */
fun kbdRoutesToSampler(): Boolean {
for (ch in project.mixer.channels) {
if (ch.midiChannel != kbdChannel) continue
val slot = project.toolbox.getOrNull(ch.instrumentSlot) ?: continue
if (slot.type == space.rcmd.android.sizzle.model.ToolboxType.SAMPLER) return true
}
return false
}
/** Live VU level (peak, ~0..1+) for mixer bus [ch] — polled at frame rate by the /** Live VU level (peak, ~0..1+) for mixer bus [ch] — polled at frame rate by the
* mixer's fader VU meters. */ * mixer's fader VU meters. */
fun channelMeter(ch: Int): Float = engine.channelMeter(ch) fun channelMeter(ch: Int): Float = engine.channelMeter(ch)

View File

@@ -28,6 +28,9 @@ import space.rcmd.android.sizzle.ui.theme.LocalRetro
private const val VEL_STEP = 8 private const val VEL_STEP = 8
/** All twelve pitch classes — used when scale filtering is bypassed (Sampler routing). */
private val CHROMATIC: Set<Int> = (0..11).toSet()
/** /**
* A stacked two-octave keyboard shared by the Toolbox (audition) and Tracker * A stacked two-octave keyboard shared by the Toolbox (audition) and Tracker
* (punch-in) tabs. Its channel, octave range and velocity live on the * (punch-in) tabs. Its channel, octave range and velocity live on the
@@ -41,9 +44,14 @@ fun StackedKeyboard(vm: AppViewModel, punchIn: Boolean, modifier: Modifier) {
val c = LocalRetro.current val c = LocalRetro.current
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // refresh key filtering on scale/root change @Suppress("UNUSED_VARIABLE") val rev = vm.revision // refresh key filtering on scale/root change
// Only in-scale keys are playable on BOTH the tracker punch-in and the toolbox // Only in-scale keys are playable on BOTH the tracker punch-in and the toolbox
// audition keyboard (Chromatic yields all twelve = every key stays enabled). // audition keyboard (Chromatic yields all twelve = every key stays enabled). But a
// Sampler maps a pad to each MIDI note, so scale filtering would hide pads — when
// the keyboard's channel routes to a Sampler, go fully chromatic so every pad is
// reachable.
val root = vm.project.rootNote val root = vm.project.rootNote
val inKey: Set<Int> = vm.project.scale.intervals.map { (it + root) % 12 }.toSet() val inKey: Set<Int> =
if (vm.kbdRoutesToSampler()) CHROMATIC
else vm.project.scale.intervals.map { (it + root) % 12 }.toSet()
Column( Column(
modifier.fillMaxWidth().background(c.background).padding(6.dp), modifier.fillMaxWidth().background(c.background).padding(6.dp),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),