Files
chanora/crates
EdisonJwa d13b56d379 perf(audio): pre-allocate capture scratch buffers to avoid realtime-thread Vec allocs (SDD-094)
The capture cpal callback (CaptureState::ingest) ran two heap
allocations per callback on the realtime audio thread:

  1. engine.rs:1196-1202 — fresh `mono: Vec<f32>` for the downmix
     output, once per cpal callback (50–100 Hz).
  2. engine.rs:1217-1218 — `pcm_accum.drain(..FRAME_SAMPLES).collect()`
     building a fresh Vec<f32> of 960 samples per Opus frame.

Both sites mirror the pattern already fixed for the output side at
engine.rs:1389-1397, where allocating per callback on glibc malloc
was correlated with user-perceptible audio popping. The output-side
fix replaced the per-callback allocation with a pre-allocated
`scratch` Vec that is cleared and resized in place; this commit
applies the same template to the capture side.

Changes:
- Add `mono_scratch: Vec<f32>` and `frame_scratch: Vec<f32>` to
  CaptureState. Initialised with Vec::with_capacity(4096) and
  Vec::with_capacity(FRAME_SAMPLES=960) respectively in
  CaptureState::new.
- Replace the downmix Vec construction with in-place push into
  `self.mono_scratch`; `clear()` retains capacity across callbacks.
- Replace the drain().collect() with `self.frame_scratch.extend(
  self.pcm_accum.drain(..FRAME_SAMPLES))`; same capacity-retention.
- The resampler call uses std::mem::take to swap the scratch buffer
  out for the duration of the &mut self call, then moves it back —
  the backing allocation is preserved across callbacks.

Algorithm semantics are unchanged: same downmix arithmetic, same
clamp loop, same Opus encode call sequence. Only the storage
strategy differs.

Out of scope (intentionally not touched):
- Android audio path (android_voice_unit.rs, mobile_voice_backend.rs):
  researcher constraint C-4 — the Android cpal data path is mid-
  migration and being replaced.
- Output callback at engine.rs:1409+: the only obvious per-callback
  allocation there (`scratch`) was already fixed; a fuller audit
  is a separate scope decision.
- The `scratch` buffer at engine.rs:1389-1397 — already correct.

Verification:
- cargo check --workspace --all-targets: passes.
- cargo test --workspace: 106 passed / 0 failed / 3 ignored.
- cargo clippy --workspace --all-targets: no new lints introduced;
  the one warning inside the edited region (clamp-like pattern at
  line 1266) was pre-existing on the copied clamp loop.
2026-05-18 13:01:52 +08:00
..