Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s
CI / build-test (push) Successful in 1m6s
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user