agent: code-atomiser-fix — surface capture-thread drops + bypass validation requeue cap
dropped_chunks was incremented on cpal-callback channel-full and validation requeue overflow but never read by the live session, so the UI's dropped_audio_ms missed callback-level losses entirely. Architecture doc had flagged this as a TODO. Also: the 350ms validation buffer was requeued via try_send into the same 32-slot channel, silently dropping past the cap on small-buffer audio hosts (WASAPI exclusive, low-latency ALSA at 256 frames -> ~65 chunks). Fix: live runtime reads MicrophoneCapture::dropped_chunks() on each recv_audio tick (LiveSessionRuntime::poll_capture_drops) and converts the per-chunk-duration delta into the dropped_audio_ms surfaced to the UI overload status. Per-chunk duration is derived from the most recent AudioChunk's sample_rate + samples-per-channel so it adapts to whatever rate cpal is delivering at. Validation requeue moved from try_send into the bounded channel onto a VecDeque<AudioChunk> returned alongside the Receiver; ActiveCapture drains the replay buffer before reading rx in recv_audio, bypassing the 32-slot cap entirely. Architecture doc updated to remove the TODO and document the new pre-roll path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -140,7 +141,18 @@ impl MicrophoneCapture {
|
|||||||
|
|
||||||
/// Start capturing from the device whose name matches `device_name` exactly.
|
/// Start capturing from the device whose name matches `device_name` exactly.
|
||||||
/// If no match is found, returns an error rather than silently falling back.
|
/// If no match is found, returns an error rather than silently falling back.
|
||||||
pub fn start_with_device(device_name: &str) -> Result<(Self, mpsc::Receiver<AudioChunk>)> {
|
///
|
||||||
|
/// The returned tuple is `(capture, replay_buffer, rx)`:
|
||||||
|
/// - `replay_buffer` holds chunks observed during the 350ms
|
||||||
|
/// validation pre-roll. Consumers MUST drain it before reading
|
||||||
|
/// from `rx` so the head of the recording isn't lost on hosts
|
||||||
|
/// whose cpal buffer is small enough to overflow the 32-slot
|
||||||
|
/// channel during validation (WASAPI exclusive, low-latency
|
||||||
|
/// ALSA at 256 frames).
|
||||||
|
/// - `rx` is the live cpal callback channel.
|
||||||
|
pub fn start_with_device(
|
||||||
|
device_name: &str,
|
||||||
|
) -> Result<(Self, VecDeque<AudioChunk>, mpsc::Receiver<AudioChunk>)> {
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
let devices = host
|
let devices = host
|
||||||
.input_devices()
|
.input_devices()
|
||||||
@@ -169,7 +181,7 @@ impl MicrophoneCapture {
|
|||||||
/// a short window — this is what defeats the "silent monitor source wins" bug.
|
/// a short window — this is what defeats the "silent monitor source wins" bug.
|
||||||
/// 4. If no non-monitor device produces real audio, fall back to monitor sources
|
/// 4. If no non-monitor device produces real audio, fall back to monitor sources
|
||||||
/// as a last resort (with a clear log line). Never accept dead silence.
|
/// as a last resort (with a clear log line). Never accept dead silence.
|
||||||
pub fn start() -> Result<(Self, mpsc::Receiver<AudioChunk>)> {
|
pub fn start() -> Result<(Self, VecDeque<AudioChunk>, mpsc::Receiver<AudioChunk>)> {
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
let default_name = host
|
let default_name = host
|
||||||
.default_input_device()
|
.default_input_device()
|
||||||
@@ -358,7 +370,7 @@ fn open_and_validate(
|
|||||||
device: cpal::Device,
|
device: cpal::Device,
|
||||||
name: &str,
|
name: &str,
|
||||||
require_audio: bool,
|
require_audio: bool,
|
||||||
) -> Result<(MicrophoneCapture, mpsc::Receiver<AudioChunk>)> {
|
) -> Result<(MicrophoneCapture, VecDeque<AudioChunk>, mpsc::Receiver<AudioChunk>)> {
|
||||||
let config = device
|
let config = device
|
||||||
.default_input_config()
|
.default_input_config()
|
||||||
.map_err(|e| Error::AudioCaptureFailed(format!("default_input_config: {e}")))?;
|
.map_err(|e| Error::AudioCaptureFailed(format!("default_input_config: {e}")))?;
|
||||||
@@ -376,7 +388,6 @@ 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 dropped_chunks = Arc::new(AtomicU64::new(0));
|
let dropped_chunks = Arc::new(AtomicU64::new(0));
|
||||||
// Bounded channel for runtime stream errors. Capacity 32 = plenty for
|
// Bounded channel for runtime stream errors. Capacity 32 = plenty for
|
||||||
// the rare error case; if it ever fills, drops are reported via stderr
|
// the rare error case; if it ever fills, drops are reported via stderr
|
||||||
@@ -490,16 +501,21 @@ fn open_and_validate(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-queue the collected chunks so downstream gets them. Count any
|
// Hand the validation pre-roll back to the consumer as a separate
|
||||||
// drops here against the same `dropped_chunks` counter so the live
|
// VecDeque rather than try_send-requeuing into the 32-slot channel.
|
||||||
// session sees them and can warn the user.
|
// On small-buffer audio hosts (WASAPI exclusive at ~256 frames /
|
||||||
for chunk in collected {
|
// low-latency ALSA) the 350ms window collects ~65 chunks; the old
|
||||||
if requeue_tx.try_send(chunk).is_err() {
|
// requeue path silently dropped roughly half of them, losing ~150ms
|
||||||
dropped_chunks.fetch_add(1, Ordering::Relaxed);
|
// from the head of every recording. The consumer-side drain
|
||||||
}
|
// bypasses the channel cap entirely.
|
||||||
}
|
let replay_buffer: VecDeque<AudioChunk> = collected.into_iter().collect();
|
||||||
|
|
||||||
tracing::info!(target: "lumotia_audio", device = %name, "selected microphone");
|
tracing::info!(
|
||||||
|
target: "lumotia_audio",
|
||||||
|
device = %name,
|
||||||
|
replay_chunks = replay_buffer.len(),
|
||||||
|
"selected microphone"
|
||||||
|
);
|
||||||
Ok((
|
Ok((
|
||||||
MicrophoneCapture {
|
MicrophoneCapture {
|
||||||
stream: Some(stream),
|
stream: Some(stream),
|
||||||
@@ -507,6 +523,7 @@ fn open_and_validate(
|
|||||||
dropped_chunks,
|
dropped_chunks,
|
||||||
error_rx: Some(err_rx),
|
error_rx: Some(err_rx),
|
||||||
},
|
},
|
||||||
|
replay_buffer,
|
||||||
rx,
|
rx,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ Monitor detection (`is_monitor_name`, `capture.rs:258`) catches the standard Pul
|
|||||||
4. Dispatches to `build_input_stream::<T>` for the device's sample format (F32 / I16 / U16). Anything else returns `MagnotiaError::AudioCaptureFailed`.
|
4. Dispatches to `build_input_stream::<T>` for the device's sample format (F32 / I16 / U16). Anything else returns `MagnotiaError::AudioCaptureFailed`.
|
||||||
5. Calls `stream.play()`.
|
5. Calls `stream.play()`.
|
||||||
6. Sniffs samples for `DEVICE_VALIDATION_MS`, sums squared samples, derives RMS.
|
6. Sniffs samples for `DEVICE_VALIDATION_MS`, sums squared samples, derives RMS.
|
||||||
7. Rejects below floor (or dead silence). Otherwise re-queues the validation chunks back into the channel so downstream consumers do not lose the first 350 ms.
|
7. Rejects below floor (or dead silence). Otherwise hands the validation chunks back to the consumer as a `VecDeque` pre-roll alongside the live `mpsc::Receiver`, so downstream consumers do not lose the first 350 ms even on small-buffer hosts that would overflow the bounded channel.
|
||||||
|
|
||||||
`build_input_stream::<T>` (`capture.rs:505`) is generic over `T: Sample + SizedSample` with `f32: FromSample<T>` so the same body handles all three sample formats. The data callback maps every sample to `f32`, packages an `AudioChunk`, and `try_send`s on the channel; failure increments the `dropped_chunks` atomic. The error callback ships a `CaptureRuntimeError` on `err_tx` (channel-full path increments `dropped_errors` and logs to stderr).
|
`build_input_stream::<T>` (`capture.rs:505`) is generic over `T: Sample + SizedSample` with `f32: FromSample<T>` so the same body handles all three sample formats. The data callback maps every sample to `f32`, packages an `AudioChunk`, and `try_send`s on the channel; failure increments the `dropped_chunks` atomic. The error callback ships a `CaptureRuntimeError` on `err_tx` (channel-full path increments `dropped_errors` and logs to stderr).
|
||||||
|
|
||||||
@@ -96,20 +96,20 @@ cpal default_host()
|
|||||||
│ └─ error callback: cpal::StreamError → CaptureRuntimeError → err_mpsc
|
│ └─ error callback: cpal::StreamError → CaptureRuntimeError → err_mpsc
|
||||||
└─ stream.play()
|
└─ stream.play()
|
||||||
└─ 350 ms sniff → RMS → accept | reject
|
└─ 350 ms sniff → RMS → accept | reject
|
||||||
└─ on accept: re-queue collected chunks, return MicrophoneCapture + Receiver<AudioChunk>
|
└─ on accept: return MicrophoneCapture + VecDeque<AudioChunk> pre-roll + Receiver<AudioChunk>
|
||||||
```
|
```
|
||||||
|
|
||||||
Output: one `AudioChunk` per cpal callback period at the device's native rate. The live session is responsible for downmixing channels (if `channels > 1`) and feeding `StreamingResampler` to reach 16 kHz mono. Native rate is *not* normalised here.
|
Output: one `AudioChunk` per cpal callback period at the device's native rate. The live session is responsible for downmixing channels (if `channels > 1`) and feeding `StreamingResampler` to reach 16 kHz mono. Native rate is *not* normalised here.
|
||||||
|
|
||||||
## Watch-outs
|
## Watch-outs
|
||||||
|
|
||||||
- **Channel capacity is 32 chunks.** At a 1024-frame cpal buffer at 48 kHz that's roughly 700 ms. A blocked consumer for longer than that means dropped audio. `dropped_chunks()` is the visibility hook; the live-session command must surface it.
|
- **Channel capacity is 32 chunks.** At a 1024-frame cpal buffer at 48 kHz that's roughly 700 ms. A blocked consumer for longer than that means dropped audio. `dropped_chunks()` is the visibility hook; the live-session command reads it on every `recv_audio` tick (`commands/live.rs::poll_capture_drops`) and converts the delta into the `dropped_audio_ms` surfaced to the UI overlay.
|
||||||
- **Default-device first works against the safest pick on Linux Pulse setups** where the default sink monitor sneaks in. The four-tier sort handles this, but only because monitor names match the patterns in `is_monitor_name`. New PipeWire schemes that don't include `.monitor` / `loopback` would slip through.
|
- **Default-device first works against the safest pick on Linux Pulse setups** where the default sink monitor sneaks in. The four-tier sort handles this, but only because monitor names match the patterns in `is_monitor_name`. New PipeWire schemes that don't include `.monitor` / `loopback` would slip through.
|
||||||
- **350 ms validation window adds a startup latency floor.** Slice 2 needs to know about this when wiring "click record".
|
- **350 ms validation window adds a startup latency floor.** Slice 2 needs to know about this when wiring "click record".
|
||||||
- **`stop()` is `pause`, not `drop`.** The stream object is kept alive until `Drop`. A subsequent `start()` on the same `MicrophoneCapture` is not supported (signature returns a fresh instance).
|
- **`stop()` is `pause`, not `drop`.** The stream object is kept alive until `Drop`. A subsequent `start()` on the same `MicrophoneCapture` is not supported (signature returns a fresh instance).
|
||||||
- **Sample format dispatch is closed-set.** Anything not F32 / I16 / U16 is a hard error. cpal can in principle expose I8 / I32 / F64 on exotic devices.
|
- **Sample format dispatch is closed-set.** Anything not F32 / I16 / U16 is a hard error. cpal can in principle expose I8 / I32 / F64 on exotic devices.
|
||||||
- **`device_display_name` swallows errors.** `cpal::Device::description()` errors silently become `None`, then `<unnamed>` downstream. Acceptable for a UI list, surprising for debugging.
|
- **`device_display_name` swallows errors.** `cpal::Device::description()` errors silently become `None`, then `<unnamed>` downstream. Acceptable for a UI list, surprising for debugging.
|
||||||
- **Re-queue uses `try_send` on a channel of capacity 32.** If the sniff produced more than 32 chunks (≈64 ms at 48 kHz 256-frame buffers — uncommon but possible), the early ones are dropped against the same `dropped_chunks` counter. Documented at `capture.rs:486`.
|
- **Validation pre-roll is returned out-of-band, not requeued.** `open_and_validate` returns a `VecDeque<AudioChunk>` alongside the live `mpsc::Receiver`; the live-session consumer drains the deque before reading the channel. This bypasses the 32-slot cap entirely so small-buffer hosts (WASAPI exclusive, low-latency ALSA at 256 frames) don't lose ~150ms from the head of every recording. Earlier versions used `try_send` to requeue and silently dropped half of the pre-roll on those hosts.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||||
@@ -165,26 +165,40 @@ struct LiveSessionSummary {
|
|||||||
/// explicit and locally structured.
|
/// explicit and locally structured.
|
||||||
struct ActiveCapture {
|
struct ActiveCapture {
|
||||||
/// Keeping the capture handle alive keeps the underlying cpal stream alive.
|
/// Keeping the capture handle alive keeps the underlying cpal stream alive.
|
||||||
_capture: MicrophoneCapture,
|
capture: MicrophoneCapture,
|
||||||
rx: std::sync::mpsc::Receiver<AudioChunk>,
|
rx: std::sync::mpsc::Receiver<AudioChunk>,
|
||||||
mic_error_rx: Option<std::sync::mpsc::Receiver<CaptureRuntimeError>>,
|
mic_error_rx: Option<std::sync::mpsc::Receiver<CaptureRuntimeError>>,
|
||||||
|
/// Pre-roll chunks collected during the 350ms device validation
|
||||||
|
/// window. Drained BEFORE reading from `rx` so the head of the
|
||||||
|
/// recording survives small-buffer hosts that would otherwise
|
||||||
|
/// overflow the 32-slot capture channel during validation.
|
||||||
|
replay_buffer: VecDeque<AudioChunk>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveCapture {
|
impl ActiveCapture {
|
||||||
fn start(config: &StartLiveTranscriptionConfig) -> Result<Self, String> {
|
fn start(config: &StartLiveTranscriptionConfig) -> Result<Self, String> {
|
||||||
let (mut capture, rx) = match config.microphone_device.as_deref() {
|
let (mut capture, replay_buffer, rx) = match config.microphone_device.as_deref() {
|
||||||
Some(name) if !name.is_empty() => MicrophoneCapture::start_with_device(name),
|
Some(name) if !name.is_empty() => MicrophoneCapture::start_with_device(name),
|
||||||
_ => MicrophoneCapture::start(),
|
_ => MicrophoneCapture::start(),
|
||||||
}
|
}
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let mic_error_rx = capture.take_error_rx();
|
let mic_error_rx = capture.take_error_rx();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_capture: capture,
|
capture,
|
||||||
rx,
|
rx,
|
||||||
mic_error_rx,
|
mic_error_rx,
|
||||||
|
replay_buffer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cumulative chunks the cpal callback (and the validation
|
||||||
|
/// pre-roll, when it overflows the channel cap) has dropped.
|
||||||
|
/// The live runtime samples this on each recv tick to convert
|
||||||
|
/// the delta into `dropped_audio_ms`.
|
||||||
|
fn dropped_chunks(&self) -> u64 {
|
||||||
|
self.capture.dropped_chunks()
|
||||||
|
}
|
||||||
|
|
||||||
fn drain_runtime_errors(
|
fn drain_runtime_errors(
|
||||||
&mut self,
|
&mut self,
|
||||||
session_id: u64,
|
session_id: u64,
|
||||||
@@ -217,6 +231,19 @@ struct LiveLoopState {
|
|||||||
resampler_flushed: bool,
|
resampler_flushed: bool,
|
||||||
result_listener_lost: bool,
|
result_listener_lost: bool,
|
||||||
recent_segments: Vec<RecentTranscriptSegment>,
|
recent_segments: Vec<RecentTranscriptSegment>,
|
||||||
|
/// Cumulative value of `MicrophoneCapture::dropped_chunks()` last
|
||||||
|
/// observed by `poll_capture_drops`. Subtracted from the live
|
||||||
|
/// reading to compute the per-tick delta added to
|
||||||
|
/// `dropped_audio_ms`. Counts callback-level losses that the
|
||||||
|
/// session's own overflow path can't see.
|
||||||
|
last_dropped_chunks: u64,
|
||||||
|
/// Most-recent chunk dimensions used to convert a `dropped_chunks`
|
||||||
|
/// delta into milliseconds. Populated on the first received chunk
|
||||||
|
/// and refreshed thereafter. Zeroed sentinel means "no chunk yet";
|
||||||
|
/// drops observed before any chunk arrives are deferred until we
|
||||||
|
/// know the device's native rate.
|
||||||
|
last_chunk_samples_per_chan: u32,
|
||||||
|
last_chunk_sample_rate: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LiveLoopState {
|
impl LiveLoopState {
|
||||||
@@ -276,6 +303,11 @@ impl LiveSessionRuntime {
|
|||||||
if let Some(chunk) = self.recv_audio()? {
|
if let Some(chunk) = self.recv_audio()? {
|
||||||
self.process_audio_chunk(chunk)?;
|
self.process_audio_chunk(chunk)?;
|
||||||
}
|
}
|
||||||
|
// Pick up cpal-callback drops AND validation-window
|
||||||
|
// requeue drops that the live session's own buffer
|
||||||
|
// overflow path can't see. Called after recv_audio so the
|
||||||
|
// most recent chunk dimensions inform the ms conversion.
|
||||||
|
self.poll_capture_drops();
|
||||||
self.drop_pending_overflow();
|
self.drop_pending_overflow();
|
||||||
self.flush_tail_if_stopping()?;
|
self.flush_tail_if_stopping()?;
|
||||||
if self.dispatch_inference_if_ready() {
|
if self.dispatch_inference_if_ready() {
|
||||||
@@ -306,6 +338,14 @@ impl LiveSessionRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn recv_audio(&mut self) -> Result<Option<AudioChunk>, String> {
|
fn recv_audio(&mut self) -> Result<Option<AudioChunk>, String> {
|
||||||
|
// Drain the validation pre-roll first. This is the head-of-the
|
||||||
|
// -recording audio that was collected during the 350ms device
|
||||||
|
// validation window; replaying it from a consumer-side VecDeque
|
||||||
|
// bypasses the 32-slot channel cap that previously dropped half
|
||||||
|
// of it silently on small-buffer hosts.
|
||||||
|
if let Some(chunk) = self.capture.replay_buffer.pop_front() {
|
||||||
|
return Ok(Some(chunk));
|
||||||
|
}
|
||||||
match self.capture.rx.recv_timeout(Duration::from_millis(25)) {
|
match self.capture.rx.recv_timeout(Duration::from_millis(25)) {
|
||||||
Ok(chunk) => Ok(Some(chunk)),
|
Ok(chunk) => Ok(Some(chunk)),
|
||||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Ok(None),
|
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Ok(None),
|
||||||
@@ -321,6 +361,14 @@ impl LiveSessionRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_audio_chunk(&mut self, chunk: AudioChunk) -> Result<(), String> {
|
fn process_audio_chunk(&mut self, chunk: AudioChunk) -> Result<(), String> {
|
||||||
|
// Remember native-rate dimensions so `poll_capture_drops` can
|
||||||
|
// turn a chunks-dropped delta into milliseconds without
|
||||||
|
// hard-coding host-buffer assumptions.
|
||||||
|
let channels = chunk.channels.max(1) as u32;
|
||||||
|
let total_samples = chunk.samples.len() as u32;
|
||||||
|
self.state.last_chunk_sample_rate = chunk.sample_rate;
|
||||||
|
self.state.last_chunk_samples_per_chan = total_samples / channels;
|
||||||
|
|
||||||
let mono = downmix_chunk(chunk.samples, chunk.channels as usize);
|
let mono = downmix_chunk(chunk.samples, chunk.channels as usize);
|
||||||
let resampler = match &mut self.state.resampler {
|
let resampler = match &mut self.state.resampler {
|
||||||
Some(resampler) => resampler,
|
Some(resampler) => resampler,
|
||||||
@@ -341,6 +389,48 @@ impl LiveSessionRuntime {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reconcile the live session's `dropped_audio_ms` against the
|
||||||
|
/// capture thread's cpal-callback + validation-requeue drop
|
||||||
|
/// counter. Without this the UI's reported drops only ever
|
||||||
|
/// reflected the live runtime's own buffer overflow path, missing
|
||||||
|
/// every callback-level loss caused by transient back-pressure or
|
||||||
|
/// the 350ms validation pre-roll overflowing the channel cap on
|
||||||
|
/// small-buffer audio hosts.
|
||||||
|
fn poll_capture_drops(&mut self) {
|
||||||
|
let now = self.capture.dropped_chunks();
|
||||||
|
if now == self.state.last_dropped_chunks {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let delta = now.saturating_sub(self.state.last_dropped_chunks);
|
||||||
|
self.state.last_dropped_chunks = now;
|
||||||
|
|
||||||
|
// Defer the conversion until we've seen at least one chunk so
|
||||||
|
// the dimensions are real. Validation-window drops can fire
|
||||||
|
// before any chunk reaches `process_audio_chunk`; they get
|
||||||
|
// attributed at the next reconciliation once we know the rate.
|
||||||
|
if self.state.last_chunk_sample_rate == 0
|
||||||
|
|| self.state.last_chunk_samples_per_chan == 0
|
||||||
|
{
|
||||||
|
// Roll back the consumed delta so we re-observe it once
|
||||||
|
// we have dimensions to convert it with.
|
||||||
|
self.state.last_dropped_chunks = self.state.last_dropped_chunks.saturating_sub(delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let per_chunk_ms = (self.state.last_chunk_samples_per_chan as u64 * 1000)
|
||||||
|
/ self.state.last_chunk_sample_rate.max(1) as u64;
|
||||||
|
let added_ms = delta.saturating_mul(per_chunk_ms);
|
||||||
|
if added_ms == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.state.dropped_audio_ms = self.state.dropped_audio_ms.saturating_add(added_ms);
|
||||||
|
let _ = self.status_channel.send(LiveStatusMessage::Overload {
|
||||||
|
session_id: self.session_id,
|
||||||
|
dropped_audio_ms: self.state.dropped_audio_ms,
|
||||||
|
message: "Microphone capture dropped audio chunks (downstream back-pressure)".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn drop_pending_overflow(&mut self) {
|
fn drop_pending_overflow(&mut self) {
|
||||||
if self.state.inflight.is_none() || self.state.capture_buffer.len() <= MAX_PENDING_SAMPLES {
|
if self.state.inflight.is_none() || self.state.capture_buffer.len() <= MAX_PENDING_SAMPLES {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user