-
Notifications
You must be signed in to change notification settings - Fork 0
Audio
src/audio/audio_engine.cpp, src/audio/dshow_audio_capture.cpp,
src/audio/mic_capture.cpp, src/audio/audio_ring.cpp
The path is: capture device → ring buffer → WASAPI render endpoint.
Routing the card's audio through CapView'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.
tapOverflows() 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 the microphone have one.
mic_capture.cpp. An optional second WASAPI capture with its own gain.
- Never played back. You already hear yourself.
- Never mixed into the playback path.
- Becomes a second input to ffmpeg, at whatever rate its device runs at — no resampling in CapView; ffmpeg is told the rate and does it on the way into AAC.
See Recording for the track layout options.
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.