restructure and implement Voice hierarchy

pull/50/head
cosmonaut 2023-08-01 11:22:49 -07:00
parent b1c7740b86
commit e2c85ec728
14 changed files with 452 additions and 330 deletions

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
namespace MoonWorks.Audio namespace MoonWorks.Audio
@ -25,6 +24,8 @@ namespace MoonWorks.Audio
private AudioTweenManager AudioTweenManager; private AudioTweenManager AudioTweenManager;
private SourceVoicePool VoicePool;
private const int Step = 200; private const int Step = 200;
private TimeSpan UpdateInterval; private TimeSpan UpdateInterval;
private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch(); private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch();
@ -106,6 +107,7 @@ namespace MoonWorks.Audio
); );
AudioTweenManager = new AudioTweenManager(); AudioTweenManager = new AudioTweenManager();
VoicePool = new SourceVoicePool(this);
Logger.LogInfo("Setting up audio thread..."); Logger.LogInfo("Setting up audio thread...");
WakeSignal = new AutoResetEvent(true); WakeSignal = new AutoResetEvent(true);
@ -260,6 +262,16 @@ namespace MoonWorks.Audio
soundSequenceReferences.Add(new WeakReference<SoundSequence>(sequence)); soundSequenceReferences.Add(new WeakReference<SoundSequence>(sequence));
} }
internal SourceVoice ObtainSourceVoice(Format format)
{
return VoicePool.Obtain(format);
}
internal void ReturnSourceVoice(SourceVoice voice)
{
VoicePool.Return(voice);
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (!IsDisposed) if (!IsDisposed)

View File

@ -24,7 +24,10 @@ namespace MoonWorks.Audio
switch (audioTween.Property) switch (audioTween.Property)
{ {
case AudioTweenProperty.Pan: case AudioTweenProperty.Pan:
audioTween.StartValue = voice.Pan; if (voice is SendableVoice pannableVoice)
{
audioTween.StartValue = pannableVoice.Pan;
}
break; break;
case AudioTweenProperty.Pitch: case AudioTweenProperty.Pitch:
@ -40,7 +43,10 @@ namespace MoonWorks.Audio
break; break;
case AudioTweenProperty.Reverb: case AudioTweenProperty.Reverb:
audioTween.StartValue = voice.Reverb; if (voice is SendableVoice reverbableVoice)
{
audioTween.StartValue = reverbableVoice.Reverb;
}
break; break;
} }
@ -133,7 +139,10 @@ namespace MoonWorks.Audio
switch (audioTween.Property) switch (audioTween.Property)
{ {
case AudioTweenProperty.Pan: case AudioTweenProperty.Pan:
audioTween.Voice.Pan = value; if (audioTween.Voice is SendableVoice pannableVoice)
{
pannableVoice.Pan = value;
}
break; break;
case AudioTweenProperty.Pitch: case AudioTweenProperty.Pitch:
@ -149,7 +158,10 @@ namespace MoonWorks.Audio
break; break;
case AudioTweenProperty.Reverb: case AudioTweenProperty.Reverb:
audioTween.Voice.Reverb = value; if (audioTween.Voice is SendableVoice reverbableVoice)
{
reverbableVoice.Reverb = value;
}
break; break;
} }

View File

@ -4,39 +4,30 @@ namespace MoonWorks.Audio
{ {
public static class AudioUtils public static class AudioUtils
{ {
public struct WaveHeaderData public static Format ReadWaveFormat(string filePath, out int dataLength)
{ {
public int FileLength;
public short FormatTag;
public short Channels;
public int SampleRate;
public short BitsPerSample;
public short BlockAlign;
public int DataLength;
}
public static WaveHeaderData ReadWaveHeaderData(string filePath)
{
WaveHeaderData headerData;
var fileInfo = new FileInfo(filePath); var fileInfo = new FileInfo(filePath);
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using BinaryReader br = new BinaryReader(fs); using BinaryReader br = new BinaryReader(fs);
headerData.FileLength = (int)fileInfo.Length - 8;
fs.Position = 20; fs.Position = 20;
headerData.FormatTag = br.ReadInt16(); var formatTag = br.ReadInt16();
fs.Position = 22; fs.Position = 22;
headerData.Channels = br.ReadInt16(); var channels = br.ReadInt16();
fs.Position = 24; fs.Position = 24;
headerData.SampleRate = br.ReadInt32(); var sampleRate = br.ReadInt32();
fs.Position = 32;
headerData.BlockAlign = br.ReadInt16();
fs.Position = 34; fs.Position = 34;
headerData.BitsPerSample = br.ReadInt16(); var bitsPerSample = br.ReadInt16();
fs.Position = 40; fs.Position = 40;
headerData.DataLength = br.ReadInt32(); dataLength = br.ReadInt32();
return headerData; return new Format
{
Tag = (FormatTag) formatTag,
Channels = (ushort) channels,
SampleRate = (uint) sampleRate,
BitsPerSample = (ushort) bitsPerSample
};
} }
} }
} }

33
src/Audio/Format.cs Normal file
View File

@ -0,0 +1,33 @@
namespace MoonWorks.Audio
{
public enum FormatTag : ushort
{
Unknown = 0,
PCM = 1,
MSADPCM = 2,
IEEE_FLOAT = 3
}
public record struct Format
{
public FormatTag Tag;
public ushort Channels;
public uint SampleRate;
public ushort BitsPerSample;
internal FAudio.FAudioWaveFormatEx ToFAudioFormat()
{
var blockAlign = (ushort) ((BitsPerSample / 8) * Channels);
return new FAudio.FAudioWaveFormatEx
{
wFormatTag = (ushort) Tag,
nChannels = Channels,
nSamplesPerSec = SampleRate,
wBitsPerSample = BitsPerSample,
nBlockAlign = blockAlign,
nAvgBytesPerSec = blockAlign * SampleRate
};
}
}
}

View File

@ -0,0 +1,7 @@
namespace MoonWorks.Audio
{
public interface IReceivableVoice
{
public System.IntPtr Handle { get; }
}
}

View File

@ -2,11 +2,8 @@ using System;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public class MasteringVoice : Voice public class MasteringVoice : Voice, IReceivableVoice
{ {
// mastering voice can't pan
public override float Pan => 0;
internal static bool Create( internal static bool Create(
AudioDevice device, AudioDevice device,
uint deviceIndex, uint deviceIndex,
@ -38,9 +35,9 @@ namespace MoonWorks.Audio
internal MasteringVoice( internal MasteringVoice(
AudioDevice device, AudioDevice device,
IntPtr handle IntPtr handle
) : base(device) ) : base(device, device.DeviceDetails.OutputFormat.Format.nChannels, 0)
{ {
Handle = handle; this.handle = handle;
} }
} }
} }

227
src/Audio/SendableVoice.cs Normal file
View File

@ -0,0 +1,227 @@
using System;
using System.Runtime.InteropServices;
using EasingFunction = System.Func<float, float>;
namespace MoonWorks.Audio
{
public unsafe class SendableVoice : Voice
{
private IReceivableVoice OutputVoice;
private ReverbEffect ReverbEffect;
byte* pMatrixCoefficients;
protected float pan = 0;
public float Pan
{
get => pan;
internal set
{
value = Math.MathHelper.Clamp(value, -1f, 1f);
if (pan != value)
{
pan = value;
if (pan < -1f)
{
pan = -1f;
}
if (pan > 1f)
{
pan = 1f;
}
if (Is3D) { return; }
SetPanMatrixCoefficients();
FAudio.FAudioVoice_SetOutputMatrix(
Handle,
OutputVoice.Handle,
SourceChannelCount,
DestinationChannelCount,
(nint) pMatrixCoefficients,
0
);
}
}
}
private float reverb;
public unsafe float Reverb
{
get => reverb;
internal set
{
if (ReverbEffect != null)
{
value = MathF.Max(0, value);
if (reverb != value)
{
reverb = value;
float* outputMatrix = (float*) pMatrixCoefficients;
outputMatrix[0] = reverb;
if (SourceChannelCount == 2)
{
outputMatrix[1] = reverb;
}
FAudio.FAudioVoice_SetOutputMatrix(
Handle,
ReverbEffect.Handle,
SourceChannelCount,
1,
(nint) pMatrixCoefficients,
0
);
}
}
#if DEBUG
if (ReverbEffect == null)
{
Logger.LogWarn("Tried to set reverb value before applying a reverb effect");
}
#endif
}
}
public SendableVoice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device, sourceChannelCount, destinationChannelCount)
{
OutputVoice = device.MasteringVoice;
nuint memsize = (nuint) (4 * sourceChannelCount * destinationChannelCount);
pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize);
SetPanMatrixCoefficients();
}
public virtual void SetPan(float targetValue)
{
Pan = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Pan);
}
public virtual void SetPan(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0);
}
public virtual void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime);
}
public virtual void SetReverb(float targetValue)
{
Reverb = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Reverb);
}
public virtual void SetReverb(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0);
}
public virtual void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime);
}
public unsafe void SetOutputVoice(IReceivableVoice send)
{
FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[1];
sendDesc[0].Flags = 0;
sendDesc[0].pOutputVoice = send.Handle;
var sends = new FAudio.FAudioVoiceSends();
sends.SendCount = 1;
sends.pSends = (nint) sendDesc;
FAudio.FAudioVoice_SetOutputVoices(
Handle,
ref sends
);
OutputVoice = send;
}
public virtual unsafe void SetReverbEffectChain(ReverbEffect reverbEffect)
{
var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2];
sendDesc[0].Flags = 0;
sendDesc[0].pOutputVoice = OutputVoice.Handle;
sendDesc[1].Flags = 0;
sendDesc[1].pOutputVoice = reverbEffect.Handle;
var sends = new FAudio.FAudioVoiceSends();
sends.SendCount = 2;
sends.pSends = (nint) sendDesc;
FAudio.FAudioVoice_SetOutputVoices(
Handle,
ref sends
);
ReverbEffect = reverbEffect;
}
// 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*) pMatrixCoefficients;
if (SourceChannelCount == 1)
{
if (DestinationChannelCount == 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 (DestinationChannelCount == 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 override unsafe void Destroy()
{
NativeMemory.Free(pMatrixCoefficients);
base.Destroy();
}
}
}

View File

@ -9,13 +9,13 @@ namespace MoonWorks.Audio
public delegate void OnSoundNeededFunc(); public delegate void OnSoundNeededFunc();
public OnSoundNeededFunc OnSoundNeeded; public OnSoundNeededFunc OnSoundNeeded;
public SoundSequence(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) public SoundSequence(AudioDevice device, Format format) : base(device, format)
{ {
device.AddSoundSequenceReference(this); device.AddSoundSequenceReference(this);
OnUpdate += Update; OnUpdate += Update;
} }
public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.FormatTag, templateSound.BitsPerSample, templateSound.BlockAlign, templateSound.Channels, templateSound.SamplesPerSecond) public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.Format)
{ {
device.AddSoundSequenceReference(this); device.AddSoundSequenceReference(this);
OnUpdate += Update; OnUpdate += Update;
@ -51,12 +51,7 @@ namespace MoonWorks.Audio
public void EnqueueSound(StaticSound sound) public void EnqueueSound(StaticSound sound)
{ {
#if DEBUG #if DEBUG
if ( if (!(sound.Format == Format))
sound.FormatTag != Format.wFormatTag ||
sound.BitsPerSample != Format.wBitsPerSample ||
sound.Channels != Format.nChannels ||
sound.SamplesPerSecond != Format.nSamplesPerSec
)
{ {
Logger.LogWarn("Playlist audio format mismatch!"); Logger.LogWarn("Playlist audio format mismatch!");
} }

View File

@ -2,9 +2,10 @@ using System;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public class SourceVoice : Voice public class SourceVoice : SendableVoice
{ {
protected FAudio.FAudioWaveFormatEx Format; private Format format;
public Format Format => format;
protected object StateLock = new object(); protected object StateLock = new object();
@ -46,27 +47,16 @@ namespace MoonWorks.Audio
public SourceVoice( public SourceVoice(
AudioDevice device, AudioDevice device,
ushort formatTag, Format format
ushort bitsPerSample, ) : base(device, format.Channels, device.DeviceDetails.OutputFormat.Format.nChannels)
ushort blockAlign,
ushort channels,
uint samplesPerSecond
) : base(device)
{ {
Format = new FAudio.FAudioWaveFormatEx this.format = format;
{ var fAudioFormat = format.ToFAudioFormat();
wFormatTag = formatTag,
wBitsPerSample = bitsPerSample,
nChannels = channels,
nBlockAlign = blockAlign,
nSamplesPerSec = samplesPerSecond,
nAvgBytesPerSec = blockAlign * samplesPerSecond
};
FAudio.FAudio_CreateSourceVoice( FAudio.FAudio_CreateSourceVoice(
device.Handle, device.Handle,
out var Handle, out var Handle,
ref Format, ref fAudioFormat,
FAudio.FAUDIO_VOICE_USEFILTER, FAudio.FAUDIO_VOICE_USEFILTER,
FAudio.FAUDIO_DEFAULT_FREQ_RATIO, FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
IntPtr.Zero, IntPtr.Zero,
@ -114,6 +104,42 @@ namespace MoonWorks.Audio
} }
} }
public void SubmitBuffer(FAudio.FAudioBuffer buffer)
{
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle,
ref buffer,
IntPtr.Zero
);
}
// FIXME: maybe this is bad
// NOTE: SourceVoices obtained this way will be returned to the voice pool when stopped!
public static SourceVoice ObtainSourceVoice(AudioDevice device, Format format)
{
return device.ObtainSourceVoice(format);
}
// intended for short-lived sound effects
public static SourceVoice PlayStaticSound(AudioDevice device, StaticSound sound, SubmixVoice sendVoice = null)
{
var voice = ObtainSourceVoice(device, sound.Format);
if (sendVoice == null)
{
voice.SetOutputVoice(device.MasteringVoice);
}
else
{
voice.SetOutputVoice(sendVoice);
}
voice.SubmitBuffer(sound.Handle);
voice.Play();
return voice;
}
protected override unsafe void Destroy() protected override unsafe void Destroy()
{ {
Stop(); Stop();

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace MoonWorks.Audio
{
internal class SourceVoicePool
{
private AudioDevice Device;
Dictionary<Format, Queue<SourceVoice>> VoiceLists = new Dictionary<Format, Queue<SourceVoice>>();
public SourceVoicePool(AudioDevice device)
{
Device = device;
}
public SourceVoice Obtain(Format format)
{
if (!VoiceLists.ContainsKey(format))
{
VoiceLists.Add(format, new Queue<SourceVoice>());
}
var list = VoiceLists[format];
if (list.Count == 0)
{
list.Enqueue(new SourceVoice(Device, format));
}
return list.Dequeue();
}
public void Return(SourceVoice voice)
{
var list = VoiceLists[voice.Format];
list.Enqueue(voice);
}
}
}

View File

@ -8,11 +8,9 @@ namespace MoonWorks.Audio
public class StaticSound : AudioResource public class StaticSound : AudioResource
{ {
internal FAudio.FAudioBuffer Handle; internal FAudio.FAudioBuffer Handle;
public ushort FormatTag { get; }
public ushort BitsPerSample { get; } private Format format;
public ushort Channels { get; } public Format Format => format;
public uint SamplesPerSecond { get; }
public ushort BlockAlign { get; }
public uint LoopStart { get; set; } = 0; public uint LoopStart { get; set; } = 0;
public uint LoopLength { get; set; } = 0; public uint LoopLength { get; set; } = 0;
@ -45,13 +43,17 @@ namespace MoonWorks.Audio
FAudio.stb_vorbis_close(filePointer); FAudio.stb_vorbis_close(filePointer);
var format = new Format
{
Tag = FormatTag.IEEE_FLOAT,
BitsPerSample = 32,
Channels = (ushort) info.channels,
SampleRate = info.sample_rate
};
return new StaticSound( return new StaticSound(
device, device,
3, format,
32,
(ushort) (4 * info.channels),
(ushort) info.channels,
info.sample_rate,
(nint) buffer, (nint) buffer,
(uint) lengthInBytes, (uint) lengthInBytes,
true); true);
@ -183,13 +185,17 @@ namespace MoonWorks.Audio
} }
// End scan // End scan
var format = new Format
{
Tag = (FormatTag) wFormatTag,
BitsPerSample = wBitsPerSample,
Channels = nChannels,
SampleRate = nSamplesPerSec
};
var sound = new StaticSound( var sound = new StaticSound(
device, device,
wFormatTag, format,
wBitsPerSample,
nBlockAlign,
nChannels,
nSamplesPerSec,
(nint) waveDataBuffer, (nint) waveDataBuffer,
(uint) waveDataLength, (uint) waveDataLength,
true true
@ -223,13 +229,17 @@ namespace MoonWorks.Audio
FAudio.qoa_close(qoaHandle); FAudio.qoa_close(qoaHandle);
NativeMemory.Free(fileDataPtr); NativeMemory.Free(fileDataPtr);
var format = new Format
{
Tag = FormatTag.PCM,
BitsPerSample = 16,
Channels = (ushort) channels,
SampleRate = samplerate
};
return new StaticSound( return new StaticSound(
device, device,
1, format,
16,
(ushort) (channels * 2),
(ushort) channels,
samplerate,
(nint) buffer, (nint) buffer,
bufferLengthInBytes, bufferLengthInBytes,
true true
@ -238,20 +248,13 @@ namespace MoonWorks.Audio
public StaticSound( public StaticSound(
AudioDevice device, AudioDevice device,
ushort formatTag, Format format,
ushort bitsPerSample,
ushort blockAlign,
ushort channels,
uint samplesPerSecond,
IntPtr bufferPtr, IntPtr bufferPtr,
uint bufferLengthInBytes, uint bufferLengthInBytes,
bool ownsBuffer) : base(device) bool ownsBuffer) : base(device)
{ {
FormatTag = formatTag; // TODO: should we wrap the format struct to make it nicer?
BitsPerSample = bitsPerSample; this.format = format;
BlockAlign = blockAlign;
Channels = channels;
SamplesPerSecond = samplesPerSecond;
Handle = new FAudio.FAudioBuffer Handle = new FAudio.FAudioBuffer
{ {

View File

@ -1,11 +0,0 @@
namespace MoonWorks.Audio
{
public class StaticSourceVoice : SourceVoice
{
public StaticSourceVoice(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
{
}
}
}

View File

@ -2,22 +2,22 @@ using System;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public class SubmixVoice : Voice public class SubmixVoice : SendableVoice, IReceivableVoice
{ {
public SubmixVoice( public SubmixVoice(
AudioDevice device, AudioDevice device,
uint inputChannels, uint sourceChannelCount,
uint sampleRate uint sampleRate
) : base(device) ) : base(device, sourceChannelCount, device.DeviceDetails.OutputFormat.Format.nChannels)
{ {
FAudio.FAudio_CreateSubmixVoice( FAudio.FAudio_CreateSubmixVoice(
device.Handle, device.Handle,
out Handle, out handle,
inputChannels, sourceChannelCount,
sampleRate, sampleRate,
0, 0,
0, 0,
IntPtr.Zero, IntPtr.Zero, // default sends to mastering voice
IntPtr.Zero IntPtr.Zero
); );
} }

View File

@ -1,50 +1,28 @@
using System; using System;
using System.Runtime.InteropServices;
using EasingFunction = System.Func<float, float>; using EasingFunction = System.Func<float, float>;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public abstract class Voice : AudioResource public abstract class Voice : AudioResource
{ {
protected IntPtr Handle; protected IntPtr handle;
public IntPtr Handle => handle;
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; public uint SourceChannelCount { get; }
public uint DestinationChannelCount { get; }
private ReverbEffect ReverbEffect;
public bool Is3D { get; protected set; } public bool Is3D { get; protected set; }
private float pan = 0; private float dopplerFactor;
public virtual float Pan public float DopplerFactor
{ {
get => pan; get => dopplerFactor;
internal set set
{ {
value = Math.MathHelper.Clamp(value, -1f, 1f); if (dopplerFactor != value)
if (pan != value)
{ {
pan = value; dopplerFactor = value;
UpdatePitch();
if (pan < -1f)
{
pan = -1f;
}
if (pan > 1f)
{
pan = 1f;
}
if (Is3D) { return; }
SetPanMatrixCoefficients();
FAudio.FAudioVoice_SetOutputMatrix(
Handle,
Device.MasteringVoice.Handle,
dspSettings.SrcChannelCount,
dspSettings.DstChannelCount,
dspSettings.pMatrixCoefficients,
0
);
} }
} }
} }
@ -172,62 +150,10 @@ namespace MoonWorks.Audio
} }
} }
private float reverb; public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device)
public unsafe float Reverb
{ {
get => reverb; SourceChannelCount = sourceChannelCount;
internal set DestinationChannelCount = destinationChannelCount;
{
if (ReverbEffect != null)
{
value = MathF.Max(0, value);
if (reverb != value)
{
reverb = value;
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
outputMatrix[0] = reverb;
if (dspSettings.SrcChannelCount == 2)
{
outputMatrix[1] = reverb;
}
FAudio.FAudioVoice_SetOutputMatrix(
Handle,
ReverbEffect.Handle,
dspSettings.SrcChannelCount,
1,
dspSettings.pMatrixCoefficients,
0
);
}
}
#if DEBUG
if (ReverbEffect == null)
{
Logger.LogWarn("Tried to set reverb value before applying a reverb effect");
}
#endif
}
}
public Voice(AudioDevice device) : base(device) { }
public void SetPan(float targetValue)
{
Pan = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Pan);
}
public void SetPan(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0);
}
public void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime);
} }
public void SetPitch(float targetValue) public void SetPitch(float targetValue)
@ -283,85 +209,6 @@ namespace MoonWorks.Audio
FilterOneOverQ = targetValue; FilterOneOverQ = targetValue;
} }
public void SetReverb(float targetValue)
{
Reverb = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Reverb);
}
public void SetReverb(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0);
}
public void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime);
}
public unsafe void SetSends(Voice send)
{
FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[1];
sendDesc[0].Flags = 0;
sendDesc[0].pOutputVoice = send.Handle;
var sends = new FAudio.FAudioVoiceSends();
sends.SendCount = 1;
sends.pSends = (nint) sendDesc;
FAudio.FAudioVoice_SetOutputVoices(
Handle,
ref sends
);
}
public unsafe void SetSends(Voice sendOne, Voice sendTwo)
{
var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2];
sendDesc[0].Flags = 0;
sendDesc[0].pOutputVoice = sendOne.Handle;
sendDesc[1].Flags = 0;
sendDesc[1].pOutputVoice = sendTwo.Handle;
var sends = new FAudio.FAudioVoiceSends();
sends.SendCount = 2;
sends.pSends = (nint) sendDesc;
FAudio.FAudioVoice_SetOutputVoices(
Handle,
ref sends
);
}
public unsafe void SetReverbEffectChain(ReverbEffect reverbEffect)
{
SetSends(Device.MasteringVoice, reverbEffect);
ReverbEffect = reverbEffect;
}
private unsafe void InitDSPSettings(uint srcChannels)
{
dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS();
dspSettings.DopplerFactor = 1f;
dspSettings.SrcChannelCount = srcChannels;
dspSettings.DstChannelCount = Device.DeviceDetails.OutputFormat.Format.nChannels;
nuint memsize = (
4 *
dspSettings.SrcChannelCount *
dspSettings.DstChannelCount
);
dspSettings.pMatrixCoefficients = (nint) NativeMemory.Alloc(memsize);
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
for (uint i = 0; i < memsize; i += 1)
{
memPtr[i] = 0;
}
SetPanMatrixCoefficients();
}
private void UpdatePitch() private void UpdatePitch()
{ {
float doppler; float doppler;
@ -372,7 +219,7 @@ namespace MoonWorks.Audio
} }
else else
{ {
doppler = dspSettings.DopplerFactor * dopplerScale; doppler = DopplerFactor * dopplerScale;
} }
FAudio.FAudioSourceVoice_SetFrequencyRatio( FAudio.FAudioSourceVoice_SetFrequencyRatio(
@ -382,65 +229,9 @@ namespace MoonWorks.Audio
); );
} }
// 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 unsafe override void Destroy() protected unsafe override void Destroy()
{ {
FAudio.FAudioVoice_DestroyVoice(Handle); FAudio.FAudioVoice_DestroyVoice(Handle);
NativeMemory.Free((void*) dspSettings.pMatrixCoefficients);
} }
} }
} }