valentine

Terminal control panel for the Focusrite Scarlett 18i20 — a from-scratch replacement for Focusrite Control.
Log | Files | Refs | README | LICENSE

commit 50199c69e1068614cec5a8dbfa5ed2e158a79f82
parent d6d4dfeaf98ef73310e931708f610c309db77dc2
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Mon,  1 Jun 2026 17:54:53 -0500

revert: remove monitor-via-mixer fader from the UI (device-safe)

The mix-insert output fader was the only thing that could corrupt routing and
wedge the device (SET_MUX 0x3). Removed it from the Monitor tab + app state:
monitor_key now only does mute/dim; panel shows the read-only hardware knob +
mute/dim. Confirmed with the user this device has no writable output volume and
the ADAT loudness is downstream (external converter / monitor knobs), not
fixable in software. Core group/route methods kept (used by spike diagnostics).
Everything else intact: routing editor, presets, per-input meters, themes,
input toggles, mixer + clear. 67 tests, 0 warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Diffstat:
Mvalentine/src/main.rs | 170+------------------------------------------------------------------------------
Mvalentine/src/panels/monitor.rs | 46++++++----------------------------------------
2 files changed, 7 insertions(+), 209 deletions(-)

diff --git a/valentine/src/main.rs b/valentine/src/main.rs @@ -162,16 +162,6 @@ struct App { modal: Modal, /// Inputs panel: stereo-pair view (default) vs. one row per channel. stereo_inputs: bool, - /// Which monitor group is selected on the Monitor tab (index into - /// `scarlett_core::matrix::MONITOR_GROUPS`: 0 = Analogue 1-2, 1 = ADAT 1-8). - monitor_group: usize, - /// Per-group software level in dB (the group's mix-bus gain when via mixer). - group_level_db: Vec<f32>, - /// Per-group: is it currently routed through the mixer (fader active)? - group_via_mixer: Vec<bool>, - /// Group index whose level changed in-memory and needs a device write on the - /// next frame (coalesces rapid fader keypresses into one write). - pending_group_write: Option<usize>, status: Option<String>, show_help: bool, last_poll: Instant, @@ -203,10 +193,6 @@ impl App { routing_pending: None, modal: Modal::None, stereo_inputs: true, - monitor_group: 0, - group_level_db: vec![0.0; scarlett_core::matrix::MONITOR_GROUPS.len()], - group_via_mixer: vec![false; scarlett_core::matrix::MONITOR_GROUPS.len()], - pending_group_write: None, status: None, show_help: false, last_poll: Instant::now(), @@ -312,24 +298,6 @@ 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 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() { - 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; - } - } - } - } - } if self.tab == 2 { if let Ok(dev) = &mut self.device { dev.load_mixer(); @@ -645,138 +613,14 @@ impl App { } fn monitor_key(&mut self, code: KeyCode) { - let ngroups = scarlett_core::matrix::MONITOR_GROUPS.len(); match code { KeyCode::Up | KeyCode::Char('k') => self.monitor_cursor.up(), KeyCode::Down | KeyCode::Char('j') => self.monitor_cursor.down(), KeyCode::Char(' ') | KeyCode::Enter => self.toggle_monitor_button(), - // Tab/[ ] selects which monitor group the fader controls. - KeyCode::Char('[') => { - self.monitor_group = self.monitor_group.saturating_sub(1); - } - KeyCode::Char(']') => { - if self.monitor_group + 1 < ngroups { - self.monitor_group += 1; - } - } - // Software fader for the selected group (works when via mixer). - KeyCode::Left | KeyCode::Char('-') => self.nudge_group(-2.0), - KeyCode::Right | KeyCode::Char('=') | KeyCode::Char('+') => self.nudge_group(2.0), - // Toggle routing the selected group through the mixer. - KeyCode::Char('v') => self.toggle_group_via_mixer(), _ => {} } } - /// 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.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 = - (self.group_level_db[gi] + delta).clamp(scarlett_core::matrix::MIXER_MIN_DB, 0.0); - self.group_level_db[gi] = new_db; - self.pending_group_write = Some(gi); - 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() { - Some(g) => g, - None => return, - }; - let g = &scarlett_core::matrix::MONITOR_GROUPS[gi]; - let inputs = S18I20_GEN3.mixer_inputs() as usize; - let db = self.group_level_db[gi]; - let dev = match &mut self.device { - Ok(d) => d, - Err(_) => return, - }; - if let Err(e) = dev.scarlett.set_group_level(g, db, inputs) { - self.status = Some(format!("level set failed: {e}")); - } else { - dev.mixer.clear(); - } - } - - /// Toggle whether the selected group runs through the mixer (fader on) or - /// straight from the DAW. - fn toggle_group_via_mixer(&mut self) { - let gi = self.monitor_group; - 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 enable = !self.group_via_mixer.get(gi).copied().unwrap_or(false); - let level = self.group_level_db[gi]; - let dev = match &mut self.device { - Ok(d) => d, - Err(_) => return, - }; - let res = if enable { - dev.scarlett.route_group_via_mixer(pc, g).and_then(|st| { - dev.routing = Some(st); - dev.scarlett.set_group_level(g, level, inputs) - }) - } else { - dev.scarlett.route_group_direct(pc, g).map(|st| { - dev.routing = Some(st); - }) - }; - match res { - Ok(()) => { - self.group_via_mixer[gi] = enable; - dev.mixer.clear(); - dev.refresh_src_meter(); - self.status = Some(if enable { - format!("{} via mixer ON — ←→ adjusts level", g.name) - } else { - format!("{} direct from DAW (fader off)", g.name) - }); - } - Err(e) => self.status = Some(format!("routing failed: {e}")), - } - } - fn toggle_monitor_button(&mut self) { let row = self.monitor_cursor.current(); let dev = match &mut self.device { @@ -1194,7 +1038,6 @@ fn run<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> { } } } - app.flush_pending_writes(); } app.tick(); } @@ -1260,18 +1103,7 @@ fn ui(f: &mut Frame, app: &App) { ); } (Ok(dev), 1) => { - let gi = app.monitor_group; - monitor::render( - f, - chunks[2], - t, - &dev.monitor, - app.monitor_cursor, - scarlett_core::matrix::MONITOR_GROUPS[gi].name, - app.group_via_mixer[gi], - app.group_level_db[gi], - true, - ); + monitor::render(f, chunks[2], t, &dev.monitor, app.monitor_cursor, true); } (Ok(dev), 2) => { mixer::render(f, chunks[2], t, &dev.mixer, app.mixer_cursor, true); diff --git a/valentine/src/panels/monitor.rs b/valentine/src/panels/monitor.rs @@ -63,24 +63,17 @@ fn row_on(state: &MonitorState, row: Row) -> bool { } } -#[allow(clippy::too_many_arguments)] pub fn render( f: &mut Frame, area: Rect, theme: &Theme, state: &MonitorState, cursor: Cursor, - group_name: &str, - via_mixer: bool, - soft_db: f32, focused: bool, ) { let border = if focused { theme.border_focus } else { theme.border }; let block = Block::default() - .title(Span::styled( - format!(" monitor · {group_name} "), - Style::default().fg(theme.accent), - )) + .title(Span::styled(" monitor ", Style::default().fg(theme.accent))) .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(border)); @@ -91,16 +84,14 @@ pub fn render( Constraint::Length(1), // hardware-knob label Constraint::Length(1), // hardware-knob gauge Constraint::Length(1), // spacer - Constraint::Length(1), // software fader label - Constraint::Length(1), // software fader gauge - Constraint::Length(1), // spacer Constraint::Length(1), // mute Constraint::Length(1), // dim Constraint::Min(0), // help ]) .split(inner); - // Hardware monitor knob (read-only). + // Hardware monitor knob (read-only — this device exposes no writable output + // volume; the physical knob and your monitors/converter set the level). let db = state.master_db; let ratio = ((db + VOLUME_BIAS) as f64 / VOLUME_BIAS as f64).clamp(0.0, 1.0); f.render_widget( @@ -119,31 +110,6 @@ pub fn render( rows[1], ); - // Software monitor fader — ←→ just works (auto-routes via mixer on use). - let fader_label = if via_mixer { - "monitor fader ←→ adjust:" - } else { - "monitor fader ←→ adjust (routes via mixer on first use):" - }; - f.render_widget( - Paragraph::new(Span::styled( - fader_label, - Style::default().fg(theme.fg), - )), - rows[3], - ); - // 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() { let row = *row; @@ -169,14 +135,14 @@ pub fn render( }, ), ]); - f.render_widget(Paragraph::new(line), rows[6 + i]); + f.render_widget(Paragraph::new(line), rows[3 + i]); } f.render_widget( Paragraph::new(Span::styled( - "↑↓ select space toggle ←→ fader v via-mixer [ ] group", + "↑↓ select space/enter toggle", Style::default().fg(theme.fg_dim), )), - rows[8], + rows[5], ); }