diff --git a/src/Audio/AudioBuffer.cs b/src/Audio/AudioBuffer.cs new file mode 100644 index 0000000..fdfb5e5 --- /dev/null +++ b/src/Audio/AudioBuffer.cs @@ -0,0 +1,71 @@ +using System; +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; + 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; + } + + /// + /// 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 Slice(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 + { + 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 new file mode 100644 index 0000000..dce9976 --- /dev/null +++ b/src/Audio/AudioDataOgg.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + /// + /// Streamable audio in Ogg format. + /// + public class AudioDataOgg : AudioDataStreamable + { + 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(AudioDevice device, string filePath) : base(device) + { + 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; + } + } + + 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 new file mode 100644 index 0000000..9bf30d6 --- /dev/null +++ b/src/Audio/AudioDataQoa.cs @@ -0,0 +1,155 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + /// + /// Streamable audio in QOA format. + /// + public class AudioDataQoa : AudioDataStreamable + { + private IntPtr QoaHandle = IntPtr.Zero; + private IntPtr FileDataPtr = IntPtr.Zero; + + private string FilePath; + + private const uint QOA_MAGIC = 0x716f6166; /* 'qoaf' */ + + public override bool Loaded => QoaHandle != IntPtr.Zero; + + private uint decodeBufferSize; + public override uint DecodeBufferSize => decodeBufferSize; + + public AudioDataQoa(AudioDevice device, string filePath) : base(device) + { + FilePath = filePath; + + using var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); + using var reader = new BinaryReader(stream); + + UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64()); + if ((fileHeader >> 32) != QOA_MAGIC) + { + throw new AudioLoadException("Specified file is not a QOA file."); + } + + uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF)); + if (totalSamplesPerChannel == 0) + { + throw new AudioLoadException("Specified file is not a valid QOA file."); + } + + UInt64 frameHeader = ReverseEndianness(reader.ReadUInt64()); + uint channels = (uint) ((frameHeader >> 56) & 0x0000FF); + uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF); + uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF); + + Format = new Format + { + Tag = FormatTag.PCM, + BitsPerSample = 16, + Channels = (ushort) channels, + SampleRate = samplerate + }; + + decodeBufferSize = channels * samplesPerChannelPerFrame * sizeof(short); + } + + 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) + { + FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame); + } + + public override unsafe void Unload() + { + if (Loaded) + { + FAudio.qoa_close(QoaHandle); + NativeMemory.Free((void*) FileDataPtr); + + QoaHandle = IntPtr.Zero; + FileDataPtr = IntPtr.Zero; + } + } + + 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; + + 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/AudioDataStreamable.cs b/src/Audio/AudioDataStreamable.cs new file mode 100644 index 0000000..a0b9d60 --- /dev/null +++ b/src/Audio/AudioDataStreamable.cs @@ -0,0 +1,45 @@ +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; } + public abstract bool Loaded { get; } + public abstract uint DecodeBufferSize { get; } + + protected AudioDataStreamable(AudioDevice device) : base(device) + { + } + + /// + /// 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. + /// + 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); + + protected override void Destroy() + { + Unload(); + } + } +} diff --git a/src/Audio/AudioDataWav.cs b/src/Audio/AudioDataWav.cs new file mode 100644 index 0000000..4b6c0a3 --- /dev/null +++ b/src/Audio/AudioDataWav.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace MoonWorks.Audio +{ + public static class AudioDataWav + { + /// + /// 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; + 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/AudioDevice.cs b/src/Audio/AudioDevice.cs index e5a86b0..c86aa60 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 @@ -9,31 +8,27 @@ namespace MoonWorks.Audio { public IntPtr Handle { get; } public byte[] Handle3D { get; } - public IntPtr MasteringVoice { get; } public FAudio.FAudioDeviceDetails DeviceDetails { get; } + 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; 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(); - private readonly List> soundSequenceReferences = 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; private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch(); @@ -93,25 +88,24 @@ namespace MoonWorks.Audio } /* Init Mastering Voice */ - IntPtr masteringVoice; - - if (FAudio.FAudio_CreateMasteringVoice( + var result = FAudio.FAudio_CreateMasteringVoice( Handle, - out masteringVoice, + out trueMasteringVoice, FAudio.FAUDIO_DEFAULT_CHANNELS, FAudio.FAUDIO_DEFAULT_SAMPLERATE, 0, i, IntPtr.Zero - ) != 0) + ); + + if (result != 0) { - Logger.LogError("No mastering voice found!"); - FAudio.FAudio_Release(Handle); - Handle = IntPtr.Zero; + Logger.LogError("Failed to create a mastering voice!"); + Logger.LogError("Audio device creation failed!"); return; } - MasteringVoice = masteringVoice; + fauxMasteringVoice = new SubmixVoice(this, DeviceDetails.OutputFormat.Format.nChannels, DeviceDetails.OutputFormat.Format.nSamplesPerSec, int.MaxValue); /* Init 3D Audio */ @@ -123,6 +117,7 @@ namespace MoonWorks.Audio ); AudioTweenManager = new AudioTweenManager(); + VoicePool = new SourceVoicePool(this); Logger.LogInfo("Setting up audio thread..."); WakeSignal = new AutoResetEvent(true); @@ -163,53 +158,60 @@ namespace MoonWorks.Audio previousTickTime = TickStopwatch.Elapsed.Ticks; float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond; - 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 = 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)) - { - soundSequence.Update(); - } - else - { - soundSequenceReferences.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(); } - public void SyncPlay() + /// + /// Triggers all pending operations with the given syncGroup value. + /// + public void TriggerSyncGroup(uint syncGroup) { - FAudio.FAudio_CommitChanges(Handle, 1); + 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 T Obtain(Format format) where T : SourceVoice, IPoolable + { + lock (StateLock) + { + var voice = VoicePool.Obtain(format); + activeSourceVoices.Add(voice); + return voice; + } + } + + /// + /// Returns the source voice to the voice pool. + /// + /// + internal void Return(SourceVoice voice) + { + lock (StateLock) + { + VoicesToReturn.Add(voice); + } } internal void CreateTween( - SoundInstance soundInstance, + Voice voice, AudioTweenProperty property, System.Func easingFunction, float start, @@ -220,7 +222,7 @@ namespace MoonWorks.Audio lock (StateLock) { AudioTweenManager.CreateTween( - soundInstance, + voice, property, easingFunction, start, @@ -232,12 +234,12 @@ namespace MoonWorks.Audio } internal void ClearTweens( - SoundInstance soundReference, + Voice voice, AudioTweenProperty property ) { lock (StateLock) { - AudioTweenManager.ClearTweens(soundReference, property); + AudioTweenManager.ClearTweens(voice, property); } } @@ -262,21 +264,6 @@ namespace MoonWorks.Audio } } - internal void AddAutoUpdateStreamingSoundInstance(StreamingSound instance) - { - autoUpdateStreamingSoundReferences.Add(instance); - } - - internal void AddAutoFreeStaticSoundInstance(StaticSoundInstance instance) - { - autoFreeStaticSoundInstanceReferences.Add(instance); - } - - internal void AddSoundSequenceReference(SoundSequence sequence) - { - soundSequenceReferences.Add(new WeakReference(sequence)); - } - protected virtual void Dispose(bool disposing) { if (!IsDisposed) @@ -286,6 +273,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 +294,11 @@ namespace MoonWorks.Audio (target as IDisposable).Dispose(); } } + resources.Clear(); } - FAudio.FAudioVoice_DestroyVoice(MasteringVoice); + FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice); 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/AudioUtils.cs b/src/Audio/AudioUtils.cs deleted file mode 100644 index 143eb8f..0000000 --- a/src/Audio/AudioUtils.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.IO; - -namespace MoonWorks.Audio -{ - public static class AudioUtils - { - public struct WaveHeaderData - { - 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(); - fs.Position = 22; - headerData.Channels = br.ReadInt16(); - fs.Position = 24; - headerData.SampleRate = br.ReadInt32(); - fs.Position = 32; - headerData.BlockAlign = br.ReadInt16(); - fs.Position = 34; - headerData.BitsPerSample = br.ReadInt16(); - fs.Position = 40; - headerData.DataLength = br.ReadInt32(); - - return headerData; - } - } -} 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/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/PersistentVoice.cs b/src/Audio/PersistentVoice.cs new file mode 100644 index 0000000..5077c15 --- /dev/null +++ b/src/Audio/PersistentVoice.cs @@ -0,0 +1,28 @@ +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) + { + } + + 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/ReverbEffect.cs b/src/Audio/ReverbEffect.cs index 42fab2c..fd3950c 100644 --- a/src/Audio/ReverbEffect.cs +++ b/src/Audio/ReverbEffect.cs @@ -3,54 +3,34 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { - // sound instances can send their audio to this voice to add reverb - public unsafe class ReverbEffect : AudioResource + /// + /// Use this in conjunction with SourceVoice.SetReverbEffectChain to add reverb to a voice. + /// + public unsafe class ReverbEffect : SubmixVoice { - private IntPtr voice; - public IntPtr Voice => voice; - - public ReverbEffect(AudioDevice audioDevice) : base(audioDevice) + public ReverbEffect(AudioDevice audioDevice, uint processingStage) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec, processingStage) { /* 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 = 1; + 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 +66,7 @@ namespace MoonWorks.Audio fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams) { FAudio.FAudioVoice_SetEffectParameters( - voice, + Handle, 0, (nint) reverbParamsPtr, (uint) Marshal.SizeOf(), @@ -94,10 +74,5 @@ namespace MoonWorks.Audio ); } } - - protected override void Destroy() - { - FAudio.FAudioVoice_DestroyVoice(Voice); - } } } diff --git a/src/Audio/SoundQueue.cs b/src/Audio/SoundQueue.cs deleted file mode 100644 index d098ecf..0000000 --- a/src/Audio/SoundQueue.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; - -namespace MoonWorks.Audio -{ - // NOTE: all sounds played with a SoundSequence must have the same audio format! - public class SoundSequence : SoundInstance - { - 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); - } - - public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.FormatTag, templateSound.BitsPerSample, templateSound.BlockAlign, templateSound.Channels, templateSound.SamplesPerSecond) - { - device.AddSoundSequenceReference(this); - } - - public void Update() - { - lock (StateLock) - { - if (IsDisposed) { return; } - if (State != SoundState.Playing) { return; } - - if (NeedSoundThreshold > 0) - { - FAudio.FAudioSourceVoice_GetState( - Voice, - out var state, - FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED - ); - - var queuedBufferCount = state.BuffersQueued; - for (int i = 0; i < NeedSoundThreshold - queuedBufferCount; i += 1) - { - if (OnSoundNeeded != null) - { - OnSoundNeeded(); - } - } - } - } - } - - public void EnqueueSound(StaticSound sound) - { -#if DEBUG - if ( - sound.FormatTag != Format.wFormatTag || - sound.BitsPerSample != Format.wBitsPerSample || - sound.Channels != Format.nChannels || - sound.SamplesPerSecond != Format.nSamplesPerSec - ) - { - Logger.LogWarn("Playlist audio format mismatch!"); - } -#endif - - lock (StateLock) - { - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Voice, - 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/SoundSequence.cs b/src/Audio/SoundSequence.cs new file mode 100644 index 0000000..466aebf --- /dev/null +++ b/src/Audio/SoundSequence.cs @@ -0,0 +1,56 @@ +namespace MoonWorks.Audio +{ + /// + /// Plays back a series of AudioBuffers in sequence. Set the OnSoundNeeded callback to add AudioBuffers dynamically. + /// + public class SoundSequence : SourceVoice + { + public int NeedSoundThreshold = 0; + public delegate void OnSoundNeededFunc(); + public OnSoundNeededFunc OnSoundNeeded; + + public SoundSequence(AudioDevice device, Format format) : base(device, format) + { + + } + + public SoundSequence(AudioDevice device, AudioBuffer templateSound) : base(device, templateSound.Format) + { + + } + + public override void Update() + { + lock (StateLock) + { + if (State != SoundState.Playing) { return; } + + if (NeedSoundThreshold > 0) + { + for (int i = 0; i < NeedSoundThreshold - BuffersQueued; i += 1) + { + if (OnSoundNeeded != null) + { + OnSoundNeeded(); + } + } + } + } + } + + public void EnqueueSound(AudioBuffer buffer) + { +#if DEBUG + if (!(buffer.Format == Format)) + { + Logger.LogWarn("Sound sequence audio format mismatch!"); + } +#endif + + lock (StateLock) + { + Submit(buffer.ToFAudioBuffer()); + } + } + } +} diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs new file mode 100644 index 0000000..e650b5b --- /dev/null +++ b/src/Audio/SourceVoice.cs @@ -0,0 +1,228 @@ +using System; + +namespace MoonWorks.Audio +{ + /// + /// Emits audio from submitted audio buffers. + /// + public abstract class SourceVoice : Voice + { + private Format format; + public Format Format => format; + + protected bool PlaybackInitiated; + + /// + /// The number of buffers queued in the voice. + /// This includes the currently playing voice! + /// + 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; + } + } + + protected object StateLock = new object(); + + public SourceVoice( + AudioDevice device, + Format format + ) : base(device, format.Channels, device.DeviceDetails.OutputFormat.Format.nChannels) + { + this.format = format; + var fAudioFormat = format.ToFAudioFormat(); + + FAudio.FAudio_CreateSourceVoice( + device.Handle, + out handle, + ref fAudioFormat, + FAudio.FAUDIO_VOICE_USEFILTER, + FAudio.FAUDIO_DEFAULT_FREQ_RATIO, + IntPtr.Zero, + IntPtr.Zero, // default sends to mastering voice! + IntPtr.Zero + ); + } + + /// + /// 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) + { + FAudio.FAudioSourceVoice_Start(Handle, 0, syncGroup); + + State = SoundState.Playing; + } + } + + /// + /// 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) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + + State = SoundState.Paused; + } + } + + /// + /// 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) + { + FAudio.FAudioSourceVoice_ExitLoop(Handle, syncGroup); + } + } + + /// + /// 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) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); + + State = SoundState.Stopped; + } + } + + /// + /// 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(AudioBuffer buffer) + { + 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; + + 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! + /// + public void Return() + { + Stop(); + Device.Return(this); + } + + /// + /// 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 + ); + } + } + + public override void Reset() + { + Stop(); + PlaybackInitiated = false; + base.Reset(); + } + + protected override unsafe void Destroy() + { + Stop(); + base.Destroy(); + } + } +} diff --git a/src/Audio/SourceVoicePool.cs b/src/Audio/SourceVoicePool.cs new file mode 100644 index 0000000..6c1ef84 --- /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<(System.Type, Format), Queue> VoiceLists = new Dictionary<(System.Type, Format), Queue>(); + + public SourceVoicePool(AudioDevice device) + { + Device = device; + } + + public T Obtain(Format format) where T : SourceVoice, IPoolable + { + if (!VoiceLists.ContainsKey((typeof(T), format))) + { + VoiceLists.Add((typeof(T), format), new Queue()); + } + + var list = VoiceLists[(typeof(T), format)]; + + if (list.Count == 0) + { + list.Enqueue(T.Create(Device, format)); + } + + return (T) list.Dequeue(); + } + + public void Return(SourceVoice voice) + { + var list = VoiceLists[(voice.GetType(), voice.Format)]; + list.Enqueue(voice); + } + } +} diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs deleted file mode 100644 index b5def25..0000000 --- a/src/Audio/StaticSound.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; - -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; } - - 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) - { - 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); - - return new StaticSound( - device, - 3, - 32, - (ushort) (4 * info.channels), - (ushort) info.channels, - info.sample_rate, - (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 sound = new StaticSound( - device, - wFormatTag, - wBitsPerSample, - nBlockAlign, - nChannels, - nSamplesPerSec, - (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); - - return new StaticSound( - device, - 1, - 16, - (ushort) (channels * 2), - (ushort) channels, - samplerate, - (nint) buffer, - bufferLengthInBytes, - true - ); - } - - public StaticSound( - AudioDevice device, - ushort formatTag, - ushort bitsPerSample, - ushort blockAlign, - ushort channels, - uint samplesPerSecond, - IntPtr bufferPtr, - uint bufferLengthInBytes, - bool ownsBuffer) : base(device) - { - FormatTag = formatTag; - BitsPerSample = bitsPerSample; - BlockAlign = blockAlign; - Channels = channels; - SamplesPerSecond = samplesPerSecond; - - Handle = new FAudio.FAudioBuffer - { - Flags = FAudio.FAUDIO_END_OF_STREAM, - pContext = IntPtr.Zero, - pAudioData = bufferPtr, - AudioBytes = bufferLengthInBytes, - PlayBegin = 0, - PlayLength = 0 - }; - - 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); - } - } - } -} 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/StreamingSound.cs b/src/Audio/StreamingSound.cs deleted file mode 100644 index 280f37a..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; - } - - 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) - { - 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/StreamingSoundQoa.cs b/src/Audio/StreamingSoundQoa.cs deleted file mode 100644 index 6c4340d..0000000 --- a/src/Audio/StreamingSoundQoa.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace MoonWorks.Audio -{ - public class StreamingSoundQoa : StreamingSoundSeekable - { - 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; - - 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); - } - - public unsafe static StreamingSoundQoa Create(AudioDevice device, string filePath) - { - using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); - using var reader = new BinaryReader(stream); - - UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64()); - if ((fileHeader >> 32) != QOA_MAGIC) - { - throw new AudioLoadException("Specified file is not a QOA file."); - } - - uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF)); - if (totalSamplesPerChannel == 0) - { - throw new AudioLoadException("Specified file is not a valid QOA file."); - } - - UInt64 frameHeader = ReverseEndianness(reader.ReadUInt64()); - uint channels = (uint) ((frameHeader >> 56) & 0x0000FF); - uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF); - uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF); - - return new StreamingSoundQoa( - device, - filePath, - channels, - samplerate, - samplesPerChannelPerFrame, - totalSamplesPerChannel - ); - } - - 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 void Seek(uint sampleFrame) - { - 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) - { - FAudio.qoa_close(QoaHandle); - NativeMemory.Free((void*) FileDataPtr); - - QoaHandle = IntPtr.Zero; - FileDataPtr = IntPtr.Zero; - } - } - - protected override unsafe void FillBuffer( - 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 * Channels; - reachedEnd = sampleCount < lengthInShorts; - filledLengthInBytes = (int) (sampleCount * sizeof(short)); - } - } -} 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 new file mode 100644 index 0000000..0b39a96 --- /dev/null +++ b/src/Audio/StreamingVoice.cs @@ -0,0 +1,150 @@ +using System; +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; + private readonly IntPtr[] buffers; + private int nextBufferIndex = 0; + private uint BufferSize; + + public bool Loop { get; set; } + + public AudioDataStreamable AudioData { get; protected set; } + + public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format) + { + buffers = new IntPtr[BUFFER_COUNT]; + } + + public static StreamingVoice Create(AudioDevice device, Format format) + { + 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) + { + if (AudioData != null) + { + AudioData.Unload(); + } + + data.Load(); + AudioData = data; + + InitializeBuffers(); + QueueBuffers(); + } + } + + /// + /// Unloads AudioDataStreamable from this voice. + /// This automatically calls Unload on the given AudioDataStreamable. + /// + public void Unload() + { + lock (StateLock) + { + if (AudioData != null) + { + Stop(); + AudioData.Unload(); + AudioData = null; + } + } + } + + public override void Reset() + { + Unload(); + base.Reset(); + } + + public override void Update() + { + lock (StateLock) + { + if (AudioData == null || State != SoundState.Playing) + { + return; + } + + 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) + { + AddBuffer(); + } + } + + private unsafe void AddBuffer() + { + var buffer = buffers[nextBufferIndex]; + nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; + + AudioData.Decode( + (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) + { + AudioData.Seek(0); + AddBuffer(); + } + } + } + + private unsafe void InitializeBuffers() + { + BufferSize = AudioData.DecodeBufferSize; + + 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); + } + } + } +} diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs new file mode 100644 index 0000000..ae64e9f --- /dev/null +++ b/src/Audio/SubmixVoice.cs @@ -0,0 +1,31 @@ +using System; + +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 processingStage + ) : base(device, sourceChannelCount, device.DeviceDetails.OutputFormat.Format.nChannels) + { + FAudio.FAudio_CreateSubmixVoice( + device.Handle, + out handle, + sourceChannelCount, + sampleRate, + FAudio.FAUDIO_VOICE_USEFILTER, + processingStage, + IntPtr.Zero, // default sends to mastering voice + IntPtr.Zero + ); + } + } +} diff --git a/src/Audio/TransientVoice.cs b/src/Audio/TransientVoice.cs new file mode 100644 index 0000000..9c747b6 --- /dev/null +++ b/src/Audio/TransientVoice.cs @@ -0,0 +1,29 @@ +namespace MoonWorks.Audio +{ + /// + /// 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 + { + 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 (PlaybackInitiated && BuffersQueued == 0) + { + Return(); + } + } + } + } +} diff --git a/src/Audio/SoundInstance.cs b/src/Audio/Voice.cs similarity index 51% rename from src/Audio/SoundInstance.cs rename to src/Audio/Voice.cs index 11d320e..c5870a4 100644 --- a/src/Audio/SoundInstance.cs +++ b/src/Audio/Voice.cs @@ -4,58 +4,60 @@ using EasingFunction = System.Func; namespace MoonWorks.Audio { - public abstract class SoundInstance : AudioResource + public abstract unsafe class Voice : AudioResource { - internal IntPtr Voice; + protected IntPtr handle; + public IntPtr Handle => handle; - private FAudio.FAudioWaveFormatEx format; - public FAudio.FAudioWaveFormatEx Format => format; - - protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; + public uint SourceChannelCount { get; } + public uint DestinationChannelCount { get; } + protected SubmixVoice OutputVoice; private ReverbEffect ReverbEffect; - private FAudio.FAudioVoiceSends ReverbSends; + + protected byte* pMatrixCoefficients; public bool Is3D { get; protected set; } - public virtual SoundState State { get; protected set; } - - private float pan = 0; - public float Pan + private float dopplerFactor; + /// + /// The strength of the doppler effect on this voice. + /// + public float DopplerFactor { - get => pan; + get => dopplerFactor; + set + { + if (dopplerFactor != value) + { + dopplerFactor = value; + UpdatePitch(); + } + } + } + + private float volume = 1; + /// + /// The overall volume level for the voice. + /// + public float Volume + { + get => volume; internal set { - value = Math.MathHelper.Clamp(value, -1f, 1f); - if (pan != value) + value = Math.MathHelper.Max(0, value); + if (volume != 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 - ); + volume = value; + FAudio.FAudioVoice_SetVolume(Handle, volume, 0); } } } private float pitch = 0; + /// + /// The pitch of the voice. + /// public float Pitch { get => pitch; @@ -70,21 +72,6 @@ namespace MoonWorks.Audio } } - 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; @@ -95,6 +82,9 @@ namespace MoonWorks.Audio OneOverQ = 1f }; + /// + /// The frequency cutoff on the voice filter. + /// public float FilterFrequency { get => filterParameters.Frequency; @@ -106,7 +96,7 @@ namespace MoonWorks.Audio filterParameters.Frequency = value; FAudio.FAudioVoice_SetFilterParameters( - Voice, + Handle, ref filterParameters, 0 ); @@ -114,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; @@ -125,7 +119,7 @@ namespace MoonWorks.Audio filterParameters.OneOverQ = value; FAudio.FAudioVoice_SetFilterParameters( - Voice, + Handle, ref filterParameters, 0 ); @@ -134,6 +128,9 @@ namespace MoonWorks.Audio } private FilterType filterType; + /// + /// The frequency filter that is applied to the voice. + /// public FilterType FilterType { get => filterType; @@ -170,7 +167,7 @@ namespace MoonWorks.Audio } FAudio.FAudioVoice_SetFilterParameters( - Voice, + Handle, ref filterParameters, 0 ); @@ -178,7 +175,49 @@ 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; + 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; + /// + /// The wet-dry mix of the reverb effect. + /// Has no effect if SetReverbEffectChain has not been called. + /// public unsafe float Reverb { get => reverb; @@ -191,19 +230,19 @@ namespace MoonWorks.Audio { reverb = value; - float* outputMatrix = (float*) dspSettings.pMatrixCoefficients; + float* outputMatrix = (float*) pMatrixCoefficients; outputMatrix[0] = reverb; - if (dspSettings.SrcChannelCount == 2) + if (SourceChannelCount == 2) { outputMatrix[1] = reverb; } FAudio.FAudioVoice_SetOutputMatrix( - Voice, - ReverbEffect.Voice, - dspSettings.SrcChannelCount, + Handle, + ReverbEffect.Handle, + SourceChannelCount, 1, - dspSettings.pMatrixCoefficients, + (nint) pMatrixCoefficients, 0 ); } @@ -218,225 +257,236 @@ namespace MoonWorks.Audio } } - public unsafe SoundInstance( - AudioDevice device, - ushort formatTag, - ushort bitsPerSample, - ushort blockAlign, - ushort channels, - uint samplesPerSecond - ) : base(device) + public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : 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); + SourceChannelCount = sourceChannelCount; + DestinationChannelCount = destinationChannelCount; + OutputVoice = device.MasteringVoice; + nuint memsize = 4 * sourceChannelCount * destinationChannelCount; + pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize); + 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, Pan, targetValue, duration, 0); + 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, Pan, targetValue, duration, delayTime); + 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; } - public void SetReverb(float 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); } - public void SetReverb(float targetValue, float duration, EasingFunction easingFunction) + /// + /// 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); } - public void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + /// + /// 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); } - 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) + /// + /// Sets the output voice for this voice. + /// + /// Where the output should be sent. + public unsafe void SetOutputVoice(SubmixVoice send) { - dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS(); - dspSettings.DopplerFactor = 1f; - dspSettings.SrcChannelCount = srcChannels; - dspSettings.DstChannelCount = Device.DeviceDetails.OutputFormat.Format.nChannels; + OutputVoice = send; - 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) + if (ReverbEffect != null) { - memPtr[i] = 0; - } - - SetPanMatrixCoefficients(); - } - - private void UpdatePitch() - { - float doppler; - float dopplerScale = Device.DopplerScale; - if (!Is3D || dopplerScale == 0.0f) - { - doppler = 1.0f; + SetReverbEffectChain(ReverbEffect); } else { - doppler = dspSettings.DopplerFactor * dopplerScale; - } + FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[1]; + sendDesc[0].Flags = 0; + sendDesc[0].pOutputVoice = send.Handle; - FAudio.FAudioSourceVoice_SetFrequencyRatio( - Voice, - (float) System.Math.Pow(2.0, pitch) * doppler, - 0 + var sends = new FAudio.FAudioVoiceSends(); + sends.SendCount = 1; + sends.pSends = (nint) sendDesc; + + FAudio.FAudioVoice_SetOutputVoices( + Handle, + ref sends + ); + } + } + + /// + /// Applies a reverb effect chain to this voice. + /// + 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; + } + + /// + /// Removes the reverb effect chain from this voice. + /// + public void RemoveReverbEffectChain() + { + if (ReverbEffect != null) + { + ReverbEffect = null; + reverb = 0; + SetOutputVoice(OutputVoice); + } + } + + /// + /// 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 @@ -449,10 +499,10 @@ namespace MoonWorks.Audio * entire channel; the two channels are blended on each side. * -flibit */ - float* outputMatrix = (float*) dspSettings.pMatrixCoefficients; - if (dspSettings.SrcChannelCount == 1) + float* outputMatrix = (float*) pMatrixCoefficients; + if (SourceChannelCount == 1) { - if (dspSettings.DstChannelCount == 1) + if (DestinationChannelCount == 1) { outputMatrix[0] = 1.0f; } @@ -464,7 +514,7 @@ namespace MoonWorks.Audio } else { - if (dspSettings.DstChannelCount == 1) + if (DestinationChannelCount == 1) { outputMatrix[0] = 1.0f; outputMatrix[1] = 1.0f; @@ -493,16 +543,30 @@ namespace MoonWorks.Audio } } + protected void UpdatePitch() + { + float doppler; + float dopplerScale = Device.DopplerScale; + if (!Is3D || dopplerScale == 0.0f) + { + doppler = 1.0f; + } + else + { + doppler = DopplerFactor * dopplerScale; + } + + FAudio.FAudioSourceVoice_SetFrequencyRatio( + Handle, + (float) System.Math.Pow(2.0, pitch) * doppler, + 0 + ); + } + protected unsafe override void Destroy() { - StopImmediate(); - FAudio.FAudioVoice_DestroyVoice(Voice); - NativeMemory.Free((void*) dspSettings.pMatrixCoefficients); - - if (ReverbEffect != null) - { - NativeMemory.Free((void*) ReverbSends.pSends); - } + NativeMemory.Free(pMatrixCoefficients); + FAudio.FAudioVoice_DestroyVoice(Handle); } } }