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: #47
pull/48/head
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.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
namespace MoonWorks.Audio
{
@ -26,13 +26,25 @@ namespace MoonWorks.Audio
}
}
private readonly List<WeakReference<AudioResource>> resources = new List<WeakReference<AudioResource>>();
private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>();
private readonly HashSet<WeakReference> resources = new HashSet<WeakReference>();
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;
public unsafe AudioDevice()
{
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
Handle = handle;
@ -90,8 +102,8 @@ namespace MoonWorks.Audio
) != 0)
{
Logger.LogError("No mastering voice found!");
Handle = IntPtr.Zero;
FAudio.FAudio_Release(Handle);
Handle = IntPtr.Zero;
return;
}
@ -105,22 +117,52 @@ namespace MoonWorks.Audio
SpeedOfSound,
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];
if (weakReference.TryGetTarget(out var streamingSound))
lock (StateLock)
{
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();
}
else
{
streamingSounds.RemoveAt(i);
autoUpdateStreamingSoundReferences.Remove(weakReference);
}
}
AudioTweenManager.Update(elapsedSeconds);
}
public void SyncPlay()
@ -128,53 +170,108 @@ namespace MoonWorks.Audio
FAudio.FAudio_CommitChanges(Handle, 1);
}
internal void AddDynamicSoundInstance(StreamingSound instance)
{
streamingSounds.Add(new WeakReference<StreamingSound>(instance));
}
internal void AddResourceReference(WeakReference<AudioResource> resourceReference)
{
lock (resources)
internal void CreateTween(
SoundInstance soundInstance,
AudioTweenProperty property,
System.Func<float, float> easingFunction,
float start,
float end,
float duration,
float delayTime
) {
lock (StateLock)
{
resources.Add(resourceReference);
AudioTweenManager.CreateTween(
soundInstance,
property,
easingFunction,
start,
end,
duration,
delayTime
);
}
}
internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference)
{
lock (resources)
internal void ClearTweens(
WeakReference soundReference,
AudioTweenProperty property
) {
lock (StateLock)
{
resources.Remove(resourceReference);
AudioTweenManager.ClearTweens(soundReference, property);
}
}
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)
{
if (!IsDisposed)
{
if (disposing)
lock (StateLock)
{
for (var i = resources.Count - 1; i >= 0; i--)
if (disposing)
{
var weakReference = resources[i];
if (weakReference.TryGetTarget(out var resource))
foreach (var weakReference in resources)
{
resource.Dispose();
var target = weakReference.Target;
if (target != null)
{
(target as IDisposable).Dispose();
}
}
resources.Clear();
}
resources.Clear();
FAudio.FAudioVoice_DestroyVoice(MasteringVoice);
FAudio.FAudio_Release(Handle);
IsDisposed = true;
}
FAudio.FAudioVoice_DestroyVoice(MasteringVoice);
FAudio.FAudio_Release(Handle);
IsDisposed = true;
}
}
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
~AudioDevice()
{
// 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; }
private WeakReference<AudioResource> selfReference;
internal WeakReference weakReference;
public AudioResource(AudioDevice device)
{
Device = device;
selfReference = new WeakReference<AudioResource>(this);
Device.AddResourceReference(selfReference);
weakReference = new WeakReference(this);
Device.AddResourceReference(this);
}
protected abstract void Destroy();
@ -26,10 +26,10 @@ namespace MoonWorks.Audio
{
Destroy();
if (selfReference != null)
if (weakReference != null)
{
Device.RemoveResourceReference(selfReference);
selfReference = null;
Device.RemoveResourceReference(this);
weakReference = null;
}
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
{
// 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;
public IntPtr Voice => voice;
private bool disposedValue;
public ReverbEffect(AudioDevice audioDevice)
public ReverbEffect(AudioDevice audioDevice) : base(audioDevice)
{
/* Init reverb */
@ -97,32 +95,9 @@ namespace MoonWorks.Audio
}
}
protected virtual void Dispose(bool disposing)
protected override void Destroy()
{
if (!disposedValue)
{
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);
FAudio.FAudioVoice_DestroyVoice(Voice);
}
}
}

View File

@ -1,12 +1,15 @@
using System;
using System.Runtime.InteropServices;
using EasingFunction = System.Func<float, float>;
namespace MoonWorks.Audio
{
public abstract class SoundInstance : AudioResource
{
internal IntPtr Voice;
internal FAudio.FAudioWaveFormatEx Format;
private FAudio.FAudioWaveFormatEx format;
public FAudio.FAudioWaveFormatEx Format => format;
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
@ -21,30 +24,34 @@ namespace MoonWorks.Audio
public float Pan
{
get => pan;
set
internal set
{
pan = value;
if (pan < -1f)
value = Math.MathHelper.Clamp(value, -1f, 1f);
if (pan != value)
{
pan = -1f;
}
if (pan > 1f)
{
pan = 1f;
}
pan = value;
if (Is3D) { return; }
if (pan < -1f)
{
pan = -1f;
}
if (pan > 1f)
{
pan = 1f;
}
SetPanMatrixCoefficients();
FAudio.FAudioVoice_SetOutputMatrix(
Voice,
Device.MasteringVoice,
dspSettings.SrcChannelCount,
dspSettings.DstChannelCount,
dspSettings.pMatrixCoefficients,
0
);
if (Is3D) { return; }
SetPanMatrixCoefficients();
FAudio.FAudioVoice_SetOutputMatrix(
Voice,
Device.MasteringVoice,
dspSettings.SrcChannelCount,
dspSettings.DstChannelCount,
dspSettings.pMatrixCoefficients,
0
);
}
}
}
@ -52,10 +59,14 @@ namespace MoonWorks.Audio
public float Pitch
{
get => pitch;
set
internal set
{
pitch = Math.MathHelper.Clamp(value, -1f, 1f);
UpdatePitch();
value = Math.MathHelper.Clamp(value, -1f, 1f);
if (pitch != value)
{
pitch = value;
UpdatePitch();
}
}
}
@ -63,10 +74,14 @@ namespace MoonWorks.Audio
public float Volume
{
get => volume;
set
internal set
{
volume = value;
FAudio.FAudioVoice_SetVolume(Voice, volume, 0);
value = Math.MathHelper.Max(0, value);
if (volume != value)
{
volume = value;
FAudio.FAudioVoice_SetVolume(Voice, volume, 0);
}
}
}
@ -80,35 +95,41 @@ namespace MoonWorks.Audio
OneOverQ = 1f
};
private float FilterFrequency
public float FilterFrequency
{
get => filterParameters.Frequency;
set
internal set
{
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY);
filterParameters.Frequency = value;
if (filterParameters.Frequency != value)
{
filterParameters.Frequency = value;
FAudio.FAudioVoice_SetFilterParameters(
Voice,
ref filterParameters,
0
);
FAudio.FAudioVoice_SetFilterParameters(
Voice,
ref filterParameters,
0
);
}
}
}
private float FilterOneOverQ
public float FilterOneOverQ
{
get => filterParameters.OneOverQ;
set
internal set
{
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ);
filterParameters.OneOverQ = value;
if (filterParameters.OneOverQ != value)
{
filterParameters.OneOverQ = value;
FAudio.FAudioVoice_SetFilterParameters(
Voice,
ref filterParameters,
0
);
FAudio.FAudioVoice_SetFilterParameters(
Voice,
ref filterParameters,
0
);
}
}
}
@ -118,37 +139,42 @@ namespace MoonWorks.Audio
get => filterType;
set
{
filterType = value;
switch (filterType)
if (filterType != value)
{
case FilterType.None:
filterParameters = new FAudio.FAudioFilterParameters
{
Type = FAudio.FAudioFilterType.FAudioLowPassFilter,
Frequency = 1f,
OneOverQ = 1f
};
break;
filterType = value;
case FilterType.LowPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter;
break;
switch (filterType)
{
case FilterType.None:
filterParameters = new FAudio.FAudioFilterParameters
{
Type = FAudio.FAudioFilterType.FAudioLowPassFilter,
Frequency = 1f,
OneOverQ = 1f
};
break;
case FilterType.BandPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter;
break;
case FilterType.LowPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter;
filterParameters.Frequency = 1f;
break;
case FilterType.HighPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
break;
case FilterType.BandPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter;
break;
case FilterType.HighPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
filterParameters.Frequency = 0f;
break;
}
FAudio.FAudioVoice_SetFilterParameters(
Voice,
ref filterParameters,
0
);
}
FAudio.FAudioVoice_SetFilterParameters(
Voice,
ref filterParameters,
0
);
}
}
@ -156,27 +182,31 @@ namespace MoonWorks.Audio
public unsafe float Reverb
{
get => reverb;
set
internal set
{
if (ReverbEffect != null)
{
reverb = value;
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
outputMatrix[0] = reverb;
if (dspSettings.SrcChannelCount == 2)
value = MathF.Max(0, value);
if (reverb != value)
{
outputMatrix[1] = reverb;
}
reverb = value;
FAudio.FAudioVoice_SetOutputMatrix(
Voice,
ReverbEffect.Voice,
dspSettings.SrcChannelCount,
1,
dspSettings.pMatrixCoefficients,
0
);
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
outputMatrix[0] = reverb;
if (dspSettings.SrcChannelCount == 2)
{
outputMatrix[1] = reverb;
}
FAudio.FAudioVoice_SetOutputMatrix(
Voice,
ReverbEffect.Voice,
dspSettings.SrcChannelCount,
1,
dspSettings.pMatrixCoefficients,
0
);
}
}
#if DEBUG
@ -188,7 +218,7 @@ namespace MoonWorks.Audio
}
}
public SoundInstance(
public unsafe SoundInstance(
AudioDevice device,
ushort formatTag,
ushort bitsPerSample,
@ -197,7 +227,7 @@ namespace MoonWorks.Audio
uint samplesPerSecond
) : base(device)
{
var format = new FAudio.FAudioWaveFormatEx
format = new FAudio.FAudioWaveFormatEx
{
wFormatTag = formatTag,
wBitsPerSample = bitsPerSample,
@ -207,12 +237,10 @@ namespace MoonWorks.Audio
nAvgBytesPerSec = blockAlign * samplesPerSecond
};
Format = format;
FAudio.FAudio_CreateSourceVoice(
Device.Handle,
out Voice,
ref Format,
ref format,
FAudio.FAUDIO_VOICE_USEFILTER,
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
IntPtr.Zero,
@ -277,6 +305,91 @@ namespace MoonWorks.Audio
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 QueueSyncPlay();
public abstract void Pause();
@ -297,14 +410,12 @@ namespace MoonWorks.Audio
);
dspSettings.pMatrixCoefficients = (nint) NativeMemory.Alloc(memsize);
unsafe
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
for (uint i = 0; i < memsize; i += 1)
{
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
for (uint i = 0; i < memsize; i += 1)
{
memPtr[i] = 0;
}
memPtr[i] = 0;
}
SetPanMatrixCoefficients();
}

View File

@ -10,11 +10,21 @@ namespace MoonWorks.Audio
/// </summary>
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 readonly IntPtr[] buffers;
private int nextBufferIndex = 0;
private uint queuedBufferCount = 0;
protected abstract int BUFFER_SIZE { get; }
private readonly object StateLock = new object();
public unsafe StreamingSound(
AudioDevice device,
@ -25,8 +35,6 @@ namespace MoonWorks.Audio
uint samplesPerSecond
) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
{
device.AddDynamicSoundInstance(this);
buffers = new IntPtr[BUFFER_COUNT];
for (int i = 0; i < BUFFER_COUNT; i += 1)
{
@ -46,47 +54,74 @@ namespace MoonWorks.Audio
private void PlayUsingOperationSet(uint operationSet)
{
if (State == SoundState.Playing)
lock (StateLock)
{
return;
if (State == SoundState.Playing)
{
return;
}
State = SoundState.Playing;
ConsumingBuffers = true;
QueueBuffers();
FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet);
}
State = SoundState.Playing;
Update();
FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet);
}
public override void Pause()
{
if (State == SoundState.Playing)
lock (StateLock)
{
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
State = SoundState.Paused;
if (State == SoundState.Playing)
{
ConsumingBuffers = false;
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
State = SoundState.Paused;
}
}
}
public override void Stop()
{
State = SoundState.Stopped;
lock (StateLock)
{
ConsumingBuffers = false;
State = SoundState.Stopped;
}
}
public override void StopImmediate()
{
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice);
ClearBuffers();
lock (StateLock)
{
ConsumingBuffers = false;
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice);
ClearBuffers();
State = SoundState.Stopped;
State = SoundState.Stopped;
}
}
internal unsafe void Update()
{
if (State != SoundState.Playing)
lock (StateLock)
{
return;
}
if (!IsDisposed)
{
if (State != SoundState.Playing)
{
return;
}
QueueBuffers();
}
}
}
protected void QueueBuffers()
{
FAudio.FAudioSourceVoice_GetState(
Voice,
out var state,
@ -95,14 +130,16 @@ namespace MoonWorks.Audio
queuedBufferCount = state.BuffersQueued;
QueueBuffers();
}
protected void QueueBuffers()
{
for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1)
if (ConsumingBuffers)
{
AddBuffer();
for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1)
{
AddBuffer();
}
}
else if (queuedBufferCount == 0)
{
Stop();
}
}
@ -124,37 +161,36 @@ namespace MoonWorks.Audio
out bool reachedEnd
);
FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
if (filledLengthInBytes > 0)
{
AudioBytes = (uint) filledLengthInBytes,
pAudioData = (IntPtr) buffer,
PlayLength = (
(uint) (filledLengthInBytes /
Format.nChannels /
(uint) (Format.wBitsPerSample / 8))
)
};
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
);
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Voice,
ref buf,
IntPtr.Zero
);
queuedBufferCount += 1;
queuedBufferCount += 1;
}
/* We have reached the end of the file, what do we do? */
if (reachedEnd)
{
/* We have reached the end of the data, what do we do? */
ConsumingBuffers = false;
OnReachedEnd();
}
}
protected virtual void OnReachedEnd()
{
Stop();
}
protected unsafe abstract void FillBuffer(
void* buffer,
int bufferLengthInBytes, /* in bytes */
@ -162,13 +198,21 @@ namespace MoonWorks.Audio
out bool reachedEnd
);
protected abstract void OnReachedEnd();
protected unsafe override void Destroy()
{
StopImmediate();
for (int i = 0; i < BUFFER_COUNT; i += 1)
lock (StateLock)
{
NativeMemory.Free((void*) buffers[i]);
if (!IsDisposed)
{
StopImmediate();
for (int i = 0; i < BUFFER_COUNT; i += 1)
{
NativeMemory.Free((void*) buffers[i]);
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ namespace MoonWorks.Graphics
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(
Backend preferredBackend,
@ -237,10 +237,9 @@ namespace MoonWorks.Graphics
{
lock (resources)
{
for (var i = resources.Count - 1; i >= 0; i--)
foreach (var weakReference in resources)
{
var resource = resources[i];
if (resource.TryGetTarget(out var target))
if (weakReference.TryGetTarget(out var target))
{
target.Dispose();
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,8 @@ namespace MoonWorks.Video
{
private IntPtr VideoHandle;
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(
AudioDevice device,
@ -32,14 +34,22 @@ namespace MoonWorks.Video
) {
var lengthInFloats = bufferLengthInBytes / sizeof(float);
int samples = Theorafile.tf_readaudio(
VideoHandle,
(IntPtr) buffer,
lengthInFloats
);
// 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(
VideoHandle,
(IntPtr) buffer,
lengthInFloats
);
filledLengthInBytes = samples * sizeof(float);
reachedEnd = Theorafile.tf_eos(VideoHandle) == 1;
filledLengthInBytes = samples * sizeof(float);
reachedEnd = Theorafile.tf_eos(VideoHandle) == 1;
}
}
protected override void OnReachedEnd() { }
}
}

View File

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

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using MoonWorks.Audio;
@ -130,6 +130,8 @@ namespace MoonWorks.Video
public void Play()
{
if (Video == null) { return; }
if (State == VideoState.Playing)
{
return;
@ -147,6 +149,8 @@ namespace MoonWorks.Video
public void Pause()
{
if (Video == null) { return; }
if (State != VideoState.Playing)
{
return;
@ -164,6 +168,8 @@ namespace MoonWorks.Video
public void Stop()
{
if (Video == null) { return; }
if (State == VideoState.Stopped)
{
return;
@ -172,20 +178,32 @@ namespace MoonWorks.Video
timer.Stop();
timer.Reset();
Theorafile.tf_reset(Video.Handle);
lastTimestamp = 0;
timeElapsed = 0;
if (audioStream != null)
{
audioStream.StopImmediate();
audioStream.Dispose();
audioStream = null;
}
DestroyAudioStream();
Theorafile.tf_reset(Video.Handle);
State = VideoState.Stopped;
}
public void Unload()
{
Stop();
Video = null;
}
public void Update()
{
if (Video == null) { return; }
if (audioStream != null)
{
audioStream.Update();
}
}
public void Render()
{
if (Video == null || State == VideoState.Stopped)
@ -203,7 +221,8 @@ namespace MoonWorks.Video
Video.Handle,
(IntPtr) yuvData,
thisFrame - currentFrame
) == 1 || currentFrame == -1) {
) == 1 || currentFrame == -1)
{
UpdateRenderTexture();
}
@ -216,12 +235,7 @@ namespace MoonWorks.Video
timer.Stop();
timer.Reset();
if (audioStream != null)
{
audioStream.Stop();
audioStream.Dispose();
audioStream = null;
}
DestroyAudioStream();
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.
if (AudioDevice != null && Theorafile.tf_hasaudio(Video.Handle) == 1)
{
DestroyAudioStream();
int channels, sampleRate;
Theorafile.tf_audioinfo(Video.Handle, out channels, out sampleRate);
audioStream = new StreamingSoundTheora(AudioDevice, Video.Handle, channels, (uint) sampleRate);
}
currentFrame = -1;
}
private void DestroyAudioStream()
{
if (audioStream != null)
{
audioStream.StopImmediate();
audioStream.Dispose();
audioStream = null;
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)