fix(audio): log dropped capture errors instead of silently discarding
The cpal stream-error closure used `let _ = err_tx.try_send(...)` against a bounded sync_channel(16). If the live session's listener stalled or the frontend disconnected, runtime stream errors were silently dropped — the diagnostic bundle showed nothing for a session that mysteriously stopped working. - Bump the error channel capacity 16 → 32 (matches AUDIO_CHANNEL_CAPACITY). - On try_send failure, log to stderr with the device name + a per-session drop counter so the symptom is visible in the diagnostic bundle even when the typed event never reached the frontend. - Plumb a new `dropped_errors: Arc<AtomicU64>` through `build_input_stream` alongside the existing `dropped_chunks`, mirroring the same pattern. (kon-audio doesn't build in the audit sandbox: it links against ALSA which the sandbox lacks. CI cross-platform compiles it.) https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
This commit is contained in:
@@ -370,11 +370,15 @@ fn open_and_validate(
|
|||||||
let (tx, rx) = mpsc::sync_channel::<AudioChunk>(AUDIO_CHANNEL_CAPACITY);
|
let (tx, rx) = mpsc::sync_channel::<AudioChunk>(AUDIO_CHANNEL_CAPACITY);
|
||||||
let requeue_tx = tx.clone();
|
let requeue_tx = tx.clone();
|
||||||
let dropped_chunks = Arc::new(AtomicU64::new(0));
|
let dropped_chunks = Arc::new(AtomicU64::new(0));
|
||||||
// Bounded channel for runtime stream errors. Capacity 16 = plenty for
|
// Bounded channel for runtime stream errors. Capacity 32 = plenty for
|
||||||
// the rare error case; if it ever fills, we drop newer errors silently
|
// the rare error case; if it ever fills, drops are reported via stderr
|
||||||
// because they would be redundant noise in a stream that is already
|
// and counted in `dropped_errors` so the symptom is visible in the
|
||||||
// failing. (Codex review 2026/04/17 M2)
|
// diagnostic bundle even when the listener has gone away. Errors
|
||||||
let (err_tx, err_rx) = mpsc::sync_channel::<CaptureRuntimeError>(16);
|
// beyond the cap are by definition redundant noise in a stream that
|
||||||
|
// is already failing. (Codex review 2026/04/17 M2; capacity bump and
|
||||||
|
// drop logging added 2026/04/25 audit pass.)
|
||||||
|
let (err_tx, err_rx) = mpsc::sync_channel::<CaptureRuntimeError>(32);
|
||||||
|
let dropped_errors = Arc::new(AtomicU64::new(0));
|
||||||
|
|
||||||
let stream = match format {
|
let stream = match format {
|
||||||
SampleFormat::F32 => build_input_stream::<f32>(
|
SampleFormat::F32 => build_input_stream::<f32>(
|
||||||
@@ -385,6 +389,7 @@ fn open_and_validate(
|
|||||||
tx,
|
tx,
|
||||||
dropped_chunks.clone(),
|
dropped_chunks.clone(),
|
||||||
err_tx.clone(),
|
err_tx.clone(),
|
||||||
|
dropped_errors.clone(),
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
),
|
),
|
||||||
SampleFormat::I16 => build_input_stream::<i16>(
|
SampleFormat::I16 => build_input_stream::<i16>(
|
||||||
@@ -395,6 +400,7 @@ fn open_and_validate(
|
|||||||
tx,
|
tx,
|
||||||
dropped_chunks.clone(),
|
dropped_chunks.clone(),
|
||||||
err_tx.clone(),
|
err_tx.clone(),
|
||||||
|
dropped_errors.clone(),
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
),
|
),
|
||||||
SampleFormat::U16 => build_input_stream::<u16>(
|
SampleFormat::U16 => build_input_stream::<u16>(
|
||||||
@@ -405,6 +411,7 @@ fn open_and_validate(
|
|||||||
tx,
|
tx,
|
||||||
dropped_chunks.clone(),
|
dropped_chunks.clone(),
|
||||||
err_tx.clone(),
|
err_tx.clone(),
|
||||||
|
dropped_errors.clone(),
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
),
|
),
|
||||||
other => {
|
other => {
|
||||||
@@ -503,6 +510,7 @@ fn build_input_stream<T>(
|
|||||||
tx: mpsc::SyncSender<AudioChunk>,
|
tx: mpsc::SyncSender<AudioChunk>,
|
||||||
dropped_chunks: Arc<AtomicU64>,
|
dropped_chunks: Arc<AtomicU64>,
|
||||||
err_tx: mpsc::SyncSender<CaptureRuntimeError>,
|
err_tx: mpsc::SyncSender<CaptureRuntimeError>,
|
||||||
|
dropped_errors: Arc<AtomicU64>,
|
||||||
device_name: String,
|
device_name: String,
|
||||||
) -> std::result::Result<cpal::Stream, cpal::BuildStreamError>
|
) -> std::result::Result<cpal::Stream, cpal::BuildStreamError>
|
||||||
where
|
where
|
||||||
@@ -532,10 +540,24 @@ where
|
|||||||
// frontend can show a toast. Also keep the eprintln for ops
|
// frontend can show a toast. Also keep the eprintln for ops
|
||||||
// logs. (Codex review 2026/04/17 M2)
|
// logs. (Codex review 2026/04/17 M2)
|
||||||
eprintln!("[kon-audio] capture error: {err}");
|
eprintln!("[kon-audio] capture error: {err}");
|
||||||
let _ = err_tx.try_send(CaptureRuntimeError {
|
if err_tx
|
||||||
|
.try_send(CaptureRuntimeError {
|
||||||
device_name: err_device_name.clone(),
|
device_name: err_device_name.clone(),
|
||||||
message: err.to_string(),
|
message: err.to_string(),
|
||||||
});
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
// Channel full — listener has stalled or detached. Note
|
||||||
|
// it in stderr and the dropped-errors counter so the
|
||||||
|
// diagnostic bundle still shows the symptom even if the
|
||||||
|
// frontend never received the typed event.
|
||||||
|
let prior = dropped_errors.fetch_add(1, Ordering::Relaxed);
|
||||||
|
eprintln!(
|
||||||
|
"[kon-audio] capture error channel full; dropped error #{} for device '{}'",
|
||||||
|
prior + 1,
|
||||||
|
err_device_name,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user