using System.IO; using System.Runtime.InteropServices; using NAudio.Wave; namespace ytLive.Services.Audio; /// /// Streams background music (TASK 9 TRAX) to the DEFAULT render device at a /// fixed quiet volume so it rides the WASAPI loopback into the mix — the same /// physical path game audio takes, so music is ducked with the game, heard in /// headphones, and bounced on the desktop audio meter. Loops on natural end. /// public sealed class MusicPlayer : IDisposable { /// The one and only music level — quiet bed, no slider (the /// creator's voice must stay on top; game/music loudness is the ducker's job). public const float MusicVolume = 0.20f; private MediaFoundationReader? _reader; private VolumeWaveProvider16? _volume; private WaveOutEvent? _output; private bool _disposed; private bool _stopping; /// Raised when playback ends or is stopped (not on natural-end loop). public event Action? PlaybackEnded; public string? TrackPath { get; private set; } public string? TrackName => TrackPath == null ? null : Path.GetFileNameWithoutExtension(TrackPath); public bool IsPlaying => _output?.PlaybackState == PlaybackState.Playing; /// Loads a track and prepares playback (does not start it). public void Load(string path) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("A music file path is required.", nameof(path)); Stop(); TrackPath = path; _reader = new MediaFoundationReader(path); _volume = new VolumeWaveProvider16(_reader) { Volume = MusicVolume }; _output = new WaveOutEvent(); _output.PlaybackStopped += OnPlaybackStopped; _output.Init(_volume); } public void Play() { if (_output == null) return; _output.Play(); } public void Pause() { if (_output == null) return; _output.Pause(); } public void Stop() { if (_output == null) return; _stopping = true; try { _output.Stop(); } catch { } _output.PlaybackStopped -= OnPlaybackStopped; _output.Dispose(); _output = null; _volume = null; _reader?.Dispose(); _reader = null; _stopping = false; TrackPath = null; } public void Dispose() { _disposed = true; Stop(); _disposed = false; } private void OnPlaybackStopped(object? sender, StoppedEventArgs e) { if (_disposed || _stopping || _output == null || _reader == null) return; // Natural end → loop the track; anything else (an explicit stop or a // device failure) surfaces via PlaybackEnded so the UI can reset the dot. if (_reader.Position >= _reader.Length) { _reader.Position = 0; _output.Play(); return; } PlaybackEnded?.Invoke(); } }