From 768bf38e3f5ab82a6bd56405f2c522b02f2353e8 Mon Sep 17 00:00:00 2001 From: cosmonaut 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 @@ netstandard2.0 x64 + true @@ -13,5 +14,6 @@ + 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* reverbChain = (FAudio.FAudioEffectChain*) chainPtr; + reverbChain->EffectCount = 1; + reverbChain->pEffectDescriptors = Marshal.AllocHGlobal( + Marshal.SizeOf() + ); + + 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* 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(), + 0 + ); + Marshal.FreeHGlobal(reverbParamsPtr); + + /* Init reverb sends */ + + reverbSends = new FAudio.FAudioVoiceSends(); + reverbSends.SendCount = 2; + reverbSends.pSends = Marshal.AllocHGlobal( + 2 * Marshal.SizeOf() + ); + 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 moonWorksToRefreshPresentMode = new Dictionary @@ -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 LogInfo; + public static Action LogWarn; + public static Action 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)