fix(audio): capture mic during system-audio silence (FR-CAP-7)
WASAPI loopback delivers no packets when the render endpoint is idle, so the recording only advanced while system audio played. The bridged mic (the user talking over silence) was lost to MicBridge overflow, and audio.wav lost those stretches entirely — corrupting diarization and dropping the user's solo remarks in real meetings. When loopback is idle, synthesize silence for the elapsed gap and fold the mic in, keeping audio.wav continuous. Loopback + mic-enabled only.
This commit is contained in:
1 parent
9468781218
commit
6dd6c3c69f
1 file changed
+34
@@ -561,6 +561,16 @@ fn audio_level(mono: &[f32], mic: bool) -> AudioLevel {
|
||||
/// without flooding the frontend with an event per ~10ms WASAPI callback.
|
||||
const LEVEL_EMIT_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
/// When the loopback (render) endpoint is idle, WASAPI delivers no packets, so
|
||||
/// the recording would stall and the bridged mic — the user talking over system
|
||||
/// silence — would be lost as the MicBridge overflows. The loopback loop then
|
||||
/// synthesizes silence for the elapsed gap and folds the mic in, keeping
|
||||
/// `audio.wav` continuous and complete (FR-CAP-7). These bound one synthesized
|
||||
/// chunk: skip gaps below MIN to avoid churn, and cap a chunk at MAX (≈ the
|
||||
/// MicBridge's own ~0.5s depth, so we never request more mic than it holds).
|
||||
const SILENCE_TICK_MIN: Duration = Duration::from_millis(10);
|
||||
const SILENCE_TICK_MAX: Duration = Duration::from_millis(500);
|
||||
|
||||
/// Runs on a dedicated OS thread for the lifetime of a `CaptureHandle`. Owns the
|
||||
/// WASAPI client and the WAV writer; exits (and finalizes the WAV) once `running`
|
||||
/// is cleared.
|
||||
@@ -614,6 +624,9 @@ fn capture_loop(
|
||||
let mut queue: std::collections::VecDeque<u8> = std::collections::VecDeque::new();
|
||||
let mut frames_written: u64 = 0;
|
||||
let mut last_level_emit = Instant::now() - LEVEL_EMIT_INTERVAL;
|
||||
// Wall-clock of the last WAV write (real or synthesized), so the loopback
|
||||
// loop can fill idle gaps with silence+mic instead of stalling.
|
||||
let mut last_write = Instant::now();
|
||||
|
||||
while running.load(Ordering::Relaxed) {
|
||||
// Short timeout so we periodically re-check `running` even with no data.
|
||||
@@ -657,8 +670,29 @@ fn capture_loop(
|
||||
continue;
|
||||
}
|
||||
if queue.is_empty() {
|
||||
// Loopback idle (nothing playing): keep audio.wav advancing with
|
||||
// silence so the bridged mic (user talking over system silence) is
|
||||
// preserved instead of lost to MicBridge overflow. Loopback +
|
||||
// mic-enabled only; the mic capture stream has no writer, and a
|
||||
// mic-off recording has nothing to preserve during silence.
|
||||
if is_loopback && !paused.load(Ordering::Relaxed) {
|
||||
if let (Some(w), Some(b)) = (writer.as_mut(), bridge) {
|
||||
let elapsed = last_write.elapsed().min(SILENCE_TICK_MAX);
|
||||
if elapsed >= SILENCE_TICK_MIN {
|
||||
let rate = session.format.get_samplespersec() as u128;
|
||||
let gap_frames = (elapsed.as_micros() * rate / 1_000_000) as usize;
|
||||
let bpf = session.format.get_nchannels() as usize
|
||||
* (session.format.get_bitspersample() as usize / 8);
|
||||
let silence = vec![0u8; gap_frames * bpf];
|
||||
let mic = b.pull(gap_frames);
|
||||
frames_written += write_wav_bytes(w, &silence, &session.format, &mic)?;
|
||||
last_write = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
last_write = Instant::now();
|
||||
let bytes: Vec<u8> = queue.drain(..).collect();
|
||||
|
||||
// Must keep pulling WASAPI buffers even while paused (required to avoid
|
||||
|
||||
Reference in new issue
Block a user