fix(audio): buffer capture frames to Opus encoder's fixed frame size
AudioEngine::on_capture() was passing miniaudio's hardware callback period (commonly 480 samples / 10 ms on WASAPI shared mode) directly to opus_encode(), which requires exactly frame_samples_ (960 for 20 ms @ 48 kHz). The mismatch returned OPUS_BAD_ARG and silently dropped every real mic frame, while screen share and injected test frames happened to be correctly sized and worked fine. Fix: accumulate PCM in a pre-allocated CaptureAccum buffer (mirroring the existing RemoteStream::ring fix on the playback side) and only call capture_cb_ when a full frame_samples_ chunk is ready. Same pattern applied to on_loopback(). Add test_capture_frame_accumulation() to verify the accumulator fires exactly the right number of callbacks for misaligned chunk sizes (480, 240+720, 1920 samples). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,60 @@ static void test_stereo_mix() {
|
||||
}
|
||||
#endif // VOICECAT_HAS_AUDIO && VOICECAT_HAS_OPUS
|
||||
|
||||
// ── 5. Capture-frame accumulation (white-box, no audio hardware needed) ──────────
|
||||
// Regression for the capture-side analogue of the playback ring fix: miniaudio's capture
|
||||
// callback fires at the hardware period (commonly 480 samples on WASAPI shared mode), while
|
||||
// opus_encode() requires exactly frame_samples_ (960). Sub-frame chunks must be accumulated;
|
||||
// the callback must receive exactly 960-sample frames regardless of input chunk size.
|
||||
#ifdef VOICECAT_HAS_AUDIO
|
||||
static void test_capture_frame_accumulation() {
|
||||
voicecat::audio::AudioEngine engine;
|
||||
voicecat::audio::AudioParams p;
|
||||
p.sample_rate = 48000;
|
||||
p.capture_channels = 1;
|
||||
p.frame_ms = 20; // frame_samples_ = 960
|
||||
|
||||
std::atomic<int> call_count{0};
|
||||
std::atomic<bool> wrong_size{false};
|
||||
constexpr int kExpected = 960;
|
||||
|
||||
// Start WITHOUT a capture callback: the real mic (if any) fires on_capture(), but
|
||||
// on_capture() returns immediately when capture_cb_ is null, so capture_accum_ is
|
||||
// never touched by the hardware thread. feed_capture_for_test() bypasses capture_cb_
|
||||
// and drives the same accumulator directly with the explicit `cb` below — no races.
|
||||
CHECK(engine.start(p));
|
||||
|
||||
auto cb = [&](int /*kind*/, const int16_t* /*pcm*/, int samples) {
|
||||
++call_count;
|
||||
if (samples != kExpected) wrong_size.store(true);
|
||||
};
|
||||
|
||||
// 480-sample (10 ms) input — WASAPI's common hardware period on modern Windows.
|
||||
// Two 480-chunk inputs → exactly one callback at 960.
|
||||
std::vector<int16_t> h(480, 1000);
|
||||
engine.feed_capture_for_test(h.data(), 480, cb);
|
||||
CHECK(call_count.load() == 0); // half a frame — no callback yet
|
||||
engine.feed_capture_for_test(h.data(), 480, cb);
|
||||
CHECK(call_count.load() == 1); // one full frame — callback fired once
|
||||
|
||||
// Mis-aligned split: 240 then 720 → still exactly one callback.
|
||||
std::vector<int16_t> s(240, 500), l(720, 500);
|
||||
engine.feed_capture_for_test(s.data(), 240, cb);
|
||||
CHECK(call_count.load() == 1);
|
||||
engine.feed_capture_for_test(l.data(), 720, cb);
|
||||
CHECK(call_count.load() == 2);
|
||||
|
||||
// 1920-sample input (two Opus frames) → exactly two callbacks.
|
||||
std::vector<int16_t> d(1920, 800);
|
||||
engine.feed_capture_for_test(d.data(), 1920, cb);
|
||||
CHECK(call_count.load() == 4);
|
||||
|
||||
CHECK(!wrong_size.load());
|
||||
engine.stop();
|
||||
std::printf("test_capture_frame_accumulation: ok (callbacks=%d)\n", call_count.load());
|
||||
}
|
||||
#endif // VOICECAT_HAS_AUDIO
|
||||
|
||||
// ── 2/3. VAD + PTT gate, through the real ABI against a real server ─────────────
|
||||
static void test_vad_and_ptt_gate() {
|
||||
auto tmp = std::filesystem::temp_directory_path() /
|
||||
@@ -385,6 +439,9 @@ int main() {
|
||||
test_device_enumeration();
|
||||
#if defined(VOICECAT_HAS_AUDIO) && defined(VOICECAT_HAS_OPUS)
|
||||
test_stereo_mix();
|
||||
#endif
|
||||
#ifdef VOICECAT_HAS_AUDIO
|
||||
test_capture_frame_accumulation();
|
||||
#endif
|
||||
test_vad_and_ptt_gate();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user