From 9ca745e5e07f2bed482d5340587aaf63ddef65f2 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 19 Jan 2021 18:06:10 -0800 Subject: [PATCH] starting audio implementation --- AudioDevice.cs | 192 +++++++++++++++++++++++++ SongOgg.cs | 36 +++++ Sound.cs | 73 ++++++++++ SoundInstance.cs | 308 +++++++++++++++++++++++++++++++++++++++++ SoundState.cs | 9 ++ StaticSoundInstance.cs | 70 ++++++++++ 6 files changed, 688 insertions(+) create mode 100644 AudioDevice.cs create mode 100644 SongOgg.cs create mode 100644 Sound.cs create mode 100644 SoundInstance.cs create mode 100644 SoundState.cs create mode 100644 StaticSoundInstance.cs diff --git a/AudioDevice.cs b/AudioDevice.cs new file mode 100644 index 0000000..35b7d74 --- /dev/null +++ b/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/SongOgg.cs b/SongOgg.cs new file mode 100644 index 0000000..387e8a2 --- /dev/null +++ b/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/Sound.cs b/Sound.cs new file mode 100644 index 0000000..5fbe489 --- /dev/null +++ b/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/SoundInstance.cs b/SoundInstance.cs new file mode 100644 index 0000000..ca2a62f --- /dev/null +++ b/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/SoundState.cs b/SoundState.cs new file mode 100644 index 0000000..2a82ea2 --- /dev/null +++ b/SoundState.cs @@ -0,0 +1,9 @@ +namespace MoonWorks.Audio +{ + public enum SoundState + { + Playing, + Paused, + Stopped + } +} diff --git a/StaticSoundInstance.cs b/StaticSoundInstance.cs new file mode 100644 index 0000000..7938775 --- /dev/null +++ b/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); + } + } + } +}