Sound instancing rework

pull/49/head
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.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
namespace MoonWorks.Audio
@ -27,7 +28,8 @@ namespace MoonWorks.Audio
}
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;
@ -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)

View File

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

View File

@ -17,7 +17,8 @@ namespace MoonWorks.Audio
public uint LoopStart { 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;
@ -267,22 +268,25 @@ namespace MoonWorks.Audio
/// <summary>
/// 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>
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()

View File

@ -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);
}

View File

@ -10,9 +10,6 @@ namespace MoonWorks.Audio
/// </summary>
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)
{

View File

@ -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<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);
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<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(
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);
}
}
}
}

View File

@ -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<byte>(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<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(
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);
}
}
}
}

View File

@ -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
) {
}

View File

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

View File

@ -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,