134 lines
4.0 KiB
C#
134 lines
4.0 KiB
C#
using ytLive.Services;
|
|
|
|
namespace ytLive.Services.Audio;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class AudioMixer : IDisposable
|
|
{
|
|
private readonly IAudioSource _mic;
|
|
private readonly IAudioSource _loopback;
|
|
private readonly AudioLevelMeter _meter;
|
|
private readonly AudioLevelMeter _loopbackMeter;
|
|
private readonly Action<string>? _log;
|
|
private bool _started;
|
|
|
|
public AudioMixer(IAudioSource mic, IAudioSource loopback, Action<string>? 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;
|
|
}
|
|
|
|
/// <summary>Current smoothed mic level (0..1).</summary>
|
|
public float MicLevel => _meter.Level;
|
|
|
|
/// <summary>Current smoothed desktop/game level (0..1).</summary>
|
|
public float LoopbackLevel => _loopbackMeter.Level;
|
|
|
|
/// <summary>Raised whenever the smoothed mic level changes.</summary>
|
|
public event Action<float>? MicLevelChanged;
|
|
|
|
/// <summary>Raised whenever the smoothed desktop/game level changes.</summary>
|
|
public event Action<float>? LoopbackLevelChanged;
|
|
|
|
/// <summary>Raised when the mic capture comes up (the status dot goes green).</summary>
|
|
public event Action? MicConnected;
|
|
|
|
/// <summary>Raised when the mic capture fails or dies (the status dot goes yellow).</summary>
|
|
public event Action<Exception>? MicFailed;
|
|
|
|
public void Start()
|
|
{
|
|
if (_started)
|
|
return;
|
|
|
|
_started = true;
|
|
_meter.Reset();
|
|
_loopback.Start();
|
|
_mic.Start();
|
|
}
|
|
|
|
/// <summary>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 <see cref="MicConnected"/> or <see cref="MicFailed"/>.</summary>
|
|
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}");
|
|
}
|
|
}
|