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