Files
LlamaCasty/Services/Audio/NamedPipeAudioWriter.cs
T
gramps f3d578c81b 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.
2026-09-10 10:55:13 -07:00

119 lines
3.3 KiB
C#

using System.IO.Pipes;
using System.Runtime.InteropServices;
namespace ytLive.Services.Audio;
/// <summary>
/// The mixer's sink for the live loop: interleaved stereo PCM float at 48 kHz
/// written to a named pipe, byte-identical to what ffmpeg expects from
/// <c>-f f32le -ar 48000 -ac 2 -i \\.\pipe\&lt;name&gt;</c>. The encoder side is
/// handled by FfmpegArgs/EncoderOptions; this side just owns the server end.
/// </summary>
public interface IAudioPipeWriter : IDisposable
{
/// <summary>Creates the named pipe server and waits (async) for the client.</summary>
void Start(string pipeName);
/// <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);
/// <summary>Closes the server end — ffmpeg sees EOF and ends the audio input.</summary>
void Stop();
}
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)
{
if (_pipe != null)
throw new InvalidOperationException("Already started.");
_pipe = new NamedPipeServerStream(
pipeName,
PipeDirection.Out,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
}
_ = Task.Run(WaitForConnectionAsync);
}
public bool IsConnected
{
get
{
lock (_sync)
{
return _pipe is { IsConnected: true };
}
}
}
public async Task WriteAsync(ReadOnlyMemory<float> samples, CancellationToken cancellationToken = default)
{
NamedPipeServerStream? pipe;
lock (_sync)
{
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);
}
public void Stop()
{
NamedPipeServerStream? pipe;
lock (_sync)
{
pipe = _pipe;
_pipe = null;
}
pipe?.Dispose();
}
public void Dispose() => Stop();
private async Task WaitForConnectionAsync()
{
NamedPipeServerStream? pipe;
lock (_sync)
{
pipe = _pipe;
}
if (pipe == null)
return;
try
{
await pipe.WaitForConnectionAsync().ConfigureAwait(false);
}
catch
{
Stop();
}
}
}