namespace ytLive.Services; /// /// Default : samples a full-screen monitor /// provider (the existing IFullScreenDetector) and the live loopback /// level and advances a . The pure transitions /// live in GameAudioHysteresis (unit-tested); this wrapper owns the providers /// and the raised-changed event. WPF-free — the VM owns the poll timer. /// public sealed class GameAudioDetector : IGameAudioDetector { private const float SoundFloor = 0.005f; private readonly Func _foregroundFullScreenMonitorProvider; private readonly Func _loopbackLevelProvider; private readonly Func _now; private readonly GameAudioHysteresis _hysteresis = new(); /// Returns the monitor a /// full-screen foreground window covers, or null when windowed/none. /// Current smoothed desktop/game level (0..1). /// Clock for the hysteresis windows; injectable for tests. public GameAudioDetector( Func foregroundFullScreenMonitorProvider, Func loopbackLevelProvider, Func? now = null) { _foregroundFullScreenMonitorProvider = foregroundFullScreenMonitorProvider; _loopbackLevelProvider = loopbackLevelProvider; _now = now ?? (() => DateTime.Now); } public bool IsGameAudioActive => _hysteresis.IsActive; public event Action? IsGameAudioActiveChanged; public void Poll() { var before = _hysteresis.IsActive; var isFullScreen = _foregroundFullScreenMonitorProvider() != null; var hasSound = _loopbackLevelProvider() > SoundFloor; _hysteresis.Update(isFullScreen, hasSound, _now()); if (before != _hysteresis.IsActive) IsGameAudioActiveChanged?.Invoke(_hysteresis.IsActive); } }