Feat: fixing stderr catching and scope not drawing completely

This commit is contained in:
2026-02-23 21:53:53 +01:00
parent e7137cc7ed
commit 502f7afe8f
4 changed files with 31 additions and 4 deletions

View File

@@ -158,6 +158,7 @@ struct CagireDesktop {
_stream: Option<cpal::Stream>, _stream: Option<cpal::Stream>,
_analysis_handle: Option<AnalysisHandle>, _analysis_handle: Option<AnalysisHandle>,
midi_rx: Receiver<MidiCommand>, midi_rx: Receiver<MidiCommand>,
stream_error_rx: crossbeam_channel::Receiver<String>,
current_font: FontChoice, current_font: FontChoice,
zoom_factor: f32, zoom_factor: f32,
fullscreen: bool, fullscreen: bool,
@@ -201,6 +202,7 @@ impl CagireDesktop {
_stream: b.stream, _stream: b.stream,
_analysis_handle: b.analysis_handle, _analysis_handle: b.analysis_handle,
midi_rx: b.midi_rx, midi_rx: b.midi_rx,
stream_error_rx: b.stream_error_rx,
current_font, current_font,
zoom_factor, zoom_factor,
fullscreen: false, fullscreen: false,
@@ -235,6 +237,9 @@ impl CagireDesktop {
max_voices: self.app.audio.config.max_voices, max_voices: self.app.audio.config.max_voices,
}; };
let (new_error_tx, new_error_rx) = crossbeam_channel::bounded(16);
self.stream_error_rx = new_error_rx;
let mut restart_samples = Vec::new(); let mut restart_samples = Vec::new();
for path in &self.app.audio.config.sample_paths { for path in &self.app.audio.config.sample_paths {
let index = doux::sampling::scan_samples_dir(path); let index = doux::sampling::scan_samples_dir(path);
@@ -257,6 +262,7 @@ impl CagireDesktop {
Arc::clone(&self.metrics), Arc::clone(&self.metrics),
restart_samples, restart_samples,
Arc::clone(&self.audio_sample_pos), Arc::clone(&self.audio_sample_pos),
new_error_tx,
) { ) {
Ok((new_stream, info, new_analysis, registry)) => { Ok((new_stream, info, new_analysis, registry)) => {
self._stream = Some(new_stream); self._stream = Some(new_stream);
@@ -350,6 +356,11 @@ impl CagireDesktop {
impl eframe::App for CagireDesktop { impl eframe::App for CagireDesktop {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.handle_audio_restart(); self.handle_audio_restart();
while let Ok(err) = self.stream_error_rx.try_recv() {
self.app.ui.flash(&err, 3000, cagire::state::FlashKind::Error);
}
self.update_metrics(); self.update_metrics();
ctx.input(|i| { ctx.input(|i| {

View File

@@ -251,7 +251,7 @@ use cpal::traits::{DeviceTrait, StreamTrait};
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use cpal::Stream; use cpal::Stream;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use crossbeam_channel::Receiver; use crossbeam_channel::{Receiver, Sender};
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use doux::{Engine, EngineMetrics}; use doux::{Engine, EngineMetrics};
@@ -274,6 +274,7 @@ pub struct AudioStreamInfo {
} }
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
#[allow(clippy::too_many_arguments)]
pub fn build_stream( pub fn build_stream(
config: &AudioStreamConfig, config: &AudioStreamConfig,
audio_rx: Receiver<AudioCommand>, audio_rx: Receiver<AudioCommand>,
@@ -282,6 +283,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>,
error_tx: Sender<String>,
) -> Result< ) -> Result<
( (
Stream, Stream,
@@ -375,15 +377,15 @@ pub fn build_stream(
engine.metrics.load.set_buffer_time(buffer_time_ns); engine.metrics.load.set_buffer_time(buffer_time_ns);
engine.process_block(data, &[], &[]); engine.process_block(data, &[], &[]);
scope_buffer.write(&engine.output); scope_buffer.write(data);
// Feed mono mix to analysis thread via ring buffer (non-blocking) // Feed mono mix to analysis thread via ring buffer (non-blocking)
for chunk in engine.output.chunks(channels) { for chunk in data.chunks(channels) {
let mono = chunk.iter().sum::<f32>() / channels as f32; let mono = chunk.iter().sum::<f32>() / channels as f32;
let _ = fft_producer.try_push(mono); let _ = fft_producer.try_push(mono);
} }
}, },
|err| eprintln!("stream error: {err}"), move |err| { let _ = error_tx.try_send(format!("stream error: {err}")); },
None, None,
) )
.map_err(|e| format!("Failed to build stream: {e}"))?; .map_err(|e| format!("Failed to build stream: {e}"))?;

View File

@@ -42,6 +42,7 @@ pub struct Init {
pub stream: Option<cpal::Stream>, pub stream: Option<cpal::Stream>,
pub analysis_handle: Option<AnalysisHandle>, pub analysis_handle: Option<AnalysisHandle>,
pub midi_rx: Receiver<MidiCommand>, pub midi_rx: Receiver<MidiCommand>,
pub stream_error_rx: crossbeam_channel::Receiver<String>,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
pub settings: Settings, pub settings: Settings,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
@@ -188,6 +189,8 @@ pub fn init(args: InitArgs) -> Init {
seq_config, seq_config,
); );
let (stream_error_tx, stream_error_rx) = crossbeam_channel::bounded(16);
let stream_config = AudioStreamConfig { let stream_config = AudioStreamConfig {
output_device: app.audio.config.output_device.clone(), output_device: app.audio.config.output_device.clone(),
channels: app.audio.config.channels, channels: app.audio.config.channels,
@@ -203,6 +206,7 @@ pub fn init(args: InitArgs) -> Init {
Arc::clone(&metrics), Arc::clone(&metrics),
initial_samples, initial_samples,
Arc::clone(&audio_sample_pos), Arc::clone(&audio_sample_pos),
stream_error_tx,
) { ) {
Ok((s, info, analysis, registry)) => { Ok((s, info, analysis, registry)) => {
app.audio.config.sample_rate = info.sample_rate; app.audio.config.sample_rate = info.sample_rate;
@@ -246,6 +250,7 @@ pub fn init(args: InitArgs) -> Init {
stream, stream,
analysis_handle, analysis_handle,
midi_rx, midi_rx,
stream_error_rx,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
settings, settings,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]

View File

@@ -83,6 +83,7 @@ fn main() -> io::Result<()> {
let mut _stream = b.stream; let mut _stream = b.stream;
let mut _analysis_handle = b.analysis_handle; let mut _analysis_handle = b.analysis_handle;
let mut midi_rx = b.midi_rx; let mut midi_rx = b.midi_rx;
let mut stream_error_rx = b.stream_error_rx;
enable_raw_mode()?; enable_raw_mode()?;
io::stdout().execute(EnableBracketedPaste)?; io::stdout().execute(EnableBracketedPaste)?;
@@ -110,6 +111,9 @@ fn main() -> io::Result<()> {
max_voices: app.audio.config.max_voices, max_voices: app.audio.config.max_voices,
}; };
let (new_error_tx, new_error_rx) = crossbeam_channel::bounded(16);
stream_error_rx = new_error_rx;
let mut restart_samples = Vec::new(); let mut restart_samples = Vec::new();
for path in &app.audio.config.sample_paths { for path in &app.audio.config.sample_paths {
let index = doux::sampling::scan_samples_dir(path); let index = doux::sampling::scan_samples_dir(path);
@@ -132,6 +136,7 @@ fn main() -> io::Result<()> {
Arc::clone(&metrics), Arc::clone(&metrics),
restart_samples, restart_samples,
Arc::clone(&audio_sample_pos), Arc::clone(&audio_sample_pos),
new_error_tx,
) { ) {
Ok((new_stream, info, new_analysis, registry)) => { Ok((new_stream, info, new_analysis, registry)) => {
_stream = Some(new_stream); _stream = Some(new_stream);
@@ -161,6 +166,10 @@ fn main() -> io::Result<()> {
} }
} }
while let Ok(err) = stream_error_rx.try_recv() {
app.ui.flash(&err, 3000, state::FlashKind::Error);
}
app.playback.playing = playing.load(Ordering::Relaxed); app.playback.playing = playing.load(Ordering::Relaxed);
while let Ok(midi_cmd) = midi_rx.try_recv() { while let Ok(midi_cmd) = midi_rx.try_recv() {