WIP: improve Linux audio support

This commit is contained in:
2026-02-03 14:42:03 +01:00
parent 243f76ce05
commit 266a625cf3
4 changed files with 64 additions and 20 deletions

View File

@@ -237,6 +237,12 @@ pub struct AudioStreamConfig {
pub max_voices: usize, pub max_voices: usize,
} }
pub struct AudioStreamInfo {
pub sample_rate: f32,
pub host_name: String,
pub channels: u16,
}
pub fn build_stream( pub fn build_stream(
config: &AudioStreamConfig, config: &AudioStreamConfig,
audio_rx: Receiver<AudioCommand>, audio_rx: Receiver<AudioCommand>,
@@ -245,7 +251,7 @@ pub fn build_stream(
metrics: Arc<EngineMetrics>, metrics: Arc<EngineMetrics>,
initial_samples: Vec<doux::sampling::SampleEntry>, initial_samples: Vec<doux::sampling::SampleEntry>,
audio_sample_pos: Arc<AtomicU64>, audio_sample_pos: Arc<AtomicU64>,
) -> Result<(Stream, f32, AnalysisHandle), String> { ) -> Result<(Stream, AudioStreamInfo, AnalysisHandle), String> {
let device = match &config.output_device { let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name) Some(name) => doux::audio::find_output_device(name)
.ok_or_else(|| format!("Device not found: {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 max_channels = doux::audio::max_output_channels(&device);
let channels = config.channels.min(max_channels); let channels = config.channels.min(max_channels);
let is_jack = doux::audio::preferred_host() let host_name = doux::audio::preferred_host().id().name().to_string();
.id() let is_jack = host_name.to_lowercase().contains("jack");
.name()
.to_lowercase()
.contains("jack");
let buffer_size = if config.buffer_size > 0 && !is_jack { let buffer_size = if config.buffer_size > 0 && !is_jack {
cpal::BufferSize::Fixed(config.buffer_size) cpal::BufferSize::Fixed(config.buffer_size)
@@ -277,6 +280,7 @@ pub fn build_stream(
}; };
let sr = sample_rate; let sr = sample_rate;
let effective_channels = channels;
let channels = channels as usize; let channels = channels as usize;
let max_voices = config.max_voices; let max_voices = config.max_voices;
@@ -347,5 +351,10 @@ pub fn build_stream(
stream stream
.play() .play()
.map_err(|e| format!("Failed to play stream: {e}"))?; .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))
} }

View File

@@ -106,7 +106,8 @@ fn main() -> io::Result<()> {
app.ui.hue_rotation = settings.display.hue_rotation; app.ui.hue_rotation = settings.display.hue_rotation;
app.audio.config.layout = settings.display.layout; app.audio.config.layout = settings.display.layout;
let base_theme = settings.display.color_scheme.to_theme(); 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); theme::set(rotated);
// Load MIDI settings // Load MIDI settings
@@ -190,9 +191,11 @@ fn main() -> io::Result<()> {
initial_samples, initial_samples,
Arc::clone(&audio_sample_pos), Arc::clone(&audio_sample_pos),
) { ) {
Ok((s, sample_rate, analysis)) => { Ok((s, info, analysis)) => {
app.audio.config.sample_rate = sample_rate; app.audio.config.sample_rate = info.sample_rate;
sample_rate_shared.store(sample_rate as u32, Ordering::Relaxed); 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)) (Some(s), Some(analysis))
} }
Err(e) => { Err(e) => {
@@ -244,11 +247,13 @@ fn main() -> io::Result<()> {
restart_samples, restart_samples,
Arc::clone(&audio_sample_pos), Arc::clone(&audio_sample_pos),
) { ) {
Ok((new_stream, sr, new_analysis)) => { Ok((new_stream, info, new_analysis)) => {
_stream = Some(new_stream); _stream = Some(new_stream);
_analysis_handle = Some(new_analysis); _analysis_handle = Some(new_analysis);
app.audio.config.sample_rate = sr; app.audio.config.sample_rate = info.sample_rate;
sample_rate_shared.store(sr as u32, Ordering::Relaxed); 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.audio.error = None;
app.ui.set_status("Audio restarted".to_string()); app.ui.set_status("Audio restarted".to_string());
} }

View File

@@ -77,6 +77,7 @@ pub struct AudioConfig {
pub buffer_size: u32, pub buffer_size: u32,
pub max_voices: usize, pub max_voices: usize,
pub sample_rate: f32, pub sample_rate: f32,
pub host_name: String,
pub sample_paths: Vec<PathBuf>, pub sample_paths: Vec<PathBuf>,
pub sample_count: usize, pub sample_count: usize,
pub refresh_rate: RefreshRate, pub refresh_rate: RefreshRate,
@@ -95,6 +96,7 @@ impl Default for AudioConfig {
buffer_size: 512, buffer_size: 512,
max_voices: 32, max_voices: 32,
sample_rate: 44100.0, sample_rate: 44100.0,
host_name: String::new(),
sample_paths: Vec::new(), sample_paths: Vec::new(),
sample_count: 0, sample_count: 0,
refresh_rate: RefreshRate::default(), refresh_rate: RefreshRate::default(),

View File

@@ -135,7 +135,12 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
let down_indicator = Paragraph::new("").style(indicator_style); let down_indicator = Paragraph::new("").style(indicator_style);
frame.render_widget( frame.render_widget(
down_indicator, 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); Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
let header_style = if focused { 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 { } 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); 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); Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
let label_style = if focused { 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 { } else if section_focused {
Style::new().fg(theme.engine.label_focused) Style::new().fg(theme.engine.label_focused)
} else { } else {
@@ -322,7 +333,9 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
render_section_header(frame, "SETTINGS", section_focused, header_area); 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 normal = Style::new().fg(theme.engine.normal);
let label_style = Style::new().fg(theme.engine.label); let label_style = Style::new().fg(theme.engine.label);
let value_style = Style::new().fg(theme.engine.value); 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, label_style,
), ),
render_selector( 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, buffer_focused,
highlight, highlight,
normal, normal,
@@ -420,6 +437,17 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
value_style, 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)]); let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);