Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions electron/native/wgc-capture/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,25 @@ target_link_libraries(cursor-sampler PRIVATE
gdi32
gdiplus
)

add_executable(audio_sample_utils_test
src/audio_sample_utils.cpp
src/audio_sample_utils.h
src/audio_sample_utils_test.cpp
)

target_compile_definitions(audio_sample_utils_test PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
_WIN32_WINNT=0x0A00
)

target_compile_options(audio_sample_utils_test PRIVATE /EHsc /W4 /utf-8)

target_link_libraries(audio_sample_utils_test PRIVATE
mf
mfplat
mfreadwrite
mfuuid
ole32
)
78 changes: 68 additions & 10 deletions electron/native/wgc-capture/src/audio_sample_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ double readMappedChannel(const BYTE* source, const AudioInputFormat& format, siz
return readSampleAsDouble(source, format, frameIndex, std::min(targetChannel, format.channels - 1));
}

UINT32 aacCompatibleSampleRate(UINT32 sampleRate) {
constexpr UINT32 kAacSampleRates[] = {
8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000,
};
if (sampleRate == 0) {
return 48000;
}
for (UINT32 rate : kAacSampleRates) {
if (sampleRate == rate) {
return rate;
}
}
return 48000;
}

} // namespace

constexpr int64_t HnsPerSecond = 10'000'000;
Expand All @@ -100,10 +115,16 @@ bool sameAudioFormatForMixing(const AudioInputFormat& left, const AudioInputForm
left.avgBytesPerSec == right.avgBytesPerSec;
}

// Microsoft AAC encoder (MFAudioFormat_AAC) sample rates. WASAPI loopback
// often reports 96000 or 192000; those are legal PCM mix rates but not AAC
// input rates, and SetInputMediaType then fails with MF_E_INVALIDMEDIATYPE
// (0xc00d36b4). Keep legal rates as-is so a working 44100/48000 path is
// unchanged; snap everything else (including 0) to 48000. The mixer already
// resamples through convertAudioWithGain when the source rate differs.
AudioInputFormat makeAacCompatibleAudioFormat(const AudioInputFormat& source) {
AudioInputFormat format{};
format.subtype = MFAudioFormat_PCM;
format.sampleRate = source.sampleRate > 0 ? source.sampleRate : 48000;
format.sampleRate = aacCompatibleSampleRate(source.sampleRate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resample high-rate input instead of decimating it

When a 96 or 192 kHz WASAPI source is snapped here, AudioMixer::append now sends every packet through convertAudioWithGain, whose rate conversion selects one nearest source frame per output frame without a low-pass filter. Downsampling therefore aliases source energy above 24 kHz into the audible 48 kHz recording—for example, 30 kHz microphone noise becomes an 18 kHz tone—so the newly supported high-rate-device path can produce audible artifacts. Use an anti-aliased, stateful resampler when the rate changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record the required Windows native smoke test

This changes the Windows native capture and audio-encoding path, but the commit does not append a Windows capture-to-export result to technical-documentation/testing/manual-e2e-checklist.md. The repository requires that pass after every native-capture change and says the run does not count until its platform, build, coverage, and skipped checks are recorded, so add the corresponding results-log row before treating this fix as validated.

AGENTS.md reference: AGENTS.md:L84-L90

Useful? React with 👍 / 👎.

format.channels = 2;
format.bitsPerSample = 16;
format.blockAlign = format.channels * (format.bitsPerSample / 8);
Expand Down Expand Up @@ -186,24 +207,61 @@ void convertAudioWithGain(
return;
}

// Integer-factor downsample (96 kHz / 192 kHz -> 48 kHz): average each
// group of source frames instead of picking one. Nearest-neighbour
// decimation aliases content above the new Nyquist into the recording.
if (sourceFormat.sampleRate > targetFormat.sampleRate &&
sourceFormat.sampleRate % targetFormat.sampleRate == 0) {
const UINT32 factor = sourceFormat.sampleRate / targetFormat.sampleRate;
const size_t targetFrames = sourceFrames / factor;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve partial frames across capture packets

When a 96 kHz WASAPI packet has an odd frame count, or a 192 kHz packet has a count not divisible by four, this integer division discards the packet's remaining source frames. AudioMixer::append invokes this conversion independently for every GetBuffer result and retains no remainder or resampling phase, so repeated non-aligned packets lose samples and introduce discontinuities or accumulated timing error in the captured audio. Carry the unused frames into the next callback instead of truncating each packet independently.

Useful? React with 👍 / 👎.

if (targetFrames > 0) {
destination.assign(targetFrames * targetFormat.blockAlign, 0);
for (size_t targetFrame = 0; targetFrame < targetFrames; ++targetFrame) {
for (UINT32 channel = 0; channel < targetFormat.channels; ++channel) {
double sum = 0.0;
for (UINT32 tap = 0; tap < factor; ++tap) {
sum += readMappedChannel(
source,
sourceFormat,
targetFrame * factor + tap,
channel,
targetFormat.channels);
}
writeSampleFromDouble(
destination.data(),
targetFormat,
targetFrame,
channel,
(sum / static_cast<double>(factor)) * gain);
}
}
return;
}
// Too few source frames for one averaged output frame (tiny WASAPI
// packets). Fall through to interpolation instead of dropping them.
}

const double rateRatio = static_cast<double>(targetFormat.sampleRate) /
static_cast<double>(sourceFormat.sampleRate);
const size_t targetFrames = std::max<size_t>(1, static_cast<size_t>(std::llround(sourceFrames * rateRatio)));
destination.assign(targetFrames * targetFormat.blockAlign, 0);

for (size_t targetFrame = 0; targetFrame < targetFrames; ++targetFrame) {
const double sourcePosition = static_cast<double>(targetFrame) / rateRatio;
const size_t sourceFrame = std::min(
sourceFrames - 1,
static_cast<size_t>(std::llround(sourcePosition)));
const size_t sourceFrame = std::min(sourceFrames - 1, static_cast<size_t>(sourcePosition));
const size_t nextFrame = std::min(sourceFrames - 1, sourceFrame + 1);
const double frac = sourcePosition - static_cast<double>(sourceFrame);
for (UINT32 channel = 0; channel < targetFormat.channels; ++channel) {
const double sample = readMappedChannel(
source,
sourceFormat,
sourceFrame,
const double a = readMappedChannel(
source, sourceFormat, sourceFrame, channel, targetFormat.channels);
const double b = readMappedChannel(
source, sourceFormat, nextFrame, channel, targetFormat.channels);
writeSampleFromDouble(
destination.data(),
targetFormat,
targetFrame,
channel,
targetFormat.channels);
writeSampleFromDouble(destination.data(), targetFormat, targetFrame, channel, sample * gain);
(a + (b - a) * frac) * gain);
}
}
}
Expand Down
Loading
Loading