using System; using System.Numerics; using OpenTK.Audio.OpenAL; using Robust.Client.Audio.Effects; using Robust.Shared.Audio.Effects; using Robust.Shared.Audio.Sources; using Robust.Shared.Audio.Systems; using Robust.Shared.Maths; namespace Robust.Client.Audio.Sources; public abstract class BaseAudioSource : IAudioSource { /* * This may look weird having all these methods here however * we need to handle disposing plus checking for errors hence we get this. */ /// /// Handle to the AL source. /// protected int SourceHandle; /// /// Source to the EFX filter if applicable. /// protected int FilterHandle; internal readonly AudioManager Master; /// /// Prior gain that was set. /// private float _gain; private float _occlusion; private bool IsEfxSupported => Master.IsEfxSupported; internal BaseAudioSource(AudioManager master, int sourceHandle) { Master = master; SourceHandle = sourceHandle; AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain); } /// public void Pause() { AL.SourcePause(SourceHandle); } /// public void StartPlaying() { if (Playing) return; Playing = true; } /// public void StopPlaying() { if (!Playing) return; Playing = false; } /// public void Restart() { AL.SourceRewind(SourceHandle); StartPlaying(); } /// public virtual bool Playing { get { _checkDisposed(); var state = AL.GetSource(SourceHandle, ALGetSourcei.SourceState); Master._checkAlError(); return state == (int)ALSourceState.Playing; } set { _checkDisposed(); if (value) { AL.SourcePlay(SourceHandle); } else { AL.SourceStop(SourceHandle); } Master._checkAlError(); } } /// public bool Looping { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourceb.Looping, out var ret); Master._checkAlError(); return ret; } set { _checkDisposed(); AL.Source(SourceHandle, ALSourceb.Looping, value); Master._checkAlError(); } } /// public bool Global { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourceb.SourceRelative, out var value); Master._checkAlError(); return value; } set { _checkDisposed(); AL.Source(SourceHandle, ALSourceb.SourceRelative, value); Master._checkAlError(); } } /// public virtual Vector2 Position { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSource3f.Position, out var x, out var y, out _); Master._checkAlError(); return new Vector2(x, y); } set { _checkDisposed(); var (x, y) = value; if (!AreFinite(x, y)) { return; } AL.Source(SourceHandle, ALSource3f.Position, x, y, 0); Master._checkAlError(); } } /// public float Pitch { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourcef.Pitch, out var value); Master._checkAlError(); return value; } set { _checkDisposed(); AL.Source(SourceHandle, ALSourcef.Pitch, value); Master._checkAlError(); } } /// public float Volume { get { var gain = Gain; var volume = SharedAudioSystem.GainToVolume(gain); return volume; } set => Gain = SharedAudioSystem.VolumeToGain(value); } /// public float Gain { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourcef.Gain, out var gain); Master._checkAlError(); return gain; } set { if (float.IsNaN(value)) { Master.LogError($"Tried to set NaN gain, setting audio source to 0f: {Environment.StackTrace}"); value = 0f; } _checkDisposed(); var priorOcclusion = 1f; if (!IsEfxSupported) { AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain); // Default to 0 to avoid spiking audio, just means it will be muted for a frame in this case. priorOcclusion = _gain == 0 ? 1f : priorGain / _gain; } _gain = value; AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion); Master.LogALError($"Gain is {_gain:0.00} and priorOcclusion is {priorOcclusion:0.00}. EFX supported: {IsEfxSupported}"); } } /// public float MaxDistance { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourcef.MaxDistance, out var value); Master._checkAlError(); return value; } set { _checkDisposed(); AL.Source(SourceHandle, ALSourcef.MaxDistance, value); Master.LogALError($"MaxDistance is {value:0.00}"); } } /// public float RolloffFactor { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourcef.RolloffFactor, out var value); Master._checkAlError(); return value; } set { _checkDisposed(); AL.Source(SourceHandle, ALSourcef.RolloffFactor, value); Master.LogALError($"RolloffFactor is {value:0.00}"); } } /// public float ReferenceDistance { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourcef.ReferenceDistance, out var value); Master._checkAlError(); return value; } set { _checkDisposed(); AL.Source(SourceHandle, ALSourcef.ReferenceDistance, value); Master.LogALError($"ReferenceDistance is {value:0.00}"); } } /// public float Occlusion { get => _occlusion; set { _checkDisposed(); var cutoff = MathF.Exp(-value * 1); var gain = MathF.Pow(cutoff, 0.1f); if (IsEfxSupported) { SetOcclusionEfx(gain, cutoff); } else { gain *= gain * gain; AL.Source(SourceHandle, ALSourcef.Gain, _gain * gain); } _occlusion = value; Master._checkAlError(); } } /// public float PlaybackPosition { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSourcef.SecOffset, out var value); Master._checkAlError(); return value; } set { _checkDisposed(); value = MathF.Max(value, 0f); AL.Source(SourceHandle, ALSourcef.SecOffset, value); Master._checkAlError($"Tried to set invalid playback position of {value:0.00}"); } } /// public Vector2 Velocity { get { _checkDisposed(); AL.GetSource(SourceHandle, ALSource3f.Velocity, out var x, out var y, out _); Master._checkAlError(); return new Vector2(x, y); } set { _checkDisposed(); var (x, y) = value; if (!AreFinite(x, y)) { return; } AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0); Master._checkAlError(); } } void IAudioSource.SetAuxiliary(IAuxiliaryAudio? audio) { _checkDisposed(); if (!IsEfxSupported) return; if (audio is AuxiliaryAudio impAudio) { ALC.EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0); } else { ALC.EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0); } Master._checkAlError(); } private void SetOcclusionEfx(float gain, float cutoff) { if (FilterHandle == 0) { FilterHandle = ALC.EFX.GenFilter(); ALC.EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass); } ALC.EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain); ALC.EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff); AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle); } protected static bool AreFinite(float x, float y) { if (float.IsFinite(x) && float.IsFinite(y)) { return true; } return false; } ~BaseAudioSource() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected abstract void Dispose(bool disposing); protected bool _isDisposed() { return SourceHandle == -1; } protected void _checkDisposed() { if (SourceHandle == -1) { throw new ObjectDisposedException(nameof(BaseAudioSource)); } } }