-
Notifications
You must be signed in to change notification settings - Fork 0
fix: snap illegal AAC sample rates to 48 kHz on Windows #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This changes the Windows native capture and audio-encoding path, but the commit does not append a Windows capture-to-export result to AGENTS.md reference: AGENTS.md:L84-L90 Useful? React with 👍 / 👎. |
||
| format.channels = 2; | ||
| format.bitsPerSample = 16; | ||
| format.blockAlign = format.channels * (format.bitsPerSample / 8); | ||
|
|
@@ -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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. 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); | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a 96 or 192 kHz WASAPI source is snapped here,
AudioMixer::appendnow sends every packet throughconvertAudioWithGain, 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 👍 / 👎.