using ytLive.Services; namespace ytLive.Services.Audio; /// /// Owns the mic + desktop/game capture sources (TASK 4 ship step 4) and drives /// the live mic meter. Runs only while live: started when go-live succeeds, /// stopped on end-stream. Mic samples are level-metered and forwarded; loopback /// samples are currently dropped (consumed by the encoder mix in a later step). /// public sealed class AudioMixer : IDisposable { private readonly IAudioSource _mic; private readonly IAudioSource _loopback; private readonly AudioLevelMeter _meter; private readonly Action? _log; private bool _started; public AudioMixer(IAudioSource mic, IAudioSource loopback, Action? log = null) { _mic = mic; _loopback = loopback; _meter = new AudioLevelMeter(); _log = log; _mic.SampleReady += OnMicSample; _loopback.SampleReady += OnLoopbackSample; _mic.Failed += OnMicFailed; _loopback.Failed += OnLoopbackFailed; } /// Current smoothed mic level (0..1). public float MicLevel => _meter.Level; /// Raised whenever the smoothed mic level changes. public event Action? MicLevelChanged; public void Start() { if (_started) return; _started = true; _meter.Reset(); _loopback.Start(); _mic.Start(); } public void Stop() { if (!_started) return; _started = false; _mic.Stop(); _loopback.Stop(); _meter.Reset(); MicLevelChanged?.Invoke(0); } public void Dispose() { Stop(); _mic.SampleReady -= OnMicSample; _loopback.SampleReady -= OnLoopbackSample; _mic.Failed -= OnMicFailed; _loopback.Failed -= OnLoopbackFailed; _mic.Dispose(); _loopback.Dispose(); } private void OnMicSample(AudioSample sample) { MicLevelChanged?.Invoke(_meter.Push(sample)); } private void OnLoopbackSample(AudioSample sample) { // Desktop/game audio: captured for the future encoder mix; no UI yet. } private void OnMicFailed(Exception ex) { _log?.Invoke($"Mic capture failed: {ex.Message}"); MicLevelChanged?.Invoke(0); } private void OnLoopbackFailed(Exception ex) { _log?.Invoke($"Desktop audio capture failed: {ex.Message}"); } }