From 0cd2c799ee0a6cd797cd089b2019c91ee8fd9b69 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Thu, 3 Aug 2023 19:54:02 +0000 Subject: [PATCH] Audio Restructuring (#50) This is a complete redesign of the MoonWorks Audio API. Voices are the new major concept. All Voices can be configured with volume, pitch, filters, panning and reverb. SourceVoices take in AudioBuffers and use them to play sound. They contain their own playback state. There are multiple kinds of SourceVoices: TransientVoice: Used for short sound effects where the client will not be keeping track of a reference over multiple frames. PersistentVoice: Used when the client needs to hold on to a Voice reference long-term. StreamingVoice: Used for playing back AudioDataStreamable objects. SoundSequence: Used to play back a series of AudioBuffers in sequence. They have a callback so that AudioBuffers can be added dynamically by the client. SourceVoices are intended to be pooled. You can obtain one from the AudioDevice pool by calling AudioDevice.Obtain where T is the type of SourceVoice you wish to obtain. When you call Return on the voice it will be returned to the pool. TransientVoices are automatically returned to the pool when they have finished playing back their AudioBuffer. SourceVoices can send audio to SubmixVoices. This is a convenient way to manage categories of audio. For example the client could have a MusicSubmix that all music-related voices send to. Then the volume of all music can be changed at once without the client having to manage all the individual music voices. By default all voices send audio to AudioDevice.MasteringVoice. This is also a SubmixVoice that can be controlled like any other voice. AudioDataStreamable is used in conjunction with a StreamingVoice to play back streaming audio from an ogg or qoa file. AudioDataWav, AudioDataOgg, and AudioDataQoa all have a static CreateBuffer method that can be used to create an AudioBuffer from an audio file. Reviewed-on: https://gitea.moonside.games/MoonsideGames/MoonWorks/pulls/50 --- src/Audio/AudioBuffer.cs | 71 ++++ src/Audio/AudioDataOgg.cs | 138 +++++++ src/Audio/AudioDataQoa.cs | 155 +++++++ src/Audio/AudioDataStreamable.cs | 45 ++ src/Audio/AudioDataWav.cs | 100 +++++ src/Audio/AudioDevice.cs | 168 ++++---- src/Audio/AudioTween.cs | 4 +- src/Audio/AudioTweenManager.cs | 36 +- src/Audio/AudioUtils.cs | 42 -- src/Audio/Format.cs | 33 ++ src/Audio/IPoolable.cs | 7 + src/Audio/PersistentVoice.cs | 28 ++ src/Audio/ReverbEffect.cs | 63 +-- src/Audio/SoundQueue.cs | 130 ------ src/Audio/SoundSequence.cs | 56 +++ src/Audio/SourceVoice.cs | 228 +++++++++++ src/Audio/SourceVoicePool.cs | 39 ++ src/Audio/StaticSound.cs | 332 --------------- src/Audio/StaticSoundInstance.cs | 141 ------- src/Audio/StreamingSound.cs | 239 ----------- src/Audio/StreamingSoundOgg.cs | 113 ------ src/Audio/StreamingSoundQoa.cs | 137 ------- src/Audio/StreamingSoundSeekable.cs | 40 -- src/Audio/StreamingVoice.cs | 150 +++++++ src/Audio/SubmixVoice.cs | 31 ++ src/Audio/TransientVoice.cs | 29 ++ src/Audio/{SoundInstance.cs => Voice.cs} | 496 +++++++++++++---------- 27 files changed, 1513 insertions(+), 1538 deletions(-) create mode 100644 src/Audio/AudioBuffer.cs create mode 100644 src/Audio/AudioDataOgg.cs create mode 100644 src/Audio/AudioDataQoa.cs create mode 100644 src/Audio/AudioDataStreamable.cs create mode 100644 src/Audio/AudioDataWav.cs delete mode 100644 src/Audio/AudioUtils.cs create mode 100644 src/Audio/Format.cs create mode 100644 src/Audio/IPoolable.cs create mode 100644 src/Audio/PersistentVoice.cs delete mode 100644 src/Audio/SoundQueue.cs create mode 100644 src/Audio/SoundSequence.cs create mode 100644 src/Audio/SourceVoice.cs create mode 100644 src/Audio/SourceVoicePool.cs delete mode 100644 src/Audio/StaticSound.cs delete mode 100644 src/Audio/StaticSoundInstance.cs delete mode 100644 src/Audio/StreamingSound.cs delete mode 100644 src/Audio/StreamingSoundOgg.cs delete mode 100644 src/Audio/StreamingSoundQoa.cs delete mode 100644 src/Audio/StreamingSoundSeekable.cs create mode 100644 src/Audio/StreamingVoice.cs create mode 100644 src/Audio/SubmixVoice.cs create mode 100644 src/Audio/TransientVoice.cs rename src/Audio/{SoundInstance.cs => Voice.cs} (51%) 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); } } }