Skip to content
NuclearMeltdown edited this page Aug 24, 2026 · 2 revisions

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.

Two capture backends

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.

Drift correction

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 and starvation

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.

The recording tap

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.

Latency settings

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.

Level meters

float inputPeak() const;   // 0..1, with a decay

Measured 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.

The microphone

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.

Device loss

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.

Clone this wiki locally