From b1c7740b8639d18b2d5cba639b3f04522a64a195 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Mon, 31 Jul 2023 01:16:19 -0700 Subject: [PATCH 01/13] started reworking audio around Voice API --- src/Audio/AudioDevice.cs | 65 ++- src/Audio/AudioTween.cs | 4 +- src/Audio/AudioTweenManager.cs | 36 +- src/Audio/MasteringVoice.cs | 46 ++ src/Audio/ReverbEffect.cs | 59 +-- src/Audio/{SoundQueue.cs => SoundSequence.cs} | 67 +-- src/Audio/SourceVoice.cs | 123 +++++ src/Audio/StaticSourceVoice.cs | 11 + src/Audio/SubmixVoice.cs | 25 + src/Audio/Voice.cs | 446 ++++++++++++++++++ 10 files changed, 723 insertions(+), 159 deletions(-) create mode 100644 src/Audio/MasteringVoice.cs rename src/Audio/{SoundQueue.cs => SoundSequence.cs} (61%) create mode 100644 src/Audio/SourceVoice.cs create mode 100644 src/Audio/StaticSourceVoice.cs create mode 100644 src/Audio/SubmixVoice.cs create mode 100644 src/Audio/Voice.cs diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index e5a86b0..9ffbdf9 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -9,24 +9,15 @@ namespace MoonWorks.Audio { public IntPtr Handle { get; } public byte[] Handle3D { get; } - public IntPtr MasteringVoice { get; } public FAudio.FAudioDeviceDetails DeviceDetails { get; } + private MasteringVoice masteringVoice; + public MasteringVoice MasteringVoice => masteringVoice; + public float CurveDistanceScalar = 1f; public float DopplerScale = 1f; public float SpeedOfSound = 343.5f; - private float masteringVolume = 1f; - public float MasteringVolume - { - get => masteringVolume; - set - { - masteringVolume = value; - FAudio.FAudioVoice_SetVolume(MasteringVoice, masteringVolume, 0); - } - } - private readonly HashSet resources = new HashSet(); private readonly List autoUpdateStreamingSoundReferences = new List(); private readonly List autoFreeStaticSoundInstanceReferences = new List(); @@ -93,26 +84,18 @@ namespace MoonWorks.Audio } /* Init Mastering Voice */ - IntPtr masteringVoice; - - if (FAudio.FAudio_CreateMasteringVoice( - Handle, - out masteringVoice, - FAudio.FAUDIO_DEFAULT_CHANNELS, - FAudio.FAUDIO_DEFAULT_SAMPLERATE, - 0, + var result = MasteringVoice.Create( + this, i, - IntPtr.Zero - ) != 0) + out masteringVoice + ); + + if (!result) { - Logger.LogError("No mastering voice found!"); - FAudio.FAudio_Release(Handle); - Handle = IntPtr.Zero; + Logger.LogError("Audio device creation failed!"); return; } - MasteringVoice = masteringVoice; - /* Init 3D Audio */ Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; @@ -192,7 +175,7 @@ namespace MoonWorks.Audio { if (soundSequenceReferences[i].TryGetTarget(out var soundSequence)) { - soundSequence.Update(); + soundSequence.OnUpdate(); } else { @@ -203,13 +186,13 @@ namespace MoonWorks.Audio AudioTweenManager.Update(elapsedSeconds); } - public void SyncPlay() + public void TriggerSyncGroup(uint syncGroup) { - FAudio.FAudio_CommitChanges(Handle, 1); + FAudio.FAudio_CommitChanges(Handle, syncGroup); } internal void CreateTween( - SoundInstance soundInstance, + Voice voice, AudioTweenProperty property, System.Func easingFunction, float start, @@ -220,7 +203,7 @@ namespace MoonWorks.Audio lock (StateLock) { AudioTweenManager.CreateTween( - soundInstance, + voice, property, easingFunction, start, @@ -232,12 +215,12 @@ namespace MoonWorks.Audio } internal void ClearTweens( - SoundInstance soundReference, + Voice voice, AudioTweenProperty property ) { lock (StateLock) { - AudioTweenManager.ClearTweens(soundReference, property); + AudioTweenManager.ClearTweens(voice, property); } } @@ -286,6 +269,18 @@ namespace MoonWorks.Audio if (disposing) { + // stop all source voices + foreach (var weakReference in resources) + { + var target = weakReference.Target; + + if (target != null && target is SourceVoice voice) + { + voice.Stop(); + } + } + + // destroy all audio resources foreach (var weakReference in resources) { var target = weakReference.Target; @@ -295,10 +290,10 @@ namespace MoonWorks.Audio (target as IDisposable).Dispose(); } } + resources.Clear(); } - FAudio.FAudioVoice_DestroyVoice(MasteringVoice); FAudio.FAudio_Release(Handle); IsDisposed = true; diff --git a/src/Audio/AudioTween.cs b/src/Audio/AudioTween.cs index dc71fef..75b2e9d 100644 --- a/src/Audio/AudioTween.cs +++ b/src/Audio/AudioTween.cs @@ -14,7 +14,7 @@ namespace MoonWorks.Audio internal class AudioTween { - public SoundInstance SoundInstance; + public Voice Voice; public AudioTweenProperty Property; public EasingFunction EasingFunction; public float Time; @@ -51,7 +51,7 @@ namespace MoonWorks.Audio public void Free(AudioTween tween) { - tween.SoundInstance = null; + tween.Voice = null; Tweens.Enqueue(tween); } } diff --git a/src/Audio/AudioTweenManager.cs b/src/Audio/AudioTweenManager.cs index f98adcc..d5b05ee 100644 --- a/src/Audio/AudioTweenManager.cs +++ b/src/Audio/AudioTweenManager.cs @@ -6,7 +6,7 @@ namespace MoonWorks.Audio internal class AudioTweenManager { private AudioTweenPool AudioTweenPool = new AudioTweenPool(); - private readonly Dictionary<(SoundInstance, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(SoundInstance, AudioTweenProperty), AudioTween>(); + private readonly Dictionary<(Voice, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(Voice, AudioTweenProperty), AudioTween>(); private readonly List DelayedAudioTweens = new List(); public void Update(float elapsedSeconds) @@ -14,7 +14,7 @@ namespace MoonWorks.Audio for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--) { var audioTween = DelayedAudioTweens[i]; - var soundInstance = audioTween.SoundInstance; + var voice = audioTween.Voice; audioTween.Time += elapsedSeconds; @@ -24,23 +24,23 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - audioTween.StartValue = soundInstance.Pan; + audioTween.StartValue = voice.Pan; break; case AudioTweenProperty.Pitch: - audioTween.StartValue = soundInstance.Pitch; + audioTween.StartValue = voice.Pitch; break; case AudioTweenProperty.Volume: - audioTween.StartValue = soundInstance.Volume; + audioTween.StartValue = voice.Volume; break; case AudioTweenProperty.FilterFrequency: - audioTween.StartValue = soundInstance.FilterFrequency; + audioTween.StartValue = voice.FilterFrequency; break; case AudioTweenProperty.Reverb: - audioTween.StartValue = soundInstance.Reverb; + audioTween.StartValue = voice.Reverb; break; } @@ -64,7 +64,7 @@ namespace MoonWorks.Audio } public void CreateTween( - SoundInstance soundInstance, + Voice voice, AudioTweenProperty property, System.Func easingFunction, float start, @@ -73,7 +73,7 @@ namespace MoonWorks.Audio float delayTime ) { var tween = AudioTweenPool.Obtain(); - tween.SoundInstance = soundInstance; + tween.Voice = voice; tween.Property = property; tween.EasingFunction = easingFunction; tween.StartValue = start; @@ -92,21 +92,21 @@ namespace MoonWorks.Audio } } - public void ClearTweens(SoundInstance soundInstance, AudioTweenProperty property) + public void ClearTweens(Voice voice, AudioTweenProperty property) { - AudioTweens.Remove((soundInstance, property)); + AudioTweens.Remove((voice, property)); } private void AddTween( AudioTween audioTween ) { // if a tween with the same sound and property already exists, get rid of it - if (AudioTweens.TryGetValue((audioTween.SoundInstance, audioTween.Property), out var currentTween)) + if (AudioTweens.TryGetValue((audioTween.Voice, audioTween.Property), out var currentTween)) { AudioTweenPool.Free(currentTween); } - AudioTweens[(audioTween.SoundInstance, audioTween.Property)] = audioTween; + AudioTweens[(audioTween.Voice, audioTween.Property)] = audioTween; } private static bool UpdateAudioTween(AudioTween audioTween, float delta) @@ -133,23 +133,23 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - audioTween.SoundInstance.Pan = value; + audioTween.Voice.Pan = value; break; case AudioTweenProperty.Pitch: - audioTween.SoundInstance.Pitch = value; + audioTween.Voice.Pitch = value; break; case AudioTweenProperty.Volume: - audioTween.SoundInstance.Volume = value; + audioTween.Voice.Volume = value; break; case AudioTweenProperty.FilterFrequency: - audioTween.SoundInstance.FilterFrequency = value; + audioTween.Voice.FilterFrequency = value; break; case AudioTweenProperty.Reverb: - audioTween.SoundInstance.Reverb = value; + audioTween.Voice.Reverb = value; break; } diff --git a/src/Audio/MasteringVoice.cs b/src/Audio/MasteringVoice.cs new file mode 100644 index 0000000..78fe624 --- /dev/null +++ b/src/Audio/MasteringVoice.cs @@ -0,0 +1,46 @@ +using System; + +namespace MoonWorks.Audio +{ + public class MasteringVoice : Voice + { + // mastering voice can't pan + public override float Pan => 0; + + internal static bool Create( + AudioDevice device, + uint deviceIndex, + out MasteringVoice masteringVoice + ) { + var result = FAudio.FAudio_CreateMasteringVoice( + device.Handle, + out var handle, + FAudio.FAUDIO_DEFAULT_CHANNELS, + FAudio.FAUDIO_DEFAULT_SAMPLERATE, + 0, + deviceIndex, + IntPtr.Zero + ); + + if (result == 0) + { + masteringVoice = new MasteringVoice(device, handle); + } + else + { + Logger.LogError("Failed to create mastering voice!"); + masteringVoice = null; + } + + return result == 0; + } + + internal MasteringVoice( + AudioDevice device, + IntPtr handle + ) : base(device) + { + Handle = handle; + } + } +} diff --git a/src/Audio/ReverbEffect.cs b/src/Audio/ReverbEffect.cs index 42fab2c..eac7cda 100644 --- a/src/Audio/ReverbEffect.cs +++ b/src/Audio/ReverbEffect.cs @@ -4,53 +4,31 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { // sound instances can send their audio to this voice to add reverb - public unsafe class ReverbEffect : AudioResource + public unsafe class ReverbEffect : SubmixVoice { - private IntPtr voice; - public IntPtr Voice => voice; - - public ReverbEffect(AudioDevice audioDevice) : base(audioDevice) + public ReverbEffect(AudioDevice audioDevice) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec) { /* Init reverb */ - IntPtr reverb; FAudio.FAudioCreateReverb(out reverb, 0); - IntPtr chainPtr; - chainPtr = (nint) NativeMemory.Alloc( - (nuint) Marshal.SizeOf() + var chain = new FAudio.FAudioEffectChain(); + var descriptor = new FAudio.FAudioEffectDescriptor(); + + descriptor.InitialState = 1; + descriptor.OutputChannels = Device.DeviceDetails.OutputFormat.Format.nChannels; + descriptor.pEffect = reverb; + + chain.EffectCount = 1; + chain.pEffectDescriptors = (nint) (&descriptor); + + FAudio.FAudioVoice_SetEffectChain( + Handle, + ref chain ); - FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr; - reverbChain->EffectCount = 1; - reverbChain->pEffectDescriptors = (nint) NativeMemory.Alloc( - (nuint) Marshal.SizeOf() - ); - - FAudio.FAudioEffectDescriptor* reverbDescriptor = - (FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors; - - reverbDescriptor->InitialState = 1; - reverbDescriptor->OutputChannels = (uint) ( - (audioDevice.DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1 - ); - reverbDescriptor->pEffect = reverb; - - FAudio.FAudio_CreateSubmixVoice( - audioDevice.Handle, - out voice, - 1, /* omnidirectional reverb */ - audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec, - 0, - 0, - IntPtr.Zero, - chainPtr - ); FAudio.FAPOBase_Release(reverb); - NativeMemory.Free((void*) reverbChain->pEffectDescriptors); - NativeMemory.Free((void*) chainPtr); - /* Init reverb params */ // Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC @@ -86,7 +64,7 @@ namespace MoonWorks.Audio fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams) { FAudio.FAudioVoice_SetEffectParameters( - voice, + Handle, 0, (nint) reverbParamsPtr, (uint) Marshal.SizeOf(), @@ -94,10 +72,5 @@ namespace MoonWorks.Audio ); } } - - protected override void Destroy() - { - FAudio.FAudioVoice_DestroyVoice(Voice); - } } } diff --git a/src/Audio/SoundQueue.cs b/src/Audio/SoundSequence.cs similarity index 61% rename from src/Audio/SoundQueue.cs rename to src/Audio/SoundSequence.cs index d098ecf..6b8c554 100644 --- a/src/Audio/SoundQueue.cs +++ b/src/Audio/SoundSequence.cs @@ -3,25 +3,25 @@ using System; namespace MoonWorks.Audio { // NOTE: all sounds played with a SoundSequence must have the same audio format! - public class SoundSequence : SoundInstance + public class SoundSequence : SourceVoice { public int NeedSoundThreshold = 0; public delegate void OnSoundNeededFunc(); public OnSoundNeededFunc OnSoundNeeded; - private object StateLock = new object(); - public SoundSequence(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) { device.AddSoundSequenceReference(this); + OnUpdate += Update; } public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.FormatTag, templateSound.BitsPerSample, templateSound.BlockAlign, templateSound.Channels, templateSound.SamplesPerSecond) { device.AddSoundSequenceReference(this); + OnUpdate += Update; } - public void Update() + private void Update() { lock (StateLock) { @@ -31,7 +31,7 @@ namespace MoonWorks.Audio if (NeedSoundThreshold > 0) { FAudio.FAudioSourceVoice_GetState( - Voice, + Handle, out var state, FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED ); @@ -65,66 +65,11 @@ namespace MoonWorks.Audio lock (StateLock) { FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Voice, + Handle, ref sound.Handle, IntPtr.Zero ); } } - - public override void Pause() - { - lock (StateLock) - { - if (State == SoundState.Playing) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - State = SoundState.Paused; - } - } - } - - public override void Play() - { - PlayUsingOperationSet(0); - } - - public override void QueueSyncPlay() - { - PlayUsingOperationSet(1); - } - - private void PlayUsingOperationSet(uint operationSet) - { - lock (StateLock) - { - if (State == SoundState.Playing) - { - return; - } - - FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); - State = SoundState.Playing; - } - } - - public override void Stop() - { - lock (StateLock) - { - FAudio.FAudioSourceVoice_ExitLoop(Voice, 0); - State = SoundState.Stopped; - } - } - - public override void StopImmediate() - { - lock (StateLock) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); - State = SoundState.Stopped; - } - } } } diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs new file mode 100644 index 0000000..55c1918 --- /dev/null +++ b/src/Audio/SourceVoice.cs @@ -0,0 +1,123 @@ +using System; + +namespace MoonWorks.Audio +{ + public class SourceVoice : Voice + { + protected FAudio.FAudioWaveFormatEx Format; + + protected object StateLock = new object(); + + public uint BuffersQueued + { + get + { + FAudio.FAudioSourceVoice_GetState( + Handle, + out var state, + FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED + ); + + return state.BuffersQueued; + } + } + + private SoundState state; + public SoundState State + { + get + { + if (BuffersQueued == 0) + { + Stop(); + } + + return state; + } + + internal set + { + state = value; + } + } + + public delegate void OnUpdateFunc(); + public OnUpdateFunc OnUpdate; // called by AudioDevice thread + + public SourceVoice( + AudioDevice device, + ushort formatTag, + ushort bitsPerSample, + ushort blockAlign, + ushort channels, + uint samplesPerSecond + ) : base(device) + { + Format = new FAudio.FAudioWaveFormatEx + { + wFormatTag = formatTag, + wBitsPerSample = bitsPerSample, + nChannels = channels, + nBlockAlign = blockAlign, + nSamplesPerSec = samplesPerSecond, + nAvgBytesPerSec = blockAlign * samplesPerSecond + }; + + FAudio.FAudio_CreateSourceVoice( + device.Handle, + out var Handle, + ref Format, + FAudio.FAUDIO_VOICE_USEFILTER, + FAudio.FAUDIO_DEFAULT_FREQ_RATIO, + IntPtr.Zero, + IntPtr.Zero, // default sends to mastering voice! + IntPtr.Zero + ); + } + + public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Start(Handle, 0, syncGroup); + + State = SoundState.Playing; + } + } + + public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + + State = SoundState.Paused; + } + } + + public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_ExitLoop(Handle, syncGroup); + } + } + + public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); + + State = SoundState.Stopped; + } + } + + protected override unsafe void Destroy() + { + Stop(); + base.Destroy(); + } + } +} diff --git a/src/Audio/StaticSourceVoice.cs b/src/Audio/StaticSourceVoice.cs new file mode 100644 index 0000000..f4f6a7b --- /dev/null +++ b/src/Audio/StaticSourceVoice.cs @@ -0,0 +1,11 @@ +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) + { + } + + + } +} diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs new file mode 100644 index 0000000..02ad4ad --- /dev/null +++ b/src/Audio/SubmixVoice.cs @@ -0,0 +1,25 @@ +using System; + +namespace MoonWorks.Audio +{ + public class SubmixVoice : Voice + { + public SubmixVoice( + AudioDevice device, + uint inputChannels, + uint sampleRate + ) : base(device) + { + FAudio.FAudio_CreateSubmixVoice( + device.Handle, + out Handle, + inputChannels, + sampleRate, + 0, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + } + } +} diff --git a/src/Audio/Voice.cs b/src/Audio/Voice.cs new file mode 100644 index 0000000..14ee3a2 --- /dev/null +++ b/src/Audio/Voice.cs @@ -0,0 +1,446 @@ +using System; +using System.Runtime.InteropServices; +using EasingFunction = System.Func; + +namespace MoonWorks.Audio +{ + public abstract class Voice : AudioResource + { + protected IntPtr Handle; + + protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; + + private ReverbEffect ReverbEffect; + + public bool Is3D { get; protected set; } + + private float pan = 0; + public virtual 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, + Device.MasteringVoice.Handle, + dspSettings.SrcChannelCount, + dspSettings.DstChannelCount, + dspSettings.pMatrixCoefficients, + 0 + ); + } + } + } + + private float pitch = 0; + public float Pitch + { + get => pitch; + internal set + { + value = Math.MathHelper.Clamp(value, -1f, 1f); + if (pitch != value) + { + pitch = value; + UpdatePitch(); + } + } + } + + private float volume = 1; + public float Volume + { + get => volume; + internal set + { + value = Math.MathHelper.Max(0, value); + if (volume != value) + { + volume = value; + FAudio.FAudioVoice_SetVolume(Handle, volume, 0); + } + } + } + + private const float MAX_FILTER_FREQUENCY = 1f; + private const float MAX_FILTER_ONEOVERQ = 1.5f; + + private FAudio.FAudioFilterParameters filterParameters = new FAudio.FAudioFilterParameters + { + Type = FAudio.FAudioFilterType.FAudioLowPassFilter, + Frequency = 1f, + OneOverQ = 1f + }; + + public float FilterFrequency + { + get => filterParameters.Frequency; + internal set + { + value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY); + if (filterParameters.Frequency != value) + { + filterParameters.Frequency = value; + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + public float FilterOneOverQ + { + get => filterParameters.OneOverQ; + internal set + { + value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ); + if (filterParameters.OneOverQ != value) + { + filterParameters.OneOverQ = value; + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + private FilterType filterType; + public FilterType FilterType + { + get => filterType; + set + { + if (filterType != value) + { + filterType = value; + + switch (filterType) + { + case FilterType.None: + filterParameters = new FAudio.FAudioFilterParameters + { + Type = FAudio.FAudioFilterType.FAudioLowPassFilter, + Frequency = 1f, + OneOverQ = 1f + }; + break; + + case FilterType.LowPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter; + filterParameters.Frequency = 1f; + break; + + case FilterType.BandPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter; + break; + + case FilterType.HighPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter; + filterParameters.Frequency = 0f; + break; + } + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 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*) 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) + { + Pitch = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Pitch); + } + + public void SetPitch(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, 0); + } + + public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, delayTime); + } + + public void SetVolume(float targetValue) + { + Volume = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Volume); + } + + public void SetVolume(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0); + } + + public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime); + } + + public void SetFilterFrequency(float targetValue) + { + FilterFrequency = targetValue; + Device.ClearTweens(this, AudioTweenProperty.FilterFrequency); + } + + public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0); + } + + public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime); + } + + public void SetFilterOneOverQ(float 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() + { + float doppler; + float dopplerScale = Device.DopplerScale; + if (!Is3D || dopplerScale == 0.0f) + { + doppler = 1.0f; + } + else + { + doppler = dspSettings.DopplerFactor * dopplerScale; + } + + FAudio.FAudioSourceVoice_SetFrequencyRatio( + Handle, + (float) System.Math.Pow(2.0, pitch) * doppler, + 0 + ); + } + + // 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() + { + FAudio.FAudioVoice_DestroyVoice(Handle); + + NativeMemory.Free((void*) dspSettings.pMatrixCoefficients); + } + } +} -- 2.25.1 From e2c85ec72890ad5e3b73dd5330001da373cc9221 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 1 Aug 2023 11:22:49 -0700 Subject: [PATCH 02/13] restructure and implement Voice hierarchy --- src/Audio/AudioDevice.cs | 14 +- src/Audio/AudioTweenManager.cs | 20 ++- src/Audio/AudioUtils.cs | 35 ++--- src/Audio/Format.cs | 33 +++++ src/Audio/IReceivableVoice.cs | 7 + src/Audio/MasteringVoice.cs | 9 +- src/Audio/SendableVoice.cs | 227 +++++++++++++++++++++++++++++++ src/Audio/SoundSequence.cs | 11 +- src/Audio/SourceVoice.cs | 62 ++++++--- src/Audio/SourceVoicePool.cs | 39 ++++++ src/Audio/StaticSound.cs | 63 ++++----- src/Audio/StaticSourceVoice.cs | 11 -- src/Audio/SubmixVoice.cs | 12 +- src/Audio/Voice.cs | 239 +++------------------------------ 14 files changed, 452 insertions(+), 330 deletions(-) create mode 100644 src/Audio/Format.cs create mode 100644 src/Audio/IReceivableVoice.cs create mode 100644 src/Audio/SendableVoice.cs create mode 100644 src/Audio/SourceVoicePool.cs delete mode 100644 src/Audio/StaticSourceVoice.cs diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index 9ffbdf9..18bb9ea 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; using System.Threading; namespace MoonWorks.Audio @@ -25,6 +24,8 @@ namespace MoonWorks.Audio private AudioTweenManager AudioTweenManager; + private SourceVoicePool VoicePool; + private const int Step = 200; private TimeSpan UpdateInterval; private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch(); @@ -106,6 +107,7 @@ namespace MoonWorks.Audio ); AudioTweenManager = new AudioTweenManager(); + VoicePool = new SourceVoicePool(this); Logger.LogInfo("Setting up audio thread..."); WakeSignal = new AutoResetEvent(true); @@ -260,6 +262,16 @@ namespace MoonWorks.Audio soundSequenceReferences.Add(new WeakReference(sequence)); } + internal SourceVoice ObtainSourceVoice(Format format) + { + return VoicePool.Obtain(format); + } + + internal void ReturnSourceVoice(SourceVoice voice) + { + VoicePool.Return(voice); + } + protected virtual void Dispose(bool disposing) { if (!IsDisposed) diff --git a/src/Audio/AudioTweenManager.cs b/src/Audio/AudioTweenManager.cs index d5b05ee..88d8203 100644 --- a/src/Audio/AudioTweenManager.cs +++ b/src/Audio/AudioTweenManager.cs @@ -24,7 +24,10 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - audioTween.StartValue = voice.Pan; + if (voice is SendableVoice pannableVoice) + { + audioTween.StartValue = pannableVoice.Pan; + } break; case AudioTweenProperty.Pitch: @@ -40,7 +43,10 @@ namespace MoonWorks.Audio break; case AudioTweenProperty.Reverb: - audioTween.StartValue = voice.Reverb; + if (voice is SendableVoice reverbableVoice) + { + audioTween.StartValue = reverbableVoice.Reverb; + } break; } @@ -133,7 +139,10 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - audioTween.Voice.Pan = value; + if (audioTween.Voice is SendableVoice pannableVoice) + { + pannableVoice.Pan = value; + } break; case AudioTweenProperty.Pitch: @@ -149,7 +158,10 @@ namespace MoonWorks.Audio break; case AudioTweenProperty.Reverb: - audioTween.Voice.Reverb = value; + if (audioTween.Voice is SendableVoice reverbableVoice) + { + reverbableVoice.Reverb = value; + } break; } diff --git a/src/Audio/AudioUtils.cs b/src/Audio/AudioUtils.cs index 143eb8f..a9caa2d 100644 --- a/src/Audio/AudioUtils.cs +++ b/src/Audio/AudioUtils.cs @@ -4,39 +4,30 @@ namespace MoonWorks.Audio { 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); using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); using BinaryReader br = new BinaryReader(fs); - headerData.FileLength = (int)fileInfo.Length - 8; fs.Position = 20; - headerData.FormatTag = br.ReadInt16(); + var formatTag = br.ReadInt16(); fs.Position = 22; - headerData.Channels = br.ReadInt16(); + var channels = br.ReadInt16(); fs.Position = 24; - headerData.SampleRate = br.ReadInt32(); - fs.Position = 32; - headerData.BlockAlign = br.ReadInt16(); + var sampleRate = br.ReadInt32(); fs.Position = 34; - headerData.BitsPerSample = br.ReadInt16(); + var bitsPerSample = br.ReadInt16(); 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 + }; } } } diff --git a/src/Audio/Format.cs b/src/Audio/Format.cs new file mode 100644 index 0000000..40d736e --- /dev/null +++ b/src/Audio/Format.cs @@ -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 + }; + } + } +} diff --git a/src/Audio/IReceivableVoice.cs b/src/Audio/IReceivableVoice.cs new file mode 100644 index 0000000..a40abeb --- /dev/null +++ b/src/Audio/IReceivableVoice.cs @@ -0,0 +1,7 @@ +namespace MoonWorks.Audio +{ + public interface IReceivableVoice + { + public System.IntPtr Handle { get; } + } +} diff --git a/src/Audio/MasteringVoice.cs b/src/Audio/MasteringVoice.cs index 78fe624..e5c23ed 100644 --- a/src/Audio/MasteringVoice.cs +++ b/src/Audio/MasteringVoice.cs @@ -2,11 +2,8 @@ using System; 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( AudioDevice device, uint deviceIndex, @@ -38,9 +35,9 @@ namespace MoonWorks.Audio internal MasteringVoice( AudioDevice device, IntPtr handle - ) : base(device) + ) : base(device, device.DeviceDetails.OutputFormat.Format.nChannels, 0) { - Handle = handle; + this.handle = handle; } } } diff --git a/src/Audio/SendableVoice.cs b/src/Audio/SendableVoice.cs new file mode 100644 index 0000000..c467584 --- /dev/null +++ b/src/Audio/SendableVoice.cs @@ -0,0 +1,227 @@ +using System; +using System.Runtime.InteropServices; +using EasingFunction = System.Func; + +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(); + } + } +} diff --git a/src/Audio/SoundSequence.cs b/src/Audio/SoundSequence.cs index 6b8c554..fdb4155 100644 --- a/src/Audio/SoundSequence.cs +++ b/src/Audio/SoundSequence.cs @@ -9,13 +9,13 @@ namespace MoonWorks.Audio public delegate void OnSoundNeededFunc(); 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); 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); OnUpdate += Update; @@ -51,12 +51,7 @@ namespace MoonWorks.Audio public void EnqueueSound(StaticSound sound) { #if DEBUG - if ( - sound.FormatTag != Format.wFormatTag || - sound.BitsPerSample != Format.wBitsPerSample || - sound.Channels != Format.nChannels || - sound.SamplesPerSecond != Format.nSamplesPerSec - ) + if (!(sound.Format == Format)) { Logger.LogWarn("Playlist audio format mismatch!"); } diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 55c1918..15462a2 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -2,9 +2,10 @@ using System; 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(); @@ -46,27 +47,16 @@ namespace MoonWorks.Audio public SourceVoice( AudioDevice device, - ushort formatTag, - ushort bitsPerSample, - ushort blockAlign, - ushort channels, - uint samplesPerSecond - ) : base(device) + Format format + ) : base(device, format.Channels, device.DeviceDetails.OutputFormat.Format.nChannels) { - Format = new FAudio.FAudioWaveFormatEx - { - wFormatTag = formatTag, - wBitsPerSample = bitsPerSample, - nChannels = channels, - nBlockAlign = blockAlign, - nSamplesPerSec = samplesPerSecond, - nAvgBytesPerSec = blockAlign * samplesPerSecond - }; + this.format = format; + var fAudioFormat = format.ToFAudioFormat(); FAudio.FAudio_CreateSourceVoice( device.Handle, out var Handle, - ref Format, + ref fAudioFormat, FAudio.FAUDIO_VOICE_USEFILTER, FAudio.FAUDIO_DEFAULT_FREQ_RATIO, 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() { Stop(); diff --git a/src/Audio/SourceVoicePool.cs b/src/Audio/SourceVoicePool.cs new file mode 100644 index 0000000..3299a1f --- /dev/null +++ b/src/Audio/SourceVoicePool.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace MoonWorks.Audio +{ + internal class SourceVoicePool + { + private AudioDevice Device; + + Dictionary> VoiceLists = new Dictionary>(); + + public SourceVoicePool(AudioDevice device) + { + Device = device; + } + + public SourceVoice Obtain(Format format) + { + if (!VoiceLists.ContainsKey(format)) + { + VoiceLists.Add(format, new Queue()); + } + + 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); + } + } +} diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs index b5def25..3c92377 100644 --- a/src/Audio/StaticSound.cs +++ b/src/Audio/StaticSound.cs @@ -8,11 +8,9 @@ namespace MoonWorks.Audio public class StaticSound : AudioResource { internal FAudio.FAudioBuffer Handle; - public ushort FormatTag { get; } - public ushort BitsPerSample { get; } - public ushort Channels { get; } - public uint SamplesPerSecond { get; } - public ushort BlockAlign { get; } + + private Format format; + public Format Format => format; public uint LoopStart { get; set; } = 0; public uint LoopLength { get; set; } = 0; @@ -45,13 +43,17 @@ namespace MoonWorks.Audio 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( device, - 3, - 32, - (ushort) (4 * info.channels), - (ushort) info.channels, - info.sample_rate, + format, (nint) buffer, (uint) lengthInBytes, true); @@ -183,13 +185,17 @@ namespace MoonWorks.Audio } // End scan + var format = new Format + { + Tag = (FormatTag) wFormatTag, + BitsPerSample = wBitsPerSample, + Channels = nChannels, + SampleRate = nSamplesPerSec + }; + var sound = new StaticSound( device, - wFormatTag, - wBitsPerSample, - nBlockAlign, - nChannels, - nSamplesPerSec, + format, (nint) waveDataBuffer, (uint) waveDataLength, true @@ -223,13 +229,17 @@ namespace MoonWorks.Audio FAudio.qoa_close(qoaHandle); NativeMemory.Free(fileDataPtr); + var format = new Format + { + Tag = FormatTag.PCM, + BitsPerSample = 16, + Channels = (ushort) channels, + SampleRate = samplerate + }; + return new StaticSound( device, - 1, - 16, - (ushort) (channels * 2), - (ushort) channels, - samplerate, + format, (nint) buffer, bufferLengthInBytes, true @@ -238,20 +248,13 @@ namespace MoonWorks.Audio public StaticSound( AudioDevice device, - ushort formatTag, - ushort bitsPerSample, - ushort blockAlign, - ushort channels, - uint samplesPerSecond, + Format format, IntPtr bufferPtr, uint bufferLengthInBytes, bool ownsBuffer) : base(device) { - FormatTag = formatTag; - BitsPerSample = bitsPerSample; - BlockAlign = blockAlign; - Channels = channels; - SamplesPerSecond = samplesPerSecond; + // TODO: should we wrap the format struct to make it nicer? + this.format = format; Handle = new FAudio.FAudioBuffer { diff --git a/src/Audio/StaticSourceVoice.cs b/src/Audio/StaticSourceVoice.cs deleted file mode 100644 index f4f6a7b..0000000 --- a/src/Audio/StaticSourceVoice.cs +++ /dev/null @@ -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) - { - } - - - } -} diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs index 02ad4ad..338e08e 100644 --- a/src/Audio/SubmixVoice.cs +++ b/src/Audio/SubmixVoice.cs @@ -2,22 +2,22 @@ using System; namespace MoonWorks.Audio { - public class SubmixVoice : Voice + public class SubmixVoice : SendableVoice, IReceivableVoice { public SubmixVoice( AudioDevice device, - uint inputChannels, + uint sourceChannelCount, uint sampleRate - ) : base(device) + ) : base(device, sourceChannelCount, device.DeviceDetails.OutputFormat.Format.nChannels) { FAudio.FAudio_CreateSubmixVoice( device.Handle, - out Handle, - inputChannels, + out handle, + sourceChannelCount, sampleRate, 0, 0, - IntPtr.Zero, + IntPtr.Zero, // default sends to mastering voice IntPtr.Zero ); } diff --git a/src/Audio/Voice.cs b/src/Audio/Voice.cs index 14ee3a2..d098186 100644 --- a/src/Audio/Voice.cs +++ b/src/Audio/Voice.cs @@ -1,50 +1,28 @@ using System; -using System.Runtime.InteropServices; using EasingFunction = System.Func; namespace MoonWorks.Audio { public abstract class Voice : AudioResource { - protected IntPtr Handle; + protected IntPtr handle; + public IntPtr Handle => handle; - protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; - - private ReverbEffect ReverbEffect; + public uint SourceChannelCount { get; } + public uint DestinationChannelCount { get; } public bool Is3D { get; protected set; } - private float pan = 0; - public virtual float Pan + private float dopplerFactor; + public float DopplerFactor { - get => pan; - internal set + get => dopplerFactor; + set { - value = Math.MathHelper.Clamp(value, -1f, 1f); - if (pan != value) + if (dopplerFactor != value) { - pan = value; - - 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 - ); + dopplerFactor = value; + UpdatePitch(); } } } @@ -172,62 +150,10 @@ namespace MoonWorks.Audio } } - private float reverb; - public unsafe float Reverb + public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device) { - get => reverb; - internal set - { - 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); + SourceChannelCount = sourceChannelCount; + DestinationChannelCount = destinationChannelCount; } public void SetPitch(float targetValue) @@ -283,85 +209,6 @@ namespace MoonWorks.Audio 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() { float doppler; @@ -372,7 +219,7 @@ namespace MoonWorks.Audio } else { - doppler = dspSettings.DopplerFactor * dopplerScale; + doppler = DopplerFactor * dopplerScale; } 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() { FAudio.FAudioVoice_DestroyVoice(Handle); - - NativeMemory.Free((void*) dspSettings.pMatrixCoefficients); } } } -- 2.25.1 From c2cb83f93fc674c5be11383d1f83e254b1cae992 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 1 Aug 2023 18:13:17 -0700 Subject: [PATCH 03/13] faux mastering voice + submitting static sounds --- src/Audio/AudioDevice.cs | 99 +++++++---- src/Audio/AudioTweenManager.cs | 20 +-- src/Audio/IReceivableVoice.cs | 7 - src/Audio/MasteringVoice.cs | 43 ----- src/Audio/SendableVoice.cs | 227 -------------------------- src/Audio/SoundSequence.cs | 6 +- src/Audio/SourceVoice.cs | 89 +++++++--- src/Audio/StaticSound.cs | 69 +------- src/Audio/StaticSoundInstance.cs | 141 ---------------- src/Audio/SubmixVoice.cs | 2 +- src/Audio/Voice.cs | 271 +++++++++++++++++++++++++++++-- 11 files changed, 396 insertions(+), 578 deletions(-) delete mode 100644 src/Audio/IReceivableVoice.cs delete mode 100644 src/Audio/MasteringVoice.cs delete mode 100644 src/Audio/SendableVoice.cs delete mode 100644 src/Audio/StaticSoundInstance.cs diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index 18bb9ea..2c39408 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -10,8 +10,12 @@ namespace MoonWorks.Audio public byte[] Handle3D { get; } public FAudio.FAudioDeviceDetails DeviceDetails { get; } - private MasteringVoice masteringVoice; - public MasteringVoice MasteringVoice => masteringVoice; + private IntPtr trueMasteringVoice; + + // this is a fun little trick where we use a submix voice as a "faux" mastering voice + // this lets us maintain API consistency for effects like panning and reverb + private SubmixVoice fauxMasteringVoice; + public SubmixVoice MasteringVoice => fauxMasteringVoice; public float CurveDistanceScalar = 1f; public float DopplerScale = 1f; @@ -19,8 +23,8 @@ namespace MoonWorks.Audio private readonly HashSet resources = new HashSet(); private readonly List autoUpdateStreamingSoundReferences = new List(); - private readonly List autoFreeStaticSoundInstanceReferences = new List(); private readonly List> soundSequenceReferences = new List>(); + private readonly List autoFreeSourceVoices = new List(); private AudioTweenManager AudioTweenManager; @@ -85,18 +89,25 @@ namespace MoonWorks.Audio } /* Init Mastering Voice */ - var result = MasteringVoice.Create( - this, + var result = FAudio.FAudio_CreateMasteringVoice( + Handle, + out trueMasteringVoice, + FAudio.FAUDIO_DEFAULT_CHANNELS, + FAudio.FAUDIO_DEFAULT_SAMPLERATE, + 0, i, - out masteringVoice + IntPtr.Zero ); - if (!result) + if (result != 0) { + Logger.LogError("Failed to create a mastering voice!"); Logger.LogError("Audio device creation failed!"); return; } + fauxMasteringVoice = new SubmixVoice(this, FAudio.FAUDIO_DEFAULT_CHANNELS, FAudio.FAUDIO_DEFAULT_SAMPLERATE); + /* Init 3D Audio */ Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; @@ -162,17 +173,6 @@ namespace MoonWorks.Audio } } - for (var i = autoFreeStaticSoundInstanceReferences.Count - 1; i >= 0; i -= 1) - { - var staticSoundInstance = autoFreeStaticSoundInstanceReferences[i]; - - if (staticSoundInstance.State == SoundState.Stopped) - { - staticSoundInstance.Free(); - autoFreeStaticSoundInstanceReferences.RemoveAt(i); - } - } - for (var i = soundSequenceReferences.Count - 1; i >= 0; i -= 1) { if (soundSequenceReferences[i].TryGetTarget(out var soundSequence)) @@ -185,14 +185,61 @@ namespace MoonWorks.Audio } } + for (var i = autoFreeSourceVoices.Count - 1; i >= 0; i -= 1) + { + var voice = autoFreeSourceVoices[i]; + if (voice.BuffersQueued == 0) + { + Return(voice); + autoFreeSourceVoices.RemoveAt(i); + } + } + AudioTweenManager.Update(elapsedSeconds); } + /// + /// Triggers all pending operations with the given syncGroup value. + /// public void TriggerSyncGroup(uint syncGroup) { FAudio.FAudio_CommitChanges(Handle, syncGroup); } + /// + /// Obtains an appropriate source voice from the voice pool. + /// + /// The format that the voice must match. + /// A source voice with the given format. + public SourceVoice Obtain(Format format) + { + lock (StateLock) + { + return VoicePool.Obtain(format); + } + } + + internal void ReturnWhenIdle(SourceVoice voice) + { + lock (StateLock) + { + autoFreeSourceVoices.Add(voice); + } + } + + /// + /// Returns the source voice to the voice pool. + /// + /// + internal void Return(SourceVoice voice) + { + lock (StateLock) + { + voice.Reset(); + VoicePool.Return(voice); + } + } + internal void CreateTween( Voice voice, AudioTweenProperty property, @@ -252,26 +299,11 @@ namespace MoonWorks.Audio autoUpdateStreamingSoundReferences.Add(instance); } - internal void AddAutoFreeStaticSoundInstance(StaticSoundInstance instance) - { - autoFreeStaticSoundInstanceReferences.Add(instance); - } - internal void AddSoundSequenceReference(SoundSequence sequence) { soundSequenceReferences.Add(new WeakReference(sequence)); } - internal SourceVoice ObtainSourceVoice(Format format) - { - return VoicePool.Obtain(format); - } - - internal void ReturnSourceVoice(SourceVoice voice) - { - VoicePool.Return(voice); - } - protected virtual void Dispose(bool disposing) { if (!IsDisposed) @@ -306,6 +338,7 @@ namespace MoonWorks.Audio resources.Clear(); } + FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice); FAudio.FAudio_Release(Handle); IsDisposed = true; diff --git a/src/Audio/AudioTweenManager.cs b/src/Audio/AudioTweenManager.cs index 88d8203..d5b05ee 100644 --- a/src/Audio/AudioTweenManager.cs +++ b/src/Audio/AudioTweenManager.cs @@ -24,10 +24,7 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - if (voice is SendableVoice pannableVoice) - { - audioTween.StartValue = pannableVoice.Pan; - } + audioTween.StartValue = voice.Pan; break; case AudioTweenProperty.Pitch: @@ -43,10 +40,7 @@ namespace MoonWorks.Audio break; case AudioTweenProperty.Reverb: - if (voice is SendableVoice reverbableVoice) - { - audioTween.StartValue = reverbableVoice.Reverb; - } + audioTween.StartValue = voice.Reverb; break; } @@ -139,10 +133,7 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - if (audioTween.Voice is SendableVoice pannableVoice) - { - pannableVoice.Pan = value; - } + audioTween.Voice.Pan = value; break; case AudioTweenProperty.Pitch: @@ -158,10 +149,7 @@ namespace MoonWorks.Audio break; case AudioTweenProperty.Reverb: - if (audioTween.Voice is SendableVoice reverbableVoice) - { - reverbableVoice.Reverb = value; - } + audioTween.Voice.Reverb = value; break; } diff --git a/src/Audio/IReceivableVoice.cs b/src/Audio/IReceivableVoice.cs deleted file mode 100644 index a40abeb..0000000 --- a/src/Audio/IReceivableVoice.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MoonWorks.Audio -{ - public interface IReceivableVoice - { - public System.IntPtr Handle { get; } - } -} diff --git a/src/Audio/MasteringVoice.cs b/src/Audio/MasteringVoice.cs deleted file mode 100644 index e5c23ed..0000000 --- a/src/Audio/MasteringVoice.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace MoonWorks.Audio -{ - public class MasteringVoice : Voice, IReceivableVoice - { - internal static bool Create( - AudioDevice device, - uint deviceIndex, - out MasteringVoice masteringVoice - ) { - var result = FAudio.FAudio_CreateMasteringVoice( - device.Handle, - out var handle, - FAudio.FAUDIO_DEFAULT_CHANNELS, - FAudio.FAUDIO_DEFAULT_SAMPLERATE, - 0, - deviceIndex, - IntPtr.Zero - ); - - if (result == 0) - { - masteringVoice = new MasteringVoice(device, handle); - } - else - { - Logger.LogError("Failed to create mastering voice!"); - masteringVoice = null; - } - - return result == 0; - } - - internal MasteringVoice( - AudioDevice device, - IntPtr handle - ) : base(device, device.DeviceDetails.OutputFormat.Format.nChannels, 0) - { - this.handle = handle; - } - } -} diff --git a/src/Audio/SendableVoice.cs b/src/Audio/SendableVoice.cs deleted file mode 100644 index c467584..0000000 --- a/src/Audio/SendableVoice.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using EasingFunction = System.Func; - -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(); - } - } -} diff --git a/src/Audio/SoundSequence.cs b/src/Audio/SoundSequence.cs index fdb4155..12c7145 100644 --- a/src/Audio/SoundSequence.cs +++ b/src/Audio/SoundSequence.cs @@ -59,11 +59,7 @@ namespace MoonWorks.Audio lock (StateLock) { - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Handle, - ref sound.Handle, - IntPtr.Zero - ); + Submit(sound); } } } diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 15462a2..696b884 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -2,7 +2,10 @@ using System; namespace MoonWorks.Audio { - public class SourceVoice : SendableVoice + /// + /// Emits audio from submitted audio buffers. + /// + public class SourceVoice : Voice { private Format format; public Format Format => format; @@ -55,7 +58,7 @@ namespace MoonWorks.Audio FAudio.FAudio_CreateSourceVoice( device.Handle, - out var Handle, + out handle, ref fAudioFormat, FAudio.FAUDIO_VOICE_USEFILTER, FAudio.FAUDIO_DEFAULT_FREQ_RATIO, @@ -65,6 +68,11 @@ namespace MoonWorks.Audio ); } + /// + /// Starts consumption and processing of audio by the voice. + /// Delivers the result to any connected submix or mastering voice. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -75,6 +83,11 @@ namespace MoonWorks.Audio } } + /// + /// Pauses playback. + /// All source buffers that are queued on the voice and the current cursor position are preserved. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -85,6 +98,11 @@ namespace MoonWorks.Audio } } + /// + /// Stops looping the voice when it reaches the end of the current loop region. + /// If the cursor for the voice is not in a loop region, ExitLoop does nothing. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -93,6 +111,10 @@ namespace MoonWorks.Audio } } + /// + /// Stops playback and removes all pending audio buffers from the voice queue. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -104,7 +126,32 @@ namespace MoonWorks.Audio } } - public void SubmitBuffer(FAudio.FAudioBuffer buffer) + /// + /// Adds a static sound to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The sound to submit to the voice. + /// Designates that the voice will loop the submitted buffer. + public void Submit(StaticSound sound, bool loop = false) + { + if (loop) + { + sound.Buffer.LoopCount = FAudio.FAUDIO_LOOP_INFINITE; + } + else + { + sound.Buffer.LoopCount = 0; + } + + Submit(sound.Buffer); + } + + /// + /// Adds an FAudio buffer to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The buffer to submit to the voice. + public void Submit(FAudio.FAudioBuffer buffer) { FAudio.FAudioSourceVoice_SubmitSourceBuffer( Handle, @@ -113,31 +160,27 @@ namespace MoonWorks.Audio ); } - // 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) + public void Submit(StreamingSound streamingSound) { - return device.ObtainSourceVoice(format); + } - // intended for short-lived sound effects - public static SourceVoice PlayStaticSound(AudioDevice device, StaticSound sound, SubmixVoice sendVoice = null) + /// + /// Designates that this source voice will return to the voice pool once all its buffers are exhausted. + /// + public void ReturnWhenIdle() { - var voice = ObtainSourceVoice(device, sound.Format); + Device.ReturnWhenIdle(this); + } - if (sendVoice == null) - { - voice.SetOutputVoice(device.MasteringVoice); - } - else - { - voice.SetOutputVoice(sendVoice); - } - - voice.SubmitBuffer(sound.Handle); - voice.Play(); - - return voice; + /// + /// Returns this source voice to the voice pool. + /// + public void Return() + { + Stop(); + Reset(); + Device.Return(this); } protected override unsafe void Destroy() diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs index 3c92377..9c29142 100644 --- a/src/Audio/StaticSound.cs +++ b/src/Audio/StaticSound.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; @@ -7,17 +6,11 @@ namespace MoonWorks.Audio { public class StaticSound : AudioResource { - internal FAudio.FAudioBuffer Handle; + internal FAudio.FAudioBuffer Buffer; private Format format; public Format Format => format; - public uint LoopStart { get; set; } = 0; - public uint LoopLength { get; set; } = 0; - - private Stack AvailableInstances = new Stack(); - private HashSet UsedInstances = new HashSet(); - private bool OwnsBuffer; public static unsafe StaticSound LoadOgg(AudioDevice device, string filePath) @@ -253,10 +246,9 @@ namespace MoonWorks.Audio uint bufferLengthInBytes, bool ownsBuffer) : base(device) { - // TODO: should we wrap the format struct to make it nicer? this.format = format; - Handle = new FAudio.FAudioBuffer + Buffer = new FAudio.FAudioBuffer { Flags = FAudio.FAUDIO_END_OF_STREAM, pContext = IntPtr.Zero, @@ -269,66 +261,11 @@ namespace MoonWorks.Audio OwnsBuffer = ownsBuffer; } - /// - /// Gets a sound instance from the pool. - /// NOTE: If AutoFree is false, you will have to call StaticSoundInstance.Free() yourself or leak the instance! - /// - public StaticSoundInstance GetInstance(bool autoFree = true) - { - StaticSoundInstance instance; - - lock (AvailableInstances) - { - if (AvailableInstances.Count == 0) - { - AvailableInstances.Push(new StaticSoundInstance(Device, this)); - } - - instance = AvailableInstances.Pop(); - } - - instance.AutoFree = autoFree; - - lock (UsedInstances) - { - UsedInstances.Add(instance); - } - - return instance; - } - - internal void FreeInstance(StaticSoundInstance instance) - { - instance.Reset(); - - lock (UsedInstances) - { - UsedInstances.Remove(instance); - } - - lock (AvailableInstances) - { - AvailableInstances.Push(instance); - } - } - protected override unsafe void Destroy() { - foreach (var instance in UsedInstances) - { - instance.Free(); - } - - foreach (var instance in AvailableInstances) - { - instance.Dispose(); - } - - AvailableInstances.Clear(); - if (OwnsBuffer) { - NativeMemory.Free((void*) Handle.pAudioData); + NativeMemory.Free((void*) Buffer.pAudioData); } } } diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs deleted file mode 100644 index e4dff7b..0000000 --- a/src/Audio/StaticSoundInstance.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; - -namespace MoonWorks.Audio -{ - public class StaticSoundInstance : SoundInstance - { - public StaticSound Parent { get; } - - public bool Loop { get; set; } - - private SoundState _state = SoundState.Stopped; - public override SoundState State - { - get - { - FAudio.FAudioSourceVoice_GetState( - Voice, - out var state, - FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED - ); - if (state.BuffersQueued == 0) - { - StopImmediate(); - } - - return _state; - } - - protected set - { - _state = value; - } - } - - public bool AutoFree { get; internal set; } - - internal StaticSoundInstance( - AudioDevice device, - StaticSound parent - ) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond) - { - Parent = parent; - } - - public override void Play() - { - PlayUsingOperationSet(0); - } - - public override void QueueSyncPlay() - { - PlayUsingOperationSet(1); - } - - private void PlayUsingOperationSet(uint operationSet) - { - if (State == SoundState.Playing) - { - return; - } - - if (Loop) - { - Parent.Handle.LoopCount = 255; - Parent.Handle.LoopBegin = Parent.LoopStart; - Parent.Handle.LoopLength = Parent.LoopLength; - } - else - { - Parent.Handle.LoopCount = 0; - Parent.Handle.LoopBegin = 0; - Parent.Handle.LoopLength = 0; - } - - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Voice, - ref Parent.Handle, - IntPtr.Zero - ); - - FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); - State = SoundState.Playing; - - if (AutoFree) - { - Device.AddAutoFreeStaticSoundInstance(this); - } - } - - public override void Pause() - { - if (State == SoundState.Playing) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - State = SoundState.Paused; - } - } - - public override void Stop() - { - FAudio.FAudioSourceVoice_ExitLoop(Voice, 0); - State = SoundState.Stopped; - } - - public override void StopImmediate() - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); - State = SoundState.Stopped; - } - - public void Seek(uint sampleFrame) - { - if (State == SoundState.Playing) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); - } - - Parent.Handle.PlayBegin = sampleFrame; - } - - // Call this when you no longer need the sound instance. - // If AutoFree is set, this will automatically be called when the sound instance stops playing. - // If the sound isn't stopped when you call this, things might get weird! - public void Free() - { - Parent.FreeInstance(this); - } - - internal void Reset() - { - Pan = 0; - Pitch = 0; - Volume = 1; - Loop = false; - Is3D = false; - FilterType = FilterType.None; - } - } -} diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs index 338e08e..580256a 100644 --- a/src/Audio/SubmixVoice.cs +++ b/src/Audio/SubmixVoice.cs @@ -2,7 +2,7 @@ using System; namespace MoonWorks.Audio { - public class SubmixVoice : SendableVoice, IReceivableVoice + public class SubmixVoice : Voice { public SubmixVoice( AudioDevice device, diff --git a/src/Audio/Voice.cs b/src/Audio/Voice.cs index d098186..771e5fa 100644 --- a/src/Audio/Voice.cs +++ b/src/Audio/Voice.cs @@ -1,9 +1,10 @@ using System; +using System.Runtime.InteropServices; using EasingFunction = System.Func; namespace MoonWorks.Audio { - public abstract class Voice : AudioResource + public abstract unsafe class Voice : AudioResource { protected IntPtr handle; public IntPtr Handle => handle; @@ -11,6 +12,11 @@ namespace MoonWorks.Audio public uint SourceChannelCount { get; } public uint DestinationChannelCount { get; } + private SubmixVoice OutputVoice; + private ReverbEffect ReverbEffect; + + byte* pMatrixCoefficients; + public bool Is3D { get; protected set; } private float dopplerFactor; @@ -27,21 +33,6 @@ namespace MoonWorks.Audio } } - private float pitch = 0; - public float Pitch - { - get => pitch; - internal set - { - value = Math.MathHelper.Clamp(value, -1f, 1f); - if (pitch != value) - { - pitch = value; - UpdatePitch(); - } - } - } - private float volume = 1; public float Volume { @@ -57,6 +48,21 @@ namespace MoonWorks.Audio } } + private float pitch = 0; + public float Pitch + { + get => pitch; + internal set + { + value = Math.MathHelper.Clamp(value, -1f, 1f); + if (pitch != value) + { + pitch = value; + UpdatePitch(); + } + } + } + private const float MAX_FILTER_FREQUENCY = 1f; private const float MAX_FILTER_ONEOVERQ = 1.5f; @@ -150,10 +156,89 @@ namespace MoonWorks.Audio } } + 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 Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device) { SourceChannelCount = sourceChannelCount; DestinationChannelCount = destinationChannelCount; + OutputVoice = device.MasteringVoice; + nuint memsize = 4 * sourceChannelCount * destinationChannelCount; + pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize); + SetPanMatrixCoefficients(); } public void SetPitch(float targetValue) @@ -209,6 +294,159 @@ namespace MoonWorks.Audio FilterOneOverQ = targetValue; } + 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(SubmixVoice send) + { + OutputVoice = send; + + if (ReverbEffect != null) + { + SetReverbEffectChain(ReverbEffect); + } + else + { + 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 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; + } + + public void RemoveReverbEffectChain() + { + if (ReverbEffect != null) + { + ReverbEffect = null; + reverb = 0; + SetOutputVoice(OutputVoice); + } + } + + // 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; + } + } + } + } + + public virtual void Reset() + { + RemoveReverbEffectChain(); + Volume = 1; + Pan = 0; + Pitch = 0; + FilterType = FilterType.None; + FilterFrequency = 1; + FilterOneOverQ = 1; + SetOutputVoice(Device.MasteringVoice); + } + private void UpdatePitch() { float doppler; @@ -231,6 +469,7 @@ namespace MoonWorks.Audio protected unsafe override void Destroy() { + NativeMemory.Free(pMatrixCoefficients); FAudio.FAudioVoice_DestroyVoice(Handle); } } -- 2.25.1 From a0408a863c6d2e337e56c91f62c1622eaf696eec Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 1 Aug 2023 19:36:07 -0700 Subject: [PATCH 04/13] start reimplementing streaming audio on voice API --- src/Audio/AudioDevice.cs | 4 ++ src/Audio/SourceVoice.cs | 5 -- src/Audio/StreamingSound.cs | 2 +- src/Audio/StreamingVoice.cs | 92 +++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 src/Audio/StreamingVoice.cs diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index 2c39408..f198920 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -159,6 +159,8 @@ namespace MoonWorks.Audio previousTickTime = TickStopwatch.Elapsed.Ticks; float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond; + // TODO: call an Update on all active voices + for (var i = autoUpdateStreamingSoundReferences.Count - 1; i >= 0; i -= 1) { var streamingSound = autoUpdateStreamingSoundReferences[i]; @@ -206,6 +208,8 @@ namespace MoonWorks.Audio FAudio.FAudio_CommitChanges(Handle, syncGroup); } + // TODO: is pooling SourceVoices generically a good idea? there are a lot of different kinds + /// /// Obtains an appropriate source voice from the voice pool. /// diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 696b884..753abb4 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -160,11 +160,6 @@ namespace MoonWorks.Audio ); } - public void Submit(StreamingSound streamingSound) - { - - } - /// /// Designates that this source voice will return to the voice pool once all its buffers are exhausted. /// diff --git a/src/Audio/StreamingSound.cs b/src/Audio/StreamingSound.cs index 280f37a..f38e64e 100644 --- a/src/Audio/StreamingSound.cs +++ b/src/Audio/StreamingSound.cs @@ -165,7 +165,7 @@ namespace MoonWorks.Audio queuedBufferCount = 0; } - protected unsafe void AddBuffer() + public unsafe void AddBuffer() { var buffer = buffers[nextBufferIndex]; nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; diff --git a/src/Audio/StreamingVoice.cs b/src/Audio/StreamingVoice.cs new file mode 100644 index 0000000..2b1ae05 --- /dev/null +++ b/src/Audio/StreamingVoice.cs @@ -0,0 +1,92 @@ +using System; + +namespace MoonWorks.Audio +{ + public abstract class StreamingVoice : SourceVoice + { + private const int BUFFER_COUNT = 3; + private readonly IntPtr[] buffers; + private int nextBufferIndex = 0; + private uint BufferSize; + + public bool Loop { get; set; } + + public StreamingVoice(AudioDevice device, Format format, uint bufferSize) : base(device, format) + { + BufferSize = bufferSize; + } + + internal unsafe void Update() + { + lock (StateLock) + { + if (!IsDisposed) + { + if (State != SoundState.Playing) + { + return; + } + + QueueBuffers(); + } + } + } + + protected void QueueBuffers() + { + var buffersQueued = BuffersQueued; + for (int i = 0; i < BUFFER_COUNT - buffersQueued; i += 1) + { + AddBuffer(); + } + } + + protected unsafe void AddBuffer() + { + var buffer = buffers[nextBufferIndex]; + nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; + + FillBuffer( + (void*) buffer, + (int) BufferSize, + out int filledLengthInBytes, + out bool reachedEnd + ); + + if (filledLengthInBytes > 0) + { + var buf = new FAudio.FAudioBuffer + { + AudioBytes = (uint) filledLengthInBytes, + pAudioData = buffer, + PlayLength = ( + (uint) (filledLengthInBytes / + Format.Channels / + (uint) (Format.BitsPerSample / 8)) + ) + }; + + Submit(buf); + } + + if (reachedEnd) + { + /* We have reached the end of the data, what do we do? */ + if (Loop) + { + SeekStart(); + AddBuffer(); + } + } + } + + protected unsafe abstract void FillBuffer( + void* buffer, + int bufferLengthInBytes, /* in bytes */ + out int filledLengthInBytes, /* in bytes */ + out bool reachedEnd + ); + + protected abstract void SeekStart(); + } +} -- 2.25.1 From eebbaeb6ae4b5687f03d08c8565fce65be1b83c8 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 14:10:48 -0700 Subject: [PATCH 05/13] new StreamingVoice and AudioData system --- src/Audio/AudioData.cs | 36 ++ src/Audio/AudioDataOgg.cs | 96 ++++ .../{StreamingSoundQoa.cs => AudioDataQoa.cs} | 133 ++--- src/Audio/AudioDevice.cs | 88 +-- src/Audio/IPoolable.cs | 7 + src/Audio/ReverbEffect.cs | 2 +- src/Audio/SoundInstance.cs | 508 ------------------ src/Audio/SoundSequence.cs | 21 +- src/Audio/SourceVoice.cs | 53 +- src/Audio/SourceVoicePool.cs | 16 +- src/Audio/StaticVoice.cs | 54 ++ src/Audio/StreamingSound.cs | 239 -------- src/Audio/StreamingSoundOgg.cs | 113 ---- src/Audio/StreamingSoundSeekable.cs | 40 -- src/Audio/StreamingVoice.cs | 76 ++- 15 files changed, 360 insertions(+), 1122 deletions(-) create mode 100644 src/Audio/AudioData.cs create mode 100644 src/Audio/AudioDataOgg.cs rename src/Audio/{StreamingSoundQoa.cs => AudioDataQoa.cs} (54%) create mode 100644 src/Audio/IPoolable.cs delete mode 100644 src/Audio/SoundInstance.cs create mode 100644 src/Audio/StaticVoice.cs delete mode 100644 src/Audio/StreamingSound.cs delete mode 100644 src/Audio/StreamingSoundOgg.cs delete mode 100644 src/Audio/StreamingSoundSeekable.cs diff --git a/src/Audio/AudioData.cs b/src/Audio/AudioData.cs new file mode 100644 index 0000000..9998179 --- /dev/null +++ b/src/Audio/AudioData.cs @@ -0,0 +1,36 @@ +using System; + +namespace MoonWorks.Audio +{ + public abstract class AudioData + { + public Format Format { get; protected set; } + public abstract uint DecodeBufferSize { get; } + + public abstract bool Loaded { get; } + + /// + /// Loads the raw audio data into memory. + /// + public abstract void Load(); + + /// + /// Seeks to the given sample frame. + /// + public abstract void Seek(uint sampleFrame); + + /// + /// Attempts to decodes data of length bufferLengthInBytes into the provided buffer. + /// + /// The buffer that decoded bytes will be placed into. + /// Requested length of decoded audio data. + /// How much data was actually filled in by the decode. + /// Whether the end of the data was reached on this decode. + public abstract unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd); + + /// + /// Unloads the raw audio data from memory. + /// + public abstract void Unload(); + } +} diff --git a/src/Audio/AudioDataOgg.cs b/src/Audio/AudioDataOgg.cs new file mode 100644 index 0000000..dbb988d --- /dev/null +++ b/src/Audio/AudioDataOgg.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + public class AudioDataOgg : AudioData + { + private IntPtr FileDataPtr = IntPtr.Zero; + private IntPtr VorbisHandle = IntPtr.Zero; + + private string FilePath; + + public override bool Loaded => VorbisHandle != IntPtr.Zero; + public override uint DecodeBufferSize => 32768; + + public AudioDataOgg(string filePath) + { + FilePath = filePath; + + var handle = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero); + + if (error != 0) + { + throw new AudioLoadException("Error loading file!"); + } + + var info = FAudio.stb_vorbis_get_info(handle); + + Format = new Format + { + Tag = FormatTag.IEEE_FLOAT, + BitsPerSample = 32, + Channels = (ushort) info.channels, + SampleRate = info.sample_rate + }; + + FAudio.stb_vorbis_close(handle); + } + + public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd) + { + var lengthInFloats = bufferLengthInBytes / sizeof(float); + + /* NOTE: this function returns samples per channel, not total samples */ + var samples = FAudio.stb_vorbis_get_samples_float_interleaved( + VorbisHandle, + Format.Channels, + (IntPtr) buffer, + lengthInFloats + ); + + var sampleCount = samples * Format.Channels; + reachedEnd = sampleCount < lengthInFloats; + filledLengthInBytes = sampleCount * sizeof(float); + } + + public override unsafe void Load() + { + if (!Loaded) + { + var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); + FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length); + var fileDataSpan = new Span((void*) FileDataPtr, (int) fileStream.Length); + fileStream.ReadExactly(fileDataSpan); + fileStream.Close(); + + VorbisHandle = FAudio.stb_vorbis_open_memory(FileDataPtr, fileDataSpan.Length, out int error, IntPtr.Zero); + if (error != 0) + { + NativeMemory.Free((void*) FileDataPtr); + Logger.LogError("Error opening OGG file!"); + Logger.LogError("Error: " + error); + throw new AudioLoadException("Error opening OGG file!"); + } + } + } + + public override void Seek(uint sampleFrame) + { + FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame); + } + + public override unsafe void Unload() + { + if (Loaded) + { + FAudio.stb_vorbis_close(VorbisHandle); + NativeMemory.Free((void*) FileDataPtr); + + VorbisHandle = IntPtr.Zero; + FileDataPtr = IntPtr.Zero; + } + } + } +} diff --git a/src/Audio/StreamingSoundQoa.cs b/src/Audio/AudioDataQoa.cs similarity index 54% rename from src/Audio/StreamingSoundQoa.cs rename to src/Audio/AudioDataQoa.cs index 6c4340d..5322095 100644 --- a/src/Audio/StreamingSoundQoa.cs +++ b/src/Audio/AudioDataQoa.cs @@ -1,37 +1,28 @@ -using System; +using System; using System.IO; using System.Runtime.InteropServices; namespace MoonWorks.Audio { - public class StreamingSoundQoa : StreamingSoundSeekable + public class AudioDataQoa : AudioData { private IntPtr QoaHandle = IntPtr.Zero; private IntPtr FileDataPtr = IntPtr.Zero; - uint Channels; - uint SamplesPerChannelPerFrame; - uint TotalSamplesPerChannel; - - public override bool Loaded => QoaHandle != IntPtr.Zero; private string FilePath; private const uint QOA_MAGIC = 0x716f6166; /* 'qoaf' */ - private static unsafe UInt64 ReverseEndianness(UInt64 value) - { - byte* bytes = (byte*) &value; + public override bool Loaded => QoaHandle != IntPtr.Zero; - return - ((UInt64)(bytes[0]) << 56) | ((UInt64)(bytes[1]) << 48) | - ((UInt64)(bytes[2]) << 40) | ((UInt64)(bytes[3]) << 32) | - ((UInt64)(bytes[4]) << 24) | ((UInt64)(bytes[5]) << 16) | - ((UInt64)(bytes[6]) << 8) | ((UInt64)(bytes[7]) << 0); - } + private uint decodeBufferSize; + public override uint DecodeBufferSize => decodeBufferSize; - public unsafe static StreamingSoundQoa Create(AudioDevice device, string filePath) + public AudioDataQoa(string filePath) { - using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + FilePath = filePath; + + using var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); using var reader = new BinaryReader(stream); UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64()); @@ -51,37 +42,47 @@ namespace MoonWorks.Audio uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF); uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF); - return new StreamingSoundQoa( - device, - filePath, - channels, - samplerate, - samplesPerChannelPerFrame, - totalSamplesPerChannel - ); + Format = new Format + { + Tag = FormatTag.PCM, + BitsPerSample = 16, + Channels = (ushort) channels, + SampleRate = samplerate + }; + + decodeBufferSize = channels * samplesPerChannelPerFrame * sizeof(short); } - internal unsafe StreamingSoundQoa( - AudioDevice device, - string filePath, - uint channels, - uint samplesPerSecond, - uint samplesPerChannelPerFrame, - uint totalSamplesPerChannel - ) : base( - device, - 1, - 16, - (ushort) (2 * channels), - (ushort) channels, - samplesPerSecond, - samplesPerChannelPerFrame * channels * sizeof(short), - true - ) { - Channels = channels; - SamplesPerChannelPerFrame = samplesPerChannelPerFrame; - TotalSamplesPerChannel = totalSamplesPerChannel; - FilePath = filePath; + public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd) + { + var lengthInShorts = bufferLengthInBytes / sizeof(short); + + // NOTE: this function returns samples per channel! + var samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer); + + var sampleCount = samples * Format.Channels; + reachedEnd = sampleCount < lengthInShorts; + filledLengthInBytes = (int) (sampleCount * sizeof(short)); + } + + public override unsafe void Load() + { + if (!Loaded) + { + var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); + FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length); + var fileDataSpan = new Span((void*) FileDataPtr, (int) fileStream.Length); + fileStream.ReadExactly(fileDataSpan); + fileStream.Close(); + + QoaHandle = FAudio.qoa_open_from_memory((char*) FileDataPtr, (uint) fileDataSpan.Length, 0); + if (QoaHandle == IntPtr.Zero) + { + NativeMemory.Free((void*) FileDataPtr); + Logger.LogError("Error opening QOA file!"); + throw new AudioLoadException("Error opening QOA file!"); + } + } } public override void Seek(uint sampleFrame) @@ -89,23 +90,6 @@ namespace MoonWorks.Audio FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame); } - public override unsafe void Load() - { - var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); - FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length); - var fileDataSpan = new Span((void*) FileDataPtr, (int) fileStream.Length); - fileStream.ReadExactly(fileDataSpan); - fileStream.Close(); - - QoaHandle = FAudio.qoa_open_from_memory((char*) FileDataPtr, (uint) fileDataSpan.Length, 0); - if (QoaHandle == IntPtr.Zero) - { - NativeMemory.Free((void*) FileDataPtr); - Logger.LogError("Error opening QOA file!"); - throw new AudioLoadException("Error opening QOA file!"); - } - } - public override unsafe void Unload() { if (Loaded) @@ -118,20 +102,15 @@ namespace MoonWorks.Audio } } - protected override unsafe void FillBuffer( - void* buffer, - int bufferLengthInBytes, - out int filledLengthInBytes, - out bool reachedEnd - ) { - var lengthInShorts = bufferLengthInBytes / sizeof(short); + private static unsafe UInt64 ReverseEndianness(UInt64 value) + { + byte* bytes = (byte*) &value; - // NOTE: this function returns samples per channel! - var samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer); - - var sampleCount = samples * Channels; - reachedEnd = sampleCount < lengthInShorts; - filledLengthInBytes = (int) (sampleCount * sizeof(short)); + return + ((UInt64)(bytes[0]) << 56) | ((UInt64)(bytes[1]) << 48) | + ((UInt64)(bytes[2]) << 40) | ((UInt64)(bytes[3]) << 32) | + ((UInt64)(bytes[4]) << 24) | ((UInt64)(bytes[5]) << 16) | + ((UInt64)(bytes[6]) << 8) | ((UInt64)(bytes[7]) << 0); } } } diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index f198920..ce3f76e 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -22,13 +22,12 @@ namespace MoonWorks.Audio public float SpeedOfSound = 343.5f; private readonly HashSet resources = new HashSet(); - private readonly List autoUpdateStreamingSoundReferences = new List(); - private readonly List> soundSequenceReferences = new List>(); - private readonly List autoFreeSourceVoices = new List(); + private readonly HashSet activeSourceVoices = new HashSet(); private AudioTweenManager AudioTweenManager; private SourceVoicePool VoicePool; + private List VoicesToReturn = new List(); private const int Step = 200; private TimeSpan UpdateInterval; @@ -106,7 +105,7 @@ namespace MoonWorks.Audio return; } - fauxMasteringVoice = new SubmixVoice(this, FAudio.FAUDIO_DEFAULT_CHANNELS, FAudio.FAUDIO_DEFAULT_SAMPLERATE); + fauxMasteringVoice = new SubmixVoice(this, DeviceDetails.OutputFormat.Format.nChannels, DeviceDetails.OutputFormat.Format.nSamplesPerSec); /* Init 3D Audio */ @@ -159,45 +158,21 @@ namespace MoonWorks.Audio previousTickTime = TickStopwatch.Elapsed.Ticks; float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond; - // TODO: call an Update on all active voices - - for (var i = autoUpdateStreamingSoundReferences.Count - 1; i >= 0; i -= 1) - { - var streamingSound = autoUpdateStreamingSoundReferences[i]; - - if (streamingSound.Loaded) - { - streamingSound.Update(); - } - else - { - autoUpdateStreamingSoundReferences.RemoveAt(i); - } - } - - for (var i = soundSequenceReferences.Count - 1; i >= 0; i -= 1) - { - if (soundSequenceReferences[i].TryGetTarget(out var soundSequence)) - { - soundSequence.OnUpdate(); - } - else - { - soundSequenceReferences.RemoveAt(i); - } - } - - for (var i = autoFreeSourceVoices.Count - 1; i >= 0; i -= 1) - { - var voice = autoFreeSourceVoices[i]; - if (voice.BuffersQueued == 0) - { - Return(voice); - autoFreeSourceVoices.RemoveAt(i); - } - } - AudioTweenManager.Update(elapsedSeconds); + + foreach (var voice in activeSourceVoices) + { + voice.Update(); + } + + foreach (var voice in VoicesToReturn) + { + voice.Reset(); + activeSourceVoices.Remove(voice); + VoicePool.Return(voice); + } + + VoicesToReturn.Clear(); } /// @@ -208,26 +183,18 @@ namespace MoonWorks.Audio FAudio.FAudio_CommitChanges(Handle, syncGroup); } - // TODO: is pooling SourceVoices generically a good idea? there are a lot of different kinds - /// /// Obtains an appropriate source voice from the voice pool. /// /// The format that the voice must match. /// A source voice with the given format. - public SourceVoice Obtain(Format format) + public T Obtain(Format format) where T : SourceVoice, IPoolable { lock (StateLock) { - return VoicePool.Obtain(format); - } - } - - internal void ReturnWhenIdle(SourceVoice voice) - { - lock (StateLock) - { - autoFreeSourceVoices.Add(voice); + var voice = VoicePool.Obtain(format); + activeSourceVoices.Add(voice); + return voice; } } @@ -239,8 +206,7 @@ namespace MoonWorks.Audio { lock (StateLock) { - voice.Reset(); - VoicePool.Return(voice); + VoicesToReturn.Add(voice); } } @@ -298,16 +264,6 @@ namespace MoonWorks.Audio } } - internal void AddAutoUpdateStreamingSoundInstance(StreamingSound instance) - { - autoUpdateStreamingSoundReferences.Add(instance); - } - - internal void AddSoundSequenceReference(SoundSequence sequence) - { - soundSequenceReferences.Add(new WeakReference(sequence)); - } - protected virtual void Dispose(bool disposing) { if (!IsDisposed) diff --git a/src/Audio/IPoolable.cs b/src/Audio/IPoolable.cs new file mode 100644 index 0000000..2e0bf92 --- /dev/null +++ b/src/Audio/IPoolable.cs @@ -0,0 +1,7 @@ +namespace MoonWorks.Audio +{ + public interface IPoolable + { + static abstract T Create(AudioDevice device, Format format); + } +} diff --git a/src/Audio/ReverbEffect.cs b/src/Audio/ReverbEffect.cs index eac7cda..064ab72 100644 --- a/src/Audio/ReverbEffect.cs +++ b/src/Audio/ReverbEffect.cs @@ -16,7 +16,7 @@ namespace MoonWorks.Audio var descriptor = new FAudio.FAudioEffectDescriptor(); descriptor.InitialState = 1; - descriptor.OutputChannels = Device.DeviceDetails.OutputFormat.Format.nChannels; + descriptor.OutputChannels = 1; descriptor.pEffect = reverb; chain.EffectCount = 1; diff --git a/src/Audio/SoundInstance.cs b/src/Audio/SoundInstance.cs deleted file mode 100644 index 11d320e..0000000 --- a/src/Audio/SoundInstance.cs +++ /dev/null @@ -1,508 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using EasingFunction = System.Func; - -namespace MoonWorks.Audio -{ - public abstract class SoundInstance : AudioResource - { - internal IntPtr Voice; - - private FAudio.FAudioWaveFormatEx format; - public FAudio.FAudioWaveFormatEx Format => format; - - protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; - - private ReverbEffect ReverbEffect; - private FAudio.FAudioVoiceSends ReverbSends; - - public bool Is3D { get; protected set; } - - public virtual SoundState State { get; protected set; } - - private 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( - Voice, - Device.MasteringVoice, - dspSettings.SrcChannelCount, - dspSettings.DstChannelCount, - dspSettings.pMatrixCoefficients, - 0 - ); - } - } - } - - private float pitch = 0; - public float Pitch - { - get => pitch; - internal set - { - value = Math.MathHelper.Clamp(value, -1f, 1f); - if (pitch != value) - { - pitch = value; - UpdatePitch(); - } - } - } - - private float volume = 1; - public float Volume - { - get => volume; - internal set - { - value = Math.MathHelper.Max(0, value); - if (volume != value) - { - volume = value; - FAudio.FAudioVoice_SetVolume(Voice, volume, 0); - } - } - } - - private const float MAX_FILTER_FREQUENCY = 1f; - private const float MAX_FILTER_ONEOVERQ = 1.5f; - - private FAudio.FAudioFilterParameters filterParameters = new FAudio.FAudioFilterParameters - { - Type = FAudio.FAudioFilterType.FAudioLowPassFilter, - Frequency = 1f, - OneOverQ = 1f - }; - - public float FilterFrequency - { - get => filterParameters.Frequency; - internal set - { - value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY); - if (filterParameters.Frequency != value) - { - filterParameters.Frequency = value; - - FAudio.FAudioVoice_SetFilterParameters( - Voice, - ref filterParameters, - 0 - ); - } - } - } - - public float FilterOneOverQ - { - get => filterParameters.OneOverQ; - internal set - { - value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ); - if (filterParameters.OneOverQ != value) - { - filterParameters.OneOverQ = value; - - FAudio.FAudioVoice_SetFilterParameters( - Voice, - ref filterParameters, - 0 - ); - } - } - } - - private FilterType filterType; - public FilterType FilterType - { - get => filterType; - set - { - if (filterType != value) - { - filterType = value; - - switch (filterType) - { - case FilterType.None: - filterParameters = new FAudio.FAudioFilterParameters - { - Type = FAudio.FAudioFilterType.FAudioLowPassFilter, - Frequency = 1f, - OneOverQ = 1f - }; - break; - - case FilterType.LowPass: - filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter; - filterParameters.Frequency = 1f; - break; - - case FilterType.BandPass: - filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter; - break; - - case FilterType.HighPass: - filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter; - filterParameters.Frequency = 0f; - break; - } - - FAudio.FAudioVoice_SetFilterParameters( - Voice, - ref filterParameters, - 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*) dspSettings.pMatrixCoefficients; - outputMatrix[0] = reverb; - if (dspSettings.SrcChannelCount == 2) - { - outputMatrix[1] = reverb; - } - - FAudio.FAudioVoice_SetOutputMatrix( - Voice, - ReverbEffect.Voice, - 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 unsafe SoundInstance( - AudioDevice device, - ushort formatTag, - ushort bitsPerSample, - ushort blockAlign, - ushort channels, - uint samplesPerSecond - ) : base(device) - { - format = new FAudio.FAudioWaveFormatEx - { - wFormatTag = formatTag, - wBitsPerSample = bitsPerSample, - nChannels = channels, - nBlockAlign = blockAlign, - nSamplesPerSec = samplesPerSecond, - nAvgBytesPerSec = blockAlign * samplesPerSecond - }; - - FAudio.FAudio_CreateSourceVoice( - Device.Handle, - out Voice, - ref format, - FAudio.FAUDIO_VOICE_USEFILTER, - FAudio.FAUDIO_DEFAULT_FREQ_RATIO, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero - ); - - if (Voice == IntPtr.Zero) - { - Logger.LogError("SoundInstance failed to initialize!"); - return; - } - - InitDSPSettings(Format.nChannels); - - State = SoundState.Stopped; - } - - public void Apply3D(AudioListener listener, AudioEmitter emitter) - { - Is3D = true; - - emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar; - emitter.emitterData.ChannelCount = dspSettings.SrcChannelCount; - - FAudio.F3DAudioCalculate( - Device.Handle3D, - ref listener.listenerData, - ref emitter.emitterData, - FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER, - ref dspSettings - ); - - UpdatePitch(); - FAudio.FAudioVoice_SetOutputMatrix( - Voice, - Device.MasteringVoice, - dspSettings.SrcChannelCount, - dspSettings.DstChannelCount, - dspSettings.pMatrixCoefficients, - 0 - ); - } - - public unsafe void ApplyReverb(ReverbEffect reverbEffect) - { - ReverbSends = new FAudio.FAudioVoiceSends(); - ReverbSends.SendCount = 2; - ReverbSends.pSends = (nint) NativeMemory.Alloc((nuint) (2 * Marshal.SizeOf())); - - FAudio.FAudioSendDescriptor* sendDesc = (FAudio.FAudioSendDescriptor*) ReverbSends.pSends; - sendDesc[0].Flags = 0; - sendDesc[0].pOutputVoice = Device.MasteringVoice; - sendDesc[1].Flags = 0; - sendDesc[1].pOutputVoice = reverbEffect.Voice; - - FAudio.FAudioVoice_SetOutputVoices( - Voice, - ref ReverbSends - ); - - ReverbEffect = reverbEffect; - } - - 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) - { - Pitch = targetValue; - Device.ClearTweens(this, AudioTweenProperty.Pitch); - } - - public void SetPitch(float targetValue, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pan, targetValue, duration, 0); - } - - public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pan, targetValue, duration, delayTime); - } - - public void SetVolume(float targetValue) - { - Volume = targetValue; - Device.ClearTweens(this, AudioTweenProperty.Volume); - } - - public void SetVolume(float targetValue, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0); - } - - public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime); - } - - public void SetFilterFrequency(float targetValue) - { - FilterFrequency = targetValue; - Device.ClearTweens(this, AudioTweenProperty.FilterFrequency); - } - - public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0); - } - - public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime); - } - - public void SetFilterOneOverQ(float 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 abstract void Play(); - public abstract void QueueSyncPlay(); - public abstract void Pause(); - public abstract void Stop(); - public abstract void StopImmediate(); - - 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() - { - float doppler; - float dopplerScale = Device.DopplerScale; - if (!Is3D || dopplerScale == 0.0f) - { - doppler = 1.0f; - } - else - { - doppler = dspSettings.DopplerFactor * dopplerScale; - } - - FAudio.FAudioSourceVoice_SetFrequencyRatio( - Voice, - (float) System.Math.Pow(2.0, pitch) * doppler, - 0 - ); - } - - // 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() - { - StopImmediate(); - FAudio.FAudioVoice_DestroyVoice(Voice); - NativeMemory.Free((void*) dspSettings.pMatrixCoefficients); - - if (ReverbEffect != null) - { - NativeMemory.Free((void*) ReverbSends.pSends); - } - } - } -} diff --git a/src/Audio/SoundSequence.cs b/src/Audio/SoundSequence.cs index 12c7145..dece5df 100644 --- a/src/Audio/SoundSequence.cs +++ b/src/Audio/SoundSequence.cs @@ -1,5 +1,3 @@ -using System; - namespace MoonWorks.Audio { // NOTE: all sounds played with a SoundSequence must have the same audio format! @@ -11,17 +9,15 @@ namespace MoonWorks.Audio public SoundSequence(AudioDevice device, Format format) : base(device, format) { - device.AddSoundSequenceReference(this); - OnUpdate += Update; + } public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.Format) { - device.AddSoundSequenceReference(this); - OnUpdate += Update; + } - private void Update() + public override void Update() { lock (StateLock) { @@ -30,14 +26,7 @@ namespace MoonWorks.Audio if (NeedSoundThreshold > 0) { - FAudio.FAudioSourceVoice_GetState( - Handle, - out var state, - FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED - ); - - var queuedBufferCount = state.BuffersQueued; - for (int i = 0; i < NeedSoundThreshold - queuedBufferCount; i += 1) + for (int i = 0; i < NeedSoundThreshold - BuffersQueued; i += 1) { if (OnSoundNeeded != null) { @@ -59,7 +48,7 @@ namespace MoonWorks.Audio lock (StateLock) { - Submit(sound); + Submit(sound.Buffer); } } } diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 753abb4..69fe34c 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -45,9 +45,6 @@ namespace MoonWorks.Audio } } - public delegate void OnUpdateFunc(); - public OnUpdateFunc OnUpdate; // called by AudioDevice thread - public SourceVoice( AudioDevice device, Format format @@ -126,26 +123,6 @@ namespace MoonWorks.Audio } } - /// - /// Adds a static sound to the voice queue. - /// The voice processes and plays back the buffers in its queue in the order that they were submitted. - /// - /// The sound to submit to the voice. - /// Designates that the voice will loop the submitted buffer. - public void Submit(StaticSound sound, bool loop = false) - { - if (loop) - { - sound.Buffer.LoopCount = FAudio.FAUDIO_LOOP_INFINITE; - } - else - { - sound.Buffer.LoopCount = 0; - } - - Submit(sound.Buffer); - } - /// /// Adds an FAudio buffer to the voice queue. /// The voice processes and plays back the buffers in its queue in the order that they were submitted. @@ -153,31 +130,31 @@ namespace MoonWorks.Audio /// The buffer to submit to the voice. public void Submit(FAudio.FAudioBuffer buffer) { - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Handle, - ref buffer, - IntPtr.Zero - ); + lock (StateLock) + { + FAudio.FAudioSourceVoice_SubmitSourceBuffer( + Handle, + ref buffer, + IntPtr.Zero + ); + } } /// - /// Designates that this source voice will return to the voice pool once all its buffers are exhausted. - /// - public void ReturnWhenIdle() - { - Device.ReturnWhenIdle(this); - } - - /// - /// Returns this source voice to the voice pool. + /// Specifies that this source voice can be returned to the voice pool. + /// Holding on to the reference after calling this will cause problems! /// public void Return() { Stop(); - Reset(); Device.Return(this); } + /// + /// Called automatically by AudioDevice in the audio thread. + /// + public virtual void Update() { } + protected override unsafe void Destroy() { Stop(); diff --git a/src/Audio/SourceVoicePool.cs b/src/Audio/SourceVoicePool.cs index 3299a1f..6c1ef84 100644 --- a/src/Audio/SourceVoicePool.cs +++ b/src/Audio/SourceVoicePool.cs @@ -6,33 +6,33 @@ namespace MoonWorks.Audio { private AudioDevice Device; - Dictionary> VoiceLists = new Dictionary>(); + Dictionary<(System.Type, Format), Queue> VoiceLists = new Dictionary<(System.Type, Format), Queue>(); public SourceVoicePool(AudioDevice device) { Device = device; } - public SourceVoice Obtain(Format format) + public T Obtain(Format format) where T : SourceVoice, IPoolable { - if (!VoiceLists.ContainsKey(format)) + if (!VoiceLists.ContainsKey((typeof(T), format))) { - VoiceLists.Add(format, new Queue()); + VoiceLists.Add((typeof(T), format), new Queue()); } - var list = VoiceLists[format]; + var list = VoiceLists[(typeof(T), format)]; if (list.Count == 0) { - list.Enqueue(new SourceVoice(Device, format)); + list.Enqueue(T.Create(Device, format)); } - return list.Dequeue(); + return (T) list.Dequeue(); } public void Return(SourceVoice voice) { - var list = VoiceLists[voice.Format]; + var list = VoiceLists[(voice.GetType(), voice.Format)]; list.Enqueue(voice); } } diff --git a/src/Audio/StaticVoice.cs b/src/Audio/StaticVoice.cs new file mode 100644 index 0000000..57600df --- /dev/null +++ b/src/Audio/StaticVoice.cs @@ -0,0 +1,54 @@ +namespace MoonWorks.Audio +{ + public class StaticVoice : SourceVoice, IPoolable + { + /// + /// Indicates if the voice should return to the voice pool when the voice is idle. + /// If you set this and then hold on to the voice reference there will be problems! + /// + public bool DeactivateWhenIdle { get; set; } + + public static StaticVoice Create(AudioDevice device, Format format) + { + return new StaticVoice(device, format); + } + + public StaticVoice(AudioDevice device, Format format) : base(device, format) + { + } + + public override void Update() + { + lock (StateLock) + { + if (DeactivateWhenIdle) + { + if (BuffersQueued == 0) + { + Return(); + } + } + } + } + + /// + /// Adds a static sound to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The sound to submit to the voice. + /// Designates that the voice will loop the submitted buffer. + public void Submit(StaticSound sound, bool loop = false) + { + if (loop) + { + sound.Buffer.LoopCount = FAudio.FAUDIO_LOOP_INFINITE; + } + else + { + sound.Buffer.LoopCount = 0; + } + + Submit(sound.Buffer); + } + } +} diff --git a/src/Audio/StreamingSound.cs b/src/Audio/StreamingSound.cs deleted file mode 100644 index f38e64e..0000000 --- a/src/Audio/StreamingSound.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace MoonWorks.Audio -{ - /// - /// For streaming long playback. - /// Must be extended with a decoder routine called by FillBuffer. - /// See StreamingSoundOgg for an example. - /// - public abstract class StreamingSound : SoundInstance - { - // Are we actively consuming buffers? - protected bool ConsumingBuffers = false; - - private const int BUFFER_COUNT = 3; - private nuint BufferSize; - private readonly IntPtr[] buffers; - private int nextBufferIndex = 0; - private uint queuedBufferCount = 0; - - private readonly object StateLock = new object(); - - public bool AutoUpdate { get; } - - public abstract bool Loaded { get; } - - public unsafe StreamingSound( - AudioDevice device, - ushort formatTag, - ushort bitsPerSample, - ushort blockAlign, - ushort channels, - uint samplesPerSecond, - uint bufferSize, - bool autoUpdate // should the AudioDevice thread automatically update this sound? - ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) - { - BufferSize = bufferSize; - - buffers = new IntPtr[BUFFER_COUNT]; - for (int i = 0; i < BUFFER_COUNT; i += 1) - { - buffers[i] = (IntPtr) NativeMemory.Alloc(bufferSize); - } - - AutoUpdate = autoUpdate; - } - - public override void Play() - { - PlayUsingOperationSet(0); - } - - public override void QueueSyncPlay() - { - PlayUsingOperationSet(1); - } - - private void PlayUsingOperationSet(uint operationSet) - { - lock (StateLock) - { - if (!Loaded) - { - Logger.LogError("Cannot play StreamingSound before calling Load!"); - return; - } - - if (State == SoundState.Playing) - { - return; - } - - State = SoundState.Playing; - - ConsumingBuffers = true; - if (AutoUpdate) - { - Device.AddAutoUpdateStreamingSoundInstance(this); - } - - QueueBuffers(); - FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); - } - } - - public override void Pause() - { - lock (StateLock) - { - if (State == SoundState.Playing) - { - ConsumingBuffers = false; - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - State = SoundState.Paused; - } - } - } - - public override void Stop() - { - lock (StateLock) - { - ConsumingBuffers = false; - State = SoundState.Stopped; - } - } - - public override void StopImmediate() - { - lock (StateLock) - { - ConsumingBuffers = false; - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); - ClearBuffers(); - - State = SoundState.Stopped; - } - } - - internal unsafe void Update() - { - lock (StateLock) - { - if (!IsDisposed) - { - if (State != SoundState.Playing) - { - return; - } - - QueueBuffers(); - } - } - } - - protected void QueueBuffers() - { - FAudio.FAudioSourceVoice_GetState( - Voice, - out var state, - FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED - ); - - queuedBufferCount = state.BuffersQueued; - - if (ConsumingBuffers) - { - for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1) - { - AddBuffer(); - } - } - else if (queuedBufferCount == 0) - { - Stop(); - } - } - - protected unsafe void ClearBuffers() - { - nextBufferIndex = 0; - queuedBufferCount = 0; - } - - public unsafe void AddBuffer() - { - var buffer = buffers[nextBufferIndex]; - nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; - - FillBuffer( - (void*) buffer, - (int) BufferSize, - out int filledLengthInBytes, - out bool reachedEnd - ); - - if (filledLengthInBytes > 0) - { - FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer - { - AudioBytes = (uint) filledLengthInBytes, - pAudioData = (IntPtr) buffer, - PlayLength = ( - (uint) (filledLengthInBytes / - Format.nChannels / - (uint) (Format.wBitsPerSample / 8)) - ) - }; - - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Voice, - ref buf, - IntPtr.Zero - ); - - queuedBufferCount += 1; - } - - if (reachedEnd) - { - /* We have reached the end of the data, what do we do? */ - ConsumingBuffers = false; - OnReachedEnd(); - } - } - - public abstract void Load(); - public abstract void Unload(); - - protected unsafe abstract void FillBuffer( - void* buffer, - int bufferLengthInBytes, /* in bytes */ - out int filledLengthInBytes, /* in bytes */ - out bool reachedEnd - ); - - protected abstract void OnReachedEnd(); - - protected unsafe override void Destroy() - { - lock (StateLock) - { - if (!IsDisposed) - { - StopImmediate(); - Unload(); - - for (int i = 0; i < BUFFER_COUNT; i += 1) - { - NativeMemory.Free((void*) buffers[i]); - } - } - } - } - } -} diff --git a/src/Audio/StreamingSoundOgg.cs b/src/Audio/StreamingSoundOgg.cs deleted file mode 100644 index 3f02c87..0000000 --- a/src/Audio/StreamingSoundOgg.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace MoonWorks.Audio -{ - public class StreamingSoundOgg : StreamingSoundSeekable - { - private IntPtr FileDataPtr = IntPtr.Zero; - private IntPtr VorbisHandle = IntPtr.Zero; - private FAudio.stb_vorbis_info Info; - - public override bool Loaded => VorbisHandle != IntPtr.Zero; - private string FilePath; - - public unsafe static StreamingSoundOgg Create(AudioDevice device, string filePath) - { - var handle = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero); - if (error != 0) - { - Logger.LogError("Error: " + error); - throw new AudioLoadException("Error opening ogg file!"); - } - - var info = FAudio.stb_vorbis_get_info(handle); - - var streamingSound = new StreamingSoundOgg( - device, - filePath, - info - ); - - FAudio.stb_vorbis_close(handle); - - return streamingSound; - } - - internal unsafe StreamingSoundOgg( - AudioDevice device, - string filePath, - FAudio.stb_vorbis_info info, - uint bufferSize = 32768 - ) : base( - device, - 3, /* float type */ - 32, /* size of float */ - (ushort) (4 * info.channels), - (ushort) info.channels, - info.sample_rate, - bufferSize, - true - ) { - Info = info; - FilePath = filePath; - } - - public override void Seek(uint sampleFrame) - { - FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame); - } - - public override unsafe void Load() - { - var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); - FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length); - var fileDataSpan = new Span((void*) FileDataPtr, (int) fileStream.Length); - fileStream.ReadExactly(fileDataSpan); - fileStream.Close(); - - VorbisHandle = FAudio.stb_vorbis_open_memory(FileDataPtr, fileDataSpan.Length, out int error, IntPtr.Zero); - if (error != 0) - { - NativeMemory.Free((void*) FileDataPtr); - Logger.LogError("Error opening OGG file!"); - Logger.LogError("Error: " + error); - throw new AudioLoadException("Error opening OGG file!"); - } - } - - public override unsafe void Unload() - { - if (Loaded) - { - FAudio.stb_vorbis_close(VorbisHandle); - NativeMemory.Free((void*) FileDataPtr); - - VorbisHandle = IntPtr.Zero; - FileDataPtr = IntPtr.Zero; - } - } - - protected unsafe override void FillBuffer( - void* buffer, - int bufferLengthInBytes, - out int filledLengthInBytes, - out bool reachedEnd - ) { - var lengthInFloats = bufferLengthInBytes / sizeof(float); - - /* NOTE: this function returns samples per channel, not total samples */ - var samples = FAudio.stb_vorbis_get_samples_float_interleaved( - VorbisHandle, - Info.channels, - (IntPtr) buffer, - lengthInFloats - ); - - var sampleCount = samples * Info.channels; - reachedEnd = sampleCount < lengthInFloats; - filledLengthInBytes = sampleCount * sizeof(float); - } - } -} diff --git a/src/Audio/StreamingSoundSeekable.cs b/src/Audio/StreamingSoundSeekable.cs deleted file mode 100644 index 2bc4905..0000000 --- a/src/Audio/StreamingSoundSeekable.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace MoonWorks.Audio -{ - public abstract class StreamingSoundSeekable : StreamingSound - { - public bool Loop { get; set; } - - protected StreamingSoundSeekable( - AudioDevice device, - ushort formatTag, - ushort bitsPerSample, - ushort blockAlign, - ushort channels, - uint samplesPerSecond, - uint bufferSize, - bool autoUpdate - ) : base( - device, - formatTag, - bitsPerSample, - blockAlign, - channels, - samplesPerSecond, - bufferSize, - autoUpdate - ) { - - } - - public abstract void Seek(uint sampleFrame); - - protected override void OnReachedEnd() - { - if (Loop) - { - ConsumingBuffers = true; - Seek(0); - } - } - } -} diff --git a/src/Audio/StreamingVoice.cs b/src/Audio/StreamingVoice.cs index 2b1ae05..3acfbe1 100644 --- a/src/Audio/StreamingVoice.cs +++ b/src/Audio/StreamingVoice.cs @@ -1,8 +1,9 @@ using System; +using System.Runtime.InteropServices; namespace MoonWorks.Audio { - public abstract class StreamingVoice : SourceVoice + public class StreamingVoice : SourceVoice, IPoolable { private const int BUFFER_COUNT = 3; private readonly IntPtr[] buffers; @@ -11,18 +12,55 @@ namespace MoonWorks.Audio public bool Loop { get; set; } - public StreamingVoice(AudioDevice device, Format format, uint bufferSize) : base(device, format) + public AudioData AudioData { get; protected set; } + + public static StreamingVoice Create(AudioDevice device, Format format) { - BufferSize = bufferSize; + return new StreamingVoice(device, format); } - internal unsafe void Update() + public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format) + { + buffers = new IntPtr[BUFFER_COUNT]; + } + + public void Load(AudioData data) + { + lock (StateLock) + { + if (AudioData != null) + { + AudioData.Unload(); + } + + data.Load(); + AudioData = data; + + InitializeBuffers(); + QueueBuffers(); + } + } + + public void Unload() + { + lock (StateLock) + { + if (AudioData != null) + { + Stop(); + AudioData.Unload(); + AudioData = null; + } + } + } + + public override void Update() { lock (StateLock) { if (!IsDisposed) { - if (State != SoundState.Playing) + if (AudioData == null || State != SoundState.Playing) { return; } @@ -34,8 +72,8 @@ namespace MoonWorks.Audio protected void QueueBuffers() { - var buffersQueued = BuffersQueued; - for (int i = 0; i < BUFFER_COUNT - buffersQueued; i += 1) + int buffersNeeded = BUFFER_COUNT - (int) BuffersQueued; // don't get got by uint underflow! + for (int i = 0; i < buffersNeeded; i += 1) { AddBuffer(); } @@ -46,7 +84,7 @@ namespace MoonWorks.Audio var buffer = buffers[nextBufferIndex]; nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; - FillBuffer( + AudioData.Decode( (void*) buffer, (int) BufferSize, out int filledLengthInBytes, @@ -74,19 +112,25 @@ namespace MoonWorks.Audio /* We have reached the end of the data, what do we do? */ if (Loop) { - SeekStart(); + AudioData.Seek(0); AddBuffer(); } } } - protected unsafe abstract void FillBuffer( - void* buffer, - int bufferLengthInBytes, /* in bytes */ - out int filledLengthInBytes, /* in bytes */ - out bool reachedEnd - ); + private unsafe void InitializeBuffers() + { + BufferSize = AudioData.DecodeBufferSize; - protected abstract void SeekStart(); + for (int i = 0; i < BUFFER_COUNT; i += 1) + { + if (buffers[i] != IntPtr.Zero) + { + NativeMemory.Free((void*) buffers[i]); + } + + buffers[i] = (IntPtr) NativeMemory.Alloc(BufferSize); + } + } } } -- 2.25.1 From 771dc6e7b33a9150975b7366b02d56ce4380e2ee Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 14:39:42 -0700 Subject: [PATCH 06/13] make submix voices respect filter settings --- src/Audio/SubmixVoice.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs index 580256a..e442a80 100644 --- a/src/Audio/SubmixVoice.cs +++ b/src/Audio/SubmixVoice.cs @@ -15,7 +15,7 @@ namespace MoonWorks.Audio out handle, sourceChannelCount, sampleRate, - 0, + FAudio.FAUDIO_VOICE_USEFILTER, 0, IntPtr.Zero, // default sends to mastering voice IntPtr.Zero -- 2.25.1 From 0500d949306e665a572cdac39790b0302adc71c7 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 17:47:44 -0700 Subject: [PATCH 07/13] more audio data and voice restructuring --- src/Audio/AudioBuffer.cs | 56 ++++ src/Audio/AudioDataOgg.cs | 43 ++- src/Audio/AudioDataQoa.cs | 40 ++- .../{AudioData.cs => AudioDataStreamable.cs} | 24 +- src/Audio/AudioDataWav.cs | 95 ++++++ src/Audio/PersistentVoice.cs | 25 ++ src/Audio/SoundSequence.cs | 11 +- src/Audio/SourceVoice.cs | 41 ++- src/Audio/StaticSound.cs | 272 ------------------ src/Audio/StaticVoice.cs | 54 ---- src/Audio/StreamingVoice.cs | 25 +- src/Audio/TransientVoice.cs | 28 ++ 12 files changed, 342 insertions(+), 372 deletions(-) create mode 100644 src/Audio/AudioBuffer.cs rename src/Audio/{AudioData.cs => AudioDataStreamable.cs} (80%) create mode 100644 src/Audio/AudioDataWav.cs create mode 100644 src/Audio/PersistentVoice.cs delete mode 100644 src/Audio/StaticSound.cs delete mode 100644 src/Audio/StaticVoice.cs create mode 100644 src/Audio/TransientVoice.cs diff --git a/src/Audio/AudioBuffer.cs b/src/Audio/AudioBuffer.cs new file mode 100644 index 0000000..306a078 --- /dev/null +++ b/src/Audio/AudioBuffer.cs @@ -0,0 +1,56 @@ +using System; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + public class AudioBuffer : AudioResource + { + IntPtr BufferDataPtr; + uint BufferDataLength; + private bool OwnsBufferData; + + public Format Format { get; } + + public AudioBuffer( + AudioDevice device, + Format format, + IntPtr bufferPtr, + uint bufferLengthInBytes, + bool ownsBufferData) : base(device) + { + Format = format; + BufferDataPtr = bufferPtr; + BufferDataLength = bufferLengthInBytes; + OwnsBufferData = ownsBufferData; + } + + public AudioBuffer CreateSubBuffer(int offset, uint length) + { + return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false); + } + + public FAudio.FAudioBuffer ToFAudioBuffer(bool loop = false) + { + return new FAudio.FAudioBuffer + { + Flags = FAudio.FAUDIO_END_OF_STREAM, + pContext = IntPtr.Zero, + pAudioData = BufferDataPtr, + AudioBytes = BufferDataLength, + PlayBegin = 0, + PlayLength = 0, + LoopBegin = 0, + LoopLength = 0, + LoopCount = loop ? FAudio.FAUDIO_LOOP_INFINITE : 0 + }; + } + + protected override unsafe void Destroy() + { + if (OwnsBufferData) + { + NativeMemory.Free((void*) BufferDataPtr); + } + } + } +} diff --git a/src/Audio/AudioDataOgg.cs b/src/Audio/AudioDataOgg.cs index dbb988d..d7f1a21 100644 --- a/src/Audio/AudioDataOgg.cs +++ b/src/Audio/AudioDataOgg.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { - public class AudioDataOgg : AudioData + public class AudioDataOgg : AudioDataStreamable { private IntPtr FileDataPtr = IntPtr.Zero; private IntPtr VorbisHandle = IntPtr.Zero; @@ -14,7 +14,7 @@ namespace MoonWorks.Audio public override bool Loaded => VorbisHandle != IntPtr.Zero; public override uint DecodeBufferSize => 32768; - public AudioDataOgg(string filePath) + public AudioDataOgg(AudioDevice device, string filePath) : base(device) { FilePath = filePath; @@ -92,5 +92,44 @@ namespace MoonWorks.Audio FileDataPtr = IntPtr.Zero; } } + + public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath) + { + var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero); + + if (error != 0) + { + throw new AudioLoadException("Error loading file!"); + } + var info = FAudio.stb_vorbis_get_info(filePointer); + var lengthInFloats = + FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels; + var lengthInBytes = lengthInFloats * Marshal.SizeOf(); + var buffer = NativeMemory.Alloc((nuint) lengthInBytes); + + FAudio.stb_vorbis_get_samples_float_interleaved( + filePointer, + info.channels, + (nint) buffer, + (int) lengthInFloats + ); + + 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 AudioBuffer( + device, + format, + (nint) buffer, + (uint) lengthInBytes, + true); + } } } diff --git a/src/Audio/AudioDataQoa.cs b/src/Audio/AudioDataQoa.cs index 5322095..02a5c43 100644 --- a/src/Audio/AudioDataQoa.cs +++ b/src/Audio/AudioDataQoa.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { - public class AudioDataQoa : AudioData + public class AudioDataQoa : AudioDataStreamable { private IntPtr QoaHandle = IntPtr.Zero; private IntPtr FileDataPtr = IntPtr.Zero; @@ -18,7 +18,7 @@ namespace MoonWorks.Audio private uint decodeBufferSize; public override uint DecodeBufferSize => decodeBufferSize; - public AudioDataQoa(string filePath) + public AudioDataQoa(AudioDevice device, string filePath) : base(device) { FilePath = filePath; @@ -102,6 +102,42 @@ namespace MoonWorks.Audio } } + public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath) + { + using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length); + var fileDataSpan = new Span(fileDataPtr, (int) fileStream.Length); + fileStream.ReadExactly(fileDataSpan); + fileStream.Close(); + + var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0); + if (qoaHandle == 0) + { + NativeMemory.Free(fileDataPtr); + Logger.LogError("Error opening QOA file!"); + throw new AudioLoadException("Error opening QOA file!"); + } + + FAudio.qoa_attributes(qoaHandle, out var channels, out var samplerate, out var samples_per_channel_per_frame, out var total_samples_per_channel); + + var bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short); + var buffer = NativeMemory.Alloc(bufferLengthInBytes); + FAudio.qoa_decode_entire(qoaHandle, (short*) buffer); + + FAudio.qoa_close(qoaHandle); + NativeMemory.Free(fileDataPtr); + + var format = new Format + { + Tag = FormatTag.PCM, + BitsPerSample = 16, + Channels = (ushort) channels, + SampleRate = samplerate + }; + + return new AudioBuffer(device, format, (nint) buffer, bufferLengthInBytes, true); + } + private static unsafe UInt64 ReverseEndianness(UInt64 value) { byte* bytes = (byte*) &value; diff --git a/src/Audio/AudioData.cs b/src/Audio/AudioDataStreamable.cs similarity index 80% rename from src/Audio/AudioData.cs rename to src/Audio/AudioDataStreamable.cs index 9998179..aab413c 100644 --- a/src/Audio/AudioData.cs +++ b/src/Audio/AudioDataStreamable.cs @@ -1,19 +1,25 @@ -using System; - namespace MoonWorks.Audio { - public abstract class AudioData + public abstract class AudioDataStreamable : AudioResource { public Format Format { get; protected set; } + public abstract bool Loaded { get; } public abstract uint DecodeBufferSize { get; } - public abstract bool Loaded { get; } + protected AudioDataStreamable(AudioDevice device) : base(device) + { + } /// - /// Loads the raw audio data into memory. + /// Loads the raw audio data into memory to prepare it for stream decoding. /// public abstract void Load(); + /// + /// Unloads the raw audio data from memory. + /// + public abstract void Unload(); + /// /// Seeks to the given sample frame. /// @@ -28,9 +34,9 @@ namespace MoonWorks.Audio /// Whether the end of the data was reached on this decode. public abstract unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd); - /// - /// Unloads the raw audio data from memory. - /// - public abstract void Unload(); + protected override void Destroy() + { + Unload(); + } } } diff --git a/src/Audio/AudioDataWav.cs b/src/Audio/AudioDataWav.cs new file mode 100644 index 0000000..5e415ca --- /dev/null +++ b/src/Audio/AudioDataWav.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + public static class AudioDataWav + { + // mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385 + public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath) + { + // WaveFormatEx data + ushort wFormatTag; + ushort nChannels; + uint nSamplesPerSec; + uint nAvgBytesPerSec; + ushort nBlockAlign; + ushort wBitsPerSample; + + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var reader = new BinaryReader(stream); + + // RIFF Signature + string signature = new string(reader.ReadChars(4)); + if (signature != "RIFF") + { + throw new NotSupportedException("Specified stream is not a wave file."); + } + + reader.ReadUInt32(); // Riff Chunk Size + + string wformat = new string(reader.ReadChars(4)); + if (wformat != "WAVE") + { + throw new NotSupportedException("Specified stream is not a wave file."); + } + + // WAVE Header + string format_signature = new string(reader.ReadChars(4)); + while (format_signature != "fmt ") + { + reader.ReadBytes(reader.ReadInt32()); + format_signature = new string(reader.ReadChars(4)); + } + + int format_chunk_size = reader.ReadInt32(); + + wFormatTag = reader.ReadUInt16(); + nChannels = reader.ReadUInt16(); + nSamplesPerSec = reader.ReadUInt32(); + nAvgBytesPerSec = reader.ReadUInt32(); + nBlockAlign = reader.ReadUInt16(); + wBitsPerSample = reader.ReadUInt16(); + + // Reads residual bytes + if (format_chunk_size > 16) + { + reader.ReadBytes(format_chunk_size - 16); + } + + // data Signature + string data_signature = new string(reader.ReadChars(4)); + while (data_signature.ToLowerInvariant() != "data") + { + reader.ReadBytes(reader.ReadInt32()); + data_signature = new string(reader.ReadChars(4)); + } + if (data_signature != "data") + { + throw new NotSupportedException("Specified wave file is not supported."); + } + + int waveDataLength = reader.ReadInt32(); + var waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength); + var waveDataSpan = new Span(waveDataBuffer, waveDataLength); + stream.ReadExactly(waveDataSpan); + + var format = new Format + { + Tag = (FormatTag) wFormatTag, + BitsPerSample = wBitsPerSample, + Channels = nChannels, + SampleRate = nSamplesPerSec + }; + + return new AudioBuffer( + device, + format, + (nint) waveDataBuffer, + (uint) waveDataLength, + true + ); + } + } +} diff --git a/src/Audio/PersistentVoice.cs b/src/Audio/PersistentVoice.cs new file mode 100644 index 0000000..7b54395 --- /dev/null +++ b/src/Audio/PersistentVoice.cs @@ -0,0 +1,25 @@ +namespace MoonWorks.Audio +{ + public class PersistentVoice : SourceVoice, IPoolable + { + public PersistentVoice(AudioDevice device, Format format) : base(device, format) + { + } + + public static PersistentVoice Create(AudioDevice device, Format format) + { + return new PersistentVoice(device, format); + } + + /// + /// Adds an AudioBuffer to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The buffer to submit to the voice. + /// Whether the voice should loop this buffer. + public void Submit(AudioBuffer buffer, bool loop = false) + { + Submit(buffer.ToFAudioBuffer(loop)); + } + } +} diff --git a/src/Audio/SoundSequence.cs b/src/Audio/SoundSequence.cs index dece5df..d88177a 100644 --- a/src/Audio/SoundSequence.cs +++ b/src/Audio/SoundSequence.cs @@ -12,7 +12,7 @@ namespace MoonWorks.Audio } - public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.Format) + public SoundSequence(AudioDevice device, AudioBuffer templateSound) : base(device, templateSound.Format) { } @@ -21,7 +21,6 @@ namespace MoonWorks.Audio { lock (StateLock) { - if (IsDisposed) { return; } if (State != SoundState.Playing) { return; } if (NeedSoundThreshold > 0) @@ -37,18 +36,18 @@ namespace MoonWorks.Audio } } - public void EnqueueSound(StaticSound sound) + public void EnqueueSound(AudioBuffer buffer) { #if DEBUG - if (!(sound.Format == Format)) + if (!(buffer.Format == Format)) { - Logger.LogWarn("Playlist audio format mismatch!"); + Logger.LogWarn("Sound sequence audio format mismatch!"); } #endif lock (StateLock) { - Submit(sound.Buffer); + Submit(buffer.ToFAudioBuffer()); } } } diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 69fe34c..7ef90e6 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -5,13 +5,15 @@ namespace MoonWorks.Audio /// /// Emits audio from submitted audio buffers. /// - public class SourceVoice : Voice + public abstract class SourceVoice : Voice { private Format format; public Format Format => format; - protected object StateLock = new object(); - + /// + /// The number of buffers queued in the voice. + /// This includes the currently playing voice! + /// public uint BuffersQueued { get @@ -45,6 +47,8 @@ namespace MoonWorks.Audio } } + protected object StateLock = new object(); + public SourceVoice( AudioDevice device, Format format @@ -124,20 +128,13 @@ namespace MoonWorks.Audio } /// - /// Adds an FAudio buffer to the voice queue. + /// Adds an AudioBuffer to the voice queue. /// The voice processes and plays back the buffers in its queue in the order that they were submitted. /// /// The buffer to submit to the voice. - public void Submit(FAudio.FAudioBuffer buffer) + public void Submit(AudioBuffer buffer) { - lock (StateLock) - { - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Handle, - ref buffer, - IntPtr.Zero - ); - } + Submit(buffer.ToFAudioBuffer()); } /// @@ -152,9 +149,27 @@ namespace MoonWorks.Audio /// /// Called automatically by AudioDevice in the audio thread. + /// Don't call this yourself! You might regret it! /// public virtual void Update() { } + /// + /// Adds an FAudio buffer to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The buffer to submit to the voice. + protected void Submit(FAudio.FAudioBuffer buffer) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_SubmitSourceBuffer( + Handle, + ref buffer, + IntPtr.Zero + ); + } + } + protected override unsafe void Destroy() { Stop(); diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs deleted file mode 100644 index 9c29142..0000000 --- a/src/Audio/StaticSound.cs +++ /dev/null @@ -1,272 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace MoonWorks.Audio -{ - public class StaticSound : AudioResource - { - internal FAudio.FAudioBuffer Buffer; - - private Format format; - public Format Format => format; - - private bool OwnsBuffer; - - public static unsafe StaticSound LoadOgg(AudioDevice device, string filePath) - { - var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero); - - if (error != 0) - { - throw new AudioLoadException("Error loading file!"); - } - var info = FAudio.stb_vorbis_get_info(filePointer); - var lengthInFloats = - FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels; - var lengthInBytes = lengthInFloats * Marshal.SizeOf(); - var buffer = NativeMemory.Alloc((nuint) lengthInBytes); - - FAudio.stb_vorbis_get_samples_float_interleaved( - filePointer, - info.channels, - (nint) buffer, - (int) lengthInFloats - ); - - 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( - device, - format, - (nint) buffer, - (uint) lengthInBytes, - true); - } - - // mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385 - public static unsafe StaticSound LoadWav(AudioDevice device, string filePath) - { - // WaveFormatEx data - ushort wFormatTag; - ushort nChannels; - uint nSamplesPerSec; - uint nAvgBytesPerSec; - ushort nBlockAlign; - ushort wBitsPerSample; - int samplerLoopStart = 0; - int samplerLoopEnd = 0; - - using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); - using var reader = new BinaryReader(stream); - - // RIFF Signature - string signature = new string(reader.ReadChars(4)); - if (signature != "RIFF") - { - throw new NotSupportedException("Specified stream is not a wave file."); - } - - reader.ReadUInt32(); // Riff Chunk Size - - string wformat = new string(reader.ReadChars(4)); - if (wformat != "WAVE") - { - throw new NotSupportedException("Specified stream is not a wave file."); - } - - // WAVE Header - string format_signature = new string(reader.ReadChars(4)); - while (format_signature != "fmt ") - { - reader.ReadBytes(reader.ReadInt32()); - format_signature = new string(reader.ReadChars(4)); - } - - int format_chunk_size = reader.ReadInt32(); - - wFormatTag = reader.ReadUInt16(); - nChannels = reader.ReadUInt16(); - nSamplesPerSec = reader.ReadUInt32(); - nAvgBytesPerSec = reader.ReadUInt32(); - nBlockAlign = reader.ReadUInt16(); - wBitsPerSample = reader.ReadUInt16(); - - // Reads residual bytes - if (format_chunk_size > 16) - { - reader.ReadBytes(format_chunk_size - 16); - } - - // data Signature - string data_signature = new string(reader.ReadChars(4)); - while (data_signature.ToLowerInvariant() != "data") - { - reader.ReadBytes(reader.ReadInt32()); - data_signature = new string(reader.ReadChars(4)); - } - if (data_signature != "data") - { - throw new NotSupportedException("Specified wave file is not supported."); - } - - int waveDataLength = reader.ReadInt32(); - var waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength); - var waveDataSpan = new Span(waveDataBuffer, waveDataLength); - stream.ReadExactly(waveDataSpan); - - // Scan for other chunks - while (reader.PeekChar() != -1) - { - char[] chunkIDChars = reader.ReadChars(4); - if (chunkIDChars.Length < 4) - { - break; // EOL! - } - byte[] chunkSizeBytes = reader.ReadBytes(4); - if (chunkSizeBytes.Length < 4) - { - break; // EOL! - } - string chunk_signature = new string(chunkIDChars); - int chunkDataSize = BitConverter.ToInt32(chunkSizeBytes, 0); - if (chunk_signature == "smpl") // "smpl", Sampler Chunk Found - { - reader.ReadUInt32(); // Manufacturer - reader.ReadUInt32(); // Product - reader.ReadUInt32(); // Sample Period - reader.ReadUInt32(); // MIDI Unity Note - reader.ReadUInt32(); // MIDI Pitch Fraction - reader.ReadUInt32(); // SMPTE Format - reader.ReadUInt32(); // SMPTE Offset - uint numSampleLoops = reader.ReadUInt32(); - int samplerData = reader.ReadInt32(); - - for (int i = 0; i < numSampleLoops; i += 1) - { - reader.ReadUInt32(); // Cue Point ID - reader.ReadUInt32(); // Type - int start = reader.ReadInt32(); - int end = reader.ReadInt32(); - reader.ReadUInt32(); // Fraction - reader.ReadUInt32(); // Play Count - - if (i == 0) // Grab loopStart and loopEnd from first sample loop - { - samplerLoopStart = start; - samplerLoopEnd = end; - } - } - - if (samplerData != 0) // Read Sampler Data if it exists - { - reader.ReadBytes(samplerData); - } - } - else // Read unwanted chunk data and try again - { - reader.ReadBytes(chunkDataSize); - } - } - // End scan - - var format = new Format - { - Tag = (FormatTag) wFormatTag, - BitsPerSample = wBitsPerSample, - Channels = nChannels, - SampleRate = nSamplesPerSec - }; - - var sound = new StaticSound( - device, - format, - (nint) waveDataBuffer, - (uint) waveDataLength, - true - ); - - return sound; - } - - public static unsafe StaticSound FromQOA(AudioDevice device, string path) - { - var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); - var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length); - var fileDataSpan = new Span(fileDataPtr, (int) fileStream.Length); - fileStream.ReadExactly(fileDataSpan); - fileStream.Close(); - - var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0); - if (qoaHandle == 0) - { - NativeMemory.Free(fileDataPtr); - Logger.LogError("Error opening QOA file!"); - throw new AudioLoadException("Error opening QOA file!"); - } - - FAudio.qoa_attributes(qoaHandle, out var channels, out var samplerate, out var samples_per_channel_per_frame, out var total_samples_per_channel); - - var bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short); - var buffer = NativeMemory.Alloc(bufferLengthInBytes); - FAudio.qoa_decode_entire(qoaHandle, (short*) buffer); - - FAudio.qoa_close(qoaHandle); - NativeMemory.Free(fileDataPtr); - - var format = new Format - { - Tag = FormatTag.PCM, - BitsPerSample = 16, - Channels = (ushort) channels, - SampleRate = samplerate - }; - - return new StaticSound( - device, - format, - (nint) buffer, - bufferLengthInBytes, - true - ); - } - - public StaticSound( - AudioDevice device, - Format format, - IntPtr bufferPtr, - uint bufferLengthInBytes, - bool ownsBuffer) : base(device) - { - this.format = format; - - Buffer = new FAudio.FAudioBuffer - { - Flags = FAudio.FAUDIO_END_OF_STREAM, - pContext = IntPtr.Zero, - pAudioData = bufferPtr, - AudioBytes = bufferLengthInBytes, - PlayBegin = 0, - PlayLength = 0 - }; - - OwnsBuffer = ownsBuffer; - } - - protected override unsafe void Destroy() - { - if (OwnsBuffer) - { - NativeMemory.Free((void*) Buffer.pAudioData); - } - } - } -} diff --git a/src/Audio/StaticVoice.cs b/src/Audio/StaticVoice.cs deleted file mode 100644 index 57600df..0000000 --- a/src/Audio/StaticVoice.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace MoonWorks.Audio -{ - public class StaticVoice : SourceVoice, IPoolable - { - /// - /// Indicates if the voice should return to the voice pool when the voice is idle. - /// If you set this and then hold on to the voice reference there will be problems! - /// - public bool DeactivateWhenIdle { get; set; } - - public static StaticVoice Create(AudioDevice device, Format format) - { - return new StaticVoice(device, format); - } - - public StaticVoice(AudioDevice device, Format format) : base(device, format) - { - } - - public override void Update() - { - lock (StateLock) - { - if (DeactivateWhenIdle) - { - if (BuffersQueued == 0) - { - Return(); - } - } - } - } - - /// - /// Adds a static sound to the voice queue. - /// The voice processes and plays back the buffers in its queue in the order that they were submitted. - /// - /// The sound to submit to the voice. - /// Designates that the voice will loop the submitted buffer. - public void Submit(StaticSound sound, bool loop = false) - { - if (loop) - { - sound.Buffer.LoopCount = FAudio.FAUDIO_LOOP_INFINITE; - } - else - { - sound.Buffer.LoopCount = 0; - } - - Submit(sound.Buffer); - } - } -} diff --git a/src/Audio/StreamingVoice.cs b/src/Audio/StreamingVoice.cs index 3acfbe1..03dcd0a 100644 --- a/src/Audio/StreamingVoice.cs +++ b/src/Audio/StreamingVoice.cs @@ -12,19 +12,19 @@ namespace MoonWorks.Audio public bool Loop { get; set; } - public AudioData AudioData { get; protected set; } - - public static StreamingVoice Create(AudioDevice device, Format format) - { - return new StreamingVoice(device, format); - } + public AudioDataStreamable AudioData { get; protected set; } public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format) { buffers = new IntPtr[BUFFER_COUNT]; } - public void Load(AudioData data) + public static StreamingVoice Create(AudioDevice device, Format format) + { + return new StreamingVoice(device, format); + } + + public void Load(AudioDataStreamable data) { lock (StateLock) { @@ -58,15 +58,12 @@ namespace MoonWorks.Audio { lock (StateLock) { - if (!IsDisposed) + if (AudioData == null || State != SoundState.Playing) { - if (AudioData == null || State != SoundState.Playing) - { - return; - } - - QueueBuffers(); + return; } + + QueueBuffers(); } } diff --git a/src/Audio/TransientVoice.cs b/src/Audio/TransientVoice.cs new file mode 100644 index 0000000..1dbc4c2 --- /dev/null +++ b/src/Audio/TransientVoice.cs @@ -0,0 +1,28 @@ +namespace MoonWorks.Audio +{ + /// + /// These voices are intended for playing one-off sound effects that don't have a long term reference. + /// + public class TransientVoice : SourceVoice, IPoolable + { + static TransientVoice IPoolable.Create(AudioDevice device, Format format) + { + return new TransientVoice(device, format); + } + + public TransientVoice(AudioDevice device, Format format) : base(device, format) + { + } + + public override void Update() + { + lock (StateLock) + { + if (BuffersQueued == 0) + { + Return(); + } + } + } + } +} -- 2.25.1 From 9a854506f346227efd5db9f36c7de814cac3f2a7 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 18:26:27 -0700 Subject: [PATCH 08/13] documentation + fix some edge cases --- src/Audio/AudioBuffer.cs | 15 +++++++++++++++ src/Audio/AudioDataOgg.cs | 3 +++ src/Audio/AudioDataQoa.cs | 3 +++ src/Audio/AudioDataStreamable.cs | 3 +++ src/Audio/AudioDataWav.cs | 7 ++++++- src/Audio/AudioUtils.cs | 33 -------------------------------- src/Audio/PersistentVoice.cs | 3 +++ src/Audio/ReverbEffect.cs | 4 +++- src/Audio/SoundSequence.cs | 4 +++- src/Audio/SourceVoice.cs | 9 +++++++++ src/Audio/StreamingVoice.cs | 13 +++++++++++-- src/Audio/SubmixVoice.cs | 3 +++ src/Audio/TransientVoice.cs | 5 +++-- 13 files changed, 65 insertions(+), 40 deletions(-) delete mode 100644 src/Audio/AudioUtils.cs diff --git a/src/Audio/AudioBuffer.cs b/src/Audio/AudioBuffer.cs index 306a078..bfdeda7 100644 --- a/src/Audio/AudioBuffer.cs +++ b/src/Audio/AudioBuffer.cs @@ -3,6 +3,10 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { + /// + /// Contains raw audio data in the format specified by Format. + /// Submit this to a SourceVoice to play audio. + /// public class AudioBuffer : AudioResource { IntPtr BufferDataPtr; @@ -24,11 +28,22 @@ namespace MoonWorks.Audio OwnsBufferData = ownsBufferData; } + /// + /// Create another AudioBuffer from this audio buffer. + /// It will not own the buffer data. + /// + /// Offset in bytes from the top of the original buffer. + /// Length in bytes of the new buffer. + /// public AudioBuffer CreateSubBuffer(int offset, uint length) { return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false); } + /// + /// Create an FAudioBuffer struct from this AudioBuffer. + /// + /// Whether we should set the FAudioBuffer to loop. public FAudio.FAudioBuffer ToFAudioBuffer(bool loop = false) { return new FAudio.FAudioBuffer diff --git a/src/Audio/AudioDataOgg.cs b/src/Audio/AudioDataOgg.cs index d7f1a21..dce9976 100644 --- a/src/Audio/AudioDataOgg.cs +++ b/src/Audio/AudioDataOgg.cs @@ -4,6 +4,9 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { + /// + /// Streamable audio in Ogg format. + /// public class AudioDataOgg : AudioDataStreamable { private IntPtr FileDataPtr = IntPtr.Zero; diff --git a/src/Audio/AudioDataQoa.cs b/src/Audio/AudioDataQoa.cs index 02a5c43..9bf30d6 100644 --- a/src/Audio/AudioDataQoa.cs +++ b/src/Audio/AudioDataQoa.cs @@ -4,6 +4,9 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { + /// + /// Streamable audio in QOA format. + /// public class AudioDataQoa : AudioDataStreamable { private IntPtr QoaHandle = IntPtr.Zero; diff --git a/src/Audio/AudioDataStreamable.cs b/src/Audio/AudioDataStreamable.cs index aab413c..a0b9d60 100644 --- a/src/Audio/AudioDataStreamable.cs +++ b/src/Audio/AudioDataStreamable.cs @@ -1,5 +1,8 @@ namespace MoonWorks.Audio { + /// + /// Use this in conjunction with a StreamingVoice to play back streaming audio data. + /// public abstract class AudioDataStreamable : AudioResource { public Format Format { get; protected set; } diff --git a/src/Audio/AudioDataWav.cs b/src/Audio/AudioDataWav.cs index 5e415ca..4b6c0a3 100644 --- a/src/Audio/AudioDataWav.cs +++ b/src/Audio/AudioDataWav.cs @@ -6,9 +6,14 @@ namespace MoonWorks.Audio { public static class AudioDataWav { - // mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385 + /// + /// Create an AudioBuffer containing all the WAV audio data in a file. + /// + /// public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath) { + // mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385 + // WaveFormatEx data ushort wFormatTag; ushort nChannels; diff --git a/src/Audio/AudioUtils.cs b/src/Audio/AudioUtils.cs deleted file mode 100644 index a9caa2d..0000000 --- a/src/Audio/AudioUtils.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.IO; - -namespace MoonWorks.Audio -{ - public static class AudioUtils - { - public static Format ReadWaveFormat(string filePath, out int dataLength) - { - var fileInfo = new FileInfo(filePath); - using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); - using BinaryReader br = new BinaryReader(fs); - - fs.Position = 20; - var formatTag = br.ReadInt16(); - fs.Position = 22; - var channels = br.ReadInt16(); - fs.Position = 24; - var sampleRate = br.ReadInt32(); - fs.Position = 34; - var bitsPerSample = br.ReadInt16(); - fs.Position = 40; - dataLength = br.ReadInt32(); - - return new Format - { - Tag = (FormatTag) formatTag, - Channels = (ushort) channels, - SampleRate = (uint) sampleRate, - BitsPerSample = (ushort) bitsPerSample - }; - } - } -} diff --git a/src/Audio/PersistentVoice.cs b/src/Audio/PersistentVoice.cs index 7b54395..5077c15 100644 --- a/src/Audio/PersistentVoice.cs +++ b/src/Audio/PersistentVoice.cs @@ -1,5 +1,8 @@ namespace MoonWorks.Audio { + /// + /// PersistentVoice should be used when you need to maintain a long-term reference to a source voice. + /// public class PersistentVoice : SourceVoice, IPoolable { public PersistentVoice(AudioDevice device, Format format) : base(device, format) diff --git a/src/Audio/ReverbEffect.cs b/src/Audio/ReverbEffect.cs index 064ab72..746e99e 100644 --- a/src/Audio/ReverbEffect.cs +++ b/src/Audio/ReverbEffect.cs @@ -3,7 +3,9 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { - // sound instances can send their audio to this voice to add reverb + /// + /// Use this in conjunction with SourceVoice.SetReverbEffectChain to add reverb to a voice. + /// public unsafe class ReverbEffect : SubmixVoice { public ReverbEffect(AudioDevice audioDevice) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec) diff --git a/src/Audio/SoundSequence.cs b/src/Audio/SoundSequence.cs index d88177a..466aebf 100644 --- a/src/Audio/SoundSequence.cs +++ b/src/Audio/SoundSequence.cs @@ -1,6 +1,8 @@ namespace MoonWorks.Audio { - // NOTE: all sounds played with a SoundSequence must have the same audio format! + /// + /// Plays back a series of AudioBuffers in sequence. Set the OnSoundNeeded callback to add AudioBuffers dynamically. + /// public class SoundSequence : SourceVoice { public int NeedSoundThreshold = 0; diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 7ef90e6..9950a28 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -10,6 +10,8 @@ namespace MoonWorks.Audio private Format format; public Format Format => format; + protected bool PlaybackInitiated; + /// /// The number of buffers queued in the voice. /// This includes the currently playing voice! @@ -170,6 +172,13 @@ namespace MoonWorks.Audio } } + public override void Reset() + { + Stop(); + PlaybackInitiated = false; + base.Reset(); + } + protected override unsafe void Destroy() { Stop(); diff --git a/src/Audio/StreamingVoice.cs b/src/Audio/StreamingVoice.cs index 03dcd0a..9a1b98f 100644 --- a/src/Audio/StreamingVoice.cs +++ b/src/Audio/StreamingVoice.cs @@ -3,6 +3,9 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { + /// + /// Use in conjunction with an AudioDataStreamable object to play back streaming audio data. + /// public class StreamingVoice : SourceVoice, IPoolable { private const int BUFFER_COUNT = 3; @@ -54,6 +57,12 @@ namespace MoonWorks.Audio } } + public override void Reset() + { + Unload(); + base.Reset(); + } + public override void Update() { lock (StateLock) @@ -67,7 +76,7 @@ namespace MoonWorks.Audio } } - protected void QueueBuffers() + private void QueueBuffers() { int buffersNeeded = BUFFER_COUNT - (int) BuffersQueued; // don't get got by uint underflow! for (int i = 0; i < buffersNeeded; i += 1) @@ -76,7 +85,7 @@ namespace MoonWorks.Audio } } - protected unsafe void AddBuffer() + private unsafe void AddBuffer() { var buffer = buffers[nextBufferIndex]; nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs index e442a80..b681024 100644 --- a/src/Audio/SubmixVoice.cs +++ b/src/Audio/SubmixVoice.cs @@ -2,6 +2,9 @@ using System; namespace MoonWorks.Audio { + /// + /// SourceVoices can send audio to a SubmixVoice for convenient effects processing. + /// public class SubmixVoice : Voice { public SubmixVoice( diff --git a/src/Audio/TransientVoice.cs b/src/Audio/TransientVoice.cs index 1dbc4c2..9c747b6 100644 --- a/src/Audio/TransientVoice.cs +++ b/src/Audio/TransientVoice.cs @@ -1,7 +1,8 @@ namespace MoonWorks.Audio { /// - /// These voices are intended for playing one-off sound effects that don't have a long term reference. + /// TransientVoice is intended for playing one-off sound effects that don't have a long term reference. + /// It will be automatically returned to the source voice pool once it is done playing back. /// public class TransientVoice : SourceVoice, IPoolable { @@ -18,7 +19,7 @@ namespace MoonWorks.Audio { lock (StateLock) { - if (BuffersQueued == 0) + if (PlaybackInitiated && BuffersQueued == 0) { Return(); } -- 2.25.1 From 8d5ac8e17bbce61ca22e681968f79d8f6ccca35d Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 18:33:16 -0700 Subject: [PATCH 09/13] add SourceVoice.Apply3D --- src/Audio/SourceVoice.cs | 35 +++++++++++++++++++++++++++++++++++ src/Audio/Voice.cs | 6 +++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 9950a28..b970051 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -139,6 +139,41 @@ namespace MoonWorks.Audio Submit(buffer.ToFAudioBuffer()); } + public unsafe void Apply3D(AudioListener listener, AudioEmitter emitter) + { + Is3D = true; + + emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar; + emitter.emitterData.ChannelCount = SourceChannelCount; + + var dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS + { + DopplerFactor = DopplerFactor, + SrcChannelCount = SourceChannelCount, + DstChannelCount = DestinationChannelCount, + pMatrixCoefficients = (nint) pMatrixCoefficients + }; + + FAudio.F3DAudioCalculate( + Device.Handle3D, + ref listener.listenerData, + ref emitter.emitterData, + FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER, + ref dspSettings + ); + + UpdatePitch(); + + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + OutputVoice.Handle, + SourceChannelCount, + DestinationChannelCount, + (nint) pMatrixCoefficients, + 0 + ); + } + /// /// Specifies that this source voice can be returned to the voice pool. /// Holding on to the reference after calling this will cause problems! diff --git a/src/Audio/Voice.cs b/src/Audio/Voice.cs index 771e5fa..cbf7243 100644 --- a/src/Audio/Voice.cs +++ b/src/Audio/Voice.cs @@ -12,10 +12,10 @@ namespace MoonWorks.Audio public uint SourceChannelCount { get; } public uint DestinationChannelCount { get; } - private SubmixVoice OutputVoice; + protected SubmixVoice OutputVoice; private ReverbEffect ReverbEffect; - byte* pMatrixCoefficients; + protected byte* pMatrixCoefficients; public bool Is3D { get; protected set; } @@ -447,7 +447,7 @@ namespace MoonWorks.Audio SetOutputVoice(Device.MasteringVoice); } - private void UpdatePitch() + protected void UpdatePitch() { float doppler; float dopplerScale = Device.DopplerScale; -- 2.25.1 From cb153cff3ac5d79cb6865695bdb86ba2bad95a47 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 18:36:00 -0700 Subject: [PATCH 10/13] document Apply3D --- src/Audio/SourceVoice.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index b970051..e650b5b 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -139,6 +139,11 @@ namespace MoonWorks.Audio Submit(buffer.ToFAudioBuffer()); } + /// + /// Calculates positional sound. This must be called continuously to update positional sound. + /// + /// + /// public unsafe void Apply3D(AudioListener listener, AudioEmitter emitter) { Is3D = true; -- 2.25.1 From 03b4b5f9a565d34d1437ca20f753ddadeeec74f6 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 21:49:07 -0700 Subject: [PATCH 11/13] add some more Voice documentation --- src/Audio/StreamingVoice.cs | 8 +++ src/Audio/Voice.cs | 120 ++++++++++++++++++++++++++++++++---- 2 files changed, 116 insertions(+), 12 deletions(-) diff --git a/src/Audio/StreamingVoice.cs b/src/Audio/StreamingVoice.cs index 9a1b98f..0b39a96 100644 --- a/src/Audio/StreamingVoice.cs +++ b/src/Audio/StreamingVoice.cs @@ -27,6 +27,10 @@ namespace MoonWorks.Audio return new StreamingVoice(device, format); } + /// + /// Loads and prepares an AudioDataStreamable for streaming playback. + /// This automatically calls Load on the given AudioDataStreamable. + /// public void Load(AudioDataStreamable data) { lock (StateLock) @@ -44,6 +48,10 @@ namespace MoonWorks.Audio } } + /// + /// Unloads AudioDataStreamable from this voice. + /// This automatically calls Unload on the given AudioDataStreamable. + /// public void Unload() { lock (StateLock) diff --git a/src/Audio/Voice.cs b/src/Audio/Voice.cs index cbf7243..c5870a4 100644 --- a/src/Audio/Voice.cs +++ b/src/Audio/Voice.cs @@ -20,6 +20,9 @@ namespace MoonWorks.Audio public bool Is3D { get; protected set; } private float dopplerFactor; + /// + /// The strength of the doppler effect on this voice. + /// public float DopplerFactor { get => dopplerFactor; @@ -34,6 +37,9 @@ namespace MoonWorks.Audio } private float volume = 1; + /// + /// The overall volume level for the voice. + /// public float Volume { get => volume; @@ -49,6 +55,9 @@ namespace MoonWorks.Audio } private float pitch = 0; + /// + /// The pitch of the voice. + /// public float Pitch { get => pitch; @@ -73,6 +82,9 @@ namespace MoonWorks.Audio OneOverQ = 1f }; + /// + /// The frequency cutoff on the voice filter. + /// public float FilterFrequency { get => filterParameters.Frequency; @@ -92,6 +104,10 @@ namespace MoonWorks.Audio } } + /// + /// Reciprocal of Q factor. + /// Controls how quickly frequencies beyond the filter frequency are dampened. + /// public float FilterOneOverQ { get => filterParameters.OneOverQ; @@ -112,6 +128,9 @@ namespace MoonWorks.Audio } private FilterType filterType; + /// + /// The frequency filter that is applied to the voice. + /// public FilterType FilterType { get => filterType; @@ -157,6 +176,9 @@ namespace MoonWorks.Audio } protected float pan = 0; + /// + /// Left-right panning. -1 is hard left pan, 1 is hard right pan. + /// public float Pan { get => pan; @@ -192,6 +214,10 @@ namespace MoonWorks.Audio } private float reverb; + /// + /// The wet-dry mix of the reverb effect. + /// Has no effect if SetReverbEffectChain has not been called. + /// public unsafe float Reverb { get => reverb; @@ -241,91 +267,154 @@ namespace MoonWorks.Audio SetPanMatrixCoefficients(); } + /// + /// Sets the pitch of the voice. Valid input range is -1f to 1f. + /// public void SetPitch(float targetValue) { Pitch = targetValue; Device.ClearTweens(this, AudioTweenProperty.Pitch); } + /// + /// Sets the pitch of the voice over a time duration in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public void SetPitch(float targetValue, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, 0); } + /// + /// Sets the pitch of the voice over a time duration in seconds after a delay in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, delayTime); } + /// + /// Sets the volume of the voice. Minimum value is 0f. + /// public void SetVolume(float targetValue) { Volume = targetValue; Device.ClearTweens(this, AudioTweenProperty.Volume); } + /// + /// Sets the volume of the voice over a time duration in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public void SetVolume(float targetValue, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0); } + /// + /// Sets the volume of the voice over a time duration in seconds after a delay in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime); } + /// + /// Sets the frequency cutoff on the voice filter. Valid range is 0.01f to 1f. + /// public void SetFilterFrequency(float targetValue) { FilterFrequency = targetValue; Device.ClearTweens(this, AudioTweenProperty.FilterFrequency); } + /// + /// Sets the frequency cutoff on the voice filter over a time duration in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0); } + /// + /// Sets the frequency cutoff on the voice filter over a time duration in seconds after a delay in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime); } + /// + /// Sets reciprocal of Q factor on the frequency filter. + /// Controls how quickly frequencies beyond the filter frequency are dampened. + /// public void SetFilterOneOverQ(float targetValue) { FilterOneOverQ = targetValue; } + /// + /// Sets a left-right panning value. -1f is hard left pan, 1f is hard right pan. + /// public virtual void SetPan(float targetValue) { Pan = targetValue; Device.ClearTweens(this, AudioTweenProperty.Pan); } + /// + /// Sets a left-right panning value over a time duration in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public virtual void SetPan(float targetValue, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0); } + /// + /// Sets a left-right panning value over a time duration in seconds after a delay in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public virtual void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime); } + /// + /// Sets the wet-dry mix value of the reverb effect. Minimum value is 0f. + /// public virtual void SetReverb(float targetValue) { Reverb = targetValue; Device.ClearTweens(this, AudioTweenProperty.Reverb); } + /// + /// Sets the wet-dry mix value of the reverb effect over a time duration in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public virtual void SetReverb(float targetValue, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0); } + /// + /// Sets the wet-dry mix value of the reverb effect over a time duration in seconds after a delay in seconds. + /// + /// An easing function. See MoonWorks.Math.Easing.Function.Float public virtual void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction) { Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime); } + /// + /// Sets the output voice for this voice. + /// + /// Where the output should be sent. public unsafe void SetOutputVoice(SubmixVoice send) { OutputVoice = send; @@ -351,6 +440,9 @@ namespace MoonWorks.Audio } } + /// + /// Applies a reverb effect chain to this voice. + /// public unsafe void SetReverbEffectChain(ReverbEffect reverbEffect) { var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2]; @@ -371,6 +463,9 @@ namespace MoonWorks.Audio ReverbEffect = reverbEffect; } + /// + /// Removes the reverb effect chain from this voice. + /// public void RemoveReverbEffectChain() { if (ReverbEffect != null) @@ -381,6 +476,19 @@ namespace MoonWorks.Audio } } + /// + /// Resets all voice parameters to defaults. + /// + public virtual void Reset() + { + RemoveReverbEffectChain(); + Volume = 1; + Pan = 0; + Pitch = 0; + FilterType = FilterType.None; + SetOutputVoice(Device.MasteringVoice); + } + // Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs private unsafe void SetPanMatrixCoefficients() { @@ -435,18 +543,6 @@ namespace MoonWorks.Audio } } - public virtual void Reset() - { - RemoveReverbEffectChain(); - Volume = 1; - Pan = 0; - Pitch = 0; - FilterType = FilterType.None; - FilterFrequency = 1; - FilterOneOverQ = 1; - SetOutputVoice(Device.MasteringVoice); - } - protected void UpdatePitch() { float doppler; -- 2.25.1 From f81133a5a33f5346cffb1b0b65a07838177825da Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 2 Aug 2023 22:30:30 -0700 Subject: [PATCH 12/13] add processingStage input to SubmixVoice --- src/Audio/AudioDevice.cs | 2 +- src/Audio/ReverbEffect.cs | 2 +- src/Audio/SubmixVoice.cs | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index ce3f76e..c86aa60 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -105,7 +105,7 @@ namespace MoonWorks.Audio return; } - fauxMasteringVoice = new SubmixVoice(this, DeviceDetails.OutputFormat.Format.nChannels, DeviceDetails.OutputFormat.Format.nSamplesPerSec); + fauxMasteringVoice = new SubmixVoice(this, DeviceDetails.OutputFormat.Format.nChannels, DeviceDetails.OutputFormat.Format.nSamplesPerSec, int.MaxValue); /* Init 3D Audio */ diff --git a/src/Audio/ReverbEffect.cs b/src/Audio/ReverbEffect.cs index 746e99e..fd3950c 100644 --- a/src/Audio/ReverbEffect.cs +++ b/src/Audio/ReverbEffect.cs @@ -8,7 +8,7 @@ namespace MoonWorks.Audio /// public unsafe class ReverbEffect : SubmixVoice { - public ReverbEffect(AudioDevice audioDevice) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec) + public ReverbEffect(AudioDevice audioDevice, uint processingStage) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec, processingStage) { /* Init reverb */ IntPtr reverb; diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs index b681024..ae64e9f 100644 --- a/src/Audio/SubmixVoice.cs +++ b/src/Audio/SubmixVoice.cs @@ -4,13 +4,16 @@ namespace MoonWorks.Audio { /// /// SourceVoices can send audio to a SubmixVoice for convenient effects processing. + /// Submixes process in order of processingStage, from lowest to highest. + /// Therefore submixes early in a chain should have a low processingStage, and later in the chain they should have a higher one. /// public class SubmixVoice : Voice { public SubmixVoice( AudioDevice device, uint sourceChannelCount, - uint sampleRate + uint sampleRate, + uint processingStage ) : base(device, sourceChannelCount, device.DeviceDetails.OutputFormat.Format.nChannels) { FAudio.FAudio_CreateSubmixVoice( @@ -19,7 +22,7 @@ namespace MoonWorks.Audio sourceChannelCount, sampleRate, FAudio.FAUDIO_VOICE_USEFILTER, - 0, + processingStage, IntPtr.Zero, // default sends to mastering voice IntPtr.Zero ); -- 2.25.1 From b5e79b0e0bc9f747a9b933f4a60e4435c66401b2 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Thu, 3 Aug 2023 12:51:56 -0700 Subject: [PATCH 13/13] rename CreateSubBuffer to Slice --- src/Audio/AudioBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Audio/AudioBuffer.cs b/src/Audio/AudioBuffer.cs index bfdeda7..fdfb5e5 100644 --- a/src/Audio/AudioBuffer.cs +++ b/src/Audio/AudioBuffer.cs @@ -35,7 +35,7 @@ namespace MoonWorks.Audio /// Offset in bytes from the top of the original buffer. /// Length in bytes of the new buffer. /// - public AudioBuffer CreateSubBuffer(int offset, uint length) + public AudioBuffer Slice(int offset, uint length) { return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false); } -- 2.25.1