Audio Improvements (#47)

- Audio is now processed on a background thread instead of the main thread
- Audio tick rate is now ~200Hz
- MoonWorks.Math.Easings class completely rewritten to be easier to understand and use
- SoundInstance properties no longer call into FAudio unless the value actually changed
- SoundInstance property values can now be interpolated over time (tweens)
- SoundInstance tweens can be delayed
- SoundInstance sets a sane filter frequency default when switching filter type
- StreamingSound classes can be designated to update automatically on the audio thread or manually
- StreamingSound buffer consumption should now set Stopped state in a more sane way
- Added ReverbEffect, which creates a submix voice for a reverb effect
- SoundInstance can apply a ReverbEffect, which enables the Reverb property
- Audio resource tracking improvements
- Some tweaks to VideoPlayer to make its behavior more consistent

Reviewed-on: MoonsideGames/MoonWorks#47
remotes/1695061714407202320/main
cosmonaut 2023-03-07 23:28:57 +00:00
parent f8b14ea94f
commit 1f0e3b5040
16 changed files with 1181 additions and 760 deletions

View File

@ -1,6 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Threading;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
@ -26,13 +26,25 @@ namespace MoonWorks.Audio
} }
} }
private readonly List<WeakReference<AudioResource>> resources = new List<WeakReference<AudioResource>>(); private readonly HashSet<WeakReference> resources = new HashSet<WeakReference>();
private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>(); private readonly HashSet<WeakReference> autoUpdateStreamingSoundReferences = new HashSet<WeakReference>();
private AudioTweenManager AudioTweenManager;
private const int Step = 200;
private TimeSpan UpdateInterval;
private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch();
private long previousTickTime;
private Thread Thread;
private AutoResetEvent WakeSignal;
internal readonly object StateLock = new object();
private bool IsDisposed; private bool IsDisposed;
public unsafe AudioDevice() public unsafe AudioDevice()
{ {
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR); FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
Handle = handle; Handle = handle;
@ -90,8 +102,8 @@ namespace MoonWorks.Audio
) != 0) ) != 0)
{ {
Logger.LogError("No mastering voice found!"); Logger.LogError("No mastering voice found!");
Handle = IntPtr.Zero;
FAudio.FAudio_Release(Handle); FAudio.FAudio_Release(Handle);
Handle = IntPtr.Zero;
return; return;
} }
@ -105,22 +117,52 @@ namespace MoonWorks.Audio
SpeedOfSound, SpeedOfSound,
Handle3D Handle3D
); );
AudioTweenManager = new AudioTweenManager();
Logger.LogInfo("Setting up audio thread...");
WakeSignal = new AutoResetEvent(true);
Thread = new Thread(ThreadMain);
Thread.IsBackground = true;
Thread.Start();
TickStopwatch.Start();
previousTickTime = 0;
} }
internal void Update() private void ThreadMain()
{ {
for (var i = streamingSounds.Count - 1; i >= 0; i--) while (!IsDisposed)
{ {
var weakReference = streamingSounds[i]; lock (StateLock)
if (weakReference.TryGetTarget(out var streamingSound)) {
ThreadMainTick();
}
WakeSignal.WaitOne(UpdateInterval);
}
}
private void ThreadMainTick()
{
long tickDelta = TickStopwatch.Elapsed.Ticks - previousTickTime;
previousTickTime = TickStopwatch.Elapsed.Ticks;
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond;
foreach (var weakReference in autoUpdateStreamingSoundReferences)
{
if (weakReference.Target is StreamingSound streamingSound)
{ {
streamingSound.Update(); streamingSound.Update();
} }
else else
{ {
streamingSounds.RemoveAt(i); autoUpdateStreamingSoundReferences.Remove(weakReference);
} }
} }
AudioTweenManager.Update(elapsedSeconds);
} }
public void SyncPlay() public void SyncPlay()
@ -128,40 +170,95 @@ namespace MoonWorks.Audio
FAudio.FAudio_CommitChanges(Handle, 1); FAudio.FAudio_CommitChanges(Handle, 1);
} }
internal void AddDynamicSoundInstance(StreamingSound instance) internal void CreateTween(
SoundInstance soundInstance,
AudioTweenProperty property,
System.Func<float, float> easingFunction,
float start,
float end,
float duration,
float delayTime
) {
lock (StateLock)
{ {
streamingSounds.Add(new WeakReference<StreamingSound>(instance)); AudioTweenManager.CreateTween(
} soundInstance,
property,
internal void AddResourceReference(WeakReference<AudioResource> resourceReference) easingFunction,
{ start,
lock (resources) end,
{ duration,
resources.Add(resourceReference); delayTime
);
} }
} }
internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference) internal void ClearTweens(
WeakReference soundReference,
AudioTweenProperty property
) {
lock (StateLock)
{ {
lock (resources) AudioTweenManager.ClearTweens(soundReference, property);
{
resources.Remove(resourceReference);
} }
} }
internal void WakeThread()
{
WakeSignal.Set();
}
internal void AddResourceReference(AudioResource resource)
{
lock (StateLock)
{
resources.Add(resource.weakReference);
if (resource is StreamingSound streamingSound && streamingSound.AutoUpdate)
{
AddAutoUpdateStreamingSoundInstance(streamingSound);
}
}
}
internal void RemoveResourceReference(AudioResource resource)
{
lock (StateLock)
{
resources.Remove(resource.weakReference);
if (resource is StreamingSound streamingSound && streamingSound.AutoUpdate)
{
RemoveAutoUpdateStreamingSoundInstance(streamingSound);
}
}
}
private void AddAutoUpdateStreamingSoundInstance(StreamingSound instance)
{
autoUpdateStreamingSoundReferences.Add(instance.weakReference);
}
private void RemoveAutoUpdateStreamingSoundInstance(StreamingSound instance)
{
autoUpdateStreamingSoundReferences.Remove(instance.weakReference);
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (!IsDisposed) if (!IsDisposed)
{
lock (StateLock)
{ {
if (disposing) if (disposing)
{ {
for (var i = resources.Count - 1; i >= 0; i--) foreach (var weakReference in resources)
{ {
var weakReference = resources[i]; var target = weakReference.Target;
if (weakReference.TryGetTarget(out var resource)) if (target != null)
{ {
resource.Dispose(); (target as IDisposable).Dispose();
} }
} }
resources.Clear(); resources.Clear();
@ -173,8 +270,8 @@ namespace MoonWorks.Audio
IsDisposed = true; IsDisposed = true;
} }
} }
}
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
~AudioDevice() ~AudioDevice()
{ {
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method

View File

@ -8,14 +8,14 @@ namespace MoonWorks.Audio
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
private WeakReference<AudioResource> selfReference; internal WeakReference weakReference;
public AudioResource(AudioDevice device) public AudioResource(AudioDevice device)
{ {
Device = device; Device = device;
selfReference = new WeakReference<AudioResource>(this); weakReference = new WeakReference(this);
Device.AddResourceReference(selfReference); Device.AddResourceReference(this);
} }
protected abstract void Destroy(); protected abstract void Destroy();
@ -26,10 +26,10 @@ namespace MoonWorks.Audio
{ {
Destroy(); Destroy();
if (selfReference != null) if (weakReference != null)
{ {
Device.RemoveResourceReference(selfReference); Device.RemoveResourceReference(this);
selfReference = null; weakReference = null;
} }
IsDisposed = true; IsDisposed = true;

57
src/Audio/AudioTween.cs Normal file
View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using EasingFunction = System.Func<float, float>;
namespace MoonWorks.Audio
{
internal enum AudioTweenProperty
{
Pan,
Pitch,
Volume,
FilterFrequency,
Reverb
}
internal class AudioTween
{
public System.WeakReference SoundInstanceReference;
public AudioTweenProperty Property;
public EasingFunction EasingFunction;
public float Time;
public float StartValue;
public float EndValue;
public float DelayTime;
public float Duration;
}
internal class AudioTweenPool
{
private Queue<AudioTween> Tweens = new Queue<AudioTween>(16);
public AudioTweenPool()
{
for (int i = 0; i < 16; i += 1)
{
Tweens.Enqueue(new AudioTween());
}
}
public AudioTween Obtain()
{
if (Tweens.Count > 0)
{
var tween = Tweens.Dequeue();
return tween;
}
else
{
return new AudioTween();
}
}
public void Free(AudioTween tween)
{
Tweens.Enqueue(tween);
}
}
}

View File

@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
namespace MoonWorks.Audio
{
internal class AudioTweenManager
{
private AudioTweenPool AudioTweenPool = new AudioTweenPool();
private readonly Dictionary<(WeakReference, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(WeakReference, AudioTweenProperty), AudioTween>();
private readonly List<AudioTween> DelayedAudioTweens = new List<AudioTween>();
public void Update(float elapsedSeconds)
{
for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--)
{
var audioTween = DelayedAudioTweens[i];
if (audioTween.SoundInstanceReference.Target is SoundInstance soundInstance)
{
audioTween.Time += elapsedSeconds;
if (audioTween.Time >= audioTween.DelayTime)
{
// set the tween start value to the current value of the property
switch (audioTween.Property)
{
case AudioTweenProperty.Pan:
audioTween.StartValue = soundInstance.Pan;
break;
case AudioTweenProperty.Pitch:
audioTween.StartValue = soundInstance.Pitch;
break;
case AudioTweenProperty.Volume:
audioTween.StartValue = soundInstance.Volume;
break;
case AudioTweenProperty.FilterFrequency:
audioTween.StartValue = soundInstance.FilterFrequency;
break;
case AudioTweenProperty.Reverb:
audioTween.StartValue = soundInstance.Reverb;
break;
}
audioTween.Time = 0;
DelayedAudioTweens.RemoveAt(i);
AddTween(audioTween);
}
}
else
{
AudioTweenPool.Free(audioTween);
DelayedAudioTweens.RemoveAt(i);
}
}
foreach (var (key, audioTween) in AudioTweens)
{
bool finished = true;
if (audioTween.SoundInstanceReference.Target is SoundInstance soundInstance)
{
finished = UpdateAudioTween(audioTween, soundInstance, elapsedSeconds);
}
if (finished)
{
AudioTweenPool.Free(audioTween);
AudioTweens.Remove(key);
}
}
}
public void CreateTween(
SoundInstance soundInstance,
AudioTweenProperty property,
System.Func<float, float> easingFunction,
float start,
float end,
float duration,
float delayTime
) {
var tween = AudioTweenPool.Obtain();
tween.SoundInstanceReference = soundInstance.weakReference;
tween.Property = property;
tween.EasingFunction = easingFunction;
tween.StartValue = start;
tween.EndValue = end;
tween.Duration = duration;
tween.Time = 0;
tween.DelayTime = delayTime;
if (delayTime == 0)
{
AddTween(tween);
}
else
{
DelayedAudioTweens.Add(tween);
}
}
public void ClearTweens(WeakReference soundReference, AudioTweenProperty property)
{
AudioTweens.Remove((soundReference, 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.SoundInstanceReference, audioTween.Property), out var currentTween))
{
AudioTweenPool.Free(currentTween);
}
AudioTweens[(audioTween.SoundInstanceReference, audioTween.Property)] = audioTween;
}
private static bool UpdateAudioTween(AudioTween audioTween, SoundInstance soundInstance, float delta)
{
float value;
audioTween.Time += delta;
var finished = audioTween.Time >= audioTween.Duration;
if (finished)
{
value = audioTween.EndValue;
}
else
{
value = MoonWorks.Math.Easing.Interp(
audioTween.StartValue,
audioTween.EndValue,
audioTween.Time,
audioTween.Duration,
audioTween.EasingFunction
);
}
switch (audioTween.Property)
{
case AudioTweenProperty.Pan:
soundInstance.Pan = value;
break;
case AudioTweenProperty.Pitch:
soundInstance.Pitch = value;
break;
case AudioTweenProperty.Volume:
soundInstance.Volume = value;
break;
case AudioTweenProperty.FilterFrequency:
soundInstance.FilterFrequency = value;
break;
case AudioTweenProperty.Reverb:
soundInstance.Reverb = value;
break;
}
return finished;
}
}
}

View File

@ -4,14 +4,12 @@ using System.Runtime.InteropServices;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
// sound instances can send their audio to this voice to add reverb // sound instances can send their audio to this voice to add reverb
public unsafe class ReverbEffect : IDisposable public unsafe class ReverbEffect : AudioResource
{ {
private IntPtr voice; private IntPtr voice;
public IntPtr Voice => voice; public IntPtr Voice => voice;
private bool disposedValue; public ReverbEffect(AudioDevice audioDevice) : base(audioDevice)
public ReverbEffect(AudioDevice audioDevice)
{ {
/* Init reverb */ /* Init reverb */
@ -97,32 +95,9 @@ namespace MoonWorks.Audio
} }
} }
protected virtual void Dispose(bool disposing) protected override void Destroy()
{ {
if (!disposedValue) FAudio.FAudioVoice_DestroyVoice(Voice);
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
}
FAudio.FAudioVoice_DestroyVoice(voice);
disposedValue = true;
}
}
~ReverbEffect()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -1,12 +1,15 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using EasingFunction = System.Func<float, float>;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public abstract class SoundInstance : AudioResource public abstract class SoundInstance : AudioResource
{ {
internal IntPtr Voice; internal IntPtr Voice;
internal FAudio.FAudioWaveFormatEx Format;
private FAudio.FAudioWaveFormatEx format;
public FAudio.FAudioWaveFormatEx Format => format;
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
@ -21,7 +24,10 @@ namespace MoonWorks.Audio
public float Pan public float Pan
{ {
get => pan; get => pan;
set internal set
{
value = Math.MathHelper.Clamp(value, -1f, 1f);
if (pan != value)
{ {
pan = value; pan = value;
@ -47,28 +53,37 @@ namespace MoonWorks.Audio
); );
} }
} }
}
private float pitch = 0; private float pitch = 0;
public float Pitch public float Pitch
{ {
get => pitch; get => pitch;
set internal set
{ {
pitch = Math.MathHelper.Clamp(value, -1f, 1f); value = Math.MathHelper.Clamp(value, -1f, 1f);
if (pitch != value)
{
pitch = value;
UpdatePitch(); UpdatePitch();
} }
} }
}
private float volume = 1; private float volume = 1;
public float Volume public float Volume
{ {
get => volume; get => volume;
set internal set
{
value = Math.MathHelper.Max(0, value);
if (volume != value)
{ {
volume = value; volume = value;
FAudio.FAudioVoice_SetVolume(Voice, volume, 0); FAudio.FAudioVoice_SetVolume(Voice, volume, 0);
} }
} }
}
private const float MAX_FILTER_FREQUENCY = 1f; private const float MAX_FILTER_FREQUENCY = 1f;
private const float MAX_FILTER_ONEOVERQ = 1.5f; private const float MAX_FILTER_ONEOVERQ = 1.5f;
@ -80,12 +95,14 @@ namespace MoonWorks.Audio
OneOverQ = 1f OneOverQ = 1f
}; };
private float FilterFrequency public float FilterFrequency
{ {
get => filterParameters.Frequency; get => filterParameters.Frequency;
set internal set
{ {
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY); value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY);
if (filterParameters.Frequency != value)
{
filterParameters.Frequency = value; filterParameters.Frequency = value;
FAudio.FAudioVoice_SetFilterParameters( FAudio.FAudioVoice_SetFilterParameters(
@ -95,13 +112,16 @@ namespace MoonWorks.Audio
); );
} }
} }
}
private float FilterOneOverQ public float FilterOneOverQ
{ {
get => filterParameters.OneOverQ; get => filterParameters.OneOverQ;
set internal set
{ {
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ); value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ);
if (filterParameters.OneOverQ != value)
{
filterParameters.OneOverQ = value; filterParameters.OneOverQ = value;
FAudio.FAudioVoice_SetFilterParameters( FAudio.FAudioVoice_SetFilterParameters(
@ -111,12 +131,15 @@ namespace MoonWorks.Audio
); );
} }
} }
}
private FilterType filterType; private FilterType filterType;
public FilterType FilterType public FilterType FilterType
{ {
get => filterType; get => filterType;
set set
{
if (filterType != value)
{ {
filterType = value; filterType = value;
@ -133,6 +156,7 @@ namespace MoonWorks.Audio
case FilterType.LowPass: case FilterType.LowPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter; filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter;
filterParameters.Frequency = 1f;
break; break;
case FilterType.BandPass: case FilterType.BandPass:
@ -141,6 +165,7 @@ namespace MoonWorks.Audio
case FilterType.HighPass: case FilterType.HighPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter; filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
filterParameters.Frequency = 0f;
break; break;
} }
@ -151,14 +176,18 @@ namespace MoonWorks.Audio
); );
} }
} }
}
private float reverb; private float reverb;
public unsafe float Reverb public unsafe float Reverb
{ {
get => reverb; get => reverb;
set internal set
{ {
if (ReverbEffect != null) if (ReverbEffect != null)
{
value = MathF.Max(0, value);
if (reverb != value)
{ {
reverb = value; reverb = value;
@ -178,6 +207,7 @@ namespace MoonWorks.Audio
0 0
); );
} }
}
#if DEBUG #if DEBUG
if (ReverbEffect == null) if (ReverbEffect == null)
@ -188,7 +218,7 @@ namespace MoonWorks.Audio
} }
} }
public SoundInstance( public unsafe SoundInstance(
AudioDevice device, AudioDevice device,
ushort formatTag, ushort formatTag,
ushort bitsPerSample, ushort bitsPerSample,
@ -197,7 +227,7 @@ namespace MoonWorks.Audio
uint samplesPerSecond uint samplesPerSecond
) : base(device) ) : base(device)
{ {
var format = new FAudio.FAudioWaveFormatEx format = new FAudio.FAudioWaveFormatEx
{ {
wFormatTag = formatTag, wFormatTag = formatTag,
wBitsPerSample = bitsPerSample, wBitsPerSample = bitsPerSample,
@ -207,12 +237,10 @@ namespace MoonWorks.Audio
nAvgBytesPerSec = blockAlign * samplesPerSecond nAvgBytesPerSec = blockAlign * samplesPerSecond
}; };
Format = format;
FAudio.FAudio_CreateSourceVoice( FAudio.FAudio_CreateSourceVoice(
Device.Handle, Device.Handle,
out Voice, out Voice,
ref Format, ref format,
FAudio.FAUDIO_VOICE_USEFILTER, FAudio.FAUDIO_VOICE_USEFILTER,
FAudio.FAUDIO_DEFAULT_FREQ_RATIO, FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
IntPtr.Zero, IntPtr.Zero,
@ -277,6 +305,91 @@ namespace MoonWorks.Audio
ReverbEffect = reverbEffect; ReverbEffect = reverbEffect;
} }
public void SetPan(float targetValue)
{
Pan = targetValue;
Device.ClearTweens(weakReference, 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);
}
public void SetPitch(float targetValue)
{
Pitch = targetValue;
Device.ClearTweens(weakReference, AudioTweenProperty.Pitch);
}
public void SetPitch(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pan, targetValue, duration, 0);
}
public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pan, targetValue, duration, delayTime);
}
public void SetVolume(float targetValue)
{
Volume = targetValue;
Device.ClearTweens(weakReference, AudioTweenProperty.Volume);
}
public void SetVolume(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0);
}
public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime);
}
public void SetFilterFrequency(float targetValue)
{
FilterFrequency = targetValue;
Device.ClearTweens(weakReference, AudioTweenProperty.FilterFrequency);
}
public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0);
}
public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime);
}
public void SetFilterOneOverQ(float targetValue)
{
FilterOneOverQ = targetValue;
}
public void SetReverb(float targetValue)
{
Reverb = targetValue;
Device.ClearTweens(weakReference, AudioTweenProperty.Reverb);
}
public 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)
{
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime);
}
public abstract void Play(); public abstract void Play();
public abstract void QueueSyncPlay(); public abstract void QueueSyncPlay();
public abstract void Pause(); public abstract void Pause();
@ -297,14 +410,12 @@ namespace MoonWorks.Audio
); );
dspSettings.pMatrixCoefficients = (nint) NativeMemory.Alloc(memsize); dspSettings.pMatrixCoefficients = (nint) NativeMemory.Alloc(memsize);
unsafe
{
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients; byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
for (uint i = 0; i < memsize; i += 1) for (uint i = 0; i < memsize; i += 1)
{ {
memPtr[i] = 0; memPtr[i] = 0;
} }
}
SetPanMatrixCoefficients(); SetPanMatrixCoefficients();
} }

View File

@ -10,11 +10,21 @@ namespace MoonWorks.Audio
/// </summary> /// </summary>
public abstract class StreamingSound : SoundInstance public abstract class StreamingSound : SoundInstance
{ {
// How big should each buffer we consume be?
protected abstract int BUFFER_SIZE { get; }
// Should the AudioDevice thread automatically update this class?
public abstract bool AutoUpdate { get; }
// Are we actively consuming buffers?
protected bool ConsumingBuffers = false;
private const int BUFFER_COUNT = 3; private const int BUFFER_COUNT = 3;
private readonly IntPtr[] buffers; private readonly IntPtr[] buffers;
private int nextBufferIndex = 0; private int nextBufferIndex = 0;
private uint queuedBufferCount = 0; private uint queuedBufferCount = 0;
protected abstract int BUFFER_SIZE { get; }
private readonly object StateLock = new object();
public unsafe StreamingSound( public unsafe StreamingSound(
AudioDevice device, AudioDevice device,
@ -25,8 +35,6 @@ namespace MoonWorks.Audio
uint samplesPerSecond uint samplesPerSecond
) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
{ {
device.AddDynamicSoundInstance(this);
buffers = new IntPtr[BUFFER_COUNT]; buffers = new IntPtr[BUFFER_COUNT];
for (int i = 0; i < BUFFER_COUNT; i += 1) for (int i = 0; i < BUFFER_COUNT; i += 1)
{ {
@ -45,6 +53,8 @@ namespace MoonWorks.Audio
} }
private void PlayUsingOperationSet(uint operationSet) private void PlayUsingOperationSet(uint operationSet)
{
lock (StateLock)
{ {
if (State == SoundState.Playing) if (State == SoundState.Playing)
{ {
@ -53,40 +63,65 @@ namespace MoonWorks.Audio
State = SoundState.Playing; State = SoundState.Playing;
Update(); ConsumingBuffers = true;
QueueBuffers();
FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet);
} }
}
public override void Pause() public override void Pause()
{
lock (StateLock)
{ {
if (State == SoundState.Playing) if (State == SoundState.Playing)
{ {
ConsumingBuffers = false;
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
State = SoundState.Paused; State = SoundState.Paused;
} }
} }
}
public override void Stop() public override void Stop()
{ {
lock (StateLock)
{
ConsumingBuffers = false;
State = SoundState.Stopped; State = SoundState.Stopped;
} }
}
public override void StopImmediate() public override void StopImmediate()
{ {
lock (StateLock)
{
ConsumingBuffers = false;
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice);
ClearBuffers(); ClearBuffers();
State = SoundState.Stopped; State = SoundState.Stopped;
} }
}
internal unsafe void Update() internal unsafe void Update()
{
lock (StateLock)
{
if (!IsDisposed)
{ {
if (State != SoundState.Playing) if (State != SoundState.Playing)
{ {
return; return;
} }
QueueBuffers();
}
}
}
protected void QueueBuffers()
{
FAudio.FAudioSourceVoice_GetState( FAudio.FAudioSourceVoice_GetState(
Voice, Voice,
out var state, out var state,
@ -95,16 +130,18 @@ namespace MoonWorks.Audio
queuedBufferCount = state.BuffersQueued; queuedBufferCount = state.BuffersQueued;
QueueBuffers(); if (ConsumingBuffers)
}
protected void QueueBuffers()
{ {
for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1) for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1)
{ {
AddBuffer(); AddBuffer();
} }
} }
else if (queuedBufferCount == 0)
{
Stop();
}
}
protected unsafe void ClearBuffers() protected unsafe void ClearBuffers()
{ {
@ -124,6 +161,8 @@ namespace MoonWorks.Audio
out bool reachedEnd out bool reachedEnd
); );
if (filledLengthInBytes > 0)
{
FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
{ {
AudioBytes = (uint) filledLengthInBytes, AudioBytes = (uint) filledLengthInBytes,
@ -142,19 +181,16 @@ namespace MoonWorks.Audio
); );
queuedBufferCount += 1; queuedBufferCount += 1;
}
/* We have reached the end of the file, what do we do? */
if (reachedEnd) if (reachedEnd)
{ {
/* We have reached the end of the data, what do we do? */
ConsumingBuffers = false;
OnReachedEnd(); OnReachedEnd();
} }
} }
protected virtual void OnReachedEnd()
{
Stop();
}
protected unsafe abstract void FillBuffer( protected unsafe abstract void FillBuffer(
void* buffer, void* buffer,
int bufferLengthInBytes, /* in bytes */ int bufferLengthInBytes, /* in bytes */
@ -162,7 +198,13 @@ namespace MoonWorks.Audio
out bool reachedEnd out bool reachedEnd
); );
protected abstract void OnReachedEnd();
protected unsafe override void Destroy() protected unsafe override void Destroy()
{
lock (StateLock)
{
if (!IsDisposed)
{ {
StopImmediate(); StopImmediate();
@ -173,3 +215,5 @@ namespace MoonWorks.Audio
} }
} }
} }
}
}

View File

@ -11,6 +11,7 @@ namespace MoonWorks.Audio
private FAudio.stb_vorbis_info Info; private FAudio.stb_vorbis_info Info;
protected override int BUFFER_SIZE => 32768; protected override int BUFFER_SIZE => 32768;
public override bool AutoUpdate => true;
public unsafe static StreamingSoundOgg Load(AudioDevice device, string filePath) public unsafe static StreamingSoundOgg Load(AudioDevice device, string filePath)
{ {
@ -35,7 +36,7 @@ namespace MoonWorks.Audio
); );
} }
internal StreamingSoundOgg( internal unsafe StreamingSoundOgg(
AudioDevice device, AudioDevice device,
IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!! IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!!
IntPtr vorbisHandle, IntPtr vorbisHandle,
@ -47,8 +48,7 @@ namespace MoonWorks.Audio
(ushort) (4 * info.channels), (ushort) (4 * info.channels),
(ushort) info.channels, (ushort) info.channels,
info.sample_rate info.sample_rate
) ) {
{
FileDataPtr = fileDataPtr; FileDataPtr = fileDataPtr;
VorbisHandle = vorbisHandle; VorbisHandle = vorbisHandle;
Info = info; Info = info;
@ -64,8 +64,7 @@ namespace MoonWorks.Audio
int bufferLengthInBytes, int bufferLengthInBytes,
out int filledLengthInBytes, out int filledLengthInBytes,
out bool reachedEnd out bool reachedEnd
) ) {
{
var lengthInFloats = bufferLengthInBytes / sizeof(float); var lengthInFloats = bufferLengthInBytes / sizeof(float);
/* NOTE: this function returns samples per channel, not total samples */ /* NOTE: this function returns samples per channel, not total samples */
@ -82,9 +81,14 @@ namespace MoonWorks.Audio
} }
protected unsafe override void Destroy() protected unsafe override void Destroy()
{
base.Destroy();
if (!IsDisposed)
{ {
FAudio.stb_vorbis_close(VorbisHandle); FAudio.stb_vorbis_close(VorbisHandle);
NativeMemory.Free((void*) FileDataPtr); NativeMemory.Free((void*) FileDataPtr);
} }
} }
} }
}

View File

@ -14,12 +14,9 @@ namespace MoonWorks.Audio
{ {
if (Loop) if (Loop)
{ {
ConsumingBuffers = true;
Seek(0); Seek(0);
} }
else
{
Stop();
}
} }
} }
} }

View File

@ -168,9 +168,8 @@ namespace MoonWorks
while (accumulatedUpdateTime >= Timestep) while (accumulatedUpdateTime >= Timestep)
{ {
Inputs.Update(); Inputs.Update();
AudioDevice.Update();
Update(Timestep); Update(Timestep);
AudioDevice.WakeThread();
accumulatedUpdateTime -= Timestep; accumulatedUpdateTime -= Timestep;
} }

View File

@ -18,7 +18,7 @@ namespace MoonWorks.Graphics
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
private readonly List<WeakReference<GraphicsResource>> resources = new List<WeakReference<GraphicsResource>>(); private readonly HashSet<WeakReference<GraphicsResource>> resources = new HashSet<WeakReference<GraphicsResource>>();
public GraphicsDevice( public GraphicsDevice(
Backend preferredBackend, Backend preferredBackend,
@ -237,10 +237,9 @@ namespace MoonWorks.Graphics
{ {
lock (resources) lock (resources)
{ {
for (var i = resources.Count - 1; i >= 0; i--) foreach (var weakReference in resources)
{ {
var resource = resources[i]; if (weakReference.TryGetTarget(out var target))
if (resource.TryGetTarget(out var target))
{ {
target.Dispose(); target.Dispose();
} }

View File

@ -10,7 +10,7 @@ namespace MoonWorks.Graphics
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; } protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; }
private WeakReference<GraphicsResource> selfReference; internal WeakReference<GraphicsResource> weakReference;
public GraphicsResource(GraphicsDevice device, bool trackResource = true) public GraphicsResource(GraphicsDevice device, bool trackResource = true)
{ {
@ -18,8 +18,8 @@ namespace MoonWorks.Graphics
if (trackResource) if (trackResource)
{ {
selfReference = new WeakReference<GraphicsResource>(this); weakReference = new WeakReference<GraphicsResource>(this);
Device.AddResourceReference(selfReference); Device.AddResourceReference(weakReference);
} }
} }
@ -27,11 +27,11 @@ namespace MoonWorks.Graphics
{ {
if (!IsDisposed) if (!IsDisposed)
{ {
if (selfReference != null) if (weakReference != null)
{ {
QueueDestroyFunction(Device.Handle, Handle); QueueDestroyFunction(Device.Handle, Handle);
Device.RemoveResourceReference(selfReference); Device.RemoveResourceReference(weakReference);
selfReference = null; weakReference = null;
} }
IsDisposed = true; IsDisposed = true;

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,8 @@ namespace MoonWorks.Video
{ {
private IntPtr VideoHandle; private IntPtr VideoHandle;
protected override int BUFFER_SIZE => 8192; protected override int BUFFER_SIZE => 8192;
// 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,
@ -32,6 +34,11 @@ namespace MoonWorks.Video
) { ) {
var lengthInFloats = bufferLengthInBytes / sizeof(float); var lengthInFloats = bufferLengthInBytes / sizeof(float);
// FIXME: this gets gnarly with theorafile being not thread safe
// is there some way we could just manually update in VideoPlayer
// instead of going through AudioDevice?
lock (Device.StateLock)
{
int samples = Theorafile.tf_readaudio( int samples = Theorafile.tf_readaudio(
VideoHandle, VideoHandle,
(IntPtr) buffer, (IntPtr) buffer,
@ -42,4 +49,7 @@ namespace MoonWorks.Video
reachedEnd = Theorafile.tf_eos(VideoHandle) == 1; reachedEnd = Theorafile.tf_eos(VideoHandle) == 1;
} }
} }
protected override void OnReachedEnd() { }
}
} }

View File

@ -27,7 +27,7 @@ namespace MoonWorks.Video
private int yWidth; private int yWidth;
private int yHeight; private int yHeight;
private bool disposed; private bool IsDisposed;
public Video(string filename) public Video(string filename)
{ {
@ -89,7 +89,7 @@ namespace MoonWorks.Video
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (!disposed) if (!IsDisposed)
{ {
if (disposing) if (disposing)
{ {
@ -100,7 +100,7 @@ namespace MoonWorks.Video
Theorafile.tf_close(ref Handle); Theorafile.tf_close(ref Handle);
NativeMemory.Free(videoData); NativeMemory.Free(videoData);
disposed = true; IsDisposed = true;
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using MoonWorks.Audio; using MoonWorks.Audio;
@ -130,6 +130,8 @@ namespace MoonWorks.Video
public void Play() public void Play()
{ {
if (Video == null) { return; }
if (State == VideoState.Playing) if (State == VideoState.Playing)
{ {
return; return;
@ -147,6 +149,8 @@ namespace MoonWorks.Video
public void Pause() public void Pause()
{ {
if (Video == null) { return; }
if (State != VideoState.Playing) if (State != VideoState.Playing)
{ {
return; return;
@ -164,6 +168,8 @@ namespace MoonWorks.Video
public void Stop() public void Stop()
{ {
if (Video == null) { return; }
if (State == VideoState.Stopped) if (State == VideoState.Stopped)
{ {
return; return;
@ -172,20 +178,32 @@ namespace MoonWorks.Video
timer.Stop(); timer.Stop();
timer.Reset(); timer.Reset();
Theorafile.tf_reset(Video.Handle);
lastTimestamp = 0; lastTimestamp = 0;
timeElapsed = 0; timeElapsed = 0;
if (audioStream != null) DestroyAudioStream();
{
audioStream.StopImmediate(); Theorafile.tf_reset(Video.Handle);
audioStream.Dispose();
audioStream = null;
}
State = VideoState.Stopped; State = VideoState.Stopped;
} }
public void Unload()
{
Stop();
Video = null;
}
public void Update()
{
if (Video == null) { return; }
if (audioStream != null)
{
audioStream.Update();
}
}
public void Render() public void Render()
{ {
if (Video == null || State == VideoState.Stopped) if (Video == null || State == VideoState.Stopped)
@ -203,7 +221,8 @@ namespace MoonWorks.Video
Video.Handle, Video.Handle,
(IntPtr) yuvData, (IntPtr) yuvData,
thisFrame - currentFrame thisFrame - currentFrame
) == 1 || currentFrame == -1) { ) == 1 || currentFrame == -1)
{
UpdateRenderTexture(); UpdateRenderTexture();
} }
@ -216,12 +235,7 @@ namespace MoonWorks.Video
timer.Stop(); timer.Stop();
timer.Reset(); timer.Reset();
if (audioStream != null) DestroyAudioStream();
{
audioStream.Stop();
audioStream.Dispose();
audioStream = null;
}
Theorafile.tf_reset(Video.Handle); Theorafile.tf_reset(Video.Handle);
@ -299,14 +313,27 @@ namespace MoonWorks.Video
// Grab the first bit of audio. We're trying to start the decoding ASAP. // Grab the first bit of audio. We're trying to start the decoding ASAP.
if (AudioDevice != null && Theorafile.tf_hasaudio(Video.Handle) == 1) if (AudioDevice != null && Theorafile.tf_hasaudio(Video.Handle) == 1)
{ {
DestroyAudioStream();
int channels, sampleRate; int channels, sampleRate;
Theorafile.tf_audioinfo(Video.Handle, out channels, out sampleRate); Theorafile.tf_audioinfo(Video.Handle, out channels, out sampleRate);
audioStream = new StreamingSoundTheora(AudioDevice, Video.Handle, channels, (uint) sampleRate); audioStream = new StreamingSoundTheora(AudioDevice, Video.Handle, channels, (uint) sampleRate);
} }
currentFrame = -1; currentFrame = -1;
} }
private void DestroyAudioStream()
{
if (audioStream != null)
{
audioStream.StopImmediate();
audioStream.Dispose();
audioStream = null;
}
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (!disposed) if (!disposed)