Captura de audio
CaptureCoordinator arranca y para tres grabadores, elige la pista principal al terminar y escribe el fichero de recuperación. Cada grabador implementa una interfaz (IProcessCapture, IDeviceCapture, IMicCapture), y los tests usan dobles en su lugar.
Los tres grabadores
| Grabador | API | Formato del WAV | Fichero temporal |
|---|---|---|---|
ProcessLoopbackRecorder |
WASAPI process loopback (COM propio en Interop/) |
PCM 16 bits, 44,1 kHz, estéreo (AUTOCONVERTPCM) |
recording_<fecha>.wav |
DeviceLoopbackRecorder |
WasapiLoopbackCapture de NAudio sobre la salida predeterminada, roles Console y Communications (sin duplicar si son el mismo dispositivo) |
El nativo del dispositivo | recording_<fecha>_console.wav, recording_<fecha>_communications.wav |
MicrophoneRecorder |
WasapiCapture de NAudio sobre la entrada predeterminada (rol Multimedia) |
El nativo del dispositivo | recording_<fecha>_mic.wav |
Todos escriben en %TEMP%\LoopbackTranscriber\, con <fecha> = yyyyMMdd_HHmmss.
Process loopback
Wasapi.ActivateProcessLoopbackAsync(pid, includeProcessTree) activa un IAudioClient virtual con AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK y el modo INCLUDE_TARGET_PROCESS_TREE. Esta API existe a partir de Windows 10 build 20348.
- El cliente se inicializa en modo compartido con
LOOPBACK | EVENTCALLBACK | AUTOCONVERTPCM | SRC_DEFAULT_QUALITY. - Un hilo dedicado (
LoopbackTranscriber-Capture) espera el evento de muestras y vuelca cada paquete al WAV. - Además de escribir en disco, cada bloque se publica en el evento
AudioCaptured, que alimenta la traducción en vivo. Solo este grabador lo publica: la sesión en vivo solo oye a la aplicación. - El WAV se abre con
FileShare.ReadWrite | FileShare.Deletey una cabecera provisional. Los tamaños reales se escriben al parar. Si la aplicación muere antes, la cabecera queda a cero y la repara la recuperación de huérfanas. - Al parar, espera al hilo 5 s. Si no termina, para el
IAudioClienty espera otros 5 s. Si sigue sin terminar, abandona la sesión: corrige la cabecera con lo escrito hasta entonces y devuelve el fichero. Un controlador colgado nunca bloquea la aplicación. - Si la captura terminó con un error pero hay datos, se conserva el audio y se registra un aviso. Sin datos, se lanza
ErrorAudioCapture.
Proceso que se graba
TargetProcessWatcher revisa cada 1,5 s las aplicaciones vigiladas (TargetApp.ProcessNames). Para cada una:
Process.GetProcessesByNamecon cada nombre candidato, por ejemploms-teamsy despuésTeams.ProcessTree.FindRootProcessIdse queda con el proceso raíz de los encontrados.AudioSessionFinder.FindActiveProcessIdbusca entre las sesiones de audio activas una de esos procesos. Si la hay, se usa ese PID.
La pantalla Aplicaciones usa AudioSessionFinder.ListRunningApps() para listar los procesos que tienen una sesión de audio.
Huecos de tiempo: CaptureTimeline
WASAPI no entrega paquetes cuando no hay nada sonando. Si se escribieran sin más, el WAV se acortaría y las pistas se desalinearían. CaptureTimeline compara la marca de tiempo de cada paquete (QPC) con las muestras ya escritas:
- Si faltan más de 250 ms, rellena con silencio (
WriteSilence) antes del paquete. - Guarda
FirstSampleTimestamp, el instante de la primera muestra, que sirve para alinear el micrófono. - Los paquetes con
AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERRORno se usan para medir huecos. Los dispositivos de NAudio, que no dan marca de tiempo, usan la hora de llegada menos la duración del paquete.
Arranque (StartAsync)
- Genera
baseName = recording_<fecha>y lo marca como propio, para que la recuperación de huérfanas no lo toque mientras se graba. - Arranca la captura del proceso. Si falla, la grabación no empieza y el error llega al usuario.
- Escribe
recording_<fecha>.session.jsoncon la aplicación y la hora. Si no puede, avisa conWarningRecoveryFileNotWritten. - Arranca la captura del dispositivo. Si falla, se registra y se sigue sin ella.
- Arranca el micrófono. Si falla, avisa con
WarningMicNotRecorded. - Reescribe el
.session.jsoncon las horas de inicio de cada pista.
Parada (StopAsync)
Para las tres capturas: el proceso primero y después el dispositivo y el micrófono, cada una en el thread pool. El fallo de una no impide parar las otras. El micrófono espera como mucho 3 s el RecordingStopped, y lo mismo cada canal del dispositivo. Después:
Elección de la pista principal
flowchart LR
A([Al parar]) --> B{"¿La pista del proceso<br/>tiene pico ≥ 0,01?"}
B -->|sí| P[Pista del proceso]
B -->|no| C{"¿Alguna pista de salida<br/>tiene pico ≥ 0,01?"}
C -->|sí| D["Pista de salida<br/>con más pico"]
C -->|no| E["Pista del proceso,<br/>aunque esté en silencio"]
class P,D ok
class E fail
El umbral es CaptureCoordinator.DeviceSignalThreshold = 0.01, en escala de 0 a 1. Si la pista del micrófono no supera ese mismo umbral, MicrophoneRecorder la borra al parar.
Desfase del micrófono
La pista del micrófono empieza en otro instante que la principal. El desfase (MicStartOffset) se calcula:
- con las marcas de la primera muestra de cada pista (
FirstSampleTimestamp), si las dos existen; - si no, con la diferencia entre las horas de inicio de cada captura.
El resultado es un StoppedRecording: pista principal, micrófono, desfase, todas las rutas, el error de parada si lo hubo, el baseName y los avisos.
Después del procesado
- Si tuvo éxito,
DeleteOriginalsborra todos los WAV temporales y el.session.json, y libera elbaseName. - Si falló,
PreserveFailedmueve la pista principal y el micrófono aFailedRecordingStore. Si ni eso es posible, los originales se quedan en la carpeta temporal y la recuperación los encontrará en el siguiente arranque. - Cada vez que se ejecuta
DeleteOriginals, llama también aDeleteStaleRecordings, que borra losrecording_*.16k.wav(ficheros derivados para Whisper) con más de 7 días que hayan quedado en la carpeta temporal.