using ytLive.Services; namespace ytLive.Services.Audio; /// /// Owns the mic + desktop/game capture sources (TASK 4 ship step 4) and drives /// the footer meters. Capture runs for the app's lifetime (started once at /// startup, stopped on shutdown) so both bars stay live in preview: mic samples /// are level-metered and forwarded, loopback samples feed the game bar's meter. /// The mixer surfaces mic connection state (Connected/Failed) for the status /// dot and can restart the mic source mid-session when a device is re-picked. /// public sealed class AudioMixer : IDisposable { private readonly IAudioSource _mic; private readonly IAudioSource _loopback; private readonly AudioLevelMeter _meter; private readonly AudioLevelMeter _loopbackMeter; private readonly Action? _log; private bool _started; public AudioMixer(IAudioSource mic, IAudioSource loopback, Action? log = null) { _mic = mic; _loopback = loopback; _meter = new AudioLevelMeter(); _loopbackMeter = new AudioLevelMeter(); _log = log; _mic.Started += OnMicStarted; _mic.SampleReady += OnMicSample; _loopback.SampleReady += OnLoopbackSample; _mic.Failed += OnMicFailed; _loopback.Failed += OnLoopbackFailed; } /// Current smoothed mic level (0..1). public float MicLevel => _meter.Level; /// Current smoothed desktop/game level (0..1). public float LoopbackLevel => _loopbackMeter.Level; /// Raised whenever the smoothed mic level changes. public event Action? MicLevelChanged; /// Raised whenever the smoothed desktop/game level changes. public event Action? LoopbackLevelChanged; /// Raised when the mic capture comes up (the status dot goes green). public event Action? MicConnected; /// Raised when the mic capture fails or dies (the status dot goes yellow). public event Action? MicFailed; public void Start() { if (_started) return; _started = true; _meter.Reset(); _loopback.Start(); _mic.Start(); } /// Swaps the mic source without touching loopback — used when the /// creator picks a different device mid-session. The level resets and the /// new source raises or . public void RestartMic() { _mic.Stop(); _meter.Reset(); MicLevelChanged?.Invoke(0); _mic.Start(); } public void Stop() { if (!_started) return; _started = false; _mic.Stop(); _loopback.Stop(); _meter.Reset(); _loopbackMeter.Reset(); MicLevelChanged?.Invoke(0); LoopbackLevelChanged?.Invoke(0); } public void Dispose() { Stop(); _mic.Started -= OnMicStarted; _mic.SampleReady -= OnMicSample; _loopback.SampleReady -= OnLoopbackSample; _mic.Failed -= OnMicFailed; _loopback.Failed -= OnLoopbackFailed; _mic.Dispose(); _loopback.Dispose(); } private void OnMicStarted() { MicConnected?.Invoke(); } private void OnMicSample(AudioSample sample) { // Push unconditionally: the ?. on the event would otherwise skip the // argument (and the meter update) when nothing is subscribed yet. var level = _meter.Push(sample); MicLevelChanged?.Invoke(level); } private void OnLoopbackSample(AudioSample sample) { var level = _loopbackMeter.Push(sample); LoopbackLevelChanged?.Invoke(level); } private void OnMicFailed(Exception ex) { _log?.Invoke($"Mic capture failed: {ex.Message}"); MicLevelChanged?.Invoke(0); MicFailed?.Invoke(ex); } private void OnLoopbackFailed(Exception ex) { _log?.Invoke($"Desktop audio capture failed: {ex.Message}"); } }