Sound instancing rework

remotes/1695061714407202320/main
cosmonaut 2023-05-11 17:56:40 -07:00
parent 537517afb9
commit 5df08727c1
11 changed files with 192 additions and 107 deletions

@ -1 +1 @@
Subproject commit aaf2568c3e5b202c5cfbd74734386e69f204482c Subproject commit 63071f2c309f6fc2193de1c6b85da0e31df80040

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
namespace MoonWorks.Audio namespace MoonWorks.Audio
@ -27,7 +28,8 @@ namespace MoonWorks.Audio
} }
private readonly HashSet<WeakReference> resources = new HashSet<WeakReference>(); private readonly HashSet<WeakReference> resources = new HashSet<WeakReference>();
private readonly HashSet<WeakReference> autoUpdateStreamingSoundReferences = new HashSet<WeakReference>(); private readonly List<WeakReference> autoUpdateStreamingSoundReferences = new List<WeakReference>();
private readonly List<WeakReference> autoFreeStaticSoundInstanceReferences = new List<WeakReference>();
private AudioTweenManager AudioTweenManager; private AudioTweenManager AudioTweenManager;
@ -150,15 +152,35 @@ namespace MoonWorks.Audio
previousTickTime = TickStopwatch.Elapsed.Ticks; previousTickTime = TickStopwatch.Elapsed.Ticks;
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond; 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(); streamingSound.Update();
} }
else 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) lock (StateLock)
{ {
resources.Add(resource.weakReference); resources.Add(resource.weakReference);
if (resource is StreamingSound streamingSound && streamingSound.AutoUpdate)
{
AddAutoUpdateStreamingSoundInstance(streamingSound);
}
} }
} }
@ -226,22 +243,17 @@ namespace MoonWorks.Audio
lock (StateLock) lock (StateLock)
{ {
resources.Remove(resource.weakReference); 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); 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) protected virtual void Dispose(bool disposing)

View File

@ -29,6 +29,7 @@ namespace MoonWorks.Audio
if (weakReference != null) if (weakReference != null)
{ {
Device.RemoveResourceReference(this); Device.RemoveResourceReference(this);
weakReference.Target = null;
weakReference = null; weakReference = null;
} }

View File

@ -17,7 +17,8 @@ namespace MoonWorks.Audio
public uint LoopStart { get; set; } = 0; public uint LoopStart { get; set; } = 0;
public uint LoopLength { get; set; } = 0; public uint LoopLength { get; set; } = 0;
private Stack<StaticSoundInstance> Instances = new Stack<StaticSoundInstance>(); private Stack<StaticSoundInstance> AvailableInstances = new Stack<StaticSoundInstance>();
private HashSet<StaticSoundInstance> UsedInstances = new HashSet<StaticSoundInstance>();
private bool OwnsBuffer; private bool OwnsBuffer;
@ -267,22 +268,25 @@ namespace MoonWorks.Audio
/// <summary> /// <summary>
/// Gets a sound instance from the pool. /// 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!
/// </summary> /// </summary>
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) internal void FreeInstance(StaticSoundInstance instance)
{ {
instance.Reset(); instance.Reset();
Instances.Push(instance); UsedInstances.Remove(instance);
AvailableInstances.Push(instance);
} }
protected override unsafe void Destroy() protected override unsafe void Destroy()

View File

@ -32,12 +32,21 @@ namespace MoonWorks.Audio
} }
} }
public bool AutoFree { get; }
internal StaticSoundInstance( internal StaticSoundInstance(
AudioDevice device, AudioDevice device,
StaticSound parent StaticSound parent,
bool autoFree
) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond) ) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond)
{ {
Parent = parent; Parent = parent;
AutoFree = autoFree;
if (AutoFree)
{
device.AddAutoFreeStaticSoundInstance(this);
}
} }
public override void Play() public override void Play()
@ -113,9 +122,11 @@ namespace MoonWorks.Audio
Parent.Handle.PlayBegin = sampleFrame; 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() public void Free()
{ {
StopImmediate();
Parent.FreeInstance(this); Parent.FreeInstance(this);
} }

View File

@ -10,9 +10,6 @@ namespace MoonWorks.Audio
/// </summary> /// </summary>
public abstract class StreamingSound : SoundInstance public abstract class StreamingSound : SoundInstance
{ {
// Should the AudioDevice thread automatically update this class?
public abstract bool AutoUpdate { get; }
// Are we actively consuming buffers? // Are we actively consuming buffers?
protected bool ConsumingBuffers = false; protected bool ConsumingBuffers = false;
@ -24,6 +21,10 @@ namespace MoonWorks.Audio
private readonly object StateLock = new object(); private readonly object StateLock = new object();
public bool AutoUpdate { get; }
public abstract bool Loaded { get; }
public unsafe StreamingSound( public unsafe StreamingSound(
AudioDevice device, AudioDevice device,
ushort formatTag, ushort formatTag,
@ -31,7 +32,8 @@ namespace MoonWorks.Audio
ushort blockAlign, ushort blockAlign,
ushort channels, ushort channels,
uint samplesPerSecond, uint samplesPerSecond,
uint bufferSize uint bufferSize,
bool autoUpdate // should the AudioDevice thread automatically update this sound?
) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
{ {
BufferSize = bufferSize; BufferSize = bufferSize;
@ -41,6 +43,8 @@ namespace MoonWorks.Audio
{ {
buffers[i] = (IntPtr) NativeMemory.Alloc(bufferSize); buffers[i] = (IntPtr) NativeMemory.Alloc(bufferSize);
} }
AutoUpdate = autoUpdate;
} }
public override void Play() public override void Play()
@ -57,6 +61,12 @@ namespace MoonWorks.Audio
{ {
lock (StateLock) lock (StateLock)
{ {
if (!Loaded)
{
Logger.LogError("Cannot play StreamingSound before calling Load!");
return;
}
if (State == SoundState.Playing) if (State == SoundState.Playing)
{ {
return; return;
@ -65,6 +75,11 @@ namespace MoonWorks.Audio
State = SoundState.Playing; State = SoundState.Playing;
ConsumingBuffers = true; ConsumingBuffers = true;
if (AutoUpdate)
{
Device.AddAutoUpdateStreamingSoundInstance(this);
}
QueueBuffers(); QueueBuffers();
FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); 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( protected unsafe abstract void FillBuffer(
void* buffer, void* buffer,
int bufferLengthInBytes, /* in bytes */ int bufferLengthInBytes, /* in bytes */
@ -208,6 +226,7 @@ namespace MoonWorks.Audio
if (!IsDisposed) if (!IsDisposed)
{ {
StopImmediate(); StopImmediate();
Unload();
for (int i = 0; i < BUFFER_COUNT; i += 1) for (int i = 0; i < BUFFER_COUNT; i += 1)
{ {

View File

@ -6,41 +6,38 @@ namespace MoonWorks.Audio
{ {
public class StreamingSoundOgg : StreamingSoundSeekable public class StreamingSoundOgg : StreamingSoundSeekable
{ {
private IntPtr VorbisHandle; private IntPtr FileDataPtr = IntPtr.Zero;
private IntPtr FileDataPtr; private IntPtr VorbisHandle = IntPtr.Zero;
private FAudio.stb_vorbis_info Info; 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 handle = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero);
var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length);
var fileDataSpan = new Span<byte>(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);
if (error != 0) if (error != 0)
{ {
NativeMemory.Free(fileDataPtr);
Logger.LogError("Error opening OGG file!");
Logger.LogError("Error: " + error); 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, device,
(IntPtr) fileDataPtr, filePath,
vorbisHandle,
info info
); );
FAudio.stb_vorbis_close(handle);
return streamingSound;
} }
internal unsafe StreamingSoundOgg( internal unsafe StreamingSoundOgg(
AudioDevice device, AudioDevice device,
IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!! string filePath,
IntPtr vorbisHandle,
FAudio.stb_vorbis_info info, FAudio.stb_vorbis_info info,
uint bufferSize = 32768 uint bufferSize = 32768
) : base( ) : base(
@ -50,11 +47,11 @@ namespace MoonWorks.Audio
(ushort) (4 * info.channels), (ushort) (4 * info.channels),
(ushort) info.channels, (ushort) info.channels,
info.sample_rate, info.sample_rate,
bufferSize bufferSize,
true
) { ) {
FileDataPtr = fileDataPtr;
VorbisHandle = vorbisHandle;
Info = info; Info = info;
FilePath = filePath;
} }
public override void Seek(uint sampleFrame) public override void Seek(uint sampleFrame)
@ -62,6 +59,36 @@ namespace MoonWorks.Audio
FAudio.stb_vorbis_seek(VorbisHandle, 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<byte>((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( protected unsafe override void FillBuffer(
void* buffer, void* buffer,
int bufferLengthInBytes, int bufferLengthInBytes,
@ -82,16 +109,5 @@ namespace MoonWorks.Audio
reachedEnd = sampleCount < lengthInFloats; reachedEnd = sampleCount < lengthInFloats;
filledLengthInBytes = sampleCount * sizeof(float); filledLengthInBytes = sampleCount * sizeof(float);
} }
protected unsafe override void Destroy()
{
base.Destroy();
if (!IsDisposed)
{
FAudio.stb_vorbis_close(VorbisHandle);
NativeMemory.Free((void*) FileDataPtr);
}
}
} }
} }

View File

@ -6,39 +6,31 @@ namespace MoonWorks.Audio
{ {
public class StreamingSoundQoa : StreamingSoundSeekable public class StreamingSoundQoa : StreamingSoundSeekable
{ {
private IntPtr QoaHandle; private IntPtr QoaHandle = IntPtr.Zero;
private IntPtr FileDataPtr; private IntPtr FileDataPtr = IntPtr.Zero;
public override bool AutoUpdate => true;
uint Channels; uint Channels;
uint SamplesPerChannelPerFrame; uint SamplesPerChannelPerFrame;
uint TotalSamplesPerChannel; uint TotalSamplesPerChannel;
public unsafe static StreamingSoundQoa Load(AudioDevice device, string filePath) public override bool Loaded => QoaHandle != IntPtr.Zero;
{ private string FilePath;
var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length);
var fileDataSpan = new Span<byte>(fileDataPtr, (int) fileStream.Length);
fileStream.ReadExactly(fileDataSpan);
fileStream.Close();
var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0); public unsafe static StreamingSoundQoa Create(AudioDevice device, string filePath)
if (qoaHandle == 0) {
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!"); 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( return new StreamingSoundQoa(
device, device,
(IntPtr) fileDataPtr, filePath,
qoaHandle,
channels, channels,
sampleRate, samplerate,
samplesPerChannelPerFrame, samplesPerChannelPerFrame,
totalSamplesPerChannel totalSamplesPerChannel
); );
@ -46,8 +38,7 @@ namespace MoonWorks.Audio
internal unsafe StreamingSoundQoa( internal unsafe StreamingSoundQoa(
AudioDevice device, AudioDevice device,
IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!! string filePath,
IntPtr qoaHandle,
uint channels, uint channels,
uint samplesPerSecond, uint samplesPerSecond,
uint samplesPerChannelPerFrame, uint samplesPerChannelPerFrame,
@ -59,13 +50,13 @@ namespace MoonWorks.Audio
(ushort) (2 * channels), (ushort) (2 * channels),
(ushort) channels, (ushort) channels,
samplesPerSecond, samplesPerSecond,
samplesPerChannelPerFrame * channels * sizeof(short) samplesPerChannelPerFrame * channels * sizeof(short),
true
) { ) {
FileDataPtr = fileDataPtr;
QoaHandle = qoaHandle;
Channels = channels; Channels = channels;
SamplesPerChannelPerFrame = samplesPerChannelPerFrame; SamplesPerChannelPerFrame = samplesPerChannelPerFrame;
TotalSamplesPerChannel = totalSamplesPerChannel; TotalSamplesPerChannel = totalSamplesPerChannel;
FilePath = filePath;
} }
public override void Seek(uint sampleFrame) public override void Seek(uint sampleFrame)
@ -73,6 +64,35 @@ namespace MoonWorks.Audio
FAudio.qoa_seek_frame(QoaHandle, (int) 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<byte>((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( protected override unsafe void FillBuffer(
void* buffer, void* buffer,
int bufferLengthInBytes, int bufferLengthInBytes,
@ -88,16 +108,5 @@ namespace MoonWorks.Audio
reachedEnd = sampleCount < lengthInShorts; reachedEnd = sampleCount < lengthInShorts;
filledLengthInBytes = (int) (sampleCount * sizeof(short)); filledLengthInBytes = (int) (sampleCount * sizeof(short));
} }
protected override unsafe void Destroy()
{
base.Destroy();
if (!IsDisposed)
{
FAudio.qoa_close(QoaHandle);
NativeMemory.Free((void*) FileDataPtr);
}
}
} }
} }

View File

@ -11,7 +11,8 @@ namespace MoonWorks.Audio
ushort blockAlign, ushort blockAlign,
ushort channels, ushort channels,
uint samplesPerSecond, uint samplesPerSecond,
uint bufferSize uint bufferSize,
bool autoUpdate
) : base( ) : base(
device, device,
formatTag, formatTag,
@ -19,7 +20,8 @@ namespace MoonWorks.Audio
blockAlign, blockAlign,
channels, channels,
samplesPerSecond, samplesPerSecond,
bufferSize bufferSize,
autoUpdate
) { ) {
} }

View File

@ -31,6 +31,7 @@ namespace MoonWorks.Graphics
{ {
QueueDestroyFunction(Device.Handle, Handle); QueueDestroyFunction(Device.Handle, Handle);
Device.RemoveResourceReference(weakReference); Device.RemoveResourceReference(weakReference);
weakReference.SetTarget(null);
weakReference = null; weakReference = null;
} }

View File

@ -3,12 +3,11 @@ using MoonWorks.Audio;
namespace MoonWorks.Video 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; private IntPtr VideoHandle;
public override bool Loaded => true;
// Theorafile is not thread safe, so let's update on the main thread.
public override bool AutoUpdate => false;
internal StreamingSoundTheora( internal StreamingSoundTheora(
AudioDevice device, AudioDevice device,
@ -23,11 +22,22 @@ namespace MoonWorks.Video
(ushort) (4 * channels), (ushort) (4 * channels),
(ushort) channels, (ushort) channels,
sampleRate, sampleRate,
bufferSize bufferSize,
false // Theorafile is not thread safe, so let's update on the main thread
) { ) {
VideoHandle = videoHandle; VideoHandle = videoHandle;
} }
public override unsafe void Load()
{
// no-op
}
public override unsafe void Unload()
{
// no-op
}
protected override unsafe void FillBuffer( protected override unsafe void FillBuffer(
void* buffer, void* buffer,
int bufferLengthInBytes, int bufferLengthInBytes,