commit 41649b95e76d4e15572f71771b4b48abcba2f10c
parent 8291d0f2b2aa4f48658acc4421a3b08a4065f72f
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date: Mon, 1 Jun 2026 15:18:38 -0500
fix: faders effective by default — no toggle needed
The adatfader probe proved the device chain works (route+level land at -40dB);
the failure was the TUI gating the fader behind a 'v' toggle that fought the
already-via-mixer state. Now: ←→ owns the routing (auto-routes the group via
the mixer on first use), Monitor-tab entry detects already-routed groups and
SEEDS each fader from the device's actual current level (get_group_level), and
the fader bar always shows. 'v' is now just an optional back-to-DAW. No toggling
required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat:
3 files changed, 77 insertions(+), 33 deletions(-)
diff --git a/scarlett-core/src/matrix.rs b/scarlett-core/src/matrix.rs
@@ -188,6 +188,18 @@ impl<T: Transport> Scarlett<T> {
Ok(())
}
+ /// Read a group's current level (dB) — the gain of its first bus on its
+ /// first mixer-input. Meaningful only when the group is routed via the mixer.
+ pub fn get_group_level(
+ &mut self,
+ g: &MonitorGroup,
+ inputs: usize,
+ ) -> Result<f32, TransportError> {
+ let raw = self.get_mix(g.bus_base, inputs)?;
+ let idx = g.mix_in_base as usize;
+ Ok(raw.get(idx).map(|&v| mixer_value_to_db(v)).unwrap_or(MIXER_MIN_DB))
+ }
+
/// True if a group is currently routed via the mixer (its first output is fed
/// by its first mix bus).
pub fn group_is_via_mixer(
diff --git a/valentine/src/main.rs b/valentine/src/main.rs
@@ -293,12 +293,20 @@ impl App {
/// Lazy-load data a freshly-opened tab needs.
fn on_tab_enter(&mut self) {
if self.tab == 1 {
- // Detect which monitor groups are currently routed via the mixer.
+ // Detect which monitor groups are already routed via the mixer, and
+ // seed each fader from the device's actual current level so it's live
+ // and accurate without any toggle.
let pc = scarlett_core::mux::PORT_COUNT_18I20_GEN3;
+ let inputs = S18I20_GEN3.mixer_inputs() as usize;
if let Ok(dev) = &mut self.device {
for (i, g) in scarlett_core::matrix::MONITOR_GROUPS.iter().enumerate() {
- self.group_via_mixer[i] =
- dev.scarlett.group_is_via_mixer(pc, g).unwrap_or(false);
+ let via = dev.scarlett.group_is_via_mixer(pc, g).unwrap_or(false);
+ self.group_via_mixer[i] = via;
+ if via {
+ if let Ok(db) = dev.scarlett.get_group_level(g, inputs) {
+ self.group_level_db[i] = db;
+ }
+ }
}
}
}
@@ -606,15 +614,21 @@ impl App {
}
}
- /// Adjust the selected group's software level by `delta` dB. This only
- /// updates the in-memory level and flags a pending device write — the actual
- /// (slow, multi-channel) write happens once per frame in
- /// [`Self::flush_pending_writes`], so rapid keypresses coalesce.
+ /// Adjust the selected group's software level by `delta` dB.
+ ///
+ /// The fader OWNS the routing: if the group isn't yet routed through the
+ /// mixer, the first nudge routes it (so the fader always works without the
+ /// user having to toggle `v` first). The level change itself only updates the
+ /// in-memory value + flags a pending write, coalesced once per frame in
+ /// [`Self::flush_pending_writes`].
fn nudge_group(&mut self, delta: f32) {
let gi = self.monitor_group;
+ // Ensure the group is routed via the mixer so the fader has an effect.
if !self.group_via_mixer.get(gi).copied().unwrap_or(false) {
- self.status = Some("fader needs mixer routing — press 'v' to enable".into());
- return;
+ self.enable_group_via_mixer(gi);
+ if !self.group_via_mixer.get(gi).copied().unwrap_or(false) {
+ return; // routing failed; status already set
+ }
}
let g = &scarlett_core::matrix::MONITOR_GROUPS[gi];
let new_db =
@@ -624,6 +638,33 @@ impl App {
self.status = Some(format!("{} level {new_db:.0} dB", g.name));
}
+ /// Route a group through the mixer and seed its bus gains at the current
+ /// in-memory level. Idempotent-ish; sets `group_via_mixer[gi]` on success.
+ fn enable_group_via_mixer(&mut self, gi: usize) {
+ let g = &scarlett_core::matrix::MONITOR_GROUPS[gi];
+ let pc = scarlett_core::mux::PORT_COUNT_18I20_GEN3;
+ let inputs = S18I20_GEN3.mixer_inputs() as usize;
+ let level = self.group_level_db[gi];
+ let dev = match &mut self.device {
+ Ok(d) => d,
+ Err(_) => return,
+ };
+ match dev
+ .scarlett
+ .route_group_via_mixer(pc, g)
+ .and_then(|st| {
+ dev.routing = Some(st);
+ dev.scarlett.set_group_level(g, level, inputs)
+ }) {
+ Ok(()) => {
+ self.group_via_mixer[gi] = true;
+ dev.mixer.clear();
+ dev.refresh_src_meter();
+ }
+ Err(e) => self.status = Some(format!("routing failed: {e}")),
+ }
+ }
+
/// Write any pending group-level change to the device (once per frame).
fn flush_pending_writes(&mut self) {
let gi = match self.pending_group_write.take() {
diff --git a/valentine/src/panels/monitor.rs b/valentine/src/panels/monitor.rs
@@ -119,39 +119,30 @@ pub fn render(
rows[1],
);
- // Software monitor fader (active only when routed via the mixer).
+ // Software monitor fader — ←→ just works (auto-routes via mixer on use).
let fader_label = if via_mixer {
- "monitor fader (via mixer) ←→ adjust:"
+ "monitor fader ←→ adjust:"
} else {
- "monitor fader — press 'v' to route via mixer & enable:"
+ "monitor fader ←→ adjust (routes via mixer on first use):"
};
f.render_widget(
Paragraph::new(Span::styled(
fader_label,
- Style::default().fg(if via_mixer { theme.fg } else { theme.fg_dim }),
+ Style::default().fg(theme.fg),
)),
rows[3],
);
- if via_mixer {
- // soft_db in MIXER_MIN..0 → ratio
- let min = scarlett_core::matrix::MIXER_MIN_DB;
- let r = ((soft_db - min) / -min).clamp(0.0, 1.0) as f64;
- f.render_widget(
- Gauge::default()
- .gauge_style(Style::default().fg(theme.armed).bg(theme.bg_elevated))
- .ratio(r)
- .label(format!("{soft_db:.0} dB")),
- rows[4],
- );
- } else {
- f.render_widget(
- Paragraph::new(Span::styled(
- " (monitors currently direct from DAW)",
- Style::default().fg(theme.fg_dim),
- )),
- rows[4],
- );
- }
+ // Always show the fader bar; dimmed until it's driving the mixer path.
+ let min = scarlett_core::matrix::MIXER_MIN_DB;
+ let r = ((soft_db - min) / -min).clamp(0.0, 1.0) as f64;
+ let bar_fg = if via_mixer { theme.armed } else { theme.fg_dim };
+ f.render_widget(
+ Gauge::default()
+ .gauge_style(Style::default().fg(bar_fg).bg(theme.bg_elevated))
+ .ratio(r)
+ .label(format!("{soft_db:.0} dB")),
+ rows[4],
+ );
// Toggle rows (mute / dim).
for (i, row) in Row::ALL.iter().enumerate() {