fix(audio): stereo screen-audio loopback capture on Windows
start_loopback_capture hardcoded channels=1, forcing miniaudio to downmix the system's stereo mix to mono before the encoder saw it -- on_capture_frame then upmixed L=R to produce fake stereo. Now the loopback device opens in the channel's mode (stereo when the channel is stereo), CaptureCallback carries an explicit channels param so the encoder gets real interleaved L/R, and a mono fallback covers unusual render endpoints. New test_loopback_stereo_capture asserts L!=R end-to-end; 18/18 ctest green.
This commit is contained in:
@@ -105,10 +105,14 @@ class AudioEngine {
|
||||
public:
|
||||
// Callback type for encoded capture frames ready to be sent. `kind` identifies which
|
||||
// local stream this PCM belongs to (a vc_stream_kind value; 0 = MIC for the real capture
|
||||
// device, which is always the "primary" tap). M3: multiple concurrent local streams are
|
||||
// possible (e.g. MIC + SCREEN_AUDIO), each fed via its own injection tap (see
|
||||
// inject_capture) since there is only one real hardware capture device.
|
||||
using CaptureCallback = std::function<void(int kind, const int16_t* pcm, int samples)>;
|
||||
// device, which is always the "primary" tap). `channels` is the channel count of the PCM
|
||||
// buffer (1 = mono, 2 = stereo interleaved) — the mic capture device is mono in v1, but
|
||||
// the WASAPI loopback path (SCREEN_AUDIO) captures in the channel's mode when stereo, so
|
||||
// the encoder sees real interleaved L/R PCM rather than a mono upmix. M3: multiple
|
||||
// concurrent local streams are possible (e.g. MIC + SCREEN_AUDIO), each fed via its own
|
||||
// injection tap (see inject_capture) since there is only one real hardware capture device.
|
||||
using CaptureCallback = std::function<void(int kind, const int16_t* pcm, int samples,
|
||||
int channels)>;
|
||||
|
||||
AudioEngine();
|
||||
~AudioEngine();
|
||||
@@ -131,8 +135,11 @@ class AudioEngine {
|
||||
|
||||
// Real desktop-audio loopback capture (Windows/WASAPI only, VOICECAT_HAS_LOOPBACK). Feeds
|
||||
// `kind`'s capture_cb_ directly, same pattern as the real mic capture device — NOT routed
|
||||
// through inject_capture()'s test-only ring. No-op (returns false) when unsupported.
|
||||
bool start_loopback_capture(int kind);
|
||||
// through inject_capture()'s test-only ring. `channels` is the channel count to open the
|
||||
// loopback device with (1 = mono downmix of the system mix, 2 = stereo capture when the
|
||||
// channel is configured stereo); the accumulator and capture_cb_ invocation are shaped to
|
||||
// match. No-op (returns false) when unsupported.
|
||||
bool start_loopback_capture(int kind, int channels);
|
||||
void stop_loopback_capture();
|
||||
|
||||
// Inject synthetic PCM directly into the capture pipeline (bypasses real device).
|
||||
@@ -193,13 +200,52 @@ class AudioEngine {
|
||||
src += copy;
|
||||
remaining -= copy;
|
||||
if (capture_accum_.count == frame_samples_) {
|
||||
cb(0, capture_accum_.buf.data(), frame_samples_);
|
||||
cb(0, capture_accum_.buf.data(), frame_samples_, 1);
|
||||
capture_accum_.count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef VOICECAT_HAS_LOOPBACK
|
||||
// TEST-ONLY — drives the loopback accumulator directly with an explicit callback, the
|
||||
// loopback analogue of feed_capture_for_test. Self-contained: sizes the accumulator and
|
||||
// sets loopback_channels_ itself, so it works on headless CI where start_loopback_capture
|
||||
// can't init a real WASAPI device. `channels` selects mono (1) or interleaved stereo (2).
|
||||
// PCM is interleaved L/R when channels==2. Invokes `cb` once per full frame_samples_
|
||||
// per-channel chunk, with the channel count passed through so the encoder branch in
|
||||
// on_capture_frame sees real stereo (channels==2) rather than a mono upmix.
|
||||
void feed_loopback_for_test(const int16_t* pcm, int frames_per_channel, int channels,
|
||||
const CaptureCallback& cb) {
|
||||
if (frame_samples_ <= 0 || !cb) return;
|
||||
const int ch = std::max(1, channels);
|
||||
const int full = frame_samples_ * ch;
|
||||
// Size the accumulator for the requested channel count (off the RT thread; this is a
|
||||
// test-only path). start_loopback_capture() does the same sizing when it opens a real
|
||||
// device, but on headless CI that init fails — so do it here too.
|
||||
if (static_cast<int>(loopback_accum_.buf.size()) != full) {
|
||||
loopback_accum_.buf.assign(static_cast<size_t>(full), 0);
|
||||
loopback_accum_.count = 0;
|
||||
}
|
||||
loopback_channels_ = ch;
|
||||
const int16_t* src = pcm;
|
||||
auto remaining = frames_per_channel * ch;
|
||||
while (remaining > 0) {
|
||||
int space = full - loopback_accum_.count;
|
||||
int copy = std::min(remaining, space);
|
||||
std::memcpy(loopback_accum_.buf.data() + loopback_accum_.count, src,
|
||||
static_cast<size_t>(copy) * sizeof(int16_t));
|
||||
loopback_accum_.count += copy;
|
||||
src += copy;
|
||||
remaining -= copy;
|
||||
if (loopback_accum_.count == full) {
|
||||
cb(loopback_kind_, loopback_accum_.buf.data(), frame_samples_, ch);
|
||||
loopback_accum_.count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
#ifdef VOICECAT_HAS_AUDIO
|
||||
static void capture_data_cb(ma_device*, void*, const void*, ma_uint32);
|
||||
@@ -222,6 +268,7 @@ class AudioEngine {
|
||||
ma_device loopback_device_{};
|
||||
bool loopback_started_ = false;
|
||||
int loopback_kind_ = 0;
|
||||
int loopback_channels_ = 1; // channel count the loopback device was opened with
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user