fix frame interval

This commit is contained in:
2026-03-07 18:31:08 +01:00
parent 6ff7fe8af2
commit 09a954ef18
4 changed files with 60 additions and 18 deletions

View File

@@ -40,6 +40,9 @@ pub struct App {
chains: Vec<EffectChain>, // one per deck, in order
osc_rx: crossbeam_channel::Receiver<OscAppCmd>,
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<CrosstermBackend<io::Stdout>>,
) -> 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;
}
}
}

View File

@@ -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<AudioCmd>, status_tx: Sender<AudioStatus>) ->
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

View File

@@ -186,7 +186,7 @@ impl EffectChain {
value = value,
);
tracing::debug!(
tracing::info!(
"set-param deck {} node {} Props {}",
self.deck.label(),
node_id,

View File

@@ -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);
}