using System; using System.Collections.Generic; using System.Threading; using EasingFunction = System.Func; namespace MoonWorks.Audio { public class AudioDevice : IDisposable { public IntPtr Handle { get; } public byte[] Handle3D { get; } public IntPtr MasteringVoice { get; } public FAudio.FAudioDeviceDetails DeviceDetails { get; } public float CurveDistanceScalar = 1f; public float DopplerScale = 1f; public float SpeedOfSound = 343.5f; private float masteringVolume = 1f; public float MasteringVolume { get => masteringVolume; set { masteringVolume = value; FAudio.FAudioVoice_SetVolume(MasteringVoice, masteringVolume, 0); } } private readonly List> resources = new List>(); private readonly List> streamingSounds = new List>(); private AudioTweenPool AudioTweenPool = new AudioTweenPool(); private readonly List AudioTweens = 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; private 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; /* 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 */ IntPtr masteringVoice; if (FAudio.FAudio_CreateMasteringVoice( Handle, out masteringVoice, FAudio.FAUDIO_DEFAULT_CHANNELS, FAudio.FAUDIO_DEFAULT_SAMPLERATE, 0, i, IntPtr.Zero ) != 0) { Logger.LogError("No mastering voice found!"); Handle = IntPtr.Zero; FAudio.FAudio_Release(Handle); return; } MasteringVoice = masteringVoice; /* Init 3D Audio */ Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; FAudio.F3DAudioInitialize( DeviceDetails.OutputFormat.dwChannelMask, SpeedOfSound, Handle3D ); Logger.LogInfo("Setting up audio thread..."); WakeSignal = new AutoResetEvent(true); Thread = new Thread(ThreadMain); Thread.IsBackground = true; Thread.Start(); TickStopwatch.Start(); previousTickTime = 0; } private void ThreadMain() { while (!IsDisposed) { 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; for (var i = streamingSounds.Count - 1; i >= 0; i--) { var weakReference = streamingSounds[i]; if (weakReference.TryGetTarget(out var streamingSound)) { streamingSound.Update(); } else { streamingSounds.RemoveAt(i); } } lock (AudioTweens) { for (var i = AudioTweens.Count - 1; i >= 0; i--) { bool finished = true; var audioTween = AudioTweens[i]; if (audioTween.SoundInstanceReference.TryGetTarget(out var soundInstance)) { finished = UpdateAudioTween(audioTween, soundInstance, elapsedSeconds); } if (finished) { AudioTweenPool.Free(audioTween); AudioTweens.RemoveAt(i); } } } } public void SyncPlay() { FAudio.FAudio_CommitChanges(Handle, 1); } internal void CreateTween( SoundInstance soundInstance, AudioTweenProperty property, EasingFunction easingFunction, float start, float end, float duration ) { var tween = AudioTweenPool.Obtain(); tween.SoundInstanceReference = new WeakReference(soundInstance); tween.Property = property; tween.EasingFunction = easingFunction; tween.Start = start; tween.End = end; tween.Duration = duration; tween.Time = 0; lock (AudioTweens) { AudioTweens.Add(tween); } } private bool UpdateAudioTween(AudioTween audioTween, SoundInstance soundInstance, float delta) { float value; audioTween.Time += delta; if (audioTween.Time > audioTween.Duration) { value = audioTween.End; } else { value = MoonWorks.Math.Easing.Interp( audioTween.Start, audioTween.End, 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 audioTween.Time > audioTween.Duration; } internal void WakeThread() { WakeSignal.Set(); } internal void AddDynamicSoundInstance(StreamingSound instance) { lock (StateLock) { streamingSounds.Add(new WeakReference(instance)); } } internal void AddResourceReference(WeakReference resourceReference) { lock (StateLock) { resources.Add(resourceReference); } } internal void RemoveResourceReference(WeakReference resourceReference) { lock (StateLock) { resources.Remove(resourceReference); } } protected virtual void Dispose(bool disposing) { if (!IsDisposed) { lock (StateLock) { if (disposing) { for (var i = resources.Count - 1; i >= 0; i--) { var weakReference = resources[i]; if (weakReference.TryGetTarget(out var resource)) { resource.Dispose(); } } resources.Clear(); } 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 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); } } }