starting audio implementation

main
cosmonaut 2021-01-19 18:06:10 -08:00
commit 9ca745e5e0
6 changed files with 688 additions and 0 deletions

192
AudioDevice.cs Normal file
View File

@ -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;
}
}
}

36
SongOgg.cs Normal file
View File

@ -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);
}
}
}

73
Sound.cs Normal file
View File

@ -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)
);
}
}
}

308
SoundInstance.cs Normal file
View File

@ -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);
}
}
}

9
SoundState.cs Normal file
View File

@ -0,0 +1,9 @@
namespace MoonWorks.Audio
{
public enum SoundState
{
Playing,
Paused,
Stopped
}
}

70
StaticSoundInstance.cs Normal file
View File

@ -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);
}
}
}
}