From 768bf38e3f5ab82a6bd56405f2c522b02f2353e8 Mon Sep 17 00:00:00 2001
From: cosmonaut <evan@moonside.games>
Date: Tue, 19 Jan 2021 18:06:10 -0800
Subject: [PATCH] starting audio implementation

---
 .gitmodules                          |   3 +
 MoonWorks.csproj                     |   2 +
 MoonWorks.sln                        |   9 +
 lib/FAudio                           |   1 +
 src/Audio/AudioDevice.cs             | 192 +++++++++++++++++
 src/Audio/SongOgg.cs                 |  36 ++++
 src/Audio/Sound.cs                   |  73 +++++++
 src/Audio/SoundInstance.cs           | 308 +++++++++++++++++++++++++++
 src/Audio/SoundState.cs              |   9 +
 src/Audio/StaticSoundInstance.cs     |  70 ++++++
 src/Exceptions/AudioLoadException.cs |  12 ++
 src/Game.cs                          |   6 +
 src/Logger.cs                        |  69 ++++++
 src/Window.cs                        |   2 +-
 14 files changed, 791 insertions(+), 1 deletion(-)
 create mode 160000 lib/FAudio
 create mode 100644 src/Audio/AudioDevice.cs
 create mode 100644 src/Audio/SongOgg.cs
 create mode 100644 src/Audio/Sound.cs
 create mode 100644 src/Audio/SoundInstance.cs
 create mode 100644 src/Audio/SoundState.cs
 create mode 100644 src/Audio/StaticSoundInstance.cs
 create mode 100644 src/Exceptions/AudioLoadException.cs
 create mode 100644 src/Logger.cs

diff --git a/.gitmodules b/.gitmodules
index 29b9556..fd23d35 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
 [submodule "lib/Campari"]
 	path = lib/Campari
 	url = https://gitea.moonside.games/MoonsideGames/Campari.git
+[submodule "lib/FAudio"]
+	path = lib/FAudio
+	url = https://github.com/FNA-XNA/FAudio.git
diff --git a/MoonWorks.csproj b/MoonWorks.csproj
index 52cbe5c..1eed3d5 100644
--- a/MoonWorks.csproj
+++ b/MoonWorks.csproj
@@ -3,6 +3,7 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
     <Platforms>x64</Platforms>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <PropertyGroup>
@@ -13,5 +14,6 @@
     <ProjectReference Include=".\lib\SDL2-CS\SDL2-CS.Core.csproj" />
     <ProjectReference Include=".\lib\Campari\Campari.csproj" />
     <ProjectReference Include=".\lib\Campari\lib\RefreshCS\RefreshCS.csproj" />
+    <ProjectReference Include=".\lib\FAudio\csharp\FAudio-CS.Core.csproj" />
   </ItemGroup>
 </Project>
diff --git a/MoonWorks.sln b/MoonWorks.sln
index 2004c14..2e2bd35 100644
--- a/MoonWorks.sln
+++ b/MoonWorks.sln
@@ -4,6 +4,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 VisualStudioVersion = 16.0.30717.126
 MinimumVisualStudioVersion = 15.0.26124.0
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonWorks", "MoonWorks.csproj", "{DDC9BA9B-4440-4CB3-BDB4-D5F91DE1686B}"
+	ProjectSection(ProjectDependencies) = postProject
+		{608AA31D-F163-4096-B4EF-B9C7D21D52BB} = {608AA31D-F163-4096-B4EF-B9C7D21D52BB}
+	EndProjectSection
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS.Core", "lib\SDL2-CS\SDL2-CS.Core.csproj", "{0929F2D8-8FE4-4452-AD1E-50760A1A19A5}"
 EndProject
@@ -11,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Campari", "lib\Campari\Camp
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefreshCS", "lib\Campari\lib\RefreshCS\RefreshCS.csproj", "{66116A40-B360-4BA3-966A-A54F3E562EC1}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FAudio-CS.Core", "lib\FAudio\csharp\FAudio-CS.Core.csproj", "{608AA31D-F163-4096-B4EF-B9C7D21D52BB}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
@@ -33,6 +38,10 @@ Global
 		{66116A40-B360-4BA3-966A-A54F3E562EC1}.Debug|x64.Build.0 = Debug|x64
 		{66116A40-B360-4BA3-966A-A54F3E562EC1}.Release|x64.ActiveCfg = Release|x64
 		{66116A40-B360-4BA3-966A-A54F3E562EC1}.Release|x64.Build.0 = Release|x64
+		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Debug|x64.ActiveCfg = Debug|x64
+		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Debug|x64.Build.0 = Debug|x64
+		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Release|x64.ActiveCfg = Release|x64
+		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Release|x64.Build.0 = Release|x64
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/lib/FAudio b/lib/FAudio
new file mode 160000
index 0000000..0f3f1e6
--- /dev/null
+++ b/lib/FAudio
@@ -0,0 +1 @@
+Subproject commit 0f3f1e6df74da481d466dd97aa4345ea9fe56ca4
diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs
new file mode 100644
index 0000000..35b7d74
--- /dev/null
+++ b/src/Audio/AudioDevice.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace MoonWorks.Audio
+{
+    public class AudioDevice
+    {
+        public IntPtr Handle { get; }
+        public byte[] Handle3D { get; }
+        public IntPtr MasteringVoice { get; }
+        public FAudio.FAudioDeviceDetails DeviceDetails { get; }
+        public IntPtr ReverbVoice { get; }
+
+        public float CurveDistanceScalar = 1f;
+        public float DopplerScale = 1f;
+        public float SpeedOfSound = 343.5f;
+
+        private FAudio.FAudioVoiceSends reverbSends;
+
+        public unsafe AudioDevice()
+        {
+            FAudio.FAudioCreate(out var handle, 0, 0);
+            Handle = handle;
+
+            /* Find a suitable device */
+
+            FAudio.FAudio_GetDeviceCount(Handle, out var devices);
+
+            if (devices == 0)
+            {
+                Logger.LogError("No audio devices found!");
+                Handle = IntPtr.Zero;
+                FAudio.FAudio_Release(Handle);
+                return;
+            }
+
+            FAudio.FAudioDeviceDetails deviceDetails;
+
+            uint i = 0;
+            for (i = 0; i < devices; i++)
+            {
+                FAudio.FAudio_GetDeviceDetails(
+                    Handle,
+                    i,
+                    out deviceDetails
+                );
+                if ((deviceDetails.Role & FAudio.FAudioDeviceRole.FAudioDefaultGameDevice) == FAudio.FAudioDeviceRole.FAudioDefaultGameDevice)
+                {
+                    DeviceDetails = deviceDetails;
+                    break;
+                }
+            }
+
+            if (i == devices)
+            {
+                i = 0; /* whatever we'll just use the first one I guess */
+                FAudio.FAudio_GetDeviceDetails(
+                    Handle,
+                    i,
+                    out deviceDetails
+                );
+                DeviceDetails = deviceDetails;
+            }
+
+            /* Init Mastering Voice */
+            IntPtr masteringVoice;
+
+            if (FAudio.FAudio_CreateMasteringVoice(
+                Handle,
+                out masteringVoice,
+                FAudio.FAUDIO_DEFAULT_CHANNELS,
+                FAudio.FAUDIO_DEFAULT_SAMPLERATE,
+                0,
+                i,
+                IntPtr.Zero
+            ) != 0)
+            {
+                Logger.LogError("No mastering voice found!");
+                Handle = IntPtr.Zero;
+                FAudio.FAudio_Release(Handle);
+                return;
+            }
+
+            MasteringVoice = masteringVoice;
+
+            /* Init 3D Audio */
+
+            Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE];
+            FAudio.F3DAudioInitialize(
+                DeviceDetails.OutputFormat.dwChannelMask,
+                SpeedOfSound,
+                Handle3D
+            );
+
+            /* Init reverb */
+
+            IntPtr reverbVoice;
+
+            IntPtr reverb;
+            FAudio.FAudioCreateReverb(out reverb, 0);
+
+            IntPtr chainPtr;
+            chainPtr = Marshal.AllocHGlobal(
+                Marshal.SizeOf<FAudio.FAudioEffectChain>()
+            );
+
+            FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr;
+            reverbChain->EffectCount = 1;
+            reverbChain->pEffectDescriptors = Marshal.AllocHGlobal(
+                Marshal.SizeOf<FAudio.FAudioEffectDescriptor>()
+            );
+
+            FAudio.FAudioEffectDescriptor* reverbDescriptor =
+                (FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors;
+
+            reverbDescriptor->InitialState = 1;
+            reverbDescriptor->OutputChannels = (uint) (
+                (DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1
+            );
+            reverbDescriptor->pEffect = reverb;
+
+            FAudio.FAudio_CreateSubmixVoice(
+                Handle,
+                out reverbVoice,
+                1, /* omnidirectional reverb */
+                DeviceDetails.OutputFormat.Format.nSamplesPerSec,
+                0,
+                0,
+                IntPtr.Zero,
+                chainPtr
+            );
+            FAudio.FAPOBase_Release(reverb);
+
+            Marshal.FreeHGlobal(reverbChain->pEffectDescriptors);
+            Marshal.FreeHGlobal(chainPtr);
+
+            ReverbVoice = reverbVoice;
+
+            /* Init reverb params */
+            // Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC
+
+            IntPtr reverbParamsPtr = Marshal.AllocHGlobal(
+                Marshal.SizeOf<FAudio.FAudioFXReverbParameters>()
+            );
+
+            FAudio.FAudioFXReverbParameters* reverbParams = (FAudio.FAudioFXReverbParameters*) reverbParamsPtr;
+            reverbParams->WetDryMix = 100.0f;
+            reverbParams->ReflectionsDelay = 7;
+            reverbParams->ReverbDelay = 11;
+            reverbParams->RearDelay = FAudio.FAUDIOFX_REVERB_DEFAULT_REAR_DELAY;
+            reverbParams->PositionLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
+            reverbParams->PositionRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
+            reverbParams->PositionMatrixLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
+            reverbParams->PositionMatrixRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
+            reverbParams->EarlyDiffusion = 15;
+            reverbParams->LateDiffusion = 15;
+            reverbParams->LowEQGain = 8;
+            reverbParams->LowEQCutoff = 4;
+            reverbParams->HighEQGain = 8;
+            reverbParams->HighEQCutoff = 6;
+            reverbParams->RoomFilterFreq = 5000f;
+            reverbParams->RoomFilterMain = -10f;
+            reverbParams->RoomFilterHF = -1f;
+            reverbParams->ReflectionsGain = -26.0200005f;
+            reverbParams->ReverbGain = 10.0f;
+            reverbParams->DecayTime = 1.49000001f;
+            reverbParams->Density = 100.0f;
+            reverbParams->RoomSize = FAudio.FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE;
+            FAudio.FAudioVoice_SetEffectParameters(
+                ReverbVoice,
+                0,
+                reverbParamsPtr,
+                (uint) Marshal.SizeOf<FAudio.FAudioFXReverbParameters>(),
+                0
+            );
+            Marshal.FreeHGlobal(reverbParamsPtr);
+
+            /* Init reverb sends */
+
+            reverbSends = new FAudio.FAudioVoiceSends();
+            reverbSends.SendCount = 2;
+            reverbSends.pSends = Marshal.AllocHGlobal(
+                2 * Marshal.SizeOf<FAudio.FAudioSendDescriptor>()
+            );
+            FAudio.FAudioSendDescriptor* sendDesc = (FAudio.FAudioSendDescriptor*) reverbSends.pSends;
+            sendDesc[0].Flags = 0;
+            sendDesc[0].pOutputVoice = MasteringVoice;
+            sendDesc[1].Flags = 0;
+            sendDesc[1].pOutputVoice = ReverbVoice;
+        }
+    }
+}
diff --git a/src/Audio/SongOgg.cs b/src/Audio/SongOgg.cs
new file mode 100644
index 0000000..387e8a2
--- /dev/null
+++ b/src/Audio/SongOgg.cs
@@ -0,0 +1,36 @@
+using System;
+using System.IO;
+
+namespace MoonWorks.Audio
+{
+    // for streaming long playback
+    public class Song
+    {
+        public IntPtr Handle { get; }
+        public FAudio.stb_vorbis_info Info { get; }
+        public uint BufferSize { get; }
+        public bool Loop { get; set; }
+        private readonly float[] buffer;
+        private const int bufferShrinkFactor = 8;
+
+        public TimeSpan Duration { get; set; }
+
+        public Song(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!");
+            }
+
+            Info = FAudio.stb_vorbis_get_info(filePointer);
+            BufferSize = (uint)(Info.sample_rate * Info.channels) / bufferShrinkFactor;
+
+            buffer = new float[BufferSize];
+
+
+            FAudio.stb_vorbis_close(filePointer);
+        }
+    }
+}
diff --git a/src/Audio/Sound.cs b/src/Audio/Sound.cs
new file mode 100644
index 0000000..5fbe489
--- /dev/null
+++ b/src/Audio/Sound.cs
@@ -0,0 +1,73 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace MoonWorks.Audio
+{
+    public class Sound
+    {
+        internal FAudio.FAudioBuffer Handle;
+        internal FAudio.FAudioWaveFormatEx Format;
+
+        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! */
+        public Sound(
+            float[] buffer,
+            uint bufferOffset,
+            ushort channels,
+            uint samplesPerSecond,
+            ushort blockAlign
+        ) {
+            var bufferLength = 4 * buffer.Length;
+
+            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)
+            );
+        }
+    }
+}
diff --git a/src/Audio/SoundInstance.cs b/src/Audio/SoundInstance.cs
new file mode 100644
index 0000000..ca2a62f
--- /dev/null
+++ b/src/Audio/SoundInstance.cs
@@ -0,0 +1,308 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace MoonWorks.Audio
+{
+    public abstract class SoundInstance : IDisposable
+    {
+        protected AudioDevice Device { get; }
+        internal IntPtr Handle { get; }
+        protected 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 float Pan
+        {
+            get => _pan;
+            set
+            {
+                _pan = value;
+
+                if (_pan < -1f)
+                {
+                    _pan = -1f;
+                }
+                if (_pan > 1f)
+                {
+                    _pan = 1f;
+                }
+
+                if (is3D) { return; }
+
+                SetPanMatrixCoefficients();
+                FAudio.FAudioVoice_SetOutputMatrix(
+                    Handle,
+                    Device.MasteringVoice,
+                    dspSettings.SrcChannelCount,
+                    dspSettings.DstChannelCount,
+                    dspSettings.pMatrixCoefficients,
+                    0
+                );
+            }
+        }
+
+        private float _pitch = 1;
+        public float Pitch
+        {
+            get => _pitch;
+            set
+            {
+                float doppler;
+                if (!is3D || Device.DopplerScale == 0f)
+                {
+                    doppler = 1f;
+                }
+                else
+                {
+                    doppler = dspSettings.DopplerFactor * Device.DopplerScale;
+                }
+
+                _pitch = value;
+                FAudio.FAudioSourceVoice_SetFrequencyRatio(
+                    Handle,
+                    (float) Math.Pow(2.0, _pitch) * doppler,
+                    0
+                );
+            }
+        }
+
+        private float _volume = 1;
+        public float Volume
+        {
+            get => _volume;
+            set
+            {
+                _volume = value;
+                FAudio.FAudioVoice_SetVolume(Handle, _volume, 0);
+            }
+        }
+
+        private float _reverb;
+        public unsafe float Reverb
+        {
+            get => _reverb;
+            set
+            {
+                _reverb = value;
+
+                float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
+                outputMatrix[0] = _reverb;
+                if (dspSettings.SrcChannelCount == 2)
+                {
+                    outputMatrix[1] = _reverb;
+                }
+
+                FAudio.FAudioVoice_SetOutputMatrix(
+                    Handle,
+                    Device.ReverbVoice,
+                    dspSettings.SrcChannelCount,
+                    1,
+                    dspSettings.pMatrixCoefficients,
+                    0
+                );
+            }
+        }
+
+        private float _lowPassFilter;
+        public float LowPassFilter
+        {
+            get => _lowPassFilter;
+            set
+            {
+                _lowPassFilter = value;
+
+                FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters();
+                p.Type = FAudio.FAudioFilterType.FAudioLowPassFilter;
+                p.Frequency = _lowPassFilter;
+                p.OneOverQ = 1f;
+                FAudio.FAudioVoice_SetFilterParameters(
+                    Handle,
+                    ref p,
+                    0
+                );
+            }
+        }
+
+        private float _highPassFilter;
+        public float HighPassFilter
+        {
+            get => _highPassFilter;
+            set
+            {
+                _highPassFilter = value;
+
+                FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters();
+                p.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
+                p.Frequency = _highPassFilter;
+                p.OneOverQ = 1f;
+                FAudio.FAudioVoice_SetFilterParameters(
+                    Handle,
+                    ref p,
+                    0
+                );
+            }
+        }
+
+        private float _bandPassFilter;
+        public float BandPassFilter
+        {
+            get => _bandPassFilter;
+            set
+            {
+                _bandPassFilter = value;
+
+                FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters();
+                p.Type = FAudio.FAudioFilterType.FAudioBandPassFilter;
+                p.Frequency = _bandPassFilter;
+                p.OneOverQ = 1f;
+                FAudio.FAudioVoice_SetFilterParameters(
+                    Handle,
+                    ref p,
+                    0
+                );
+            }
+        }
+
+        public SoundInstance(AudioDevice device, Sound parent, bool is3D)
+        {
+            Device = device;
+            Parent = parent;
+
+            FAudio.FAudioWaveFormatEx format = Parent.Format;
+
+            FAudio.FAudio_CreateSourceVoice(
+                Device.Handle,
+                out var handle,
+                ref format,
+                FAudio.FAUDIO_VOICE_USEFILTER,
+                FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
+                IntPtr.Zero,
+                IntPtr.Zero,
+                IntPtr.Zero
+            );
+
+            if (handle == IntPtr.Zero)
+            {
+                Logger.LogError("SoundInstance failed to initialize!");
+                return;
+            }
+
+            Handle = handle;
+            this.is3D = is3D;
+            InitDSPSettings(Parent.Format.nChannels);
+
+        }
+
+        private void InitDSPSettings(uint srcChannels)
+        {
+            dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS();
+            dspSettings.DopplerFactor = 1f;
+            dspSettings.SrcChannelCount = srcChannels;
+            dspSettings.DstChannelCount = Device.DeviceDetails.OutputFormat.Format.nChannels;
+
+            int memsize = (
+                4 *
+                (int) dspSettings.SrcChannelCount *
+                (int) dspSettings.DstChannelCount
+            );
+
+            dspSettings.pMatrixCoefficients = Marshal.AllocHGlobal(memsize);
+            unsafe
+            {
+                byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
+                for (int i = 0; i < memsize; i += 1)
+                {
+                    memPtr[i] = 0;
+                }
+            }
+            SetPanMatrixCoefficients();
+        }
+
+        // Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs
+        private unsafe void SetPanMatrixCoefficients()
+        {
+            /* Two major things to notice:
+			 * 1. The spec assumes any speaker count >= 2 has Front Left/Right.
+			 * 2. Stereo panning is WAY more complicated than you think.
+			 *    The main thing is that hard panning does NOT eliminate an
+			 *    entire channel; the two channels are blended on each side.
+			 * -flibit
+			 */
+			float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
+			if (dspSettings.SrcChannelCount == 1)
+			{
+				if (dspSettings.DstChannelCount == 1)
+				{
+					outputMatrix[0] = 1.0f;
+				}
+				else
+				{
+					outputMatrix[0] = (_pan > 0.0f) ? (1.0f - _pan) : 1.0f;
+					outputMatrix[1] = (_pan < 0.0f) ? (1.0f  + _pan) : 1.0f;
+				}
+			}
+			else
+			{
+				if (dspSettings.DstChannelCount == 1)
+				{
+					outputMatrix[0] = 1.0f;
+					outputMatrix[1] = 1.0f;
+				}
+				else
+				{
+					if (_pan <= 0.0f)
+					{
+						// Left speaker blends left/right channels
+						outputMatrix[0] = 0.5f * _pan + 1.0f;
+						outputMatrix[1] = 0.5f * -_pan;
+						// Right speaker gets less of the right channel
+						outputMatrix[2] = 0.0f;
+						outputMatrix[3] = _pan + 1.0f;
+					}
+					else
+					{
+						// Left speaker gets less of the left channel
+						outputMatrix[0] = -_pan + 1.0f;
+						outputMatrix[1] = 0.0f;
+						// Right speaker blends right/left channels
+						outputMatrix[2] = 0.5f * _pan;
+						outputMatrix[3] = 0.5f * -_pan + 1.0f;
+					}
+				}
+			}
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!IsDisposed)
+            {
+                if (disposing)
+                {
+                    // dispose managed state (managed objects)
+                }
+
+                FAudio.FAudioVoice_DestroyVoice(Handle);
+                Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients);
+                IsDisposed = true;
+            }
+        }
+
+        ~SoundInstance()
+        {
+            // 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/SoundState.cs b/src/Audio/SoundState.cs
new file mode 100644
index 0000000..2a82ea2
--- /dev/null
+++ b/src/Audio/SoundState.cs
@@ -0,0 +1,9 @@
+namespace MoonWorks.Audio
+{
+    public enum SoundState
+    {
+        Playing,
+        Paused,
+        Stopped
+    }
+}
diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs
new file mode 100644
index 0000000..7938775
--- /dev/null
+++ b/src/Audio/StaticSoundInstance.cs
@@ -0,0 +1,70 @@
+using System;
+
+namespace MoonWorks.Audio
+{
+    public class StaticSoundInstance : SoundInstance
+    {
+        public bool Loop { get; protected set; }
+
+        public StaticSoundInstance(
+            AudioDevice device,
+            Sound parent,
+            bool is3D
+        ) : base(device, parent, is3D) { }
+
+        public void Play(bool loop = false)
+        {
+            if (State == SoundState.Playing)
+            {
+                return;
+            }
+
+            if (loop)
+            {
+                Loop = true;
+                Parent.Handle.LoopCount = 255;
+                Parent.Handle.LoopBegin = 0;
+                Parent.Handle.LoopLength = Parent.LoopLength;
+            }
+            else
+            {
+                Loop = false;
+                Parent.Handle.LoopCount = 0;
+                Parent.Handle.LoopBegin = 0;
+                Parent.Handle.LoopLength = 0;
+            }
+
+            FAudio.FAudioSourceVoice_SubmitSourceBuffer(
+                Handle,
+                ref Parent.Handle,
+                IntPtr.Zero
+            );
+
+            FAudio.FAudioSourceVoice_Start(Handle, 0, 0);
+            State = SoundState.Playing;
+        }
+
+        public void Pause()
+        {
+            if (State == SoundState.Paused)
+            {
+                FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
+                State = SoundState.Paused;
+            }
+        }
+
+        public void Stop(bool immediate = true)
+        {
+            if (immediate)
+            {
+                FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
+                FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
+                State = SoundState.Stopped;
+            }
+            else
+            {
+                FAudio.FAudioSourceVoice_ExitLoop(Handle, 0);
+            }
+        }
+    }
+}
diff --git a/src/Exceptions/AudioLoadException.cs b/src/Exceptions/AudioLoadException.cs
new file mode 100644
index 0000000..33bcdf3
--- /dev/null
+++ b/src/Exceptions/AudioLoadException.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace MoonWorks
+{
+    public class AudioLoadException : Exception
+    {
+        public AudioLoadException(string message) : base(message)
+        {
+
+        }
+    }
+}
diff --git a/src/Game.cs b/src/Game.cs
index ca96301..e8b1eb8 100644
--- a/src/Game.cs
+++ b/src/Game.cs
@@ -1,6 +1,7 @@
 using SDL2;
 using Campari;
 using System.Collections.Generic;
+using MoonWorks.Audio;
 
 namespace MoonWorks
 {
@@ -14,6 +15,7 @@ namespace MoonWorks
 
         public Window Window { get; }
         public GraphicsDevice GraphicsDevice { get; }
+        public AudioDevice AudioDevice { get; }
         public Input Input { get; }
 
         private Dictionary<PresentMode, RefreshCS.Refresh.PresentMode> moonWorksToRefreshPresentMode = new Dictionary<PresentMode, RefreshCS.Refresh.PresentMode>
@@ -38,6 +40,8 @@ namespace MoonWorks
                 return;
             }
 
+            Logger.Initialize();
+
             Input = new Input();
 
             Window = new Window(windowCreateInfo);
@@ -48,6 +52,8 @@ namespace MoonWorks
                 debugMode
             );
 
+            AudioDevice = new AudioDevice();
+
             this.debugMode = debugMode;
         }
 
diff --git a/src/Logger.cs b/src/Logger.cs
new file mode 100644
index 0000000..3720806
--- /dev/null
+++ b/src/Logger.cs
@@ -0,0 +1,69 @@
+using System;
+using RefreshCS;
+
+namespace MoonWorks
+{
+    public static class Logger
+    {
+        public static Action<string> LogInfo;
+        public static Action<string> LogWarn;
+        public static Action<string> LogError;
+
+        private static RefreshCS.Refresh.Refresh_LogFunc LogInfoFunc = RefreshLogInfo;
+        private static RefreshCS.Refresh.Refresh_LogFunc LogWarnFunc = RefreshLogWarn;
+        private static RefreshCS.Refresh.Refresh_LogFunc LogErrorFunc = RefreshLogError;
+
+        internal static void Initialize()
+        {
+            if (Logger.LogInfo == null)
+            {
+                Logger.LogInfo = Console.WriteLine;
+            }
+            if (Logger.LogWarn == null)
+            {
+                Logger.LogWarn = Console.WriteLine;
+            }
+            if (Logger.LogError == null)
+            {
+                Logger.LogError = Console.WriteLine;
+            }
+
+            Refresh.Refresh_HookLogFunctions(
+                LogInfoFunc,
+                LogWarnFunc,
+                LogErrorFunc
+            );
+        }
+
+        private static void RefreshLogInfo(IntPtr msg)
+        {
+            LogInfo(UTF8_ToManaged(msg));
+        }
+
+        private static void RefreshLogWarn(IntPtr msg)
+        {
+            LogWarn(UTF8_ToManaged(msg));
+        }
+
+        private static void RefreshLogError(IntPtr msg)
+        {
+            LogError(UTF8_ToManaged(msg));
+        }
+
+        private unsafe static string UTF8_ToManaged(IntPtr s)
+        {
+            byte* ptr = (byte*) s;
+            while (*ptr != 0)
+            {
+                ptr += 1;
+            }
+
+            string result = System.Text.Encoding.UTF8.GetString(
+                (byte*) s,
+                (int) (ptr - (byte*) s)
+            );
+
+            return result;
+        }
+    }
+}
diff --git a/src/Window.cs b/src/Window.cs
index 385d207..7e72f5a 100644
--- a/src/Window.cs
+++ b/src/Window.cs
@@ -5,7 +5,7 @@ namespace MoonWorks
 {
     public class Window
     {
-        public IntPtr Handle { get; }
+        internal IntPtr Handle { get; }
         public ScreenMode ScreenMode { get; }
 
         public Window(WindowCreateInfo windowCreateInfo)