2022-02-23 05:14:32 +00:00
|
|
|
|
using System;
|
2021-01-20 05:33:25 +00:00
|
|
|
|
using System.Collections.Generic;
|
2023-11-21 01:52:44 +00:00
|
|
|
|
using System.Runtime.InteropServices;
|
2023-03-07 23:28:57 +00:00
|
|
|
|
using System.Threading;
|
2021-01-20 02:06:10 +00:00
|
|
|
|
|
|
|
|
|
namespace MoonWorks.Audio
|
|
|
|
|
{
|
2023-09-19 20:19:41 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// AudioDevice manages all audio-related concerns.
|
|
|
|
|
/// </summary>
|
2022-02-23 05:14:32 +00:00
|
|
|
|
public class AudioDevice : IDisposable
|
|
|
|
|
{
|
|
|
|
|
public IntPtr Handle { get; }
|
|
|
|
|
public byte[] Handle3D { get; }
|
|
|
|
|
public FAudio.FAudioDeviceDetails DeviceDetails { get; }
|
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
private IntPtr trueMasteringVoice;
|
|
|
|
|
|
|
|
|
|
// this is a fun little trick where we use a submix voice as a "faux" mastering voice
|
|
|
|
|
// this lets us maintain API consistency for effects like panning and reverb
|
|
|
|
|
private SubmixVoice fauxMasteringVoice;
|
|
|
|
|
public SubmixVoice MasteringVoice => fauxMasteringVoice;
|
|
|
|
|
|
2022-02-23 05:14:32 +00:00
|
|
|
|
public float CurveDistanceScalar = 1f;
|
|
|
|
|
public float DopplerScale = 1f;
|
|
|
|
|
public float SpeedOfSound = 343.5f;
|
|
|
|
|
|
2023-11-21 01:52:44 +00:00
|
|
|
|
private readonly HashSet<GCHandle> resources = new HashSet<GCHandle>();
|
2023-11-21 02:56:22 +00:00
|
|
|
|
private readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>();
|
2023-03-07 23:28:57 +00:00
|
|
|
|
|
|
|
|
|
private AudioTweenManager AudioTweenManager;
|
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
private SourceVoicePool VoicePool;
|
|
|
|
|
private List<SourceVoice> VoicesToReturn = new List<SourceVoice>();
|
|
|
|
|
|
2023-03-07 23:28:57 +00:00
|
|
|
|
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();
|
2022-02-23 05:14:32 +00:00
|
|
|
|
|
2023-07-28 22:02:20 +00:00
|
|
|
|
private bool Running;
|
2023-11-21 02:56:22 +00:00
|
|
|
|
public bool IsDisposed { get; private set; }
|
2022-02-23 05:14:32 +00:00
|
|
|
|
|
2023-09-20 00:04:03 +00:00
|
|
|
|
internal unsafe AudioDevice()
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-03-07 23:28:57 +00:00
|
|
|
|
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
|
|
|
|
|
|
2022-08-30 17:09:32 +00:00
|
|
|
|
FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
|
2022-02-23 05:14:32 +00:00
|
|
|
|
Handle = handle;
|
|
|
|
|
|
|
|
|
|
/* Find a suitable device */
|
|
|
|
|
|
|
|
|
|
FAudio.FAudio_GetDeviceCount(Handle, out var devices);
|
|
|
|
|
|
|
|
|
|
if (devices == 0)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError("No audio devices found!");
|
|
|
|
|
FAudio.FAudio_Release(Handle);
|
2022-11-30 17:43:49 +00:00
|
|
|
|
Handle = IntPtr.Zero;
|
2022-02-23 05:14:32 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FAudio.FAudioDeviceDetails deviceDetails;
|
|
|
|
|
|
|
|
|
|
uint i = 0;
|
|
|
|
|
for (i = 0; i < devices; i++)
|
|
|
|
|
{
|
|
|
|
|
FAudio.FAudio_GetDeviceDetails(
|
|
|
|
|
Handle,
|
|
|
|
|
i,
|
|
|
|
|
out deviceDetails
|
|
|
|
|
);
|
|
|
|
|
if ((deviceDetails.Role & FAudio.FAudioDeviceRole.FAudioDefaultGameDevice) == FAudio.FAudioDeviceRole.FAudioDefaultGameDevice)
|
|
|
|
|
{
|
|
|
|
|
DeviceDetails = deviceDetails;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i == devices)
|
|
|
|
|
{
|
|
|
|
|
i = 0; /* whatever we'll just use the first one I guess */
|
|
|
|
|
FAudio.FAudio_GetDeviceDetails(
|
|
|
|
|
Handle,
|
|
|
|
|
i,
|
|
|
|
|
out deviceDetails
|
|
|
|
|
);
|
|
|
|
|
DeviceDetails = deviceDetails;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Init Mastering Voice */
|
2023-08-03 19:54:02 +00:00
|
|
|
|
var result = FAudio.FAudio_CreateMasteringVoice(
|
2022-02-23 05:14:32 +00:00
|
|
|
|
Handle,
|
2023-08-03 19:54:02 +00:00
|
|
|
|
out trueMasteringVoice,
|
2022-02-23 05:14:32 +00:00
|
|
|
|
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
|
|
|
|
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
|
|
|
|
0,
|
|
|
|
|
i,
|
|
|
|
|
IntPtr.Zero
|
2023-08-03 19:54:02 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result != 0)
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-08-03 19:54:02 +00:00
|
|
|
|
Logger.LogError("Failed to create a mastering voice!");
|
|
|
|
|
Logger.LogError("Audio device creation failed!");
|
2022-02-23 05:14:32 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 17:46:19 +00:00
|
|
|
|
fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this);
|
2022-02-23 05:14:32 +00:00
|
|
|
|
|
|
|
|
|
/* Init 3D Audio */
|
|
|
|
|
|
|
|
|
|
Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE];
|
|
|
|
|
FAudio.F3DAudioInitialize(
|
|
|
|
|
DeviceDetails.OutputFormat.dwChannelMask,
|
|
|
|
|
SpeedOfSound,
|
|
|
|
|
Handle3D
|
|
|
|
|
);
|
2023-03-07 23:28:57 +00:00
|
|
|
|
|
|
|
|
|
AudioTweenManager = new AudioTweenManager();
|
2023-08-03 19:54:02 +00:00
|
|
|
|
VoicePool = new SourceVoicePool(this);
|
2023-03-07 23:28:57 +00:00
|
|
|
|
|
|
|
|
|
Logger.LogInfo("Setting up audio thread...");
|
|
|
|
|
WakeSignal = new AutoResetEvent(true);
|
|
|
|
|
|
|
|
|
|
Thread = new Thread(ThreadMain);
|
|
|
|
|
Thread.IsBackground = true;
|
|
|
|
|
Thread.Start();
|
|
|
|
|
|
2023-07-28 22:02:20 +00:00
|
|
|
|
Running = true;
|
|
|
|
|
|
2023-03-07 23:28:57 +00:00
|
|
|
|
TickStopwatch.Start();
|
|
|
|
|
previousTickTime = 0;
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-07 23:28:57 +00:00
|
|
|
|
private void ThreadMain()
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-07-28 22:02:20 +00:00
|
|
|
|
while (Running)
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-03-07 23:28:57 +00:00
|
|
|
|
lock (StateLock)
|
|
|
|
|
{
|
2023-06-28 20:20:18 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ThreadMainTick();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError(e.ToString());
|
|
|
|
|
}
|
2023-03-07 23:28:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WakeSignal.WaitOne(UpdateInterval);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ThreadMainTick()
|
|
|
|
|
{
|
|
|
|
|
long tickDelta = TickStopwatch.Elapsed.Ticks - previousTickTime;
|
|
|
|
|
previousTickTime = TickStopwatch.Elapsed.Ticks;
|
|
|
|
|
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond;
|
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
AudioTweenManager.Update(elapsedSeconds);
|
2023-05-12 00:56:40 +00:00
|
|
|
|
|
2023-11-21 02:56:22 +00:00
|
|
|
|
foreach (var voice in updatingSourceVoices)
|
2023-08-03 19:54:02 +00:00
|
|
|
|
{
|
|
|
|
|
voice.Update();
|
2023-05-12 00:56:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
foreach (var voice in VoicesToReturn)
|
2023-05-12 00:56:40 +00:00
|
|
|
|
{
|
2023-11-21 02:56:22 +00:00
|
|
|
|
if (voice is UpdatingSourceVoice updatingSourceVoice)
|
|
|
|
|
{
|
|
|
|
|
updatingSourceVoices.Remove(updatingSourceVoice);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
voice.Reset();
|
|
|
|
|
VoicePool.Return(voice);
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
2023-03-07 23:28:57 +00:00
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
VoicesToReturn.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Triggers all pending operations with the given syncGroup value.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void TriggerSyncGroup(uint syncGroup)
|
|
|
|
|
{
|
|
|
|
|
FAudio.FAudio_CommitChanges(Handle, syncGroup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Obtains an appropriate source voice from the voice pool.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="format">The format that the voice must match.</param>
|
|
|
|
|
/// <returns>A source voice with the given format.</returns>
|
|
|
|
|
public T Obtain<T>(Format format) where T : SourceVoice, IPoolable<T>
|
|
|
|
|
{
|
|
|
|
|
lock (StateLock)
|
2023-07-28 20:02:50 +00:00
|
|
|
|
{
|
2023-08-03 19:54:02 +00:00
|
|
|
|
var voice = VoicePool.Obtain<T>(format);
|
2023-11-21 02:56:22 +00:00
|
|
|
|
|
|
|
|
|
if (voice is UpdatingSourceVoice updatingSourceVoice)
|
|
|
|
|
{
|
|
|
|
|
updatingSourceVoices.Add(updatingSourceVoice);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
return voice;
|
2023-07-28 20:02:50 +00:00
|
|
|
|
}
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the source voice to the voice pool.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="voice"></param>
|
|
|
|
|
internal void Return(SourceVoice voice)
|
2023-02-16 23:12:35 +00:00
|
|
|
|
{
|
2023-08-03 19:54:02 +00:00
|
|
|
|
lock (StateLock)
|
|
|
|
|
{
|
|
|
|
|
VoicesToReturn.Add(voice);
|
|
|
|
|
}
|
2023-02-16 23:12:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-07 23:28:57 +00:00
|
|
|
|
internal void CreateTween(
|
2023-08-03 19:54:02 +00:00
|
|
|
|
Voice voice,
|
2023-03-07 23:28:57 +00:00
|
|
|
|
AudioTweenProperty property,
|
|
|
|
|
System.Func<float, float> easingFunction,
|
|
|
|
|
float start,
|
|
|
|
|
float end,
|
|
|
|
|
float duration,
|
|
|
|
|
float delayTime
|
|
|
|
|
) {
|
|
|
|
|
lock (StateLock)
|
|
|
|
|
{
|
|
|
|
|
AudioTweenManager.CreateTween(
|
2023-08-03 19:54:02 +00:00
|
|
|
|
voice,
|
2023-03-07 23:28:57 +00:00
|
|
|
|
property,
|
|
|
|
|
easingFunction,
|
|
|
|
|
start,
|
|
|
|
|
end,
|
|
|
|
|
duration,
|
|
|
|
|
delayTime
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void ClearTweens(
|
2023-08-03 19:54:02 +00:00
|
|
|
|
Voice voice,
|
2023-03-07 23:28:57 +00:00
|
|
|
|
AudioTweenProperty property
|
|
|
|
|
) {
|
|
|
|
|
lock (StateLock)
|
|
|
|
|
{
|
2023-08-03 19:54:02 +00:00
|
|
|
|
AudioTweenManager.ClearTweens(voice, property);
|
2023-03-07 23:28:57 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void WakeThread()
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-03-07 23:28:57 +00:00
|
|
|
|
WakeSignal.Set();
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 01:52:44 +00:00
|
|
|
|
internal void AddResourceReference(GCHandle resourceReference)
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-03-07 23:28:57 +00:00
|
|
|
|
lock (StateLock)
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-11-21 01:52:44 +00:00
|
|
|
|
resources.Add(resourceReference);
|
2023-08-09 22:58:18 +00:00
|
|
|
|
|
2023-11-21 02:56:22 +00:00
|
|
|
|
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
2023-08-09 22:58:18 +00:00
|
|
|
|
{
|
2023-11-21 02:56:22 +00:00
|
|
|
|
updatingSourceVoices.Add(updatableVoice);
|
2023-08-09 22:58:18 +00:00
|
|
|
|
}
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 01:52:44 +00:00
|
|
|
|
internal void RemoveResourceReference(GCHandle resourceReference)
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-03-07 23:28:57 +00:00
|
|
|
|
lock (StateLock)
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-11-21 01:52:44 +00:00
|
|
|
|
resources.Remove(resourceReference);
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (!IsDisposed)
|
|
|
|
|
{
|
2023-07-28 22:02:20 +00:00
|
|
|
|
Running = false;
|
|
|
|
|
|
|
|
|
|
if (disposing)
|
2022-02-23 05:14:32 +00:00
|
|
|
|
{
|
2023-11-21 01:59:26 +00:00
|
|
|
|
Thread.Join();
|
|
|
|
|
|
2023-12-08 23:06:17 +00:00
|
|
|
|
// dispose all voices first
|
2023-11-21 01:52:44 +00:00
|
|
|
|
foreach (var resource in resources)
|
2023-08-03 19:54:02 +00:00
|
|
|
|
{
|
2023-12-08 23:06:17 +00:00
|
|
|
|
if (resource.Target is Voice voice)
|
2023-08-03 19:54:02 +00:00
|
|
|
|
{
|
2023-12-08 23:06:17 +00:00
|
|
|
|
voice.Dispose();
|
2023-08-03 19:54:02 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-08 23:06:17 +00:00
|
|
|
|
// destroy all other audio resources
|
2023-11-21 01:52:44 +00:00
|
|
|
|
foreach (var resource in resources)
|
2022-02-23 00:44:39 +00:00
|
|
|
|
{
|
2023-11-21 01:52:44 +00:00
|
|
|
|
if (resource.Target is IDisposable disposable)
|
2023-07-28 22:02:20 +00:00
|
|
|
|
{
|
2023-11-21 01:52:44 +00:00
|
|
|
|
disposable.Dispose();
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
2022-02-23 00:44:39 +00:00
|
|
|
|
}
|
2023-08-03 19:54:02 +00:00
|
|
|
|
|
2023-07-28 22:02:20 +00:00
|
|
|
|
resources.Clear();
|
|
|
|
|
}
|
2021-01-29 02:01:42 +00:00
|
|
|
|
|
2023-08-03 19:54:02 +00:00
|
|
|
|
FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice);
|
2023-07-28 22:02:20 +00:00
|
|
|
|
FAudio.FAudio_Release(Handle);
|
2022-02-23 05:14:32 +00:00
|
|
|
|
|
2023-07-28 22:02:20 +00:00
|
|
|
|
IsDisposed = true;
|
2022-02-23 05:14:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~AudioDevice()
|
|
|
|
|
{
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-01-20 02:06:10 +00:00
|
|
|
|
}
|