From b1b6b84809cc0c31231b72eb5cf84c2e97e7f982 Mon Sep 17 00:00:00 2001
From: cosmonaut <evan@moonside.games>
Date: Mon, 4 Apr 2022 23:33:36 -0700
Subject: [PATCH] WAV static sounds + static sound instance pool

---
 lib/FAudio                       |   2 +-
 lib/SDL2-CS                      |   2 +-
 src/Audio/SoundInstance.cs       |  24 ++--
 src/Audio/StaticSound.cs         | 218 ++++++++++++++++++++++++++++++-
 src/Audio/StaticSoundInstance.cs |   7 +-
 src/Audio/StreamingSound.cs      |   5 +-
 src/Audio/StreamingSoundOgg.cs   |  11 +-
 7 files changed, 251 insertions(+), 18 deletions(-)

diff --git a/lib/FAudio b/lib/FAudio
index de0c1f8..0b6d5da 160000
--- a/lib/FAudio
+++ b/lib/FAudio
@@ -1 +1 @@
-Subproject commit de0c1f833c12a992af5c7daebe1705cd2c72f743
+Subproject commit 0b6d5dabbf428633482fe3a956fbdb53228fcf35
diff --git a/lib/SDL2-CS b/lib/SDL2-CS
index 4e9088b..b35aaa4 160000
--- a/lib/SDL2-CS
+++ b/lib/SDL2-CS
@@ -1 +1 @@
-Subproject commit 4e9088b49de46ea8b4285948cfe69875ac4c2290
+Subproject commit b35aaa494e44d08242788ff0ba2cb7a508f4d8f0
diff --git a/src/Audio/SoundInstance.cs b/src/Audio/SoundInstance.cs
index 9d99697..db128e8 100644
--- a/src/Audio/SoundInstance.cs
+++ b/src/Audio/SoundInstance.cs
@@ -6,8 +6,8 @@ namespace MoonWorks.Audio
 {
 	public abstract class SoundInstance : AudioResource
 	{
-		internal IntPtr Handle { get; }
-		internal FAudio.FAudioWaveFormatEx Format { get; }
+		internal IntPtr Handle;
+		internal FAudio.FAudioWaveFormatEx Format;
 		public bool Loop { get; }
 
 		protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
@@ -163,17 +163,19 @@ namespace MoonWorks.Audio
 
 		public SoundInstance(
 			AudioDevice device,
+			ushort formatTag,
+			ushort bitsPerSample,
+			ushort blockAlign,
 			ushort channels,
 			uint samplesPerSecond,
 			bool is3D,
 			bool loop
 		) : base(device)
 		{
-			var blockAlign = (ushort) (4 * channels);
 			var format = new FAudio.FAudioWaveFormatEx
 			{
-				wFormatTag = 3,
-				wBitsPerSample = 32,
+				wFormatTag = formatTag,
+				wBitsPerSample = bitsPerSample,
 				nChannels = channels,
 				nBlockAlign = blockAlign,
 				nSamplesPerSec = samplesPerSecond,
@@ -184,8 +186,8 @@ namespace MoonWorks.Audio
 
 			FAudio.FAudio_CreateSourceVoice(
 				Device.Handle,
-				out var handle,
-				ref format,
+				out Handle,
+				ref Format,
 				FAudio.FAUDIO_VOICE_USEFILTER,
 				FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
 				IntPtr.Zero,
@@ -193,20 +195,22 @@ namespace MoonWorks.Audio
 				IntPtr.Zero
 			);
 
-			if (handle == IntPtr.Zero)
+			if (Handle == IntPtr.Zero)
 			{
 				Logger.LogError("SoundInstance failed to initialize!");
 				return;
 			}
 
-			Handle = handle;
 			this.is3D = is3D;
 			InitDSPSettings(Format.nChannels);
 
+			// FIXME: not everything should be running through reverb...
+			/*
 			FAudio.FAudioVoice_SetOutputVoices(
-				handle,
+				Handle,
 				ref Device.ReverbSends
 			);
+			*/
 
 			Loop = loop;
 			State = SoundState.Stopped;
diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs
index 526c384..15434d4 100644
--- a/src/Audio/StaticSound.cs
+++ b/src/Audio/StaticSound.cs
@@ -1,4 +1,6 @@
-using System;
+using System;
+using System.Collections.Generic;
+using System.IO;
 using System.Runtime.InteropServices;
 
 namespace MoonWorks.Audio
@@ -6,12 +8,17 @@ namespace MoonWorks.Audio
 	public class StaticSound : AudioResource
 	{
 		internal FAudio.FAudioBuffer Handle;
+		public ushort FormatTag { get; }
+		public ushort BitsPerSample { get; }
 		public ushort Channels { get; }
 		public uint SamplesPerSecond { get; }
+		public ushort BlockAlign { get; }
 
 		public uint LoopStart { get; set; } = 0;
 		public uint LoopLength { get; set; } = 0;
 
+		private Stack<StaticSoundInstance> Instances = new Stack<StaticSoundInstance>();
+
 		public static StaticSound LoadOgg(AudioDevice device, string filePath)
 		{
 			var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
@@ -43,6 +50,194 @@ namespace MoonWorks.Audio
 			);
 		}
 
+		// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
+		public static StaticSound LoadWav(AudioDevice device, string filePath)
+		{
+			// Sample data
+			byte[] data;
+
+			// WaveFormatEx data
+			ushort wFormatTag;
+			ushort nChannels;
+			uint nSamplesPerSec;
+			uint nAvgBytesPerSec;
+			ushort nBlockAlign;
+			ushort wBitsPerSample;
+			int samplerLoopStart = 0;
+			int samplerLoopEnd = 0;
+
+			using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
+			{
+				// RIFF Signature
+				string signature = new string(reader.ReadChars(4));
+				if (signature != "RIFF")
+				{
+					throw new NotSupportedException("Specified stream is not a wave file.");
+				}
+
+				reader.ReadUInt32(); // Riff Chunk Size
+
+				string wformat = new string(reader.ReadChars(4));
+				if (wformat != "WAVE")
+				{
+					throw new NotSupportedException("Specified stream is not a wave file.");
+				}
+
+				// WAVE Header
+				string format_signature = new string(reader.ReadChars(4));
+				while (format_signature != "fmt ")
+				{
+					reader.ReadBytes(reader.ReadInt32());
+					format_signature = new string(reader.ReadChars(4));
+				}
+
+				int format_chunk_size = reader.ReadInt32();
+
+				wFormatTag = reader.ReadUInt16();
+				nChannels = reader.ReadUInt16();
+				nSamplesPerSec = reader.ReadUInt32();
+				nAvgBytesPerSec = reader.ReadUInt32();
+				nBlockAlign = reader.ReadUInt16();
+				wBitsPerSample = reader.ReadUInt16();
+
+				// Reads residual bytes
+				if (format_chunk_size > 16)
+				{
+					reader.ReadBytes(format_chunk_size - 16);
+				}
+
+				// data Signature
+				string data_signature = new string(reader.ReadChars(4));
+				while (data_signature.ToLowerInvariant() != "data")
+				{
+					reader.ReadBytes(reader.ReadInt32());
+					data_signature = new string(reader.ReadChars(4));
+				}
+				if (data_signature != "data")
+				{
+					throw new NotSupportedException("Specified wave file is not supported.");
+				}
+
+				int waveDataLength = reader.ReadInt32();
+				data = reader.ReadBytes(waveDataLength);
+
+				// Scan for other chunks
+				while (reader.PeekChar() != -1)
+				{
+					char[] chunkIDChars = reader.ReadChars(4);
+					if (chunkIDChars.Length < 4)
+					{
+						break; // EOL!
+					}
+					byte[] chunkSizeBytes = reader.ReadBytes(4);
+					if (chunkSizeBytes.Length < 4)
+					{
+						break; // EOL!
+					}
+					string chunk_signature = new string(chunkIDChars);
+					int chunkDataSize = BitConverter.ToInt32(chunkSizeBytes, 0);
+					if (chunk_signature == "smpl") // "smpl", Sampler Chunk Found
+					{
+						reader.ReadUInt32(); // Manufacturer
+						reader.ReadUInt32(); // Product
+						reader.ReadUInt32(); // Sample Period
+						reader.ReadUInt32(); // MIDI Unity Note
+						reader.ReadUInt32(); // MIDI Pitch Fraction
+						reader.ReadUInt32(); // SMPTE Format
+						reader.ReadUInt32(); // SMPTE Offset
+						uint numSampleLoops = reader.ReadUInt32();
+						int samplerData = reader.ReadInt32();
+
+						for (int i = 0; i < numSampleLoops; i += 1)
+						{
+							reader.ReadUInt32(); // Cue Point ID
+							reader.ReadUInt32(); // Type
+							int start = reader.ReadInt32();
+							int end = reader.ReadInt32();
+							reader.ReadUInt32(); // Fraction
+							reader.ReadUInt32(); // Play Count
+
+							if (i == 0) // Grab loopStart and loopEnd from first sample loop
+							{
+								samplerLoopStart = start;
+								samplerLoopEnd = end;
+							}
+						}
+
+						if (samplerData != 0) // Read Sampler Data if it exists
+						{
+							reader.ReadBytes(samplerData);
+						}
+					}
+					else // Read unwanted chunk data and try again
+					{
+						reader.ReadBytes(chunkDataSize);
+					}
+				}
+				// End scan
+			}
+
+			return new StaticSound(
+				device,
+				wFormatTag,
+				wBitsPerSample,
+				nBlockAlign,
+				nChannels,
+				nSamplesPerSec,
+				data,
+				0,
+				(uint) data.Length
+			);
+		}
+
+		public StaticSound(
+			AudioDevice device,
+			ushort formatTag,
+			ushort bitsPerSample,
+			ushort blockAlign,
+			ushort channels,
+			uint samplesPerSecond,
+			byte[] buffer,
+			uint bufferOffset, /* number of bytes */
+			uint bufferLength /* number of bytes */
+		) : base(device)
+		{
+			FormatTag = formatTag;
+			BitsPerSample = bitsPerSample;
+			BlockAlign = blockAlign;
+			Channels = channels;
+			SamplesPerSecond = samplesPerSecond;
+
+			Handle = new FAudio.FAudioBuffer();
+			Handle.Flags = FAudio.FAUDIO_END_OF_STREAM;
+			Handle.pContext = IntPtr.Zero;
+			Handle.AudioBytes = bufferLength;
+			Handle.pAudioData = Marshal.AllocHGlobal((int) bufferLength);
+			Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength);
+			Handle.PlayBegin = 0;
+			Handle.PlayLength = 0;
+
+			if (formatTag == 1)
+			{
+				Handle.PlayLength = (uint) (
+					bufferLength /
+					channels /
+					(bitsPerSample / 8)
+				);
+			}
+			else if (formatTag == 2)
+			{
+				Handle.PlayLength = (uint) (
+					bufferLength /
+					blockAlign *
+					(((blockAlign / channels) - 6) * 2)
+				);
+			}
+
+			LoopStart = 0;
+			LoopLength = 0;
+		}
+
 		public StaticSound(
 			AudioDevice device,
 			ushort channels,
@@ -52,6 +247,9 @@ namespace MoonWorks.Audio
 			uint bufferLength  /* in floats */
 		) : base(device)
 		{
+			FormatTag = 3;
+			BitsPerSample = 32;
+			BlockAlign = (ushort) (4 * channels);
 			Channels = channels;
 			SamplesPerSecond = samplesPerSecond;
 
@@ -69,9 +267,23 @@ namespace MoonWorks.Audio
 			LoopLength = 0;
 		}
 
-		public StaticSoundInstance CreateInstance(bool loop = false)
+		/// <summary>
+		/// Gets a sound instance from the pool.
+		/// NOTE: If you lose track of instances, you will create garbage collection pressure!
+		/// </summary>
+		public StaticSoundInstance GetInstance(bool loop = false)
 		{
-			return new StaticSoundInstance(Device, this, false, loop);
+			if (Instances.Count == 0)
+			{
+				Instances.Push(new StaticSoundInstance(Device, this, false, loop));
+			}
+
+			return Instances.Pop();
+		}
+
+		internal void FreeInstance(StaticSoundInstance instance)
+		{
+			Instances.Push(instance);
 		}
 
 		protected override void Destroy()
diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs
index 7fac5e8..bb472fc 100644
--- a/src/Audio/StaticSoundInstance.cs
+++ b/src/Audio/StaticSoundInstance.cs
@@ -35,7 +35,7 @@ namespace MoonWorks.Audio
 			StaticSound parent,
 			bool is3D,
 			bool loop
-		) : base(device, parent.Channels, parent.SamplesPerSecond, is3D, loop)
+		) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond, is3D, loop)
 		{
 			Parent = parent;
 		}
@@ -92,5 +92,10 @@ namespace MoonWorks.Audio
 				FAudio.FAudioSourceVoice_ExitLoop(Handle, 0);
 			}
 		}
+
+		public void Free()
+		{
+			Parent.FreeInstance(this);
+		}
 	}
 }
diff --git a/src/Audio/StreamingSound.cs b/src/Audio/StreamingSound.cs
index 238a001..a045bc9 100644
--- a/src/Audio/StreamingSound.cs
+++ b/src/Audio/StreamingSound.cs
@@ -18,11 +18,14 @@ namespace MoonWorks.Audio
 
 		public StreamingSound(
 			AudioDevice device,
+			ushort formatTag,
+			ushort bitsPerSample,
+			ushort blockAlign,
 			ushort channels,
 			uint samplesPerSecond,
 			bool is3D,
 			bool loop
-		) : base(device, channels, samplesPerSecond, is3D, loop) { }
+		) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond, is3D, loop) { }
 
 		public override void Play()
 		{
diff --git a/src/Audio/StreamingSoundOgg.cs b/src/Audio/StreamingSoundOgg.cs
index b962d40..6d31aa6 100644
--- a/src/Audio/StreamingSoundOgg.cs
+++ b/src/Audio/StreamingSoundOgg.cs
@@ -46,7 +46,16 @@ namespace MoonWorks.Audio
 			FAudio.stb_vorbis_info info,
 			bool is3D,
 			bool loop
-		) : base(device, (ushort) info.channels, info.sample_rate, is3D, loop)
+		) : base(
+			device,
+			3, /* float type */
+			32, /* size of float */
+			(ushort) (4 * info.channels),
+			(ushort) info.channels,
+			info.sample_rate,
+			is3D,
+			loop
+		)
 		{
 			FileHandle = fileHandle;
 			Info = info;