Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s

This commit is contained in:
Leonid Pershin
2026-06-11 04:03:07 +03:00
parent 31aba3aeee
commit ff2231a8ab
72 changed files with 4113 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
using Microsoft.Xna.Framework.Audio;
using MrGameEng.Core;
using NVorbis;
namespace MrGameEng.Audio;
/// <summary>
/// Streams ogg music from disk through a <see cref="DynamicSoundEffectInstance"/> 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.
/// </summary>
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;
/// <summary>Volume 0..1 applied to the playing and future tracks.</summary>
public float Volume
{
get => _volume;
set
{
_volume = Math.Clamp(value, 0f, 1f);
if (_instance is not null)
{
_instance.Volume = _volume;
}
}
}
/// <summary>True while a track is playing (not stopped or paused).</summary>
public bool IsPlaying => _instance?.State == SoundState.Playing;
/// <summary>Starts streaming <paramref name="track"/>, stopping the previous one.</summary>
public void Play(MusicTrack track, bool loop = true)
{
Stop();
_loop = loop;
_reader = new VorbisReader(track.FullPath);
// ~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();
}
/// <summary>Pauses the current track.</summary>
public void Pause() => _instance?.Pause();
/// <summary>Resumes a paused track.</summary>
public void Resume() => _instance?.Resume();
/// <summary>Stops playback and releases the decoder.</summary>
public void Stop()
{
_instance?.Dispose();
_instance = null;
_reader?.Dispose();
_reader = null;
}
/// <inheritdoc />
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)
{
if (!_loop)
{
return;
}
_reader.SamplePosition = 0;
continue;
}
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);
}
}
}