Repository navigation
Audio
src/audio/audio_engine.cpp, src/audio/dshow_audio_capture.cpp,
src/audio/source_capture.cpp, src/audio/audio_ring.cpp
The path is: capture device → ring buffer → WASAPI render endpoint.
Routing the card's audio through qBlank's own ring is what makes the delay adjustable at all. A DirectShow audio renderer would hand you whatever buffering it feels like, typically over a hundred milliseconds; here the target fill of the ring is the latency.
WASAPI for anything Windows exposes as a recording device — which is most cards' embedded audio.
DirectShow (dshow_audio_capture.cpp) for cards whose embedded audio Windows
does not expose as a sound device. That builds a small graph with a sample-grab
sink of the same shape as the video one.
Playback is always WASAPI, so the output device picker and the exclusive-mode option work either way.
OnCapturedAudio() is the single entry point for captured audio, whichever
backend produced it. It feeds the playback ring and, when recording, the tap.
The card's sample clock and the sound device's sample clock are not the same clock and never will be. Over an hour they separate by enough to matter.
The correction is a playback rate nudge, computed each callback from how full the ring is against where it should be:
double ratio = (double)captureRate / (double)fmt.sampleRate;
const double err = ((double)available - (double)targetFrames) / (double)targetFrames;
ratio *= Clamp(1.0 + 0.05 * err, 0.997, 1.003);Three tenths of one per cent at the extreme — well under the threshold of audibility for pitch, and it absorbs the drift without ever cutting the stream. No resampler, no dropped blocks, no clicks.
The resampling itself is linear interpolation between the two neighbouring
frames, with prev holding the last frame of the previous callback so the
interpolation is continuous across callback boundaries.
const double endPos = srcFrac + ratio * frames;
const size_t shift = (size_t)std::floor(endPos);Everything up to shift is consumed; the rest stays in pending for the next
callback, so no sample is ever silently dropped.
Priming: nothing is played until the ring holds a full target's worth. Until then, silence.
Starvation: if the ring falls below a quarter of the target, the engine un-primes and writes a clean gap of silence while it refills.
} else if (available * 4 < targetFrames) {
primed = false;
writeSilence(frames);
continue;
}That is what a card with no signal locked does — it delivers less than real time. One clean gap beats grinding along on held samples.
Underrun: when the ring runs short mid-callback, the last known sample value
is held rather than dropping to silence. Far less clicky, and a short hold is
nearly inaudible. underruns_ counts it, and the statistics overlay reports it.
A second ring receives the captured audio exactly as it arrived: no resampling, no drift correction, no volume, no mute.
The playback ring is deliberately not usable for this. It nudges the playback rate by a fraction of a per cent to hold its target fill, which is right for listening and wrong for a file that has to stay in sync over an hour — and it would bake the volume slider into the recording.
tapOverruns() counts how often the tap ring overflowed, which is the recorder
falling behind.
Buffer target is the fill the ring is held at, in milliseconds, and is
therefore the latency. AudioEngine raises what it actually aims for above the
configured value to clear the playback device's own buffer — asking for 10 ms
when the endpoint's own period is 10 ms would leave nothing to absorb a single
late callback.
Exclusive mode takes the endpoint away from the rest of Windows, which removes the mixer from the path and lowers the floor. Nothing else on the machine can make a sound while it is held.
A/V offset shifts the audio against the picture, in milliseconds, either way. The card's audio and video paths do not necessarily have the same delay, and no card reports what the difference is.
float inputPeak() const; // 0..1, with a decayMeasured before volume and mute, so the meter shows what the card is delivering rather than how loud you have it. A silent meter therefore means no signal, not a muted output — which is the question you actually want answered when nothing is coming out.
Both the capture input and every recording source have one.
source_capture.cpp. Any number of extra sounds for the recording, each with
its own gain, added and removed on the audio tab.
| Kind | Records |
|---|---|
| Input | a microphone, or any other input device |
| Output device | what plays on that device (WASAPI loopback) |
| System sound | everything Windows plays except qBlank (process loopback, Windows 10 2004+) |
- Never played back and never mixed into what you hear.
- System sound leaves qBlank out, so the game is not in the file twice. An output device has no such filter: if the card plays there too, the settings warn that it would be recorded twice.
- An idle output delivers nothing: a loopback on a device that plays nothing gets no packets at all. qBlank plays silence on it while the source is open. Process loopback delivers silence by itself.
- Open only while used: while recording, and while the settings are open so the meter means something. Longer would light the Windows privacy indicator for nothing.
- The card's sound heads the list, fixed, with its meter; it is picked under Source. The rest starts empty and is added by hand below it. Settings from before the list keep their microphone if it was used.
- During a recording only the gain can change: the tracks are laid out.
See Recording for the track layout.
AudioEngine sets a flag when a device disappears (unplugged, driver reload,
default device changed). The app notices and offers a restart rather than silently
running with no sound.