import { Disposable } from "@codingame/monaco-vscode-api/vscode/vs/base/common/lifecycle"; import { Event } from "@codingame/monaco-vscode-api/vscode/vs/base/common/event"; import { IStorageService } from "@codingame/monaco-vscode-api/vscode/vs/platform/storage/common/storage.service"; import { INotificationService } from "@codingame/monaco-vscode-api/vscode/vs/platform/notification/common/notification.service"; import { ILogService } from "@codingame/monaco-vscode-api/vscode/vs/platform/log/common/log.service"; import { IMicCaptureService } from "@codingame/monaco-vscode-api/vscode/vs/workbench/contrib/chat/browser/voiceClient/micCaptureService.service"; /** * Per-PTT-press diagnostic emitted after `pttUp` once the diagnostic * window closes. Logged + sent to backend so we can correlate frontend * audio bookkeeping with backend ASR results via `turnId`. * * Drain model: after `pttUp` the service keeps streaming audio chunks * for a fixed "drain window" (~500ms by default). The drain ends as * soon as it has shipped enough samples to cover the window (or a * fallback timer trips if `onaudioprocess` stops firing). Only AFTER * the drain has closed does `_onPttEnd` fire. The diagnostic window is * intentionally LONGER than the drain window so any audio still * produced after drain end (within the diagnostic window) is counted * as `postReleaseCallbacks` -- a direct signal that the drain window * is too short for this device/load and the fix needs to extend it. * * Field interpretation: * - `drainChunks` / `drainSamples` => audio captured during the drain * window and shipped to the backend. Non-zero in normal operation. * - `postReleaseCallbacks > 0` => the WebAudio pipeline produced more * audio AFTER the drain window closed but before the diagnostic * window. This audio was DROPPED; if it happens often the drain * window needs to grow. * - `drainSkippedBy*` > 0 => the drain was muted or AEC-suppressed. * Tail audio for that press was lost; investigate the mute / AEC * suppression path rather than the drain window. * - `pttUpWithoutCapture` => pttUp arrived while mic was not capturing. * - `releasedDuringAcquire` => user released while mic was still being * acquired; no audio was ever recorded for this press. */ export interface IPttDiagnostic { readonly turnId: string; readonly msHeld: number; readonly chunksSent: number; readonly samplesSent: number; readonly drainFired: boolean; readonly drainChunks: number; readonly drainSamples: number; readonly drainWindowMs: number; readonly drainSkippedByMute: number; readonly drainSkippedBySuppression: number; readonly postReleaseCallbacks: number; readonly postReleaseSamples: number; readonly postReleaseSkippedByMute: number; readonly postReleaseSkippedBySuppression: number; readonly postReleaseWindowMs: number; readonly releasedDuringAcquire: boolean; readonly pttUpWithoutCapture: boolean; } export declare class MicCaptureService extends Disposable implements IMicCaptureService { private readonly storageService; private readonly notificationService; private readonly logService; readonly _serviceBrand: undefined; constructor(storageService: IStorageService, notificationService: INotificationService, logService: ILogService); private _window; private _micStream; private _micCtx; private _scriptNode; private _analyserNode; private _isCapturing; private _pttHeld; private _pttStreaming; private _isMuted; private _suppressUntilTs; private _pttAcquiring; private _pttReleasedDuringAcquire; private static readonly _PTT_DRAIN_WINDOW_MS; private _pttDrainTargetSamples; private _pttDrainSamplesSent; private _pttDrainFallbackTimer; private static readonly _DIAG_POST_RELEASE_WINDOW_MS; private _diagTurnId; private _diagPttDownTs; private _diagPttUpTs; private _diagChunksSent; private _diagSamplesSent; private _diagDrainFired; private _diagDrainChunks; private _diagDrainSamples; private _diagDrainSkippedByMute; private _diagDrainSkippedBySuppression; private _diagPostReleaseCallbacks; private _diagPostReleaseSamples; private _diagPostReleaseSkippedByMute; private _diagPostReleaseSkippedBySuppression; private _diagReleasedDuringAcquire; private _diagPttUpWithoutCapture; private _diagFireTimer; private readonly _onPttStart; readonly onPttStart: Event; private readonly _onPttAudioChunk; readonly onPttAudioChunk: Event; private readonly _onPttEnd; readonly onPttEnd: Event; private readonly _onPttDiagnostic; readonly onPttDiagnostic: Event; get isCapturing(): boolean; get analyserNode(): AnalyserNode | undefined; get isMuted(): boolean; set isMuted(value: boolean); suppressUntil(timestamp: number): void; prepare(window: Window & typeof globalThis): void; pttDown(turnId: string): Promise; pttUp(): void; startCapture(window: Window & typeof globalThis): Promise; private _notifyMicPermissionDenied; stopCapture(): void; dispose(): void; /** * End the post-release drain phase: stop accepting more audio for * this turn and fire `_onPttEnd`. Idempotent. Safe to call when no * drain is in progress. */ private _finishDrain; private _resetDiagnosticCounters; private _scheduleDiagnosticFire; private _flushPendingDiagnostic; private _emitDiagnostic; }