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:
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();
}