fix(audio): add per-5s live-loop telemetry to name the silent-recording stage
The recorded audio is full-length silent AAC (−91dB, 1124 frames/23.95s) —
the named pipe carried ~9.1MB of zeros the entire take. The mixer's loop
ran, the pipe connected, ffmpeg read it, and the file is full-length
silence. Sources started fine ('Audio: using system default mic...' logged),
no failure callbacks fired, and the existing integration test
(Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe) proves the loop→pipe path
carries real audio when fed — the fault is capture-side.
Added permanent per-5s live-loop telemetry to startup.log so the next take
names the exact stage without new code:
Audio live: pipe connected= dropped writes= micLevel= loopLevel= drained
N/N samples peakMix N (per 5s)
- AudioMixer.FillAndMix now returns (MicRms, MicDrained, LoopDrained) for
the accumulation; StartLive/StopLive log start/stop lines.
- NamedPipeAudioWriter.DroppedWrites: nonzero = audio dropped before ffmpeg
connected (names 'pipe never connected' stage).
Likely root cause: WASAPI loopback captures the default render endpoint —
if audio plays on a non-default device the recording is silently silent.
The fix requires device enumeration + selection (slice 13 candidate).
Suite 290/291 — same sole pre-existing compositor pixel failure.
This commit is contained in:
@@ -50,6 +50,10 @@ public sealed class AudioMixer : IDisposable
|
||||
private float[]? _mixBuffer;
|
||||
private float[]? _delayedMix;
|
||||
private DateTime _lastLoopErrorLogged = DateTime.MinValue;
|
||||
private DateTime _nextStatsLog = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
||||
private long _micDrainedTotal;
|
||||
private long _loopDrainedTotal;
|
||||
private float _peakMix;
|
||||
|
||||
public AudioMixer(
|
||||
IAudioSource mic,
|
||||
@@ -159,6 +163,7 @@ public sealed class AudioMixer : IDisposable
|
||||
_liveCts = cts;
|
||||
_pipe = new NamedPipeAudioWriter();
|
||||
_pipe.Start(pipeName);
|
||||
_log?.Invoke($"Audio live: started, pipe '{pipeName}'");
|
||||
_ = Task.Run(() => LiveLoopAsync(_pipe, cts.Token));
|
||||
}
|
||||
|
||||
@@ -169,7 +174,11 @@ public sealed class AudioMixer : IDisposable
|
||||
{
|
||||
var cts = _liveCts;
|
||||
_liveCts = null;
|
||||
cts?.Cancel();
|
||||
if (cts != null)
|
||||
{
|
||||
_log?.Invoke("Audio live: stopped");
|
||||
cts.Cancel();
|
||||
}
|
||||
_pipe.Stop();
|
||||
}
|
||||
|
||||
@@ -274,8 +283,28 @@ public sealed class AudioMixer : IDisposable
|
||||
var nextTick = DateTime.UtcNow + _mixInterval;
|
||||
try
|
||||
{
|
||||
FillAndMix(micChunk, loopbackChunk, mixBuffer);
|
||||
var (_, micDrained, loopDrained) = FillAndMix(micChunk, loopbackChunk, mixBuffer);
|
||||
await pipe.WriteAsync(mixBuffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_micDrainedTotal += micDrained;
|
||||
_loopDrainedTotal += loopDrained;
|
||||
for (var i = 0; i < mixBuffer.Length; i++)
|
||||
{
|
||||
var abs = Math.Abs(mixBuffer[i]);
|
||||
if (abs > _peakMix) _peakMix = abs;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (now >= _nextStatsLog && _log != null)
|
||||
{
|
||||
_nextStatsLog = now + TimeSpan.FromSeconds(5);
|
||||
_log($"Audio live: pipe connected={pipe.IsConnected} dropped writes={pipe.DroppedWrites} " +
|
||||
$"micLevel={_meter.Level:F3} loopLevel={_loopbackMeter.Level:F3} " +
|
||||
$"drained {_micDrainedTotal:N0}/{_loopDrainedTotal:N0} samples peakMix {_peakMix:F3} (per 5s)");
|
||||
_micDrainedTotal = 0;
|
||||
_loopDrainedTotal = 0;
|
||||
_peakMix = 0;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -313,8 +342,11 @@ public sealed class AudioMixer : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Drains one tick's worth of mic + loopback, silence-fills any
|
||||
/// underrun, applies the ducker and the honest gains, and mixes to stereo.</summary>
|
||||
private float FillAndMix(float[] micChunk, float[] loopbackChunk, float[] mix)
|
||||
/// underrun, applies the ducker and the honest gains, and mixes to stereo.
|
||||
/// Returns the pre-duck mic RMS and how many samples each buffer contributed
|
||||
/// (the 5s live-loop telemetry reads those to name whether capture or the
|
||||
/// pipe starved a silent recording).</summary>
|
||||
private (float MicRms, int MicDrained, int LoopDrained) FillAndMix(float[] micChunk, float[] loopbackChunk, float[] mix)
|
||||
{
|
||||
var micCount = _micBuffer.Read(micChunk, micChunk.Length);
|
||||
var loopCount = _loopbackBuffer.Read(loopbackChunk, loopbackChunk.Length);
|
||||
@@ -362,6 +394,6 @@ public sealed class AudioMixer : IDisposable
|
||||
delayed = _delayedMix = new float[mix.Length];
|
||||
_syncDelay.Process(mix, delayed);
|
||||
Array.Copy(delayed, mix, mix.Length);
|
||||
return micRms;
|
||||
return (micRms, micCount, loopCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ public interface IAudioPipeWriter : IDisposable
|
||||
/// <summary>True once ffmpeg has connected and the pipe is writable.</summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>Count of <see cref="WriteAsync"/> calls that wrote nothing because
|
||||
/// the pipe wasn't connected yet or was already closed. Part of the silent-
|
||||
/// recording diagnosis: nonzero means audio was dropped before ffmpeg ever
|
||||
/// connected to the pipe.</summary>
|
||||
long DroppedWrites { get; }
|
||||
|
||||
/// <summary>Writes interleaved float samples; dropped until the client connects.</summary>
|
||||
Task WriteAsync(ReadOnlyMemory<float> samples, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -29,6 +35,9 @@ public sealed class NamedPipeAudioWriter : IAudioPipeWriter
|
||||
private readonly object _sync = new();
|
||||
private NamedPipeServerStream? _pipe;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long DroppedWrites { get; private set; }
|
||||
|
||||
public void Start(string pipeName)
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -64,7 +73,11 @@ public sealed class NamedPipeAudioWriter : IAudioPipeWriter
|
||||
pipe = _pipe;
|
||||
}
|
||||
if (pipe == null || !pipe.IsConnected || samples.IsEmpty)
|
||||
{
|
||||
if (pipe?.IsConnected == false || pipe == null)
|
||||
DroppedWrites++;
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = MemoryMarshal.AsBytes(samples.Span).ToArray();
|
||||
await pipe.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user