From 266a625cf3ecbea27295cc2411cf91d88b71f9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 3 Feb 2026 14:42:03 +0100 Subject: [PATCH] WIP: improve Linux audio support --- src/engine/audio.rs | 23 ++++++++++++++++------- src/main.rs | 19 ++++++++++++------- src/state/audio.rs | 2 ++ src/views/engine_view.rs | 40 ++++++++++++++++++++++++++++++++++------ 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 104917b..7d15be6 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -237,6 +237,12 @@ pub struct AudioStreamConfig { pub max_voices: usize, } +pub struct AudioStreamInfo { + pub sample_rate: f32, + pub host_name: String, + pub channels: u16, +} + pub fn build_stream( config: &AudioStreamConfig, audio_rx: Receiver, @@ -245,7 +251,7 @@ pub fn build_stream( metrics: Arc, initial_samples: Vec, audio_sample_pos: Arc, -) -> Result<(Stream, f32, AnalysisHandle), String> { +) -> Result<(Stream, AudioStreamInfo, AnalysisHandle), String> { let device = match &config.output_device { Some(name) => doux::audio::find_output_device(name) .ok_or_else(|| format!("Device not found: {name}"))?, @@ -258,11 +264,8 @@ pub fn build_stream( let max_channels = doux::audio::max_output_channels(&device); let channels = config.channels.min(max_channels); - let is_jack = doux::audio::preferred_host() - .id() - .name() - .to_lowercase() - .contains("jack"); + let host_name = doux::audio::preferred_host().id().name().to_string(); + let is_jack = host_name.to_lowercase().contains("jack"); let buffer_size = if config.buffer_size > 0 && !is_jack { cpal::BufferSize::Fixed(config.buffer_size) @@ -277,6 +280,7 @@ pub fn build_stream( }; let sr = sample_rate; + let effective_channels = channels; let channels = channels as usize; let max_voices = config.max_voices; @@ -347,5 +351,10 @@ pub fn build_stream( stream .play() .map_err(|e| format!("Failed to play stream: {e}"))?; - Ok((stream, sample_rate, analysis_handle)) + let info = AudioStreamInfo { + sample_rate, + host_name, + channels: effective_channels, + }; + Ok((stream, info, analysis_handle)) } diff --git a/src/main.rs b/src/main.rs index 5e69a29..40ea374 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,7 +106,8 @@ fn main() -> io::Result<()> { app.ui.hue_rotation = settings.display.hue_rotation; app.audio.config.layout = settings.display.layout; let base_theme = settings.display.color_scheme.to_theme(); - let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, settings.display.hue_rotation); + let rotated = + cagire_ratatui::theme::transform::rotate_theme(base_theme, settings.display.hue_rotation); theme::set(rotated); // Load MIDI settings @@ -190,9 +191,11 @@ fn main() -> io::Result<()> { initial_samples, Arc::clone(&audio_sample_pos), ) { - Ok((s, sample_rate, analysis)) => { - app.audio.config.sample_rate = sample_rate; - sample_rate_shared.store(sample_rate as u32, Ordering::Relaxed); + Ok((s, info, analysis)) => { + app.audio.config.sample_rate = info.sample_rate; + app.audio.config.host_name = info.host_name; + app.audio.config.channels = info.channels; + sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed); (Some(s), Some(analysis)) } Err(e) => { @@ -244,11 +247,13 @@ fn main() -> io::Result<()> { restart_samples, Arc::clone(&audio_sample_pos), ) { - Ok((new_stream, sr, new_analysis)) => { + Ok((new_stream, info, new_analysis)) => { _stream = Some(new_stream); _analysis_handle = Some(new_analysis); - app.audio.config.sample_rate = sr; - sample_rate_shared.store(sr as u32, Ordering::Relaxed); + app.audio.config.sample_rate = info.sample_rate; + app.audio.config.host_name = info.host_name; + app.audio.config.channels = info.channels; + sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed); app.audio.error = None; app.ui.set_status("Audio restarted".to_string()); } diff --git a/src/state/audio.rs b/src/state/audio.rs index fca19fb..b7d6071 100644 --- a/src/state/audio.rs +++ b/src/state/audio.rs @@ -77,6 +77,7 @@ pub struct AudioConfig { pub buffer_size: u32, pub max_voices: usize, pub sample_rate: f32, + pub host_name: String, pub sample_paths: Vec, pub sample_count: usize, pub refresh_rate: RefreshRate, @@ -95,6 +96,7 @@ impl Default for AudioConfig { buffer_size: 512, max_voices: 32, sample_rate: 44100.0, + host_name: String::new(), sample_paths: Vec::new(), sample_count: 0, refresh_rate: RefreshRate::default(), diff --git a/src/views/engine_view.rs b/src/views/engine_view.rs index c0e26e4..bbd793c 100644 --- a/src/views/engine_view.rs +++ b/src/views/engine_view.rs @@ -135,7 +135,12 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) { let down_indicator = Paragraph::new("▼").style(indicator_style); frame.render_widget( down_indicator, - Rect::new(indicator_x, padded.y + padded.height.saturating_sub(1), 1, 1), + Rect::new( + indicator_x, + padded.y + padded.height.saturating_sub(1), + 1, + 1, + ), ); } } @@ -211,9 +216,13 @@ fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Re Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); let header_style = if focused { - Style::new().fg(theme.engine.header_focused).add_modifier(Modifier::BOLD) + Style::new() + .fg(theme.engine.header_focused) + .add_modifier(Modifier::BOLD) } else { - Style::new().fg(theme.engine.header).add_modifier(Modifier::BOLD) + Style::new() + .fg(theme.engine.header) + .add_modifier(Modifier::BOLD) }; frame.render_widget(Paragraph::new(title).style(header_style), header_area); @@ -292,7 +301,9 @@ fn render_device_column( Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area); let label_style = if focused { - Style::new().fg(theme.engine.focused).add_modifier(Modifier::BOLD) + Style::new() + .fg(theme.engine.focused) + .add_modifier(Modifier::BOLD) } else if section_focused { Style::new().fg(theme.engine.label_focused) } else { @@ -322,7 +333,9 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) { render_section_header(frame, "SETTINGS", section_focused, header_area); - let highlight = Style::new().fg(theme.engine.focused).add_modifier(Modifier::BOLD); + let highlight = Style::new() + .fg(theme.engine.focused) + .add_modifier(Modifier::BOLD); let normal = Style::new().fg(theme.engine.normal); let label_style = Style::new().fg(theme.engine.label); let value_style = Style::new().fg(theme.engine.value); @@ -373,7 +386,11 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) { label_style, ), render_selector( - &format!("{}", app.audio.config.buffer_size), + &if app.audio.config.host_name.to_lowercase().contains("jack") { + "JACK managed".to_string() + } else { + format!("{}", app.audio.config.buffer_size) + }, buffer_focused, highlight, normal, @@ -420,6 +437,17 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) { value_style, ), ]), + Row::new(vec![ + Span::styled(" Audio host", label_style), + Span::styled( + if app.audio.config.host_name.is_empty() { + "-".to_string() + } else { + app.audio.config.host_name.clone() + }, + value_style, + ), + ]), ]; let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);