using Microsoft.Xna.Framework.Audio;
using MrGameEng.Core;
using NVorbis;
namespace MrGameEng.Audio;
///
/// Streams ogg music from disk through a using NVorbis.
/// One track plays at a time; samples are decoded on demand in ~0.5 s buffers, so even long
/// tracks use almost no memory.
///
public sealed class MusicPlayer : IDisposable
{
private const int BufferedSubmissions = 3;
private VorbisReader? _reader;
private DynamicSoundEffectInstance? _instance;
private float[] _sampleBuffer = [];
private byte[] _byteBuffer = [];
private bool _loop;
private float _volume = 1f;
/// Volume 0..1 applied to the playing and future tracks.
public float Volume
{
get => _volume;
set
{
_volume = Math.Clamp(value, 0f, 1f);
if (_instance is not null)
{
_instance.Volume = _volume;
}
}
}
/// True while a track is playing (not stopped or paused).
public bool IsPlaying => _instance?.State == SoundState.Playing;
/// Starts streaming , stopping the previous one.
public void Play(MusicTrack track, bool loop = true)
{
// Валидация до Stop(): негодный файл не должен обрывать играющий трек.
var reader = new VorbisReader(track.FullPath);
if (reader.Channels is < 1 or > 2)
{
reader.Dispose();
throw new NotSupportedException(
$"Music '{track.FullPath}' has {reader.Channels} channels; only mono and stereo are supported.");
}
if (reader.SampleRate is < 8000 or > 48000)
{
reader.Dispose();
throw new NotSupportedException(
$"Music '{track.FullPath}' has sample rate {reader.SampleRate} Hz; supported range is 8000–48000 Hz.");
}
Stop();
_loop = loop;
_reader = reader;
// ~0.5 seconds of samples per submitted buffer.
var samplesPerBuffer = _reader.SampleRate * _reader.Channels / 2;
_sampleBuffer = new float[samplesPerBuffer];
_byteBuffer = new byte[samplesPerBuffer * 2];
_instance = new DynamicSoundEffectInstance(
_reader.SampleRate,
_reader.Channels == 1 ? AudioChannels.Mono : AudioChannels.Stereo)
{
Volume = _volume,
};
_instance.BufferNeeded += (_, _) => FillBuffers();
FillBuffers();
_instance.Play();
}
/// Pauses the current track.
public void Pause() => _instance?.Pause();
/// Resumes a paused track.
public void Resume() => _instance?.Resume();
/// Stops playback and releases the decoder.
public void Stop()
{
_instance?.Dispose();
_instance = null;
_reader?.Dispose();
_reader = null;
}
///
public void Dispose() => Stop();
private void FillBuffers()
{
if (_instance is null || _reader is null)
{
return;
}
while (_instance.PendingBufferCount < BufferedSubmissions)
{
var read = _reader.ReadSamples(_sampleBuffer, 0, _sampleBuffer.Length);
if (read == 0)
{
// SamplePosition > 0 отличает конец трека от пустого файла: после перемотки
// на 0 повторный read == 0 не зацикливается, а завершает воспроизведение.
if (_loop && _reader.SamplePosition > 0)
{
_reader.SamplePosition = 0;
continue;
}
// Конец незацикленного трека (или пустой файл): когда буферы доиграли,
// останавливаем инстанс — иначе IsPlaying остаётся true навсегда.
if (_instance.PendingBufferCount == 0)
{
_instance.Stop();
}
return;
}
for (var i = 0; i < read; i++)
{
var sample = (short)(Math.Clamp(_sampleBuffer[i], -1f, 1f) * short.MaxValue);
_byteBuffer[i * 2] = (byte)sample;
_byteBuffer[i * 2 + 1] = (byte)(sample >> 8);
}
_instance.SubmitBuffer(_byteBuffer, 0, read * 2);
}
}
}