diff --git a/lib/FAudio b/lib/FAudio index aaf2568c..63071f2c 160000 --- a/lib/FAudio +++ b/lib/FAudio @@ -1 +1 @@ -Subproject commit aaf2568c3e5b202c5cfbd74734386e69f204482c +Subproject commit 63071f2c309f6fc2193de1c6b85da0e31df80040 diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index a0e4bf74..b61975f7 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading; namespace MoonWorks.Audio @@ -27,7 +28,8 @@ namespace MoonWorks.Audio } private readonly HashSet resources = new HashSet(); - private readonly HashSet autoUpdateStreamingSoundReferences = new HashSet(); + private readonly List autoUpdateStreamingSoundReferences = new List(); + private readonly List autoFreeStaticSoundInstanceReferences = new List(); private AudioTweenManager AudioTweenManager; @@ -150,15 +152,35 @@ namespace MoonWorks.Audio previousTickTime = TickStopwatch.Elapsed.Ticks; float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond; - foreach (var weakReference in autoUpdateStreamingSoundReferences) + for (var i = autoUpdateStreamingSoundReferences.Count - 1; i >= 0; i -= 1) { - if (weakReference.Target is StreamingSound streamingSound) + var weakReference = autoUpdateStreamingSoundReferences[i]; + + if (weakReference.Target is StreamingSound streamingSound && streamingSound.Loaded) { streamingSound.Update(); } else { - autoUpdateStreamingSoundReferences.Remove(weakReference); + autoUpdateStreamingSoundReferences.RemoveAt(i); + } + } + + for (var i = autoFreeStaticSoundInstanceReferences.Count - 1; i >= 0; i -= 1) + { + var weakReference = autoFreeStaticSoundInstanceReferences[i]; + + if (weakReference.Target is StaticSoundInstance staticSoundInstance) + { + if (staticSoundInstance.State == SoundState.Stopped) + { + staticSoundInstance.Free(); + autoFreeStaticSoundInstanceReferences.RemoveAt(i); + } + } + else + { + autoFreeStaticSoundInstanceReferences.RemoveAt(i); } } @@ -213,11 +235,6 @@ namespace MoonWorks.Audio lock (StateLock) { resources.Add(resource.weakReference); - - if (resource is StreamingSound streamingSound && streamingSound.AutoUpdate) - { - AddAutoUpdateStreamingSoundInstance(streamingSound); - } } } @@ -226,22 +243,17 @@ namespace MoonWorks.Audio lock (StateLock) { resources.Remove(resource.weakReference); - - if (resource is StreamingSound streamingSound && streamingSound.AutoUpdate) - { - RemoveAutoUpdateStreamingSoundInstance(streamingSound); - } } } - private void AddAutoUpdateStreamingSoundInstance(StreamingSound instance) + internal void AddAutoUpdateStreamingSoundInstance(StreamingSound instance) { autoUpdateStreamingSoundReferences.Add(instance.weakReference); } - private void RemoveAutoUpdateStreamingSoundInstance(StreamingSound instance) + internal void AddAutoFreeStaticSoundInstance(StaticSoundInstance instance) { - autoUpdateStreamingSoundReferences.Remove(instance.weakReference); + autoFreeStaticSoundInstanceReferences.Add(instance.weakReference); } protected virtual void Dispose(bool disposing) diff --git a/src/Audio/AudioResource.cs b/src/Audio/AudioResource.cs index 045fe264..c8a1360c 100644 --- a/src/Audio/AudioResource.cs +++ b/src/Audio/AudioResource.cs @@ -29,6 +29,7 @@ namespace MoonWorks.Audio if (weakReference != null) { Device.RemoveResourceReference(this); + weakReference.Target = null; weakReference = null; } diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs index 11d98df9..74a82d80 100644 --- a/src/Audio/StaticSound.cs +++ b/src/Audio/StaticSound.cs @@ -17,7 +17,8 @@ namespace MoonWorks.Audio public uint LoopStart { get; set; } = 0; public uint LoopLength { get; set; } = 0; - private Stack Instances = new Stack(); + private Stack AvailableInstances = new Stack(); + private HashSet UsedInstances = new HashSet(); private bool OwnsBuffer; @@ -267,22 +268,25 @@ namespace MoonWorks.Audio /// /// Gets a sound instance from the pool. - /// NOTE: If you lose track of instances, you will create garbage collection pressure! + /// NOTE: If AutoFree is false, you will have to call StaticSoundInstance.Free() yourself or leak the instance! /// - public StaticSoundInstance GetInstance() + public StaticSoundInstance GetInstance(bool autoFree = true) { - if (Instances.Count == 0) + if (AvailableInstances.Count == 0) { - Instances.Push(new StaticSoundInstance(Device, this)); + AvailableInstances.Push(new StaticSoundInstance(Device, this, autoFree)); } - return Instances.Pop(); + var instance = AvailableInstances.Pop(); + UsedInstances.Add(instance); + return instance; } internal void FreeInstance(StaticSoundInstance instance) { instance.Reset(); - Instances.Push(instance); + UsedInstances.Remove(instance); + AvailableInstances.Push(instance); } protected override unsafe void Destroy() diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs index b104a6c3..d958606c 100644 --- a/src/Audio/StaticSoundInstance.cs +++ b/src/Audio/StaticSoundInstance.cs @@ -32,12 +32,21 @@ namespace MoonWorks.Audio } } + public bool AutoFree { get; } + internal StaticSoundInstance( AudioDevice device, - StaticSound parent + StaticSound parent, + bool autoFree ) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond) { Parent = parent; + AutoFree = autoFree; + + if (AutoFree) + { + device.AddAutoFreeStaticSoundInstance(this); + } } public override void Play() @@ -113,9 +122,11 @@ namespace MoonWorks.Audio 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() { - StopImmediate(); Parent.FreeInstance(this); } diff --git a/src/Audio/StreamingSound.cs b/src/Audio/StreamingSound.cs index b5985554..280f37a9 100644 --- a/src/Audio/StreamingSound.cs +++ b/src/Audio/StreamingSound.cs @@ -10,9 +10,6 @@ namespace MoonWorks.Audio /// public abstract class StreamingSound : SoundInstance { - // Should the AudioDevice thread automatically update this class? - public abstract bool AutoUpdate { get; } - // Are we actively consuming buffers? protected bool ConsumingBuffers = false; @@ -24,6 +21,10 @@ namespace MoonWorks.Audio private readonly object StateLock = new object(); + public bool AutoUpdate { get; } + + public abstract bool Loaded { get; } + public unsafe StreamingSound( AudioDevice device, ushort formatTag, @@ -31,7 +32,8 @@ namespace MoonWorks.Audio ushort blockAlign, ushort channels, uint samplesPerSecond, - uint bufferSize + uint bufferSize, + bool autoUpdate // should the AudioDevice thread automatically update this sound? ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) { BufferSize = bufferSize; @@ -41,6 +43,8 @@ namespace MoonWorks.Audio { buffers[i] = (IntPtr) NativeMemory.Alloc(bufferSize); } + + AutoUpdate = autoUpdate; } public override void Play() @@ -57,6 +61,12 @@ namespace MoonWorks.Audio { lock (StateLock) { + if (!Loaded) + { + Logger.LogError("Cannot play StreamingSound before calling Load!"); + return; + } + if (State == SoundState.Playing) { return; @@ -65,6 +75,11 @@ namespace MoonWorks.Audio State = SoundState.Playing; ConsumingBuffers = true; + if (AutoUpdate) + { + Device.AddAutoUpdateStreamingSoundInstance(this); + } + QueueBuffers(); FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); } @@ -192,6 +207,9 @@ namespace MoonWorks.Audio } } + public abstract void Load(); + public abstract void Unload(); + protected unsafe abstract void FillBuffer( void* buffer, int bufferLengthInBytes, /* in bytes */ @@ -208,6 +226,7 @@ namespace MoonWorks.Audio if (!IsDisposed) { StopImmediate(); + Unload(); for (int i = 0; i < BUFFER_COUNT; i += 1) { diff --git a/src/Audio/StreamingSoundOgg.cs b/src/Audio/StreamingSoundOgg.cs index ba04697c..3f02c87f 100644 --- a/src/Audio/StreamingSoundOgg.cs +++ b/src/Audio/StreamingSoundOgg.cs @@ -6,41 +6,38 @@ namespace MoonWorks.Audio { public class StreamingSoundOgg : StreamingSoundSeekable { - private IntPtr VorbisHandle; - private IntPtr FileDataPtr; + private IntPtr FileDataPtr = IntPtr.Zero; + private IntPtr VorbisHandle = IntPtr.Zero; private FAudio.stb_vorbis_info Info; - public override bool AutoUpdate => true; - public unsafe static StreamingSoundOgg Load(AudioDevice device, string filePath) + public override bool Loaded => VorbisHandle != IntPtr.Zero; + private string FilePath; + + public unsafe static StreamingSoundOgg Create(AudioDevice device, string filePath) { - 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 vorbisHandle = FAudio.stb_vorbis_open_memory((IntPtr) fileDataPtr, fileDataSpan.Length, out int error, IntPtr.Zero); + var handle = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero); if (error != 0) { - NativeMemory.Free(fileDataPtr); - Logger.LogError("Error opening OGG file!"); Logger.LogError("Error: " + error); - throw new AudioLoadException("Error opening OGG file!"); + throw new AudioLoadException("Error opening ogg file!"); } - var info = FAudio.stb_vorbis_get_info(vorbisHandle); - return new StreamingSoundOgg( + var info = FAudio.stb_vorbis_get_info(handle); + + var streamingSound = new StreamingSoundOgg( device, - (IntPtr) fileDataPtr, - vorbisHandle, + filePath, info ); + + FAudio.stb_vorbis_close(handle); + + return streamingSound; } internal unsafe StreamingSoundOgg( AudioDevice device, - IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!! - IntPtr vorbisHandle, + string filePath, FAudio.stb_vorbis_info info, uint bufferSize = 32768 ) : base( @@ -50,11 +47,11 @@ namespace MoonWorks.Audio (ushort) (4 * info.channels), (ushort) info.channels, info.sample_rate, - bufferSize + bufferSize, + true ) { - FileDataPtr = fileDataPtr; - VorbisHandle = vorbisHandle; Info = info; + FilePath = filePath; } public override void Seek(uint sampleFrame) @@ -62,6 +59,36 @@ namespace MoonWorks.Audio 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, @@ -82,16 +109,5 @@ namespace MoonWorks.Audio reachedEnd = sampleCount < lengthInFloats; filledLengthInBytes = sampleCount * sizeof(float); } - - protected unsafe override void Destroy() - { - base.Destroy(); - - if (!IsDisposed) - { - FAudio.stb_vorbis_close(VorbisHandle); - NativeMemory.Free((void*) FileDataPtr); - } - } } } diff --git a/src/Audio/StreamingSoundQoa.cs b/src/Audio/StreamingSoundQoa.cs index 178c9897..eb2dd6fe 100644 --- a/src/Audio/StreamingSoundQoa.cs +++ b/src/Audio/StreamingSoundQoa.cs @@ -6,39 +6,31 @@ namespace MoonWorks.Audio { public class StreamingSoundQoa : StreamingSoundSeekable { - private IntPtr QoaHandle; - private IntPtr FileDataPtr; - - public override bool AutoUpdate => true; + private IntPtr QoaHandle = IntPtr.Zero; + private IntPtr FileDataPtr = IntPtr.Zero; uint Channels; uint SamplesPerChannelPerFrame; uint TotalSamplesPerChannel; - public unsafe static StreamingSoundQoa Load(AudioDevice device, string filePath) - { - 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(); + public override bool Loaded => QoaHandle != IntPtr.Zero; + private string FilePath; - var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0); - if (qoaHandle == 0) + public unsafe static StreamingSoundQoa Create(AudioDevice device, string filePath) + { + var handle = FAudio.qoa_open_from_filename(filePath); + if (handle == IntPtr.Zero) { - 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 samplesPerChannelPerFrame, out var totalSamplesPerChannel); + FAudio.qoa_attributes(handle, out var channels, out var samplerate, out var samplesPerChannelPerFrame, out var totalSamplesPerChannel); return new StreamingSoundQoa( device, - (IntPtr) fileDataPtr, - qoaHandle, + filePath, channels, - sampleRate, + samplerate, samplesPerChannelPerFrame, totalSamplesPerChannel ); @@ -46,8 +38,7 @@ namespace MoonWorks.Audio internal unsafe StreamingSoundQoa( AudioDevice device, - IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!! - IntPtr qoaHandle, + string filePath, uint channels, uint samplesPerSecond, uint samplesPerChannelPerFrame, @@ -59,13 +50,13 @@ namespace MoonWorks.Audio (ushort) (2 * channels), (ushort) channels, samplesPerSecond, - samplesPerChannelPerFrame * channels * sizeof(short) + samplesPerChannelPerFrame * channels * sizeof(short), + true ) { - FileDataPtr = fileDataPtr; - QoaHandle = qoaHandle; Channels = channels; SamplesPerChannelPerFrame = samplesPerChannelPerFrame; TotalSamplesPerChannel = totalSamplesPerChannel; + FilePath = filePath; } public override void Seek(uint sampleFrame) @@ -73,6 +64,35 @@ namespace MoonWorks.Audio FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame); } + public override unsafe void Load() + { + var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); + FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length); + var fileDataSpan = new Span((void*) FileDataPtr, (int) fileStream.Length); + fileStream.ReadExactly(fileDataSpan); + fileStream.Close(); + + QoaHandle = FAudio.qoa_open_from_memory((char*) FileDataPtr, (uint) fileDataSpan.Length, 0); + if (QoaHandle == IntPtr.Zero) + { + NativeMemory.Free((void*) FileDataPtr); + Logger.LogError("Error opening QOA file!"); + throw new AudioLoadException("Error opening QOA file!"); + } + } + + public override unsafe void Unload() + { + if (Loaded) + { + FAudio.qoa_close(QoaHandle); + NativeMemory.Free((void*) FileDataPtr); + + QoaHandle = IntPtr.Zero; + FileDataPtr = IntPtr.Zero; + } + } + protected override unsafe void FillBuffer( void* buffer, int bufferLengthInBytes, @@ -88,16 +108,5 @@ namespace MoonWorks.Audio reachedEnd = sampleCount < lengthInShorts; filledLengthInBytes = (int) (sampleCount * sizeof(short)); } - - protected override unsafe void Destroy() - { - base.Destroy(); - - if (!IsDisposed) - { - FAudio.qoa_close(QoaHandle); - NativeMemory.Free((void*) FileDataPtr); - } - } } } diff --git a/src/Audio/StreamingSoundSeekable.cs b/src/Audio/StreamingSoundSeekable.cs index 9f0f7185..2bc4905e 100644 --- a/src/Audio/StreamingSoundSeekable.cs +++ b/src/Audio/StreamingSoundSeekable.cs @@ -11,7 +11,8 @@ namespace MoonWorks.Audio ushort blockAlign, ushort channels, uint samplesPerSecond, - uint bufferSize + uint bufferSize, + bool autoUpdate ) : base( device, formatTag, @@ -19,7 +20,8 @@ namespace MoonWorks.Audio blockAlign, channels, samplesPerSecond, - bufferSize + bufferSize, + autoUpdate ) { } diff --git a/src/Graphics/GraphicsResource.cs b/src/Graphics/GraphicsResource.cs index 0819a637..8b53a269 100644 --- a/src/Graphics/GraphicsResource.cs +++ b/src/Graphics/GraphicsResource.cs @@ -31,6 +31,7 @@ namespace MoonWorks.Graphics { QueueDestroyFunction(Device.Handle, Handle); Device.RemoveResourceReference(weakReference); + weakReference.SetTarget(null); weakReference = null; } diff --git a/src/Video/StreamingSoundTheora.cs b/src/Video/StreamingSoundTheora.cs index a830c334..95f8e576 100644 --- a/src/Video/StreamingSoundTheora.cs +++ b/src/Video/StreamingSoundTheora.cs @@ -3,12 +3,11 @@ using MoonWorks.Audio; namespace MoonWorks.Video { - public unsafe class StreamingSoundTheora : StreamingSound + // TODO: should we just not handle theora sound? it sucks! + internal unsafe class StreamingSoundTheora : StreamingSound { private IntPtr VideoHandle; - - // Theorafile is not thread safe, so let's update on the main thread. - public override bool AutoUpdate => false; + public override bool Loaded => true; internal StreamingSoundTheora( AudioDevice device, @@ -23,11 +22,22 @@ namespace MoonWorks.Video (ushort) (4 * channels), (ushort) channels, sampleRate, - bufferSize + bufferSize, + false // Theorafile is not thread safe, so let's update on the main thread ) { VideoHandle = videoHandle; } + public override unsafe void Load() + { + // no-op + } + + public override unsafe void Unload() + { + // no-op + } + protected override unsafe void FillBuffer( void* buffer, int bufferLengthInBytes,