fix frame interval
This commit is contained in:
42
src/app.rs
42
src/app.rs
@@ -40,6 +40,9 @@ pub struct App {
|
|||||||
chains: Vec<EffectChain>, // one per deck, in order
|
chains: Vec<EffectChain>, // one per deck, in order
|
||||||
osc_rx: crossbeam_channel::Receiver<OscAppCmd>,
|
osc_rx: crossbeam_channel::Receiver<OscAppCmd>,
|
||||||
audio_dir: PathBuf,
|
audio_dir: PathBuf,
|
||||||
|
|
||||||
|
/// Track if we need to redraw the UI
|
||||||
|
dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -91,6 +94,7 @@ impl App {
|
|||||||
chains,
|
chains,
|
||||||
osc_rx,
|
osc_rx,
|
||||||
audio_dir,
|
audio_dir,
|
||||||
|
dirty: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +139,36 @@ impl App {
|
|||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
loop {
|
// Track if any deck is playing to determine frame rate
|
||||||
terminal.draw(|f| tui::draw(f, self))?;
|
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() {
|
while let Ok(status) = self.audio.status_rx.try_recv() {
|
||||||
self.handle_audio_status(status);
|
self.handle_audio_status(status);
|
||||||
|
status_count += 1;
|
||||||
|
if status_count >= 100 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain OSC commands
|
// Drain OSC commands
|
||||||
@@ -148,8 +176,8 @@ impl App {
|
|||||||
if self.handle_osc_cmd(cmd)? { return Ok(()); }
|
if self.handle_osc_cmd(cmd)? { return Ok(()); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard (16ms poll → ~60fps)
|
// Keyboard (use adaptive poll interval)
|
||||||
if event::poll(Duration::from_millis(16))? {
|
if event::poll(frame_interval)? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if self.handle_key(key)? { return Ok(()); }
|
if self.handle_key(key)? { return Ok(()); }
|
||||||
}
|
}
|
||||||
@@ -186,6 +214,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
AudioStatus::Position { deck, position } => {
|
AudioStatus::Position { deck, position } => {
|
||||||
self.decks[deck as usize].position = position;
|
self.decks[deck as usize].position = position;
|
||||||
|
self.dirty = true;
|
||||||
osc::send_osc(
|
osc::send_osc(
|
||||||
&format!("/deck/{}/position", deck as usize),
|
&format!("/deck/{}/position", deck as usize),
|
||||||
vec![rosc::OscType::Float(
|
vec![rosc::OscType::Float(
|
||||||
@@ -197,6 +226,7 @@ impl App {
|
|||||||
let d = &mut self.decks[deck as usize];
|
let d = &mut self.decks[deck as usize];
|
||||||
d.vu.0 = (d.vu.0 * 0.65).max(l);
|
d.vu.0 = (d.vu.0 * 0.65).max(l);
|
||||||
d.vu.1 = (d.vu.1 * 0.65).max(r);
|
d.vu.1 = (d.vu.1 * 0.65).max(r);
|
||||||
|
self.dirty = true;
|
||||||
osc::send_osc(
|
osc::send_osc(
|
||||||
&format!("/deck/{}/vu", deck as usize),
|
&format!("/deck/{}/vu", deck as usize),
|
||||||
vec![rosc::OscType::Float(l), rosc::OscType::Float(r)],
|
vec![rosc::OscType::Float(l), rosc::OscType::Float(r)],
|
||||||
@@ -204,10 +234,12 @@ impl App {
|
|||||||
}
|
}
|
||||||
AudioStatus::TrackEnded(deck) => {
|
AudioStatus::TrackEnded(deck) => {
|
||||||
self.decks[deck as usize].play_state = PlayState::Stopped;
|
self.decks[deck as usize].play_state = PlayState::Stopped;
|
||||||
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
AudioStatus::Error { deck, msg } => {
|
AudioStatus::Error { deck, msg } => {
|
||||||
tracing::error!("Deck {:?}: {}", deck, msg);
|
tracing::error!("Deck {:?}: {}", deck, msg);
|
||||||
self.decks[deck as usize].track_name = Some(format!("ERR: {}", msg));
|
self.decks[deck as usize].track_name = Some(format!("ERR: {}", msg));
|
||||||
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/audio.rs
21
src/audio.rs
@@ -79,6 +79,9 @@ struct DeckState {
|
|||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
read_pos: usize, // byte index into pcm (always even)
|
read_pos: usize, // byte index into pcm (always even)
|
||||||
cue_pos: usize,
|
cue_pos: usize,
|
||||||
|
|
||||||
|
/// Track last VU update time to rate-limit updates
|
||||||
|
last_vu_update: std::time::Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeckState {
|
impl DeckState {
|
||||||
@@ -90,6 +93,7 @@ impl DeckState {
|
|||||||
sample_rate: 44100,
|
sample_rate: 44100,
|
||||||
read_pos: 0,
|
read_pos: 0,
|
||||||
cue_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,
|
deck: ds.id,
|
||||||
position: pos,
|
position: pos,
|
||||||
});
|
});
|
||||||
let _ = status_tx_cb.try_send(AudioStatus::Vu {
|
|
||||||
deck: ds.id,
|
// Rate-limit VU updates to ~60Hz (16ms) to reduce CPU usage
|
||||||
l: vu_l,
|
// while still providing smooth updates during playback
|
||||||
r: vu_r,
|
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
|
.register()?; // Connect the stream with F32LE stereo format
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ impl EffectChain {
|
|||||||
value = value,
|
value = value,
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::info!(
|
||||||
"set-param deck {} node {} Props {}",
|
"set-param deck {} node {} Props {}",
|
||||||
self.deck.label(),
|
self.deck.label(),
|
||||||
node_id,
|
node_id,
|
||||||
|
|||||||
13
src/tui.rs
13
src/tui.rs
@@ -109,7 +109,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) {
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.title(title)
|
.title(title)
|
||||||
.border_style(border_style)
|
.border_style(border_style)
|
||||||
.style(Style::default().bg(Color::Indexed(0)));
|
.style(Style::default());
|
||||||
f.render_widget(outer, area);
|
f.render_widget(outer, area);
|
||||||
|
|
||||||
let inner = area.inner(ratatui::layout::Margin { horizontal: 1, vertical: 1 });
|
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
|
// Progress
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Gauge::default()
|
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)
|
.percent((deck.progress() * 100.0) as u16)
|
||||||
.label(""),
|
.label(""),
|
||||||
rows[1],
|
rows[1],
|
||||||
@@ -153,8 +153,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) {
|
|||||||
f.render_widget(
|
f.render_widget(
|
||||||
Gauge::default()
|
Gauge::default()
|
||||||
.gauge_style(Style::default()
|
.gauge_style(Style::default()
|
||||||
.fg(if fader_pct > 10 { Color::White } else { Color::DarkGray })
|
.fg(if fader_pct > 10 { Color::White } else { Color::DarkGray }))
|
||||||
.bg(Color::Indexed(0)))
|
|
||||||
.percent(fader_pct.min(100))
|
.percent(fader_pct.min(100))
|
||||||
.label(format!("VOL {:3}%", fader_pct)),
|
.label(format!("VOL {:3}%", fader_pct)),
|
||||||
rows[3],
|
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 lp = (vu.0 * 100.0) as u16;
|
||||||
let rp = (vu.1 * 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(lp))).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(rp))).percent(rp.min(100)).label("R"), ra);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_help(f: &mut Frame, area: Rect) {
|
fn draw_help(f: &mut Frame, area: Rect) {
|
||||||
@@ -225,7 +224,7 @@ fn draw_help(f: &mut Frame, area: Rect) {
|
|||||||
.block(Block::default()
|
.block(Block::default()
|
||||||
.borders(Borders::TOP)
|
.borders(Borders::TOP)
|
||||||
.border_style(Style::default().fg(Color::DarkGray)))
|
.border_style(Style::default().fg(Color::DarkGray)))
|
||||||
.style(Style::default().bg(Color::Indexed(0)));
|
.style(Style::default());
|
||||||
|
|
||||||
f.render_widget(help, area);
|
f.render_widget(help, area);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user