Audio capture
CaptureCoordinator starts and stops three recorders, picks the main track when recording ends and writes the recovery file. Each recorder implements an interface (IProcessCapture, IDeviceCapture, IMicCapture), and the tests use doubles instead.
The three recorders
| Recorder | API | WAV format | Temporary file |
|---|---|---|---|
ProcessLoopbackRecorder |
WASAPI process loopback (own COM interop in Interop/) |
16-bit PCM, 44.1 kHz, stereo (AUTOCONVERTPCM) |
recording_<date>.wav |
DeviceLoopbackRecorder |
NAudio WasapiLoopbackCapture on the default output, Console and Communications roles (not duplicated when they are the same device) |
The device's native format | recording_<date>_console.wav, recording_<date>_communications.wav |
MicrophoneRecorder |
NAudio WasapiCapture on the default input (Multimedia role) |
The device's native format | recording_<date>_mic.wav |
They all write to %TEMP%\LoopbackTranscriber\, with <date> = yyyyMMdd_HHmmss.
Process loopback
Wasapi.ActivateProcessLoopbackAsync(pid, includeProcessTree) activates a virtual IAudioClient with AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK and the INCLUDE_TARGET_PROCESS_TREE mode. This API exists from Windows 10 build 20348 onwards.
- The client is initialized in shared mode with
LOOPBACK | EVENTCALLBACK | AUTOCONVERTPCM | SRC_DEFAULT_QUALITY. - A dedicated thread (
LoopbackTranscriber-Capture) waits for the sample event and writes each packet to the WAV. - Besides being written to disk, each block is published on the
AudioCapturedevent, which feeds live translation. Only this recorder publishes it: the live session only hears the application. - The WAV is opened with
FileShare.ReadWrite | FileShare.Deleteand a placeholder header. The real sizes are written when recording stops. If the application dies before then, the header stays at zero and orphan recovery repairs it. - When stopping, it waits 5 s for the thread. If it does not finish, it stops the
IAudioClientand waits another 5 s. If it still does not finish, it abandons the session: it fixes the header with what was written so far and returns the file. A hung driver never blocks the application. - If the capture ended with an error but there is data, the audio is kept and a warning is logged. With no data,
ErrorAudioCaptureis thrown.
Which process is recorded
TargetProcessWatcher checks the watched applications (TargetApp.ProcessNames) every 1.5 s. For each one:
Process.GetProcessesByNamewith each candidate name, for examplems-teamsand thenTeams.ProcessTree.FindRootProcessIdkeeps the root process among those found.AudioSessionFinder.FindActiveProcessIdlooks among the active audio sessions for one belonging to those processes. If there is one, that PID is used.
The Applications screen uses AudioSessionFinder.ListRunningApps() to list processes that have an audio session.
Time gaps: CaptureTimeline
WASAPI delivers no packets when nothing is playing. Written as-is, the WAV would get shorter and the tracks would drift apart. CaptureTimeline compares each packet's timestamp (QPC) with the samples already written:
- If more than 250 ms are missing, it fills the gap with silence (
WriteSilence) before the packet. - It keeps
FirstSampleTimestamp, the instant of the first sample, which is used to align the microphone. - Packets flagged
AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERRORare not used to measure gaps. NAudio devices, which provide no timestamp, use the arrival time minus the packet duration.
Start (StartAsync)
- Generates
baseName = recording_<date>and marks it as owned, so orphan recovery does not touch it while recording. - Starts the process capture. If that fails, recording does not start and the error reaches the user.
- Writes
recording_<date>.session.jsonwith the application and the time. If it cannot, it warns withWarningRecoveryFileNotWritten. - Starts the device capture. If that fails, it is logged and recording continues without it.
- Starts the microphone. If that fails, it warns with
WarningMicNotRecorded. - Rewrites the
.session.jsonwith the start time of each track.
Stop (StopAsync)
Stops the three captures: the process first, then the device and the microphone, each on the thread pool. A failure in one does not prevent stopping the others. The microphone waits at most 3 s for RecordingStopped, and so does each device channel. Then:
Choosing the main track
flowchart LR
A([On stop]) --> B{"Does the process track<br/>have a peak ≥ 0.01?"}
B -->|yes| P[Process track]
B -->|no| C{"Does any output track<br/>have a peak ≥ 0.01?"}
C -->|yes| D["Output track with<br/>the highest peak"]
C -->|no| E["Process track,<br/>even if silent"]
class P,D ok
class E fail
The threshold is CaptureCoordinator.DeviceSignalThreshold = 0.01, on a 0-to-1 scale. If the microphone track does not exceed that same threshold, MicrophoneRecorder deletes it when stopping.
Microphone offset
The microphone track starts at a different instant from the main one. The offset (MicStartOffset) is computed:
- from the first-sample timestamps of each track (
FirstSampleTimestamp), if both exist; - otherwise, from the difference between the start times of each capture.
The result is a StoppedRecording: main track, microphone, offset, all paths, the stop error if any, the baseName and the warnings.
After processing
- On success,
DeleteOriginalsdeletes all the temporary WAVs and the.session.json, and releases thebaseName. - On failure,
PreserveFailedmoves the main track and the microphone toFailedRecordingStore. If even that is not possible, the originals stay in the temporary folder and recovery will find them on the next start. - Every time
DeleteOriginalsruns, it also callsDeleteStaleRecordings, which deletes therecording_*.16k.wavfiles (derived files for Whisper) older than 7 days left in the temporary folder.