From 09a954ef18f3d279f65337a28764c0df5d91426b Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Sat, 7 Mar 2026 18:31:08 +0100 Subject: [PATCH] fix frame interval --- src/app.rs | 42 +++++++++++++++++++++++++++++++++++++----- src/audio.rs | 21 ++++++++++++++++----- src/effect_chain.rs | 2 +- src/tui.rs | 13 ++++++------- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7dc770d..a61556f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -40,6 +40,9 @@ pub struct App { chains: Vec, // one per deck, in order osc_rx: crossbeam_channel::Receiver, audio_dir: PathBuf, + + /// Track if we need to redraw the UI + dirty: bool, } impl App { @@ -91,6 +94,7 @@ impl App { chains, osc_rx, audio_dir, + dirty: true, }) } @@ -135,12 +139,36 @@ impl App { &mut self, terminal: &mut Terminal>, ) -> Result<()> { - loop { - terminal.draw(|f| tui::draw(f, self))?; + // Track if any deck is playing to determine frame rate + let mut playing = false; + let mut last_frame_time = std::time::Instant::now(); - // Drain audio status + loop { + // Check if any deck is playing + playing = self.decks.iter().any(|d| d.play_state == PlayState::Playing); + + // Use adaptive frame rate: 60fps when playing, 30fps when idle + let frame_interval = if playing { + Duration::from_millis(16) // ~60fps + } else { + Duration::from_millis(33) // ~30fps + }; + + // Only redraw if dirty or if we're at the right time + if self.dirty || last_frame_time.elapsed() >= frame_interval { + terminal.draw(|f| tui::draw(f, self))?; + self.dirty = false; + last_frame_time = std::time::Instant::now(); + } + + // Drain audio status (limit to 100 messages per frame to prevent spinning) + let mut status_count = 0; while let Ok(status) = self.audio.status_rx.try_recv() { self.handle_audio_status(status); + status_count += 1; + if status_count >= 100 { + break; + } } // Drain OSC commands @@ -148,8 +176,8 @@ impl App { if self.handle_osc_cmd(cmd)? { return Ok(()); } } - // Keyboard (16ms poll → ~60fps) - if event::poll(Duration::from_millis(16))? { + // Keyboard (use adaptive poll interval) + if event::poll(frame_interval)? { if let Event::Key(key) = event::read()? { if self.handle_key(key)? { return Ok(()); } } @@ -186,6 +214,7 @@ impl App { } AudioStatus::Position { deck, position } => { self.decks[deck as usize].position = position; + self.dirty = true; osc::send_osc( &format!("/deck/{}/position", deck as usize), vec![rosc::OscType::Float( @@ -197,6 +226,7 @@ impl App { let d = &mut self.decks[deck as usize]; d.vu.0 = (d.vu.0 * 0.65).max(l); d.vu.1 = (d.vu.1 * 0.65).max(r); + self.dirty = true; osc::send_osc( &format!("/deck/{}/vu", deck as usize), vec![rosc::OscType::Float(l), rosc::OscType::Float(r)], @@ -204,10 +234,12 @@ impl App { } AudioStatus::TrackEnded(deck) => { self.decks[deck as usize].play_state = PlayState::Stopped; + self.dirty = true; } AudioStatus::Error { deck, msg } => { tracing::error!("Deck {:?}: {}", deck, msg); self.decks[deck as usize].track_name = Some(format!("ERR: {}", msg)); + self.dirty = true; } } } diff --git a/src/audio.rs b/src/audio.rs index 9977fa1..04e083f 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -79,6 +79,9 @@ struct DeckState { sample_rate: u32, read_pos: usize, // byte index into pcm (always even) cue_pos: usize, + + /// Track last VU update time to rate-limit updates + last_vu_update: std::time::Instant, } impl DeckState { @@ -90,6 +93,7 @@ impl DeckState { sample_rate: 44100, read_pos: 0, cue_pos: 0, + last_vu_update: std::time::Instant::now(), } } } @@ -220,11 +224,18 @@ fn engine_thread(cmd_rx: Receiver, status_tx: Sender) -> deck: ds.id, position: pos, }); - let _ = status_tx_cb.try_send(AudioStatus::Vu { - deck: ds.id, - l: vu_l, - r: vu_r, - }); + + // Rate-limit VU updates to ~60Hz (16ms) to reduce CPU usage + // while still providing smooth updates during playback + let now = std::time::Instant::now(); + if now.duration_since(ds.last_vu_update) >= std::time::Duration::from_millis(16) { + let _ = status_tx_cb.try_send(AudioStatus::Vu { + deck: ds.id, + l: vu_l, + r: vu_r, + }); + ds.last_vu_update = now; + } } }) .register()?; // Connect the stream with F32LE stereo format diff --git a/src/effect_chain.rs b/src/effect_chain.rs index 7cfbb54..e1ae8e2 100644 --- a/src/effect_chain.rs +++ b/src/effect_chain.rs @@ -186,7 +186,7 @@ impl EffectChain { value = value, ); - tracing::debug!( + tracing::info!( "set-param deck {} node {} Props {}", self.deck.label(), node_id, diff --git a/src/tui.rs b/src/tui.rs index 726e4c6..7dbd27c 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -109,7 +109,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) { .borders(Borders::ALL) .title(title) .border_style(border_style) - .style(Style::default().bg(Color::Indexed(0))); + .style(Style::default()); f.render_widget(outer, area); let inner = area.inner(ratatui::layout::Margin { horizontal: 1, vertical: 1 }); @@ -142,7 +142,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) { // Progress f.render_widget( Gauge::default() - .gauge_style(Style::default().fg(color).bg(Color::Indexed(0))) + .gauge_style(Style::default().fg(color)) .percent((deck.progress() * 100.0) as u16) .label(""), rows[1], @@ -153,8 +153,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) { f.render_widget( Gauge::default() .gauge_style(Style::default() - .fg(if fader_pct > 10 { Color::White } else { Color::DarkGray }) - .bg(Color::Indexed(0))) + .fg(if fader_pct > 10 { Color::White } else { Color::DarkGray })) .percent(fader_pct.min(100)) .label(format!("VOL {:3}%", fader_pct)), rows[3], @@ -191,8 +190,8 @@ fn draw_vu(f: &mut Frame, vu: (f32, f32), area: Rect, color: Color) { let lp = (vu.0 * 100.0) as u16; let rp = (vu.1 * 100.0) as u16; - f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(lp)).bg(Color::Indexed(0))).percent(lp.min(100)).label("L"), la); - f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(rp)).bg(Color::Indexed(0))).percent(rp.min(100)).label("R"), ra); + f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(lp))).percent(lp.min(100)).label("L"), la); + f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(rp))).percent(rp.min(100)).label("R"), ra); } fn draw_help(f: &mut Frame, area: Rect) { @@ -225,7 +224,7 @@ fn draw_help(f: &mut Frame, area: Rect) { .block(Block::default() .borders(Borders::TOP) .border_style(Style::default().fg(Color::DarkGray))) - .style(Style::default().bg(Color::Indexed(0))); + .style(Style::default()); f.render_widget(help, area); }