using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; namespace MoonWorks.Audio { /// /// AudioDevice manages all audio-related concerns. /// public class AudioDevice : IDisposable { public IntPtr Handle { get; } public byte[] Handle3D { get; } public FAudio.FAudioDeviceDetails DeviceDetails { get; } 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; public float CurveDistanceScalar = 1f; public float DopplerScale = 1f; public float SpeedOfSound = 343.5f; private readonly HashSet resources = new HashSet(); private readonly HashSet updatingSourceVoices = new HashSet(); private AudioTweenManager AudioTweenManager; private SourceVoicePool VoicePool; private List VoicesToReturn = new List(); 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 Running; public bool IsDisposed { get; private set; } internal unsafe AudioDevice() { UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step); FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR); 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); Handle = IntPtr.Zero; 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 */ var result = FAudio.FAudio_CreateMasteringVoice( Handle, out trueMasteringVoice, FAudio.FAUDIO_DEFAULT_CHANNELS, FAudio.FAUDIO_DEFAULT_SAMPLERATE, 0, i, IntPtr.Zero ); if (result != 0) { Logger.LogError("Failed to create a mastering voice!"); Logger.LogError("Audio device creation failed!"); return; } fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this); /* Init 3D Audio */ Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; FAudio.F3DAudioInitialize( DeviceDetails.OutputFormat.dwChannelMask, SpeedOfSound, Handle3D ); AudioTweenManager = new AudioTweenManager(); VoicePool = new SourceVoicePool(this); Logger.LogInfo("Setting up audio thread..."); WakeSignal = new AutoResetEvent(true); Thread = new Thread(ThreadMain); Thread.IsBackground = true; Thread.Start(); Running = true; TickStopwatch.Start(); previousTickTime = 0; } private void ThreadMain() { while (Running) { lock (StateLock) { try { ThreadMainTick(); } catch (Exception e) { Logger.LogError(e.ToString()); } } WakeSignal.WaitOne(UpdateInterval); } } private void ThreadMainTick() { long tickDelta = TickStopwatch.Elapsed.Ticks - previousTickTime; previousTickTime = TickStopwatch.Elapsed.Ticks; float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond; AudioTweenManager.Update(elapsedSeconds); foreach (var voice in updatingSourceVoices) { voice.Update(); } foreach (var voice in VoicesToReturn) { if (voice is UpdatingSourceVoice updatingSourceVoice) { updatingSourceVoices.Remove(updatingSourceVoice); } voice.Reset(); VoicePool.Return(voice); } VoicesToReturn.Clear(); } /// /// Triggers all pending operations with the given syncGroup value. /// public void TriggerSyncGroup(uint syncGroup) { FAudio.FAudio_CommitChanges(Handle, syncGroup); } /// /// Obtains an appropriate source voice from the voice pool. /// /// The format that the voice must match. /// A source voice with the given format. public T Obtain(Format format) where T : SourceVoice, IPoolable { lock (StateLock) { var voice = VoicePool.Obtain(format); if (voice is UpdatingSourceVoice updatingSourceVoice) { updatingSourceVoices.Add(updatingSourceVoice); } return voice; } } /// /// Returns the source voice to the voice pool. /// /// internal void Return(SourceVoice voice) { lock (StateLock) { VoicesToReturn.Add(voice); } } internal void CreateTween( Voice voice, AudioTweenProperty property, System.Func easingFunction, float start, float end, float duration, float delayTime ) { lock (StateLock) { AudioTweenManager.CreateTween( voice, property, easingFunction, start, end, duration, delayTime ); } } internal void ClearTweens( Voice voice, AudioTweenProperty property ) { lock (StateLock) { AudioTweenManager.ClearTweens(voice, property); } } internal void WakeThread() { WakeSignal.Set(); } internal void AddResourceReference(GCHandle resourceReference) { lock (StateLock) { resources.Add(resourceReference); if (resourceReference.Target is UpdatingSourceVoice updatableVoice) { updatingSourceVoices.Add(updatableVoice); } } } internal void RemoveResourceReference(GCHandle resourceReference) { lock (StateLock) { resources.Remove(resourceReference); } } protected virtual void Dispose(bool disposing) { if (!IsDisposed) { Running = false; if (disposing) { Thread.Join(); // dispose all voices first foreach (var resource in resources) { if (resource.Target is Voice voice) { voice.Dispose(); } } // destroy all other audio resources foreach (var resource in resources) { if (resource.Target is IDisposable disposable) { disposable.Dispose(); } } resources.Clear(); } FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice); FAudio.FAudio_Release(Handle); IsDisposed = true; } } ~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); } } }