Make mixer VU meters optional via a Settings toggle (v0.9.1)
Add a persisted "Mixer VU meters" switch (Settings -> Interface, on by default), backed by a DataStore boolean. When off, the mixer skips the per-frame poll loop entirely (the LaunchedEffect is keyed on the flag) and doesn't draw the meter, so disabling it removes the metering cost, not just the visuals; the volume fill and thumb render exactly as before. 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 = 25
|
||||
versionName = "0.9.0"
|
||||
versionCode = 26
|
||||
versionName = "0.9.1"
|
||||
|
||||
// We provide our own instrumentation runner if/when tests are added.
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -6,6 +6,7 @@ package space.rcmd.android.sizzle.io
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
@@ -26,6 +27,8 @@ data class AppSettings(
|
||||
val kbdChannel: Int = 0,
|
||||
/** Base octave for the on-screen keyboards. */
|
||||
val kbdOctave: Int = 3,
|
||||
/** Show the live VU meters inside the mixer volume faders. */
|
||||
val vuMeters: Boolean = true,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -47,6 +50,7 @@ class SettingsStore(private val context: Context) {
|
||||
recordTailBars = prefs[RECORD_TAIL] ?: 2,
|
||||
kbdChannel = prefs[KBD_CHANNEL] ?: 0,
|
||||
kbdOctave = prefs[KBD_OCTAVE] ?: 3,
|
||||
vuMeters = prefs[VU_METERS] ?: true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,11 +77,17 @@ class SettingsStore(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist whether the mixer's fader VU meters are shown. */
|
||||
suspend fun saveVuMeters(enabled: Boolean) {
|
||||
context.settingsDataStore.edit { prefs -> prefs[VU_METERS] = enabled }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val PALETTE = stringPreferencesKey("palette")
|
||||
val RECORD_DIR = stringPreferencesKey("recordDirUri")
|
||||
val RECORD_TAIL = intPreferencesKey("recordTailBars")
|
||||
val KBD_CHANNEL = intPreferencesKey("kbdChannel")
|
||||
val KBD_OCTAVE = intPreferencesKey("kbdOctave")
|
||||
val VU_METERS = booleanPreferencesKey("vuMeters")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,15 @@ class AppViewModel(
|
||||
var kbdOctave by mutableIntStateOf(3); private set
|
||||
/** Velocity for keyboard notes (0..127). */
|
||||
var kbdVelocity by mutableIntStateOf(Cell.MAX_VELOCITY); private set
|
||||
/** Whether the mixer's fader VU meters are shown (Settings toggle). */
|
||||
var vuMeters by mutableStateOf(true); private set
|
||||
|
||||
/** Enable/disable the mixer fader VU meters and persist the choice. */
|
||||
fun updateVuMeters(enabled: Boolean) {
|
||||
vuMeters = enabled
|
||||
viewModelScope.launch { settingsStore.saveVuMeters(enabled) }
|
||||
touched()
|
||||
}
|
||||
|
||||
fun updateSkipStep(n: Int) { skipStep = n.coerceIn(0, 8) }
|
||||
fun togglePunchIn() { punchInMode = !punchInMode }
|
||||
@@ -780,6 +789,7 @@ class AppViewModel(
|
||||
// Restore the on-screen keyboard's channel + octave.
|
||||
kbdChannel = s.kbdChannel.coerceIn(0, Cell.MAX_CHANNEL)
|
||||
kbdOctave = s.kbdOctave.coerceIn(0, 8)
|
||||
vuMeters = s.vuMeters
|
||||
engine.recTempDir = appContext.cacheDir
|
||||
engine.setRecordTailBars(recordTailBars)
|
||||
engine.onRecordingStarted = {
|
||||
|
||||
@@ -70,9 +70,16 @@ fun MixerScreen(vm: AppViewModel) {
|
||||
// stable state objects so only the fader's draw phase (which reads them) repaints —
|
||||
// the strips themselves never recompose from meter movement. Writing an unchanged
|
||||
// value is a no-op, so an idle (silent) mixer costs nothing but the frame tick.
|
||||
// The whole poll loop is skipped when VU meters are turned off in Settings.
|
||||
val vuOn = vm.vuMeters
|
||||
val levels = remember { List(Pattern.TRACK_COUNT) { mutableFloatStateOf(0f) } }
|
||||
val clips = remember { List(Pattern.TRACK_COUNT) { mutableStateOf(false) } }
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(vuOn) {
|
||||
if (!vuOn) {
|
||||
// Clear any lingering values so nothing shows if re-enabled while silent.
|
||||
for (ch in 0 until Pattern.TRACK_COUNT) { levels[ch].floatValue = 0f; clips[ch].value = false }
|
||||
return@LaunchedEffect
|
||||
}
|
||||
while (true) {
|
||||
withFrameNanos { }
|
||||
for (ch in 0 until Pattern.TRACK_COUNT) {
|
||||
@@ -89,7 +96,7 @@ fun MixerScreen(vm: AppViewModel) {
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
vm.project.mixer.channels.forEach { channel ->
|
||||
ChannelStrip(vm, channel, levels[channel.index], clips[channel.index],
|
||||
ChannelStrip(vm, channel, levels[channel.index], clips[channel.index], vuOn,
|
||||
Modifier.weight(1f).fillMaxHeight())
|
||||
}
|
||||
}
|
||||
@@ -157,6 +164,7 @@ private fun ChannelStrip(
|
||||
channel: MixerChannel,
|
||||
level: MutableFloatState,
|
||||
clipped: State<Boolean>,
|
||||
showVu: Boolean,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val c = LocalRetro.current
|
||||
@@ -182,12 +190,12 @@ private fun ChannelStrip(
|
||||
Column(
|
||||
Modifier.weight(1f).fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) { ChannelLevels(vm, channel, level, clipped) }
|
||||
) { ChannelLevels(vm, channel, level, clipped, showVu) }
|
||||
}
|
||||
} else {
|
||||
// Portrait: one column, the fader filling the leftover height.
|
||||
ChannelRouting(vm, channel)
|
||||
ChannelLevels(vm, channel, level, clipped)
|
||||
ChannelLevels(vm, channel, level, clipped, showVu)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,6 +252,7 @@ private fun ColumnScope.ChannelLevels(
|
||||
channel: MixerChannel,
|
||||
level: MutableFloatState,
|
||||
clipped: State<Boolean>,
|
||||
showVu: Boolean,
|
||||
) {
|
||||
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // defeat strong-skipping so LIM/mute/solo refresh
|
||||
SectionLabel("Volume")
|
||||
@@ -253,6 +262,7 @@ private fun ColumnScope.ChannelLevels(
|
||||
onValueChange = { channel.volume = it; vm.bumpForToolbar() },
|
||||
level = level,
|
||||
clipped = clipped,
|
||||
showVu = showVu,
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
)
|
||||
// Per-bus soft-clip limiter, sitting above mute/solo.
|
||||
@@ -285,6 +295,7 @@ private fun VerticalFader(
|
||||
onValueChange: (Float) -> Unit,
|
||||
level: MutableFloatState,
|
||||
clipped: State<Boolean>,
|
||||
showVu: Boolean,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val c = LocalRetro.current
|
||||
@@ -308,10 +319,11 @@ private fun VerticalFader(
|
||||
// VU meter, contained within the fill. Colour maps to ABSOLUTE level
|
||||
// over the full track height (bottom green → top red), so a taller bar
|
||||
// is visibly hotter; the gradient's red tip is what "hard clipping turns
|
||||
// to a red hue" looks like as the signal climbs.
|
||||
val lvl = level.floatValue.coerceIn(0f, 1f)
|
||||
// to a red hue" looks like as the signal climbs. Skipped when the VU
|
||||
// meters are turned off in Settings.
|
||||
val lvl = if (showVu) level.floatValue.coerceIn(0f, 1f) else 0f
|
||||
val meterFrac = if (lvl < vol) lvl else vol // stay inside the filled area
|
||||
if (meterFrac > 0.001f) {
|
||||
if (showVu && meterFrac > 0.001f) {
|
||||
val mH = meterFrac * h
|
||||
val top = h - mH
|
||||
if (clipped.value) {
|
||||
|
||||
@@ -47,6 +47,7 @@ import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -93,6 +94,7 @@ fun SettingsScreen(vm: AppViewModel) {
|
||||
item { PanicButton(vm) }
|
||||
item { ProjectCard(vm) }
|
||||
item { AudioDevicesCard(vm) }
|
||||
item { InterfaceCard(vm) }
|
||||
item { ThemeCard(vm) }
|
||||
item {
|
||||
NavCard("Gamepad Bindings", "Rebind buttons & combos", Icons.Filled.SportsEsports) {
|
||||
@@ -368,6 +370,29 @@ private fun AudioDevicesCard(vm: AppViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface options: currently the mixer VU-meter toggle. */
|
||||
@Composable
|
||||
private fun InterfaceCard(vm: AppViewModel) {
|
||||
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // refresh the switch state
|
||||
SettingsCard("Interface", subtitle = "On-screen display options") {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text("Mixer VU meters", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
"Show live level meters inside the mixer volume faders.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(checked = vm.vuMeters, onCheckedChange = { vm.updateVuMeters(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeCard(vm: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Reference in New Issue
Block a user