TASK 4 ship step 4: WASAPI audio capture — NAudio loopback + mic behind an IAudioSource seam, AudioMixer driving AudioLevel while live, pure level meter + WaveToFloat, NAudio.Wasapi 2.2.1 (MIT, notices item 9) — 139 tests passing, 0 warnings

This commit is contained in:
2026-08-12 21:18:07 -07:00
parent ba427e85e1
commit 58c0f8e8c4
16 changed files with 814 additions and 34 deletions
+26
View File
@@ -14,6 +14,7 @@ using Microsoft.Win32;
using ytLive.Helpers;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Audio;
namespace ytLive.ViewModels;
@@ -79,6 +80,12 @@ public class MainViewModel : ViewModelBase
private SocialsConfig? _socials;
private readonly IMicrophoneEnumerator _microphoneEnumerator;
// Live audio capture (TASK 4 ship step 4): desktop/game via WASAPI loopback,
// mic via WASAPI capture, both owned by the mixer and running only while
// live. Mic level feeds AudioLevel (the meter); loopback is for the future
// encoder mix. Private by design — no UI beyond the existing mic controls.
private readonly AudioMixer _audioMixer;
// Screen backdrop: a permanent live capture (desktop/game) that every scene
// shows at the bottom layer. One shared capture session per key — the
// ScreenCaptureManager refcounts by key, mirroring CameraManager.
@@ -832,6 +839,12 @@ public class MainViewModel : ViewModelBase
_microphoneEnumerator = new WinRtMicrophoneEnumerator();
_audioMixer = new AudioMixer(
new WasapiMicAudioSource(() => MicSourceName),
new WasapiLoopbackAudioSource(),
message => AppLog.Write(message));
_audioMixer.MicLevelChanged += OnMicLevelChanged;
_fullScreenDetector = new Win32FullScreenDetector();
foreach (var display in _fullScreenDetector.GetDisplays())
Displays.Add(display);
@@ -1725,6 +1738,7 @@ public class MainViewModel : ViewModelBase
? "ytLlive"
: $"{dialog.StreamTitle} — ytLlive";
StreamStatus = StreamStatus.Streaming;
_audioMixer.Start();
}
}
@@ -1732,6 +1746,7 @@ public class MainViewModel : ViewModelBase
{
StreamStatus = StreamStatus.Offline;
WindowTitle = "ytLlive";
_audioMixer.Stop();
// Graceful end completes the session = signs out (the DPAPI token is
// cleared so the next Start Stream requires a fresh sign-in). A crash
// never runs this, so the token survives and the creator stays signed in.
@@ -1742,6 +1757,17 @@ public class MainViewModel : ViewModelBase
AppLog.Write("Stream ended; session signed out");
}
private void OnMicLevelChanged(float level)
{
// NAudio raises on its capture thread; marshal to the UI thread so the
// meter binding updates safely.
var dispatcher = System.Windows.Application.Current?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
dispatcher.BeginInvoke(() => AudioLevel = level);
else
AudioLevel = level;
}
private void UpdateLiveVisuals()
{
var live = IsLive;