valentine

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

commit 9d02e362e6366d46b63e5282b34f919a9c37a2ce
parent b873468a5e13f618f1058f5f4d6d5256f6229b2d
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Mon,  1 Jun 2026 14:58:56 -0500

fix: coalesce fader writes per-frame so the fader actually works

Previous drain dropped queued events (fader did nothing); before that, a
per-keypress 8-write fader cascaded to silence. Now a fader keypress only
updates the in-memory level + flags a pending write; all queued key events are
processed in one batch per frame, then flush_pending_writes does ONE device
write at the final value. Holding the key ramps smoothly; no cascade, stays
responsive.

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

Diffstat:
Mvalentine/src/main.rs | 52+++++++++++++++++++++++++++++++++++++---------------
1 file changed, 37 insertions(+), 15 deletions(-)

diff --git a/valentine/src/main.rs b/valentine/src/main.rs @@ -164,6 +164,9 @@ struct App { 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, @@ -193,6 +196,7 @@ impl App { 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(), @@ -602,7 +606,10 @@ impl App { } } - /// Adjust the selected group's software level by `delta` dB. + /// 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. fn nudge_group(&mut self, delta: f32) { let gi = self.monitor_group; if !self.group_via_mixer.get(gi).copied().unwrap_or(false) { @@ -610,20 +617,30 @@ impl App { return; } let g = &scarlett_core::matrix::MONITOR_GROUPS[gi]; - let inputs = S18I20_GEN3.mixer_inputs() as usize; 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)); + } + + /// 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, }; - match dev.scarlett.set_group_level(g, new_db, inputs) { - Ok(()) => { - self.group_level_db[gi] = new_db; - dev.mixer.clear(); - self.status = Some(format!("{} level {new_db:.0} dB", g.name)); - } - Err(e) => self.status = Some(format!("level set failed: {e}")), + if let Err(e) = dev.scarlett.set_group_level(g, db, inputs) { + self.status = Some(format!("level set failed: {e}")); + } else { + dev.mixer.clear(); } } @@ -1031,19 +1048,24 @@ fn run<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> { while !app.should_quit { terminal.draw(|f| ui(f, app))?; if event::poll(Duration::from_millis(100))? { + // Process the first event, then every other one already queued, in + // one batch. Fader keys only update an in-memory level (cheap), so a + // held/repeated key collapses into the final value; the actual (slow, + // multi-write) device update happens once below, after the batch — + // no cascade, stays responsive. if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { app.on_key(key.code); } } - // Drain any input that queued up while on_key ran. Each control - // change can take many USB writes (e.g. 8 for an ADAT group fader), - // during which held/repeated keys pile up in the terminal buffer; - // without draining, that backlog replays and a single press cascades - // (a fader would run straight to silence). One action per redraw. while event::poll(Duration::from_millis(0))? { - let _ = event::read()?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + app.on_key(key.code); + } + } } + app.flush_pending_writes(); } app.tick(); }