From 2b58edb25714a8b223a1b591fdf8b61110d140a7 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 19 Jan 2021 19:26:30 -0800 Subject: [PATCH] more audio implementation --- src/Audio/DynamicSound.cs | 61 ++++++++++++ src/Audio/DynamicSoundInstance.cs | 154 ++++++++++++++++++++++++++++++ src/Audio/Sound.cs | 77 +++------------ src/Audio/SoundInstance.cs | 16 ++-- src/Audio/StaticSound.cs | 87 +++++++++++++++++ src/Audio/StaticSoundInstance.cs | 60 ++++++++---- 6 files changed, 370 insertions(+), 85 deletions(-) create mode 100644 src/Audio/DynamicSound.cs create mode 100644 src/Audio/DynamicSoundInstance.cs create mode 100644 src/Audio/StaticSound.cs diff --git a/src/Audio/DynamicSound.cs b/src/Audio/DynamicSound.cs new file mode 100644 index 0000000..636c4c4 --- /dev/null +++ b/src/Audio/DynamicSound.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; + +namespace MoonWorks.Audio +{ + /// + /// For streaming long playback. + /// + public class DynamicSound : Sound, IDisposable + { + public const int BUFFER_SIZE = 1024 * 128; + + internal IntPtr FileHandle { get; } + internal FAudio.stb_vorbis_info Info { get; } + + private bool IsDisposed; + + // FIXME: what should this value be? + + public DynamicSound(FileInfo fileInfo, ushort channels, uint samplesPerSecond) : base(channels, samplesPerSecond) + { + FileHandle = FAudio.stb_vorbis_open_filename(fileInfo.FullName, out var error, IntPtr.Zero); + + if (error != 0) + { + Logger.LogError("Error opening OGG file!"); + throw new AudioLoadException("Error opening OGG file!"); + } + + Info = FAudio.stb_vorbis_get_info(FileHandle); + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + // dispose managed state (managed objects) + } + + FAudio.stb_vorbis_close(FileHandle); + IsDisposed = true; + } + } + + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~DynamicSound() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Audio/DynamicSoundInstance.cs b/src/Audio/DynamicSoundInstance.cs new file mode 100644 index 0000000..a36824b --- /dev/null +++ b/src/Audio/DynamicSoundInstance.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + public class DynamicSoundInstance : SoundInstance + { + private List queuedBuffers; + private List queuedSizes; + private const int MINIMUM_BUFFER_CHECK = 3; + + public int PendingBufferCount => queuedBuffers.Count; + + private readonly float[] buffer; + + public override SoundState State { get; protected set; } + + public DynamicSoundInstance( + AudioDevice device, + DynamicSound parent, + bool is3D + ) : base(device, parent, is3D) + { + queuedBuffers = new List(); + queuedSizes = new List(); + + buffer = new float[DynamicSound.BUFFER_SIZE]; + } + + public void Play() + { + Update(); + + if (State == SoundState.Playing) + { + return; + } + + QueueBuffers(); + + FAudio.FAudioSourceVoice_Start(Handle, 0, 0); + State = SoundState.Playing; + } + + public void Pause() + { + if (State == SoundState.Playing) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, 0); + State = SoundState.Paused; + } + } + + public void Stop() + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, 0); + FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); + State = SoundState.Stopped; + ClearBuffers(); + } + + private void Update() + { + if (State != SoundState.Playing) + { + return; + } + + FAudio.FAudioSourceVoice_GetState( + Handle, + out var state, + FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED + ); + + while (PendingBufferCount > state.BuffersQueued) + lock (queuedBuffers) + { + Marshal.FreeHGlobal(queuedBuffers[0]); + queuedBuffers.RemoveAt(0); + queuedSizes.RemoveAt(0); + } + } + + private void QueueBuffers() + { + for ( + int i = MINIMUM_BUFFER_CHECK - PendingBufferCount; + i > 0; + i -= 1 + ) { + AddBuffer(); + } + } + + private void ClearBuffers() + { + lock (queuedBuffers) + { + foreach (IntPtr buf in queuedBuffers) + { + Marshal.FreeHGlobal(buf); + } + queuedBuffers.Clear(); + queuedBuffers.Clear(); + } + } + + private void AddBuffer() + { + var parent = (DynamicSound) Parent; + + var samples = FAudio.stb_vorbis_get_samples_float_interleaved( + parent.FileHandle, + parent.Info.channels, + buffer, + buffer.Length + ); + + IntPtr next = Marshal.AllocHGlobal(buffer.Length); + Marshal.Copy(buffer, 0, next, buffer.Length); + + lock (queuedBuffers) + { + var lengthInBytes = (uint) buffer.Length * sizeof(float); + + queuedBuffers.Add(next); + if (State != SoundState.Stopped) + { + FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer + { + AudioBytes = lengthInBytes, + pAudioData = next, + PlayLength = ( + lengthInBytes / + (uint) parent.Info.channels / + (uint) (parent.Format.wBitsPerSample / 8) + ) + }; + + FAudio.FAudioSourceVoice_SubmitSourceBuffer( + Handle, + ref buf, + IntPtr.Zero + ); + } + else + { + queuedSizes.Add(lengthInBytes); + } + } + } + } +} diff --git a/src/Audio/Sound.cs b/src/Audio/Sound.cs index 5fbe489..714824b 100644 --- a/src/Audio/Sound.cs +++ b/src/Audio/Sound.cs @@ -1,73 +1,26 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - namespace MoonWorks.Audio { - public class Sound + public abstract class Sound { - internal FAudio.FAudioBuffer Handle; - internal FAudio.FAudioWaveFormatEx Format; + internal FAudio.FAudioWaveFormatEx Format { get; } - public uint LoopStart { get; set; } = 0; - public uint LoopLength { get; set; } = 0; - - public static Sound FromFile(FileInfo fileInfo) - { - var filePointer = FAudio.stb_vorbis_open_filename(fileInfo.FullName, out var error, IntPtr.Zero); - - if (error != 0) - { - throw new AudioLoadException("Error loading file!"); - } - var info = FAudio.stb_vorbis_get_info(filePointer); - var bufferSize = (uint)(info.sample_rate * info.channels); - var buffer = new float[bufferSize]; - var align = (ushort) (4 * info.channels); - - FAudio.stb_vorbis_close(filePointer); - - return new Sound( - buffer, - 0, - (ushort) info.channels, - info.sample_rate, - align - ); - } - - /* we only support float decoding! WAV sucks! */ + /* NOTE: we only support float decoding! WAV sucks! */ public Sound( - float[] buffer, - uint bufferOffset, ushort channels, - uint samplesPerSecond, - ushort blockAlign + uint samplesPerSecond ) { - var bufferLength = 4 * buffer.Length; + var blockAlign = (ushort) (4 * channels); - Format = new FAudio.FAudioWaveFormatEx(); - Format.wFormatTag = 3; - Format.wBitsPerSample = 32; - Format.nChannels = channels; - Format.nBlockAlign = (ushort) (4 * Format.nChannels); - Format.nSamplesPerSec = samplesPerSecond; - Format.nAvgBytesPerSec = Format.nBlockAlign * Format.nSamplesPerSec; - Format.nBlockAlign = blockAlign; - Format.cbSize = 0; - - Handle = new FAudio.FAudioBuffer(); - Handle.Flags = FAudio.FAUDIO_END_OF_STREAM; - Handle.pContext = IntPtr.Zero; - Handle.AudioBytes = (uint) bufferLength; - Handle.pAudioData = Marshal.AllocHGlobal((int) bufferLength); - Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength); - Handle.PlayBegin = 0; - Handle.PlayLength = ( - Handle.AudioBytes / - (uint) Format.nChannels / - (uint) (Format.wBitsPerSample / 8) - ); + Format = new FAudio.FAudioWaveFormatEx + { + wFormatTag = 3, + wBitsPerSample = 32, + nChannels = channels, + nBlockAlign = blockAlign, + nSamplesPerSec = samplesPerSecond, + nAvgBytesPerSec = blockAlign * samplesPerSecond, + cbSize = 0 + }; } } } diff --git a/src/Audio/SoundInstance.cs b/src/Audio/SoundInstance.cs index ca2a62f..c4cc917 100644 --- a/src/Audio/SoundInstance.cs +++ b/src/Audio/SoundInstance.cs @@ -7,15 +7,15 @@ namespace MoonWorks.Audio { protected AudioDevice Device { get; } internal IntPtr Handle { get; } - protected Sound Parent { get; } + public Sound Parent { get; } protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; - public SoundState State { get; protected set; } protected bool is3D; - - private float _pan = 0; private bool IsDisposed; + public abstract SoundState State { get; protected set; } + + private float _pan = 0; public float Pan { get => _pan; @@ -168,8 +168,11 @@ namespace MoonWorks.Audio } } - public SoundInstance(AudioDevice device, Sound parent, bool is3D) - { + public SoundInstance( + AudioDevice device, + Sound parent, + bool is3D + ) { Device = device; Parent = parent; @@ -195,7 +198,6 @@ namespace MoonWorks.Audio Handle = handle; this.is3D = is3D; InitDSPSettings(Parent.Format.nChannels); - } private void InitDSPSettings(uint srcChannels) diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs new file mode 100644 index 0000000..f812a70 --- /dev/null +++ b/src/Audio/StaticSound.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + public class StaticSound : Sound, IDisposable + { + internal FAudio.FAudioBuffer Handle; + private bool IsDisposed; + + public uint LoopStart { get; set; } = 0; + public uint LoopLength { get; set; } = 0; + + public static StaticSound FromOgg(FileInfo fileInfo) + { + var filePointer = FAudio.stb_vorbis_open_filename(fileInfo.FullName, out var error, IntPtr.Zero); + + if (error != 0) + { + throw new AudioLoadException("Error loading file!"); + } + var info = FAudio.stb_vorbis_get_info(filePointer); + var bufferSize = (uint)(info.sample_rate * info.channels); + var buffer = new float[bufferSize]; + + FAudio.stb_vorbis_close(filePointer); + + return new StaticSound( + buffer, + 0, + (ushort) info.channels, + info.sample_rate + ); + } + + public StaticSound( + float[] buffer, + uint bufferOffset, + ushort channels, + uint samplesPerSecond + ) : base(channels, samplesPerSecond) { + var bufferLength = 4 * buffer.Length; + + Handle = new FAudio.FAudioBuffer(); + Handle.Flags = FAudio.FAUDIO_END_OF_STREAM; + Handle.pContext = IntPtr.Zero; + Handle.AudioBytes = (uint) bufferLength; + Handle.pAudioData = Marshal.AllocHGlobal((int) bufferLength); + Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength); + Handle.PlayBegin = 0; + Handle.PlayLength = ( + Handle.AudioBytes / + (uint) Format.nChannels / + (uint) (Format.wBitsPerSample / 8) + ); + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + // dispose managed state (managed objects) + } + + Marshal.FreeHGlobal(Handle.pAudioData); + IsDisposed = true; + } + } + + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~StaticSound() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs index 7938775..5f39f21 100644 --- a/src/Audio/StaticSoundInstance.cs +++ b/src/Audio/StaticSoundInstance.cs @@ -4,39 +4,67 @@ namespace MoonWorks.Audio { public class StaticSoundInstance : SoundInstance { - public bool Loop { get; protected set; } + public bool Loop { get; } + + private SoundState _state = SoundState.Stopped; + public override SoundState State + { + get + { + FAudio.FAudioSourceVoice_GetState( + Handle, + out var state, + FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED + ); + if (state.BuffersQueued == 0) + { + Stop(true); + } + + return _state; + } + + protected set + { + _state = value; + } + } public StaticSoundInstance( AudioDevice device, - Sound parent, - bool is3D - ) : base(device, parent, is3D) { } - - public void Play(bool loop = false) + StaticSound parent, + bool is3D, + bool loop = false + ) : base(device, parent, is3D) { + Loop = loop; + } + + public void Play() + { + var parent = (StaticSound) Parent; + if (State == SoundState.Playing) { return; } - if (loop) + if (Loop) { - Loop = true; - Parent.Handle.LoopCount = 255; - Parent.Handle.LoopBegin = 0; - Parent.Handle.LoopLength = Parent.LoopLength; + parent.Handle.LoopCount = 255; + parent.Handle.LoopBegin = parent.LoopStart; + parent.Handle.LoopLength = parent.LoopLength; } else { - Loop = false; - Parent.Handle.LoopCount = 0; - Parent.Handle.LoopBegin = 0; - Parent.Handle.LoopLength = 0; + parent.Handle.LoopCount = 0; + parent.Handle.LoopBegin = 0; + parent.Handle.LoopLength = 0; } FAudio.FAudioSourceVoice_SubmitSourceBuffer( Handle, - ref Parent.Handle, + ref parent.Handle, IntPtr.Zero );