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();
}
}