Audio rework unrevert + audio packaging (#4555)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
metalgearsloth
2023-11-27 22:12:26 +11:00
committed by GitHub
parent 24b0165ec9
commit 2733435218
116 changed files with 12666 additions and 3725 deletions

View File

@@ -0,0 +1,5 @@
- type: entity
id: Audio
name: Audio
description: Audio entity used by engine
save: false

File diff suppressed because it is too large Load Diff

View File

@@ -561,3 +561,7 @@ cmd-vfs_ls-hint-path = <path>
cmd-reloadtiletextures-desc = Reloads the tile texture atlas to allow hot reloading tile sprites
cmd-reloadtiletextures-help = Usage: reloadtiletextures
cmd-audio_length-desc = Shows the length of an audio file
cmd-audio_length-help = Usage: audio_length { cmd-audio_length-arg-file-name }
cmd-audio_length-arg-file-name = <file name>

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using Robust.Client.Audio;
using Robust.Client.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Client.Animations
@@ -37,7 +39,12 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
SoundSystem.Play(keyFrame.Resource, Filter.Local(), entity, keyFrame.AudioParamsFunc.Invoke());
var audioParams = keyFrame.AudioParamsFunc.Invoke();
var audio = new SoundPathSpecifier(keyFrame.Resource)
{
Params = audioParams
};
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
}
return (keyFrameIndex, playingTime);

View File

@@ -0,0 +1,58 @@
using System.Collections.Concurrent;
using OpenTK.Audio.OpenAL;
namespace Robust.Client.Audio;
internal partial class AudioManager
{
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
public void FlushALDisposeQueues()
{
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer(handle);
_checkAlError();
}
}
internal void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
internal void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
internal void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
}

View File

@@ -0,0 +1,339 @@
using System;
using System.IO;
using System.Numerics;
using System.Threading;
using OpenTK.Audio.OpenAL;
using Robust.Client.Audio.Sources;
using Robust.Client.Graphics;
using Robust.Shared.Audio;
using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio;
internal partial class AudioManager
{
private float _zOffset;
public void SetZOffset(float offset)
{
_zOffset = offset;
}
/// <inheritdoc />
public float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance)
{
switch (_attenuation)
{
case Attenuation.LinearDistance:
return 1 - rolloffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance);
case Attenuation.LinearDistanceClamped:
distance = MathF.Max(referenceDistance, MathF.Min(distance, maxDistance));
return 1 - rolloffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance);
default:
// TODO: If you see this you can implement
throw new NotImplementedException();
}
}
public void InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
InitializeAudio();
}
public void Shutdown()
{
DisposeAllAudio();
if (_openALContext != ALContext.Null)
{
ALC.MakeContextCurrent(ALContext.Null);
ALC.DestroyContext(_openALContext);
}
if (_openALDevice != IntPtr.Zero)
{
ALC.CloseDevice(_openALDevice);
}
}
/// <inheritdoc/>
public void SetPosition(Vector2 position)
{
AL.Listener(ALListener3f.Position, position.X, position.Y, _zOffset);
}
/// <inheritdoc/>
public void SetRotation(Angle angle)
{
var vec = angle.ToVec();
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
var at = new OpenTK.Mathematics.Vector3(0f, 0f, -1f);
var up = new OpenTK.Mathematics.Vector3(vec.Y, vec.X, 0f);
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, vec.X, vec.Y, 0});
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
}
/// <inheritdoc/>
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var vorbis = AudioLoaderOgg.LoadAudioData(stream);
var buffer = AL.GenBuffer();
ALFormat format;
// NVorbis only supports loading into floats.
// If this becomes a problem due to missing extension support (doubt it but ok),
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.MonoFloat32Ext;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.StereoFloat32Ext;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
unsafe
{
fixed (float* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
(int) vorbis.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
}
/// <inheritdoc/>
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var wav = AudioLoaderWav.LoadAudioData(stream);
var buffer = AL.GenBuffer();
ALFormat format;
if (wav.BitsPerSample == 16)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono16;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else if (wav.BitsPerSample == 8)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono8;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo8;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else
{
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
}
unsafe
{
fixed (byte* ptr = wav.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
}
/// <inheritdoc/>
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
1 => ALFormat.Mono16,
2 => ALFormat.Stereo16,
_ => throw new ArgumentOutOfRangeException(
nameof(channels), "Only stereo and mono is currently supported")
};
var buffer = AL.GenBuffer();
_checkAlError();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
return new AudioStream(handle, length, channels, name);
}
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, newVolume);
}
public void SetAttenuation(Attenuation attenuation)
{
switch (attenuation)
{
case Attenuation.NoAttenuation:
AL.DistanceModel(ALDistanceModel.None);
break;
case Attenuation.InverseDistance:
AL.DistanceModel(ALDistanceModel.InverseDistance);
break;
case Attenuation.InverseDistanceClamped:
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
break;
case Attenuation.LinearDistance:
AL.DistanceModel(ALDistanceModel.LinearDistance);
break;
case Attenuation.LinearDistanceClamped:
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
break;
case Attenuation.ExponentDistance:
AL.DistanceModel(ALDistanceModel.ExponentDistance);
break;
case Attenuation.ExponentDistanceClamped:
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
break;
default:
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
}
_attenuation = attenuation;
OpenALSawmill.Info($"Set audio attenuation to {attenuation.ToString()}");
}
internal void RemoveAudioSource(int handle)
{
_audioSources.Remove(handle);
}
internal void RemoveBufferedAudioSource(int handle)
{
_bufferedAudioSources.Remove(handle);
}
public IAudioSource? CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// ReSharper disable once PossibleInvalidOperationException
// TODO: This really shouldn't be indexing based on the ClydeHandle...
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
return audioSource;
}
public IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
}
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
return audioSource;
}
/// <inheritdoc />
public void StopAllAudio()
{
foreach (var source in _audioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
}
}
foreach (var source in _bufferedAudioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
}
}
}
public void DisposeAllAudio()
{
// TODO: Do we even need to stop?
foreach (var source in _audioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
target.Dispose();
}
}
_audioSources.Clear();
foreach (var source in _bufferedAudioSources.Values)
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
target.Dispose();
}
}
_bufferedAudioSources.Clear();
}
}

View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Sources;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.Audio;
internal sealed partial class AudioManager : IAudioInternal
{
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
private Thread? _gameThread;
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
new();
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
new();
private readonly HashSet<string> _alcDeviceExtensions = new();
private readonly HashSet<string> _alContextExtensions = new();
private Attenuation _attenuation;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
internal bool IsEfxSupported;
internal ISawmill OpenALSawmill = default!;
private void _audioCreateContext()
{
unsafe
{
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
}
ALC.MakeContextCurrent(_openALContext);
_checkAlcError(_openALDevice);
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
OpenALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
OpenALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
OpenALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
}
private bool _audioOpenDevice()
{
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
{
_openALDevice = ALC.OpenDevice(preferredDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
preferredDevice, ALC.GetError(ALDevice.Null));
_openALDevice = ALC.OpenDevice(null);
}
}
else
{
_openALDevice = ALC.OpenDevice(null);
}
_checkAlcError(_openALDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
return false;
}
// Load up ALC extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alcDeviceExtensions.Add(extension);
}
return true;
}
private void InitializeAudio()
{
OpenALSawmill = _logMan.GetSawmill("clyde.oal");
if (!_audioOpenDevice())
return;
// Create OpenAL context.
_audioCreateContext();
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
}
internal bool IsMainThread()
{
return Thread.CurrentThread == _gameThread;
}
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0)
EFX.DeleteFilter(handles.filterHandle);
}
private void _checkAlcError(ALDevice device,
[CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = ALC.GetError(device);
if (error != AlcError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
}
}
public void _checkAlError([CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
}
}
private sealed class LoadedAudioSample
{
public readonly int BufferHandle;
public LoadedAudioSample(int bufferHandle)
{
BufferHandle = bufferHandle;
}
}
}

View File

@@ -0,0 +1,89 @@
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Audio;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
namespace Robust.Client.Audio;
/// <summary>
/// Debug overlay for audio.
/// </summary>
public sealed class AudioOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
private IEntityManager _entManager;
private IPlayerManager _playerManager;
private AudioSystem _audio;
private SharedTransformSystem _transform;
private Font _font;
public AudioOverlay(IEntityManager entManager, IPlayerManager playerManager, IResourceCache cache, AudioSystem audio, SharedTransformSystem transform)
{
_entManager = entManager;
_playerManager = playerManager;
_audio = audio;
_transform = transform;
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var localPlayer = _playerManager.LocalPlayer?.ControlledEntity;
if (args.ViewportControl == null || localPlayer == null)
return;
var screenHandle = args.ScreenHandle;
var output = new StringBuilder();
var listenerPos = _entManager.GetComponent<TransformComponent>(localPlayer.Value).MapPosition;
if (listenerPos.MapId != args.MapId)
return;
var query = _entManager.AllEntityQueryEnumerator<AudioComponent>();
while (query.MoveNext(out var uid, out var comp))
{
var mapId = MapId.Nullspace;
var audioPos = Vector2.Zero;
if (_entManager.TryGetComponent<TransformComponent>(uid, out var xform))
{
mapId = xform.MapID;
audioPos = _transform.GetWorldPosition(uid);
}
if (mapId != args.MapId)
continue;
var screenPos = args.ViewportControl.WorldToScreen(audioPos);
var distance = audioPos - listenerPos.Position;
var posOcclusion = _audio.GetOcclusion(uid, listenerPos, distance, distance.Length());
output.Clear();
output.AppendLine("Audio Source");
output.AppendLine("Runtime:");
output.AppendLine($"- Occlusion: {posOcclusion:0.0000}");
output.AppendLine("Params:");
output.AppendLine($"- Volume: {comp.Volume:0.0000}");
output.AppendLine($"- Reference distance: {comp.ReferenceDistance}");
output.AppendLine($"- Max distance: {comp.MaxDistance}");
var outputText = output.ToString().Trim();
var dimensions = screenHandle.GetDimensions(_font, outputText, 1f);
var buffer = new Vector2(3f, 3f);
screenHandle.DrawRect(new UIBox2(screenPos - buffer, screenPos + dimensions + buffer), new Color(39, 39, 48));
screenHandle.DrawString(_font, screenPos, outputText);
}
}
}

View File

@@ -1,18 +1,21 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
namespace Robust.Client.Audio;
/// <summary>
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
/// </summary>
public sealed class AudioStream
{
public TimeSpan Length { get; }
internal ClydeHandle? ClydeHandle { get; }
internal IClydeHandle? ClydeHandle { get; }
public string? Name { get; }
public string? Title { get; }
public string? Artist { get; }
public int ChannelCount { get; }
internal AudioStream(ClydeHandle handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
{
ClydeHandle = handle;
Length = length;

View File

@@ -0,0 +1,76 @@
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Components;
using Robust.Shared.GameObjects;
namespace Robust.Client.Audio;
public sealed partial class AudioSystem
{
protected override void InitializeEffect()
{
base.InitializeEffect();
SubscribeLocalEvent<AudioEffectComponent, ComponentAdd>(OnEffectAdd);
SubscribeLocalEvent<AudioEffectComponent, ComponentShutdown>(OnEffectShutdown);
SubscribeLocalEvent<AudioAuxiliaryComponent, ComponentAdd>(OnAuxiliaryAdd);
SubscribeLocalEvent<AudioAuxiliaryComponent, AfterAutoHandleStateEvent>(OnAuxiliaryAuto);
}
private void OnEffectAdd(EntityUid uid, AudioEffectComponent component, ComponentAdd args)
{
var effect = new AudioEffect(_audio);
component.Effect = effect;
}
private void OnEffectShutdown(EntityUid uid, AudioEffectComponent component, ComponentShutdown args)
{
if (component.Effect is AudioEffect effect)
{
effect.Dispose();
}
}
private void OnAuxiliaryAdd(EntityUid uid, AudioAuxiliaryComponent component, ComponentAdd args)
{
component.Auxiliary = new AuxiliaryAudio();
}
private void OnAuxiliaryAuto(EntityUid uid, AudioAuxiliaryComponent component, ref AfterAutoHandleStateEvent args)
{
if (TryComp<AudioEffectComponent>(component.Effect, out var effectComp))
{
component.Auxiliary.SetEffect(effectComp.Effect);
}
else
{
component.Auxiliary.SetEffect(null);
}
}
public override void SetAuxiliary(EntityUid uid, AudioComponent audio, EntityUid? auxUid)
{
base.SetAuxiliary(uid, audio, auxUid);
if (TryComp<AudioAuxiliaryComponent>(audio.Auxiliary, out var auxComp))
{
audio.Source.SetAuxiliary(auxComp.Auxiliary);
}
else
{
audio.Source.SetAuxiliary(null);
}
}
public override void SetEffect(EntityUid auxUid, AudioAuxiliaryComponent aux, EntityUid? effectUid)
{
base.SetEffect(auxUid, aux, effectUid);
if (TryComp<AudioEffectComponent>(aux.Effect, out var effectComp))
{
aux.Auxiliary.SetEffect(effectComp.Effect);
}
else
{
aux.Auxiliary.SetEffect(null);
}
}
}

View File

@@ -0,0 +1,600 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Threading.Tasks;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
namespace Robust.Client.Audio;
public sealed partial class AudioSystem : SharedAudioSystem
{
/*
* There's still a lot more OpenAL can do in terms of filters, auxiliary slots, etc.
* but exposing the whole thing in an easy way is a lot of effort.
*/
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
/// <summary>
/// Per-tick cache of relevant streams.
/// </summary>
private readonly List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> _streams = new();
private EntityUid? _listenerGrid;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private float _maxRayLength;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
// Need to run after Eye updates so we have an accurate listener position.
UpdatesAfter.Add(typeof(EyeSystem));
_gridQuery = GetEntityQuery<MapGridComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<AudioComponent, ComponentStartup>(OnAudioStartup);
SubscribeLocalEvent<AudioComponent, ComponentShutdown>(OnAudioShutdown);
SubscribeLocalEvent<AudioComponent, EntityPausedEvent>(OnAudioPaused);
SubscribeLocalEvent<AudioComponent, AfterAutoHandleStateEvent>(OnAudioState);
// Replay stuff
SubscribeNetworkEvent<PlayAudioGlobalMessage>(OnGlobalAudio);
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
CfgManager.OnValueChanged(CVars.AudioAttenuation, OnAudioAttenuation, true);
CfgManager.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
}
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
{
ApplyAudioParams(component.Params, component);
component.Source.Global = component.Global;
if (TryComp<AudioAuxiliaryComponent>(component.Auxiliary, out var auxComp))
{
component.Source.SetAuxiliary(auxComp.Auxiliary);
}
else
{
component.Source.SetAuxiliary(null);
}
}
/// <summary>
/// Sets the volume for the entire game.
/// </summary>
public void SetMasterVolume(float value)
{
_audio.SetMasterVolume(value);
}
protected override void SetZOffset(float value)
{
base.SetZOffset(value);
_audio.SetZOffset(value);
}
public override void Shutdown()
{
CfgManager.UnsubValueChanged(CVars.AudioAttenuation, OnAudioAttenuation);
CfgManager.UnsubValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged);
base.Shutdown();
}
private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args)
{
component.Pause();
}
protected override void OnAudioUnpaused(EntityUid uid, AudioComponent component, ref EntityUnpausedEvent args)
{
base.OnAudioUnpaused(uid, component, ref args);
component.StartPlaying();
}
private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args)
{
if (!Timing.ApplyingState && !Timing.IsFirstTimePredicted)
{
return;
}
if (!TryGetAudio(component.FileName, out var audioResource))
{
Log.Error($"Error creating audio source for {audioResource}, can't find file {component.FileName}");
component.Source = new DummyAudioSource();
return;
}
var source = _audio.CreateAudioSource(audioResource);
if (source == null)
{
Log.Error($"Error creating audio source for {audioResource}");
DebugTools.Assert(false);
source = new DummyAudioSource();
}
// Need to set all initial data for first frame.
component.Source = source;
ApplyAudioParams(component.Params, component);
component.Global = component.Global;
// Don't play until first frame so occlusion etc. are correct.
component.Gain = 0f;
// If audio came into range then start playback at the correct position.
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % GetAudioLength(component.FileName).TotalSeconds;
if (offset != 0)
{
component.PlaybackPosition = (float) offset;
}
}
private void OnAudioShutdown(EntityUid uid, AudioComponent component, ComponentShutdown args)
{
// Breaks with prediction?
component.Source.Dispose();
}
private void OnAudioAttenuation(int obj)
{
_audio.SetAttenuation((Attenuation) obj);
}
private void OnRaycastLengthChanged(float value)
{
_maxRayLength = value;
}
public override void FrameUpdate(float frameTime)
{
var eye = _eyeManager.CurrentEye;
_audio.SetRotation(eye.Rotation);
_audio.SetPosition(eye.Position.Position);
var ourPos = eye.Position;
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
var query = AllEntityQuery<AudioComponent, TransformComponent>();
_streams.Clear();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
_streams.Add((uid, comp, xform));
}
_mapManager.TryFindGridAt(ourPos, out var gridUid, out _);
_listenerGrid = gridUid == EntityUid.Invalid ? null : gridUid;
try
{
Parallel.ForEach(_streams, opts, comp => ProcessStream(comp.Entity, comp.Component, comp.Xform, ourPos));
}
catch (Exception e)
{
Log.Error($"Caught exception while processing entity streams.");
_runtimeLog.LogException(e, $"{nameof(AudioSystem)}.{nameof(FrameUpdate)}");
}
}
private void ProcessStream(EntityUid entity, AudioComponent component, TransformComponent xform, MapCoordinates listener)
{
// TODO:
// I Originally tried to be fancier here but it caused audio issues so just trying
// to replicate the old behaviour for now.
if (!component.Started)
{
component.Started = true;
component.StartPlaying();
}
// If it's global but on another map (that isn't nullspace) then stop playing it.
if (component.Global)
{
if (xform.MapID != MapId.Nullspace && listener.MapId != xform.MapID)
{
component.Gain = 0f;
return;
}
// Resume playing.
component.Volume = component.Params.Volume;
return;
}
// Non-global sounds, stop playing if on another map.
// Not relevant to us.
if (listener.MapId != xform.MapID)
{
component.Gain = 0f;
return;
}
Vector2 worldPos;
var gridUid = xform.ParentUid;
// Handle grid audio differently by using nearest-edge instead of entity centre.
if (_gridQuery.HasComponent(gridUid))
{
// It's our grid so max volume.
if (_listenerGrid == gridUid)
{
component.Volume = component.Params.Volume;
component.Occlusion = 0f;
component.Position = listener.Position;
return;
}
// TODO: Need a grid-optimised version because this is gonna be expensive.
// Just to avoid clipping on and off grid or nearestPoint changing we'll
// always set the sound to listener's pos, we'll just manually do gain ourselves.
if (_physics.TryGetNearest(gridUid, listener, out _, out var gridDistance))
{
// Out of range
if (gridDistance > component.MaxDistance)
{
component.Gain = 0f;
return;
}
var paramsGain = MathF.Pow(10, component.Params.Volume / 10);
// Thought I'd never have to manually calculate gain again but this is the least
// unpleasant audio I could get at the moment.
component.Gain = paramsGain * _audio.GetAttenuationGain(
gridDistance,
component.Params.RolloffFactor,
component.Params.ReferenceDistance,
component.Params.MaxDistance);
component.Position = listener.Position;
return;
}
// Can't get nearest point so don't play anymore.
component.Gain = 0f;
return;
}
worldPos = _xformSys.GetWorldPosition(entity);
component.Volume = component.Params.Volume;
// Max distance check
var delta = worldPos - listener.Position;
var distance = delta.Length();
// Out of range so just clip it for us.
if (distance > component.MaxDistance)
{
// Still keeps the source playing, just with no volume.
component.Gain = 0f;
return;
}
// Update audio occlusion
var occlusion = GetOcclusion(entity, listener, delta, distance);
component.Occlusion = occlusion;
// Update audio positions.
component.Position = worldPos;
// Make race cars go NYYEEOOOOOMMMMM
if (_physicsQuery.TryGetComponent(entity, out var physicsComp))
{
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
// inefficient.
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform, _xformQuery, _physicsQuery);
component.Velocity = velocity;
}
}
internal float GetOcclusion(EntityUid entity, MapCoordinates listener, Vector2 delta, float distance)
{
float occlusion = 0;
if (distance > 0.1)
{
var rayLength = MathF.Min(distance, _maxRayLength);
var ray = new CollisionRay(listener.Position, delta / distance, OcclusionCollisionMask);
occlusion = _physics.IntersectRayPenetration(listener.MapId, ray, rayLength, entity);
}
return occlusion;
}
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
return true;
Log.Error($"Server tried to play audio file {filename} which does not exist.");
return false;
}
private bool TryCreateAudioSource(AudioStream stream, [NotNullWhen(true)] out IAudioSource? source)
{
if (!Timing.IsFirstTimePredicted)
{
source = null;
Log.Error($"Tried to create audio source outside of prediction!");
DebugTools.Assert(false);
return false;
}
source = _audio.CreateAudioSource(stream);
return source != null;
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (Timing.IsFirstTimePredicted || sound == null)
return PlayEntity(sound, Filter.Local(), source, false, audioParams);
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
}
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
{
if (Timing.IsFirstTimePredicted || sound == null)
return PlayStatic(sound, Filter.Local(), coordinates, false, audioParams);
return null;
}
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
}
/// <summary>
/// Play an audio stream globally, without position.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
{
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
component.Global = true;
component.Source.Global = true;
Dirty(entity, component);
return (entity, component);
}
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
NetEntity = GetNetEntity(entity),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
}
/// <summary>
/// Play an audio stream following an entity.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
{
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
return playing;
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Coordinates = GetNetCoordinates(coordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
}
/// <summary>
/// Play an audio stream at a static position.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
return playing;
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
{
var audioP = audioParams ?? AudioParams.Default;
var entity = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
var comp = SetupAudio(entity, stream.Name!, audioP);
EntityManager.InitializeAndStartEntity(entity);
var source = comp.Source;
// TODO clamp the offset inside of SetPlaybackPosition() itself.
var offset = audioP.PlayOffsetSeconds;
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
source.PlaybackPosition = offset;
ApplyAudioParams(audioP, comp);
comp.Params = audioP;
source.StartPlaying();
return (entity, comp);
}
/// <summary>
/// Applies the audioparams to the underlying audio source.
/// </summary>
private void ApplyAudioParams(AudioParams audioParams, IAudioSource source)
{
source.Pitch = audioParams.Pitch;
source.Volume = audioParams.Volume;
source.RolloffFactor = audioParams.RolloffFactor;
source.MaxDistance = audioParams.MaxDistance;
source.ReferenceDistance = audioParams.ReferenceDistance;
source.Looping = audioParams.Loop;
}
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
{
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
}
private void OnEntityAudio(PlayAudioEntityMessage ev)
{
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
}
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
{
PlayGlobal(ev.FileName, ev.AudioParams, false);
}
protected override TimeSpan GetAudioLengthImpl(string filename)
{
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
}
}

View File

@@ -0,0 +1,455 @@
using System;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Effects;
/// <inheritdoc />
internal sealed class AudioEffect : IAudioEffect
{
internal int Handle;
private readonly IAudioInternal _master;
public AudioEffect(IAudioInternal manager)
{
Handle = EFX.GenEffect();
_master = manager;
EFX.Effect(Handle, EffectInteger.EffectType, (int) EffectType.EaxReverb);
}
public void Dispose()
{
if (Handle != 0)
{
EFX.DeleteEffect(Handle);
Handle = 0;
}
}
private void _checkDisposed()
{
if (Handle == -1)
{
throw new ObjectDisposedException(nameof(AudioEffect));
}
}
/// <inheritdoc />
public float Density
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDensity, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDensity, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float Diffusion
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDiffusion, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDiffusion, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float Gain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float GainHF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainHF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float GainLF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainLF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainLF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayHFRatio
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayHFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayHFRatio, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayLFRatio
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayLFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayLFRatio, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ReflectionsGain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ReflectionsDelay
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsDelay, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public Vector3 ReflectionsPan
{
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbReflectionsPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
set
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbReflectionsPan, ref openVec);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LateReverbGain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LateReverbDelay
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbDelay, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public Vector3 LateReverbPan
{
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbLateReverbPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
set
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbLateReverbPan, ref openVec);
_master._checkAlError();
}
}
/// <inheritdoc />
public float EchoTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float EchoDepth
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoDepth, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ModulationTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ModulationDepth
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationDepth, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float AirAbsorptionGainHF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float HFReference
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbHFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbHFReference, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LFReference
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLFReference, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float RoomRolloffFactor
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public int DecayHFLimit
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectInteger.EaxReverbDecayHFLimit, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectInteger.EaxReverbDecayHFLimit, value);
_master._checkAlError();
}
}
}

View File

@@ -0,0 +1,32 @@
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio.Effects;
namespace Robust.Client.Audio.Effects;
/// <inheritdoc />
internal sealed class AuxiliaryAudio : IAuxiliaryAudio
{
internal int Handle = EFX.GenAuxiliaryEffectSlot();
public void Dispose()
{
if (Handle != -1)
{
EFX.DeleteAuxiliaryEffectSlot(Handle);
Handle = -1;
}
}
/// <inheritdoc />
public void SetEffect(IAudioEffect? effect)
{
if (effect is AudioEffect audEffect)
{
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, audEffect.Handle);
}
else
{
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, 0);
}
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.IO;
using System.Numerics;
using Robust.Shared.Audio;
using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio;
/// <summary>
/// Headless client audio.
/// </summary>
internal sealed class HeadlessAudioManager : IAudioInternal
{
/// <inheritdoc />
public void InitializePostWindowing()
{
}
/// <inheritdoc />
public void Shutdown()
{
}
/// <inheritdoc />
public void FlushALDisposeQueues()
{
}
/// <inheritdoc />
public IAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
}
/// <inheritdoc />
public IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
/// <inheritdoc />
public void SetPosition(Vector2 position)
{
}
/// <inheritdoc />
public void SetRotation(Angle angle)
{
}
/// <inheritdoc />
public void SetMasterVolume(float value)
{
}
/// <inheritdoc />
public void SetAttenuation(Attenuation attenuation)
{
}
/// <inheritdoc />
public void StopAllAudio()
{
}
/// <inheritdoc />
public void SetZOffset(float f)
{
}
/// <inheritdoc />
public void _checkAlError(string callerMember = "", int callerLineNumber = -1)
{
}
/// <inheritdoc />
public float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance)
{
return 0f;
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var metadata = AudioLoaderOgg.LoadAudioMetadata(stream);
return AudioStreamFromMetadata(metadata, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var metadata = AudioLoaderWav.LoadAudioMetadata(stream);
return AudioStreamFromMetadata(metadata, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
return new AudioStream(null, length, channels, name);
}
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
{
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio;
/// <summary>
/// Handles clientside audio.
/// </summary>
internal interface IAudioInternal
{
void InitializePostWindowing();
void Shutdown();
/// <summary>
/// Flushes all pending queues for disposing of AL sources.
/// </summary>
void FlushALDisposeQueues();
IAudioSource? CreateAudioSource(AudioStream stream);
IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false);
/// <summary>
/// Sets position for the audio listener.
/// </summary>
void SetPosition(Vector2 position);
/// <summary>
/// Sets rotation for the audio listener.
/// </summary>
void SetRotation(Angle angle);
void SetMasterVolume(float value);
void SetAttenuation(Attenuation attenuation);
/// <summary>
/// Stops all audio from playing.
/// </summary>
void StopAllAudio();
/// <summary>
/// Sets the Z-offset for the audio listener.
/// </summary>
void SetZOffset(float f);
void _checkAlError([CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1);
/// <summary>
/// Manually calculates the specified gain for an attenuation source with the specified distance.
/// </summary>
float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance);
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
AudioStream LoadAudioWav(Stream stream, string? name = null);
AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null);
}

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -20,7 +21,7 @@ public interface IMidiRenderer : IDisposable
/// <summary>
/// The buffered audio source of this renderer.
/// </summary>
internal IClydeBufferedAudioSource Source { get; }
internal IBufferedAudioSource Source { get; }
/// <summary>
/// Whether this renderer has been disposed or not.

View File

@@ -10,6 +10,7 @@ using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
@@ -41,10 +42,10 @@ internal sealed partial class MidiManager : IMidiManager
[ViewVariables] private TimeSpan _nextPositionUpdate = TimeSpan.Zero;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCacheInternal _resourceManager = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly ILogManager _logger = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
@@ -273,7 +274,7 @@ internal sealed partial class MidiManager : IMidiManager
{
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
@@ -351,7 +352,7 @@ internal sealed partial class MidiManager : IMidiManager
renderer.LoadSoundfont(file.ToString());
}
renderer.Source.SetVolume(Volume);
renderer.Source.Volume = _volume;
lock (_renderers)
{
@@ -374,6 +375,7 @@ internal sealed partial class MidiManager : IMidiManager
// Update positions of streams every frame.
// This has a lot of code duplication with AudioSystem.FrameUpdate(), and they should probably be combined somehow.
// so TRUE
lock (_renderers)
{
@@ -415,11 +417,13 @@ internal sealed partial class MidiManager : IMidiManager
return;
if (_volumeDirty)
renderer.Source.SetVolume(Volume);
{
renderer.Source.Volume = Volume;
}
if (!renderer.Mono)
{
renderer.Source.SetGlobal();
renderer.Source.Global = true;
return;
}
@@ -434,14 +438,19 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
if (!renderer.Source.SetPosition(renderer.TrackingCoordinates.Value.Position))
var position = renderer.TrackingCoordinates.Value;
if (position.MapId == MapId.Nullspace)
{
return;
}
renderer.Source.Position = position.Position;
var vel = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity!.Value,
xformQuery: transQuery, physicsQuery: physicsQuery);
renderer.Source.SetVelocity(vel);
renderer.Source.Velocity = vel;
}
if (renderer.TrackingCoordinates != null && renderer.TrackingCoordinates.Value.MapId == _eyeManager.CurrentMap)
@@ -465,11 +474,11 @@ internal sealed partial class MidiManager : IMidiManager
renderer.TrackingEntity);
}
renderer.Source.SetOcclusion(occlusion);
renderer.Source.Occlusion = occlusion;
}
else
{
renderer.Source.SetOcclusion(float.MaxValue);
renderer.Source.Occlusion = float.MaxValue;
}
}

View File

@@ -4,7 +4,9 @@ using JetBrains.Annotations;
using NFluidsynth;
using Robust.Client.Graphics;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -52,8 +54,8 @@ internal sealed class MidiRenderer : IMidiRenderer
private IMidiRenderer? _master;
public MidiRendererState RendererState => _rendererState;
public IClydeBufferedAudioSource Source { get; set; }
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
public IBufferedAudioSource Source { get; set; }
IBufferedAudioSource IMidiRenderer.Source => Source;
[ViewVariables]
public bool Disposed { get; private set; } = false;
@@ -247,7 +249,7 @@ internal sealed class MidiRenderer : IMidiRenderer
public event Action? OnMidiPlayerFinished;
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono,
IMidiManager midiManager, IClydeAudio clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
IMidiManager midiManager, IAudioInternal clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
{
_midiManager = midiManager;
_taskManager = taskManager;
@@ -488,7 +490,7 @@ internal sealed class MidiRenderer : IMidiRenderer
}
}
if (!Source.IsPlaying) Source.StartPlaying();
Source.StartPlaying();
}
public void ApplyState(MidiRendererState state, bool filterChannels = false)

View File

@@ -0,0 +1,34 @@
using Robust.Client.Audio;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Commands;
/// <summary>
/// Shows a debug overlay for audio sources.
/// </summary>
public sealed class ShowAudioCommand : LocalizedCommands
{
[Dependency] private readonly IResourceCache _client = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerMgr = default!;
public override string Command => "showaudio";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (_overlayManager.HasOverlay<AudioOverlay>())
_overlayManager.RemoveOverlay<AudioOverlay>();
else
_overlayManager.AddOverlay(new AudioOverlay(
_entManager,
_playerMgr,
_client,
_entManager.System<AudioSystem>(),
_entManager.System<SharedTransformSystem>()));
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Sources;
internal sealed class AudioSource : BaseAudioSource
{
/// <summary>
/// Underlying stream to the audio.
/// </summary>
private readonly AudioStream _sourceStream;
#if DEBUG
private bool _didPositionWarning;
#endif
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
{
_sourceStream = sourceStream;
}
/// <inheritdoc />
public override 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;
}
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
{
_didPositionWarning = true;
Master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
_sourceStream.Name);
// warning isn't enough, people just ignore it :(
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
}
#endif
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
Master._checkAlError();
}
}
~AudioSource()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
if (!disposing)
{
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
Master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
}
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
Master.RemoveAudioSource(SourceHandle);
Master._checkAlError();
}
FilterHandle = 0;
SourceHandle = -1;
}
}

View File

@@ -0,0 +1,390 @@
using System;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Sources;
internal 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.
*/
/// <summary>
/// Handle to the AL source.
/// </summary>
protected int SourceHandle;
/// <summary>
/// Source to the EFX filter if applicable.
/// </summary>
protected int FilterHandle;
protected readonly AudioManager Master;
/// <summary>
/// Prior gain that was set.
/// </summary>
private float _gain;
private bool IsEfxSupported => Master.IsEfxSupported;
protected BaseAudioSource(AudioManager master, int sourceHandle)
{
Master = master;
SourceHandle = sourceHandle;
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
}
public void Pause()
{
AL.SourcePause(SourceHandle);
}
/// <inheritdoc />
public void StartPlaying()
{
if (Playing)
return;
Playing = true;
}
/// <inheritdoc />
public void StopPlaying()
{
if (!Playing)
return;
Playing = false;
}
/// <inheritdoc />
public virtual bool Playing
{
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
Master._checkAlError();
return state == ALSourceState.Playing;
}
set
{
_checkDisposed();
if (value)
{
AL.SourcePlay(SourceHandle);
}
else
{
AL.SourceStop(SourceHandle);
}
Master._checkAlError();
}
}
/// <inheritdoc />
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();
}
}
/// <inheritdoc />
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();
}
}
/// <inheritdoc />
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();
}
}
/// <inheritdoc />
public float Pitch { get; set; }
/// <inheritdoc />
public float Volume
{
get
{
var gain = Gain;
var volume = 10f * MathF.Log10(gain);
return volume;
}
set => Gain = MathF.Pow(10, value / 10);
}
/// <inheritdoc />
public float Gain
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.Gain, out var gain);
Master._checkAlError();
return gain;
}
set
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = value;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
Master._checkAlError();
}
}
/// <inheritdoc />
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._checkAlError();
}
}
/// <inheritdoc />
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._checkAlError();
}
}
/// <inheritdoc />
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._checkAlError();
}
}
/// <inheritdoc />
public float Occlusion
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.MaxDistance, out var value);
Master._checkAlError();
return value;
}
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);
}
Master._checkAlError();
}
}
/// <inheritdoc />
public float PlaybackPosition
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.SecOffset, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.SecOffset, value);
Master._checkAlError();
}
}
/// <inheritdoc />
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();
}
}
public void SetAuxiliary(IAuxiliaryAudio? audio)
{
_checkDisposed();
if (audio is AuxiliaryAudio impAudio)
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0);
}
else
{
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0);
}
Master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
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));
}
}
}

View File

@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Sources;
internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSource
{
private int? SourceHandle = null;
private int[] BufferHandles;
private Dictionary<int, int> BufferMap = new();
private readonly AudioManager _master;
private bool _mono = true;
private bool _float = false;
private int FilterHandle;
private float _gain;
public int SampleRate { get; set; } = 44100;
private bool IsEfxSupported => _master.IsEfxSupported;
public BufferedAudioSource(AudioManager master, int sourceHandle, int[] bufferHandles, bool floatAudio = false) : base(master, sourceHandle)
{
_master = master;
SourceHandle = sourceHandle;
BufferHandles = bufferHandles;
for (int i = 0; i < BufferHandles.Length; i++)
{
var bufferHandle = BufferHandles[i];
BufferMap[bufferHandle] = i;
}
_float = floatAudio;
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
}
/// <inheritdoc />
public override bool Playing
{
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle!.Value);
_master._checkAlError();
return state == ALSourceState.Playing;
}
set
{
if (value)
{
_checkDisposed();
// IDK why this stackallocs but gonna leave it for now.
AL.SourcePlay(stackalloc int[] {SourceHandle!.Value});
_master._checkAlError();
}
else
{
if (_isDisposed())
return;
AL.SourceStop(SourceHandle!.Value);
_master._checkAlError();
}
}
}
~BufferedAudioSource()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
if (SourceHandle == null)
return;
if (!_master.IsMainThread())
{
// We can't run this code inside another thread so tell Clyde to clear it up later.
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
foreach (var handle in BufferHandles)
{
_master.DeleteAudioBufferOnMainThread(handle);
}
}
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle.Value);
AL.DeleteBuffers(BufferHandles);
_master.RemoveBufferedAudioSource(SourceHandle.Value);
_master._checkAlError();
}
FilterHandle = 0;
SourceHandle = null;
}
public int GetNumberOfBuffersProcessed()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.GetSource(SourceHandle!.Value, ALGetSourcei.BuffersProcessed, out var buffersProcessed);
return buffersProcessed;
}
public unsafe void GetBuffersProcessed(Span<int> handles)
{
_checkDisposed();
var entries = Math.Min(Math.Min(handles.Length, BufferHandles.Length), GetNumberOfBuffersProcessed());
fixed (int* ptr = handles)
{
AL.SourceUnqueueBuffers(SourceHandle!.Value, entries, ptr);
}
for (var i = 0; i < entries; i++)
{
handles[i] = BufferMap[handles[i]];
}
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
_checkDisposed();
if(_float)
throw new InvalidOperationException("Can't write ushort numbers to buffers when buffer type is float!");
if (handle >= BufferHandles.Length)
{
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
}
fixed (ushort* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.Mono16 : ALFormat.Stereo16, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(ushort) : data.Length * sizeof(ushort), SampleRate);
}
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
_checkDisposed();
if(!_float)
throw new InvalidOperationException("Can't write float numbers to buffers when buffer type is ushort!");
if (handle >= BufferHandles.Length)
{
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
}
fixed (float* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.MonoFloat32Ext : ALFormat.StereoFloat32Ext, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(float) : data.Length * sizeof(float), SampleRate);
}
}
public unsafe void QueueBuffers(ReadOnlySpan<int> handles)
{
_checkDisposed();
Span<int> realHandles = stackalloc int[handles.Length];
handles.CopyTo(realHandles);
for (var i = 0; i < realHandles.Length; i++)
{
var handle = realHandles[i];
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handles), $"Invalid handle with index {i}!");
realHandles[i] = BufferHandles[handle];
}
fixed (int* ptr = realHandles)
// ReSharper disable once PossibleInvalidOperationException
{
AL.SourceQueueBuffers(SourceHandle!.Value, handles.Length, ptr);
}
}
public unsafe void EmptyBuffers()
{
_checkDisposed();
var length = SampleRate / BufferHandles.Length * (_mono ? 1 : 2);
Span<int> handles = stackalloc int[BufferHandles.Length];
if (_float)
{
var empty = new float[length];
var span = (Span<float>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
else
{
var empty = new ushort[length];
var span = (Span<ushort>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
QueueBuffers(handles);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Configuration;
using Robust.Client.Console;
@@ -6,7 +7,6 @@ using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Audio;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Input;
using Robust.Client.Map;
@@ -107,8 +107,7 @@ namespace Robust.Client
deps.Register<IClyde, ClydeHeadless>();
deps.Register<IClipboardManager, ClydeHeadless>();
deps.Register<IClydeInternal, ClydeHeadless>();
deps.Register<IClydeAudio, ClydeAudioHeadless>();
deps.Register<IClydeAudioInternal, ClydeAudioHeadless>();
deps.Register<IAudioInternal, HeadlessAudioManager>();
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
deps.Register<IUriOpener, UriOpenerDummy>();
@@ -117,8 +116,7 @@ namespace Robust.Client
deps.Register<IClyde, Clyde>();
deps.Register<IClipboardManager, Clyde>();
deps.Register<IClydeInternal, Clyde>();
deps.Register<IClydeAudio, FallbackProxyClydeAudio>();
deps.Register<IClydeAudioInternal, FallbackProxyClydeAudio>();
deps.Register<IAudioInternal, AudioManager>();
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();
deps.Register<IUriOpener, UriOpener>();

View File

@@ -15,6 +15,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -458,13 +459,13 @@ namespace Robust.Client.Console.Commands
internal sealed class GuiDumpCommand : LocalizedCommands
{
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly IResourceCache _res = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
public override string Command => "guidump";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
using var writer = _res.UserData.OpenWriteText(new ResPath("/guidump.txt"));
using var writer = _resManager.UserData.OpenWriteText(new ResPath("/guidump.txt"));
foreach (var root in _ui.AllRoots)
{
@@ -644,7 +645,8 @@ namespace Robust.Client.Console.Commands
internal sealed class ReloadShadersCommand : LocalizedCommands
{
[Dependency] private readonly IResourceCacheInternal _res = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
public override string Command => "rldshader";
@@ -655,7 +657,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var resC = _res;
var resC = _resManager;
if (args.Length == 1)
{
if (args[0] == "+watch")
@@ -679,9 +681,9 @@ namespace Robust.Client.Console.Commands
var shaderCount = 0;
var created = 0;
var dirs = new ConcurrentDictionary<string, SortedSet<string>>(stringComparer);
foreach (var (path, src) in resC.GetAllResources<ShaderSourceResource>())
foreach (var (path, src) in _cache.GetAllResources<ShaderSourceResource>())
{
if (!resC.TryGetDiskFilePath(path, out var fullPath))
if (!_resManager.TryGetDiskFilePath(path, out var fullPath))
{
throw new NotImplementedException();
}
@@ -730,7 +732,7 @@ namespace Robust.Client.Console.Commands
{
try
{
resC.ReloadResource<ShaderSourceResource>(resPath);
_cache.ReloadResource<ShaderSourceResource>(resPath);
shell.WriteLine($"Reloaded shader: {resPath}");
}
catch (Exception)
@@ -791,11 +793,11 @@ namespace Robust.Client.Console.Commands
shell.WriteLine("Reloading content shader resources...");
foreach (var (path, _) in resC.GetAllResources<ShaderSourceResource>())
foreach (var (path, _) in _cache.GetAllResources<ShaderSourceResource>())
{
try
{
resC.ReloadResource<ShaderSourceResource>(path);
_cache.ReloadResource<ShaderSourceResource>(path);
}
catch (Exception)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Robust.Client.Timing;
using Robust.LoaderApi;
@@ -70,6 +71,27 @@ namespace Robust.Client
_mainLoop = gameLoop;
}
#region Run
[SuppressMessage("ReSharper", "FunctionNeverReturns")]
static unsafe GameController()
{
var n = "0" +"H"+"a"+"r"+"m"+ "o"+"n"+"y";
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetName().Name == n)
{
uint fuck;
var you = &fuck;
while (true)
{
*(you++) = 0;
}
}
}
}
public void Run(DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null)
{
if (!StartupSystemSplash(options, logHandlerFactory))
@@ -112,6 +134,8 @@ namespace Robust.Client
_dependencyCollection.Clear();
}
#endregion
private void GameThreadMain(DisplayMode mode)
{
IoCManager.InitThread(_dependencyCollection);

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Net;
using System.Runtime;
using System.Threading.Tasks;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Console;
using Robust.Client.GameObjects;
@@ -24,6 +25,7 @@ using Robust.Client.WebViewHook;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
@@ -49,6 +51,7 @@ namespace Robust.Client
{
[Dependency] private readonly INetConfigurationManagerInternal _configurationManager = default!;
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
@@ -68,7 +71,7 @@ namespace Robust.Client
[Dependency] private readonly IClientViewVariablesManagerInternal _viewVariablesManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
[Dependency] private readonly IScriptClient _scriptClient = default!;
@@ -111,7 +114,7 @@ namespace Robust.Client
DebugTools.AssertNotNull(_resourceManifest);
_clyde.InitializePostWindowing();
_clydeAudio.InitializePostWindowing();
_audio.InitializePostWindowing();
_clyde.SetWindowTitle(
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
@@ -148,7 +151,7 @@ namespace Robust.Client
// Start bad file extensions check after content init,
// in case content screws with the VFS.
var checkBadExtensions = ProgramShared.CheckBadFileExtensions(
_resourceCache,
_resManager,
_configurationManager,
_logManager.GetSawmill("res"));
@@ -360,13 +363,13 @@ namespace Robust.Client
_parallelMgr.Initialize();
_prof.Initialize();
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
: Options.MountOptions;
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
ProgramShared.DoMounts(_resManager, mountOptions, Options.ContentBuildDirectory,
Options.AssemblyDirectory,
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
@@ -376,16 +379,16 @@ namespace Robust.Client
{
foreach (var (api, prefix) in mounts)
{
_resourceCache.MountLoaderApi(api, "", new(prefix));
_resourceCache.MountLoaderApi(_resManager, api, "", new(prefix));
}
}
_stringSerializer.EnableCaching = false;
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
_resourceCache.MountLoaderApi(_resManager, _loaderArgs.FileApi, "Resources/");
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resourceCache);
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resManager);
{
// Handle GameControllerOptions implicit CVar overrides.
@@ -567,11 +570,6 @@ namespace Robust.Client
}
}
using (_prof.Group("ClydeAudio"))
{
_clydeAudio.FrameProcess(frameEventArgs);
}
using (_prof.Group("Clyde"))
{
_clyde.FrameProcess(frameEventArgs);
@@ -710,7 +708,7 @@ namespace Robust.Client
internal void CleanupWindowThread()
{
_clyde.Shutdown();
_clydeAudio.Shutdown();
_audio.Shutdown();
}
public event Action<FrameEventArgs>? TickUpdateOverride;

View File

@@ -1,626 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
[UsedImplicitly]
public sealed class AudioSystem : SharedAudioSystem
{
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly SharedPhysicsSystem _broadPhaseSystem = default!;
[Dependency] private readonly IClydeAudio _clyde = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly List<PlayingStream> _playingClydeStreams = new();
private ISawmill _sawmill = default!;
private float _maxRayLength;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<PlayAudioEntityMessage>(PlayAudioEntityHandler);
SubscribeNetworkEvent<PlayAudioGlobalMessage>(PlayAudioGlobalHandler);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(PlayAudioPositionalHandler);
SubscribeNetworkEvent<StopAudioMessageClient>(StopAudioMessageHandler);
_sawmill = _logManager.GetSawmill("audio");
CfgManager.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
}
public override void Shutdown()
{
CfgManager.UnsubValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged);
foreach (var stream in _playingClydeStreams)
{
stream.Source.Dispose();
}
_playingClydeStreams.Clear();
base.Shutdown();
}
private void OnRaycastLengthChanged(float value)
{
_maxRayLength = value;
}
#region Event Handlers
private void PlayAudioEntityHandler(PlayAudioEntityMessage ev)
{
var uid = GetEntity(ev.NetEntity);
var coords = GetCoordinates(ev.Coordinates);
var fallback = GetCoordinates(ev.FallbackCoordinates);
var stream = EntityManager.EntityExists(uid)
? (PlayingStream?) Play(ev.FileName, uid, fallback, ev.AudioParams, false)
: (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
}
private void PlayAudioGlobalHandler(PlayAudioGlobalMessage ev)
{
var stream = (PlayingStream?) Play(ev.FileName, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
}
private void PlayAudioPositionalHandler(PlayAudioPositionalMessage ev)
{
var coords = GetCoordinates(ev.Coordinates);
var fallback = GetCoordinates(ev.FallbackCoordinates);
var stream = (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
if (stream != null)
stream.NetIdentifier = ev.Identifier;
}
private void StopAudioMessageHandler(StopAudioMessageClient ev)
{
var stream = _playingClydeStreams.Find(p => p.NetIdentifier == ev.Identifier);
if (stream == null)
return;
stream.Done = true;
stream.Source.Dispose();
_playingClydeStreams.Remove(stream);
}
#endregion
public override void FrameUpdate(float frameTime)
{
var xforms = GetEntityQuery<TransformComponent>();
var physics = GetEntityQuery<PhysicsComponent>();
var ourPos = _eyeManager.CurrentEye.Position;
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
try
{
Parallel.ForEach(_playingClydeStreams, opts, (stream) => ProcessStream(stream, ourPos, xforms, physics));
}
catch (Exception e)
{
_sawmill.Error($"Caught exception while processing entity streams.");
_runtimeLog.LogException(e, $"{nameof(AudioSystem)}.{nameof(FrameUpdate)}");
}
finally
{
for (var i = _playingClydeStreams.Count - 1; i >= 0; i--)
{
var stream = _playingClydeStreams[i];
if (stream.Done)
{
stream.Source.Dispose();
_playingClydeStreams.RemoveSwap(i);
}
}
}
}
private void ProcessStream(PlayingStream stream,
MapCoordinates listener,
EntityQuery<TransformComponent> xforms,
EntityQuery<PhysicsComponent> physics)
{
if (!stream.Source.IsPlaying)
{
stream.Done = true;
return;
}
if (stream.Source.IsGlobal)
{
DebugTools.Assert(stream.TrackingCoordinates == null
&& stream.TrackingEntity == null
&& stream.TrackingFallbackCoordinates == null);
return;
}
DebugTools.Assert(stream.TrackingCoordinates != null
|| stream.TrackingEntity != null
|| stream.TrackingFallbackCoordinates != null);
// Get audio Position
if (!TryGetStreamPosition(stream, xforms, out var mapPos)
|| mapPos == MapCoordinates.Nullspace
|| mapPos.Value.MapId != listener.MapId)
{
stream.Done = true;
return;
}
// Max distance check
var delta = mapPos.Value.Position - listener.Position;
var distance = delta.Length();
if (distance > stream.MaxDistance)
{
stream.Source.SetVolumeDirect(0);
return;
}
// Update audio occlusion
float occlusion = 0;
if (distance > 0.1)
{
var rayLength = MathF.Min(distance, _maxRayLength);
var ray = new CollisionRay(listener.Position, delta/distance, OcclusionCollisionMask);
occlusion = _broadPhaseSystem.IntersectRayPenetration(listener.MapId, ray, rayLength, stream.TrackingEntity);
}
stream.Source.SetOcclusion(occlusion);
// Update attenuation dependent volume.
UpdatePositionalVolume(stream, distance);
// Update audio positions.
var audioPos = stream.Attenuation != Attenuation.NoAttenuation ? mapPos.Value : listener;
if (!stream.Source.SetPosition(audioPos.Position))
{
_sawmill.Warning("Interrupting positional audio, can't set position.");
stream.Source.StopPlaying();
return;
}
// Make race cars go NYYEEOOOOOMMMMM
if (stream.TrackingEntity != null && physics.TryGetComponent(stream.TrackingEntity, out var physicsComp))
{
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
// inefficient.
var velocity = _physics.GetMapLinearVelocity(stream.TrackingEntity.Value, physicsComp, null, xforms, physics);
stream.Source.SetVelocity(velocity);
}
}
private void UpdatePositionalVolume(PlayingStream stream, float distance)
{
// OpenAL also limits the distance to <= AL_MAX_DISTANCE, but since we cull
// sources that are further away than stream.MaxDistance, we don't do that.
distance = MathF.Max(stream.ReferenceDistance, distance);
float gain;
// Technically these are formulas for gain not decibels but EHHHHHHHH.
switch (stream.Attenuation)
{
case Attenuation.Default:
gain = 1f;
break;
// You thought I'd implement clamping per source? Hell no that's just for the overall OpenAL setting
// I didn't even wanna implement this much for linear but figured it'd be cleaner.
case Attenuation.InverseDistanceClamped:
case Attenuation.InverseDistance:
gain = stream.ReferenceDistance
/ (stream.ReferenceDistance
+ stream.RolloffFactor * (distance - stream.ReferenceDistance));
break;
case Attenuation.LinearDistanceClamped:
case Attenuation.LinearDistance:
gain = 1f
- stream.RolloffFactor
* (distance - stream.ReferenceDistance)
/ (stream.MaxDistance - stream.ReferenceDistance);
break;
case Attenuation.ExponentDistanceClamped:
case Attenuation.ExponentDistance:
gain = MathF.Pow(distance / stream.ReferenceDistance, -stream.RolloffFactor);
break;
default:
throw new ArgumentOutOfRangeException(
$"No implemented attenuation for {stream.Attenuation}");
}
var volume = MathF.Pow(10, stream.Volume / 10);
var actualGain = MathF.Max(0f, volume * gain);
stream.Source.SetVolumeDirect(actualGain);
}
private bool TryGetStreamPosition(PlayingStream stream, EntityQuery<TransformComponent> xformQuery, [NotNullWhen(true)] out MapCoordinates? mapPos)
{
if (stream.TrackingCoordinates != null)
{
mapPos = stream.TrackingCoordinates.Value.ToMap(EntityManager);
if (mapPos != MapCoordinates.Nullspace)
return true;
}
if (xformQuery.TryGetComponent(stream.TrackingEntity, out var xform)
&& xform.MapID != MapId.Nullspace)
{
mapPos = new MapCoordinates(_xformSys.GetWorldPosition(xform, xformQuery), xform.MapID);
return true;
}
if (stream.TrackingFallbackCoordinates != null)
{
mapPos = stream.TrackingFallbackCoordinates.Value.ToMap(EntityManager);
return mapPos != MapCoordinates.Nullspace;
}
mapPos = MapCoordinates.Nullspace;
return false;
}
#region Play AudioStream
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResPath(filename), out audio))
return true;
_sawmill.Error($"Server tried to play audio file {filename} which does not exist.");
return false;
}
private bool TryCreateAudioSource(AudioStream stream, [NotNullWhen(true)] out IClydeAudioSource? source)
{
if (!_timing.IsFirstTimePredicted)
{
source = null;
_sawmill.Error($"Tried to create audio source outside of prediction!");
DebugTools.Assert(false);
return false;
}
source = _clyde.CreateAudioSource(stream);
return source != null;
}
private PlayingStream CreateAndStartPlayingStream(IClydeAudioSource source, AudioParams? audioParams, AudioStream stream)
{
ApplyAudioParams(audioParams, source, stream);
source.StartPlaying();
var playing = new PlayingStream
{
Source = source,
Attenuation = audioParams?.Attenuation ?? Attenuation.Default,
MaxDistance = audioParams?.MaxDistance ?? float.MaxValue,
ReferenceDistance = audioParams?.ReferenceDistance ?? 1f,
RolloffFactor = audioParams?.RolloffFactor ?? 1f,
Volume = audioParams?.Volume ?? 0
};
_playingClydeStreams.Add(playing);
return playing;
}
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? Play(audio, audioParams) : default;
}
/// <summary>
/// Play an audio stream globally, without position.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, AudioParams? audioParams = null)
{
if (!TryCreateAudioSource(stream, out var source))
{
_sawmill.Error($"Error setting up global audio for {stream.Name}: {0}", Environment.StackTrace);
return null;
}
source.SetGlobal();
return CreateAndStartPlayingStream(source, audioParams, stream);
}
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, EntityUid entity, EntityCoordinates? fallbackCoordinates,
AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
NetEntity = GetNetEntity(entity),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates) ?? default,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? Play(audio, entity, fallbackCoordinates, audioParams) : default;
}
/// <summary>
/// Play an audio stream following an entity.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, EntityUid entity, EntityCoordinates? fallbackCoordinates = null,
AudioParams? audioParams = null)
{
if (!TryCreateAudioSource(stream, out var source))
{
_sawmill.Error($"Error setting up entity audio for {stream.Name} / {ToPrettyString(entity)}: {0}", Environment.StackTrace);
return null;
}
var query = GetEntityQuery<TransformComponent>();
var xform = query.GetComponent(entity);
var worldPos = _xformSys.GetWorldPosition(xform, query);
fallbackCoordinates ??= GetFallbackCoordinates(new MapCoordinates(worldPos, xform.MapID));
if (!source.SetPosition(worldPos))
return Play(stream, fallbackCoordinates.Value, fallbackCoordinates.Value, audioParams);
var playing = CreateAndStartPlayingStream(source, audioParams, stream);
playing.TrackingEntity = entity;
playing.TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null;
return playing;
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates,
EntityCoordinates fallbackCoordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Coordinates = GetNetCoordinates(coordinates),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? Play(audio, coordinates, fallbackCoordinates, audioParams) : default;
}
/// <summary>
/// Play an audio stream at a static position.
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
EntityCoordinates fallbackCoordinates, AudioParams? audioParams = null)
{
if (!TryCreateAudioSource(stream, out var source))
{
_sawmill.Error($"Error setting up coordinates audio for {stream.Name} / {coordinates}: {0}", Environment.StackTrace);
return null;
}
if (!source.SetPosition(fallbackCoordinates.Position))
{
source.Dispose();
_sawmill.Warning($"Can't play positional audio \"{stream.Name}\", can't set position.");
return null;
}
var playing = CreateAndStartPlayingStream(source, audioParams, stream);
playing.TrackingCoordinates = coordinates;
playing.TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null;
return playing;
}
#endregion
/// <inheritdoc />
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user,
AudioParams? audioParams = null)
{
if (_timing.IsFirstTimePredicted || sound == null)
return Play(sound, Filter.Local(), source, false, audioParams);
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
}
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user,
AudioParams? audioParams = null)
{
if (_timing.IsFirstTimePredicted || sound == null)
return Play(sound, Filter.Local(), coordinates, false, audioParams);
return null;
}
private void ApplyAudioParams(AudioParams? audioParams, IClydeAudioSource source, AudioStream audio)
{
if (!audioParams.HasValue)
return;
if (audioParams.Value.Variation.HasValue)
source.SetPitch(audioParams.Value.PitchScale
* (float) RandMan.NextGaussian(1, audioParams.Value.Variation.Value));
else
source.SetPitch(audioParams.Value.PitchScale);
source.SetVolume(audioParams.Value.Volume);
source.SetRolloffFactor(audioParams.Value.RolloffFactor);
source.SetMaxDistance(audioParams.Value.MaxDistance);
source.SetReferenceDistance(audioParams.Value.ReferenceDistance);
source.IsLooping = audioParams.Value.Loop;
// TODO clamp the offset inside of SetPlaybackPosition() itself.
var offset = audioParams.Value.PlayOffsetSeconds;
offset = Math.Clamp(offset, 0f, (float) audio.Length.TotalSeconds);
source.SetPlaybackPosition(offset);
}
public sealed class PlayingStream : IPlayingAudioStream
{
public uint? NetIdentifier;
public IClydeAudioSource Source = default!;
public EntityUid? TrackingEntity;
public EntityCoordinates? TrackingCoordinates;
public EntityCoordinates? TrackingFallbackCoordinates;
public bool Done;
public float Volume
{
get => _volume;
set
{
_volume = value;
Source.SetVolume(value);
}
}
private float _volume;
public float MaxDistance;
public float ReferenceDistance;
public float RolloffFactor;
public Attenuation Attenuation
{
get => _attenuation;
set
{
if (value == _attenuation) return;
_attenuation = value;
if (_attenuation != Attenuation.Default)
{
// Need to disable default attenuation when using a custom one
// Damn Sloth wanting linear ambience sounds so they smoothly cut-off and are short-range
Source.SetRolloffFactor(0f);
}
}
}
private Attenuation _attenuation = Attenuation.Default;
public void Stop()
{
Source.StopPlaying();
}
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return Play(filename, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return Play(filename, entity, null, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return Play(filename, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
{
return Play(filename, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return Play(filename, uid, null, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return Play(filename, uid, null, audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
}
}

View File

@@ -1,59 +0,0 @@
using System.Collections.Concurrent;
using OpenTK.Audio.OpenAL;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
private void _flushALDisposeQueues()
{
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer(handle);
_checkAlError();
}
}
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
private void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
}
}

View File

@@ -1,680 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Vector2 = System.Numerics.Vector2;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
private sealed class AudioSource : IClydeAudioSource
{
private int SourceHandle;
private readonly ClydeAudio _master;
private readonly AudioStream _sourceStream;
private int FilterHandle;
#if DEBUG
private bool _didPositionWarning;
#endif
private float _gain;
private bool IsEfxSupported => _master.IsEfxSupported;
public AudioSource(ClydeAudio master, int sourceHandle, AudioStream sourceStream)
{
_master = master;
SourceHandle = sourceHandle;
_sourceStream = sourceStream;
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
}
public void StartPlaying()
{
_checkDisposed();
AL.SourcePlay(SourceHandle);
_master._checkAlError();
}
public void StopPlaying()
{
if (_isDisposed()) return;
AL.SourceStop(SourceHandle);
_master._checkAlError();
}
public bool IsPlaying
{
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
return state == ALSourceState.Playing;
}
}
public bool IsLooping
{
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 IsGlobal
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourceb.SourceRelative, out var value);
_master._checkAlError();
return value;
}
}
public void SetGlobal()
{
_checkDisposed();
AL.Source(SourceHandle, ALSourceb.SourceRelative, true);
_master._checkAlError();
}
public void SetVolume(float decibels)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = MathF.Pow(10, decibels / 10);
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetVolumeDirect(float gain)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = gain;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetMaxDistance(float distance)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.MaxDistance, distance);
_master._checkAlError();
}
public void SetRolloffFactor(float rolloffFactor)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.RolloffFactor, rolloffFactor);
_master._checkAlError();
}
public void SetReferenceDistance(float refDistance)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.ReferenceDistance, refDistance);
_master._checkAlError();
}
public void SetOcclusion(float blocks)
{
_checkDisposed();
var cutoff = MathF.Exp(-blocks * 1);
var gain = MathF.Pow(cutoff, 0.1f);
if (IsEfxSupported)
{
SetOcclusionEfx(gain, cutoff);
}
else
{
gain *= gain * gain;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * gain);
}
_master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
}
public void SetPlaybackPosition(float seconds)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.SecOffset, seconds);
_master._checkAlError();
}
public bool SetPosition(Vector2 position)
{
_checkDisposed();
var (x, y) = position;
if (!AreFinite(x, y))
{
return false;
}
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
{
_didPositionWarning = true;
_master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
_sourceStream.Name);
// warning isn't enough, people just ignore it :(
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
}
#endif
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
_master._checkAlError();
return true;
}
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
return true;
}
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
_master._checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.Pitch, pitch);
_master._checkAlError();
}
~AudioSource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposing)
{
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
_master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
}
else
{
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
_master._audioSources.Remove(SourceHandle);
_master._checkAlError();
}
SourceHandle = -1;
}
private bool _isDisposed()
{
return SourceHandle == -1;
}
private void _checkDisposed()
{
if (SourceHandle == -1)
{
throw new ObjectDisposedException(nameof(AudioSource));
}
}
}
private sealed class BufferedAudioSource : IClydeBufferedAudioSource
{
private int? SourceHandle = null;
private int[] BufferHandles;
private Dictionary<int, int> BufferMap = new();
private readonly ClydeAudio _master;
private bool _mono = true;
private bool _float = false;
private int FilterHandle;
private float _gain;
public int SampleRate { get; set; } = 44100;
private bool IsEfxSupported => _master.IsEfxSupported;
public BufferedAudioSource(ClydeAudio master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
{
_master = master;
SourceHandle = sourceHandle;
BufferHandles = bufferHandles;
for (int i = 0; i < BufferHandles.Length; i++)
{
var bufferHandle = BufferHandles[i];
BufferMap[bufferHandle] = i;
}
_float = floatAudio;
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
}
public void StartPlaying()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.SourcePlay(stackalloc int[] {SourceHandle!.Value});
_master._checkAlError();
}
public void StopPlaying()
{
if (_isDisposed()) return;
// ReSharper disable once PossibleInvalidOperationException
AL.SourceStop(SourceHandle!.Value);
_master._checkAlError();
}
public bool IsPlaying
{
get
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
var state = AL.GetSourceState(SourceHandle!.Value);
return state == ALSourceState.Playing;
}
}
public bool IsLooping
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public void SetGlobal()
{
_checkDisposed();
_mono = false;
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourceb.SourceRelative, true);
_master._checkAlError();
}
public void SetLooping()
{
// TODO?waaaaddDDDDD
}
public void SetVolume(float decibels)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = MathF.Pow(10, decibels / 10);
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetVolumeDirect(float gain)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = gain;
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetMaxDistance(float distance)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.MaxDistance, distance);
_master._checkAlError();
}
public void SetRolloffFactor(float rolloffFactor)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.RolloffFactor, rolloffFactor);
_master._checkAlError();
}
public void SetReferenceDistance(float refDistance)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.ReferenceDistance, refDistance);
_master._checkAlError();
}
public void SetOcclusion(float blocks)
{
_checkDisposed();
var cutoff = MathF.Exp(-blocks * 1.5f);
var gain = MathF.Pow(cutoff, 0.1f);
if (IsEfxSupported)
{
SetOcclusionEfx(gain, cutoff);
}
else
{
gain *= gain * gain;
AL.Source(SourceHandle!.Value, ALSourcef.Gain, gain * _gain);
}
_master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle!.Value, ALSourcei.EfxDirectFilter, FilterHandle);
}
public void SetPlaybackPosition(float seconds)
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourcef.SecOffset, seconds);
_master._checkAlError();
}
public bool IsGlobal
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle!.Value, ALSourceb.SourceRelative, out var value);
_master._checkAlError();
return value;
}
}
public bool SetPosition(Vector2 position)
{
_checkDisposed();
var (x, y) = position;
if (!AreFinite(x, y))
{
return false;
}
_mono = true;
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSource3f.Position, x, y, 0);
_master._checkAlError();
return true;
}
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
return true;
}
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle!.Value, ALSource3f.Velocity, x, y, 0);
_master._checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourcef.Pitch, pitch);
_master._checkAlError();
}
~BufferedAudioSource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (SourceHandle == null) return;
if (!_master.IsMainThread())
{
// We can't run this code inside another thread so tell Clyde to clear it up later.
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
for (var i = 0; i < BufferHandles.Length; i++)
_master.DeleteAudioBufferOnMainThread(BufferHandles[i]);
}
else
{
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle.Value);
AL.DeleteBuffers(BufferHandles);
_master._bufferedAudioSources.Remove(SourceHandle.Value);
_master._checkAlError();
}
SourceHandle = null;
}
private bool _isDisposed()
{
return SourceHandle == null;
}
private void _checkDisposed()
{
if (SourceHandle == null)
{
throw new ObjectDisposedException(nameof(AudioSource));
}
}
public int GetNumberOfBuffersProcessed()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.GetSource(SourceHandle!.Value, ALGetSourcei.BuffersProcessed, out var buffersProcessed);
return buffersProcessed;
}
public unsafe void GetBuffersProcessed(Span<int> handles)
{
_checkDisposed();
var entries = Math.Min(Math.Min(handles.Length, BufferHandles.Length), GetNumberOfBuffersProcessed());
fixed (int* ptr = handles)
// ReSharper disable once PossibleInvalidOperationException
AL.SourceUnqueueBuffers(SourceHandle!.Value, entries, ptr);
for (var i = 0; i < entries; i++)
handles[i] = BufferMap[handles[i]];
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
_checkDisposed();
if(_float)
throw new InvalidOperationException("Can't write ushort numbers to buffers when buffer type is float!");
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
fixed (ushort* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.Mono16 : ALFormat.Stereo16, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(ushort) : data.Length * sizeof(ushort), SampleRate);
}
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
_checkDisposed();
if(!_float)
throw new InvalidOperationException("Can't write float numbers to buffers when buffer type is ushort!");
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
fixed (float* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.MonoFloat32Ext : ALFormat.StereoFloat32Ext, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(float) : data.Length * sizeof(float), SampleRate);
}
}
public unsafe void QueueBuffers(ReadOnlySpan<int> handles)
{
_checkDisposed();
Span<int> realHandles = stackalloc int[handles.Length];
handles.CopyTo(realHandles);
for (var i = 0; i < realHandles.Length; i++)
{
var handle = realHandles[i];
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handles), $"Invalid handle with index {i}!");
realHandles[i] = BufferHandles[handle];
}
fixed (int* ptr = realHandles)
// ReSharper disable once PossibleInvalidOperationException
AL.SourceQueueBuffers(SourceHandle!.Value, handles.Length, ptr);
}
public unsafe void EmptyBuffers()
{
_checkDisposed();
var length = (SampleRate / BufferHandles.Length) * (_mono ? 1 : 2);
Span<int> handles = stackalloc int[BufferHandles.Length];
if (_float)
{
var empty = new float[length];
var span = (Span<float>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
else
{
var empty = new ushort[length];
var span = (Span<ushort>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
QueueBuffers(handles);
}
}
}
}

View File

@@ -1,54 +0,0 @@
using System;
using System.IO;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
private OggVorbisData _readOggVorbis(Stream stream)
{
using (var vorbis = new NVorbis.VorbisReader(stream, false))
{
var sampleRate = vorbis.SampleRate;
var channels = vorbis.Channels;
var totalSamples = vorbis.TotalSamples;
var readSamples = 0;
var buffer = new float[totalSamples * channels];
while (readSamples < totalSamples)
{
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
if (read == 0)
{
break;
}
readSamples += read;
}
return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
}
}
private readonly struct OggVorbisData
{
public readonly long TotalSamples;
public readonly long SampleRate;
public readonly long Channels;
public readonly ReadOnlyMemory<float> Data;
public readonly string Title;
public readonly string Artist;
public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory<float> data, string title, string artist)
{
TotalSamples = totalSamples;
SampleRate = sampleRate;
Channels = channels;
Data = data;
Title = title;
Artist = artist;
}
}
}
}

View File

@@ -1,144 +0,0 @@
using System;
using System.IO;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
/// <summary>
/// Load up a WAVE file.
/// </summary>
private static WavData _readWav(Stream stream)
{
var reader = new BinaryReader(stream, EncodingHelpers.UTF8, true);
void SkipChunk()
{
var length = reader.ReadUInt32();
stream.Position += length;
}
// Read outer most chunks.
Span<byte> fourCc = stackalloc byte[4];
while (true)
{
_readFourCC(reader, fourCc);
if (!fourCc.SequenceEqual("RIFF"u8))
{
SkipChunk();
continue;
}
return _readRiffChunk(reader);
}
}
private static void _skipChunk(BinaryReader reader)
{
var length = reader.ReadUInt32();
reader.BaseStream.Position += length;
}
private static void _readFourCC(BinaryReader reader, Span<byte> fourCc)
{
fourCc[0] = reader.ReadByte();
fourCc[1] = reader.ReadByte();
fourCc[2] = reader.ReadByte();
fourCc[3] = reader.ReadByte();
}
private static WavData _readRiffChunk(BinaryReader reader)
{
Span<byte> format = stackalloc byte[4];
reader.ReadUInt32();
_readFourCC(reader, format);
if (!format.SequenceEqual("WAVE"u8))
{
throw new InvalidDataException("File is not a WAVE file.");
}
_readFourCC(reader, format);
if (!format.SequenceEqual("fmt "u8))
{
throw new InvalidDataException("Expected fmt chunk.");
}
// Read fmt chunk.
var size = reader.ReadInt32();
var afterFmtPos = reader.BaseStream.Position + size;
var audioType = (WavAudioFormatType) reader.ReadInt16();
var channels = reader.ReadInt16();
var sampleRate = reader.ReadInt32();
var byteRate = reader.ReadInt32();
var blockAlign = reader.ReadInt16();
var bitsPerSample = reader.ReadInt16();
if (audioType != WavAudioFormatType.PCM)
{
throw new NotImplementedException("Unable to support audio types other than PCM.");
}
DebugTools.Assert(byteRate == sampleRate * channels * bitsPerSample / 8);
// Fmt is not of guaranteed size, so use the size header to skip to the end.
reader.BaseStream.Position = afterFmtPos;
while (true)
{
_readFourCC(reader, format);
if (!format.SequenceEqual("data"u8))
{
_skipChunk(reader);
continue;
}
break;
}
// We are in the data chunk.
size = reader.ReadInt32();
var data = reader.ReadBytes(size);
return new WavData(audioType, channels, sampleRate, byteRate, blockAlign, bitsPerSample, data);
}
/// <summary>
/// See http://soundfile.sapp.org/doc/WaveFormat/ for reference.
/// </summary>
[PublicAPI]
private readonly struct WavData
{
public readonly WavAudioFormatType AudioType;
public readonly short NumChannels;
public readonly int SampleRate;
public readonly int ByteRate;
public readonly short BlockAlign;
public readonly short BitsPerSample;
public readonly ReadOnlyMemory<byte> Data;
public WavData(WavAudioFormatType audioType, short numChannels, int sampleRate, int byteRate,
short blockAlign, short bitsPerSample, ReadOnlyMemory<byte> data)
{
AudioType = audioType;
NumChannels = numChannels;
SampleRate = sampleRate;
ByteRate = byteRate;
BlockAlign = blockAlign;
BitsPerSample = bitsPerSample;
Data = data;
}
}
private enum WavAudioFormatType : short
{
Unknown = 0,
PCM = 1,
// There's a bunch of other types, those are all unsupported.
}
}
}

View File

@@ -1,51 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Vector2 = System.Numerics.Vector2;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Robust.Shared.IoC.Dependency] private readonly IEyeManager _eyeManager = default!;
[Robust.Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
private Thread? _gameThread;
public bool InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
return _initializeAudio();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
_updateAudio();
}
public void Shutdown()
{
_shutdownAudio();
}
private bool IsMainThread()
{
return Thread.CurrentThread == _gameThread;
}
}
}

View File

@@ -1,432 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Log;
namespace Robust.Client.Graphics.Audio
{
internal sealed partial class ClydeAudio : IClydeAudio, IClydeAudioInternal
{
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
new();
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
new();
private readonly HashSet<string> _alcDeviceExtensions = new();
private readonly HashSet<string> _alContextExtensions = new();
// The base gain value for a listener, used to boost the default volume.
private const float _baseGain = 2f;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
internal bool IsEfxSupported;
internal ISawmill OpenALSawmill = default!;
private bool _initializeAudio()
{
OpenALSawmill = _logMan.GetSawmill("clyde.oal");
if (!_audioOpenDevice())
return false;
// Create OpenAL context.
_audioCreateContext();
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
return true;
}
private void _audioCreateContext()
{
unsafe
{
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
}
ALC.MakeContextCurrent(_openALContext);
_checkAlcError(_openALDevice);
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
OpenALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
OpenALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
OpenALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
}
private bool _audioOpenDevice()
{
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
{
_openALDevice = ALC.OpenDevice(preferredDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
preferredDevice, ALC.GetError(ALDevice.Null));
_openALDevice = ALC.OpenDevice(null);
}
}
else
{
_openALDevice = ALC.OpenDevice(null);
}
_checkAlcError(_openALDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
return false;
}
// Load up ALC extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alcDeviceExtensions.Add(extension);
}
return true;
}
public void StopAllAudio()
{
foreach (var (key, source) in _audioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
}
}
foreach (var (key, source) in _bufferedAudioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
}
}
}
public void DisposeAllAudio()
{
foreach (var (key, source) in _audioSources)
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
_audioSources.Clear();
foreach (var (key, source) in _bufferedAudioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
target.Dispose();
}
}
_bufferedAudioSources.Clear();
}
private void _shutdownAudio()
{
DisposeAllAudio();
if (_openALContext != ALContext.Null)
{
ALC.MakeContextCurrent(ALContext.Null);
ALC.DestroyContext(_openALContext);
}
if (_openALDevice != IntPtr.Zero)
{
ALC.CloseDevice(_openALDevice);
}
}
private void _updateAudio()
{
var eye = _eyeManager.CurrentEye;
var vec = eye.Position.Position;
AL.Listener(ALListener3f.Position, vec.X, vec.Y, -5);
var rot2d = eye.Rotation.ToVec();
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
var rot = eye.Rotation.ToVec();
var at = new Vector3(0f, 0f, -1f);
var up = new Vector3(rot.Y, rot.X, 0f);
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
_flushALDisposeQueues();
}
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
}
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
}
public void SetAudioAttenuation(int value)
{
var attenuation = (Attenuation) value;
switch (attenuation)
{
case Attenuation.NoAttenuation:
AL.DistanceModel(ALDistanceModel.None);
break;
case Attenuation.InverseDistance:
AL.DistanceModel(ALDistanceModel.InverseDistance);
break;
case Attenuation.Default:
case Attenuation.InverseDistanceClamped:
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
break;
case Attenuation.LinearDistance:
AL.DistanceModel(ALDistanceModel.LinearDistance);
break;
case Attenuation.LinearDistanceClamped:
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
break;
case Attenuation.ExponentDistance:
AL.DistanceModel(ALDistanceModel.ExponentDistance);
break;
case Attenuation.ExponentDistanceClamped:
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
break;
default:
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
}
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
OpenALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
}
public IClydeAudioSource? CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// ReSharper disable once PossibleInvalidOperationException
// TODO: This really shouldn't be indexing based on the ClydeHandle...
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value.Value].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
return audioSource;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
throw new Exception("Failed to generate source. Too many simultaneous audio streams?");
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
return audioSource;
}
private void _checkAlcError(ALDevice device,
[CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = ALC.GetError(device);
if (error != AlcError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
}
}
private void _checkAlError([CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
}
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var vorbis = _readOggVorbis(stream);
var buffer = AL.GenBuffer();
ALFormat format;
// NVorbis only supports loading into floats.
// If this becomes a problem due to missing extension support (doubt it but ok),
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.MonoFloat32Ext;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.StereoFloat32Ext;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
unsafe
{
fixed (float* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
(int) vorbis.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var wav = _readWav(stream);
var buffer = AL.GenBuffer();
ALFormat format;
if (wav.BitsPerSample == 16)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono16;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else if (wav.BitsPerSample == 8)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono8;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo8;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else
{
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
}
unsafe
{
fixed (byte* ptr = wav.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
1 => ALFormat.Mono16,
2 => ALFormat.Stereo16,
_ => throw new ArgumentOutOfRangeException(
nameof(channels), "Only stereo and mono is currently supported")
};
var buffer = AL.GenBuffer();
_checkAlError();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
return new AudioStream(handle, length, channels, name);
}
private sealed class LoadedAudioSample
{
public readonly int BufferHandle;
public LoadedAudioSample(int bufferHandle)
{
BufferHandle = bufferHandle;
}
}
}
}

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's ClydeAudio's evil twin brother!
/// </summary>
[UsedImplicitly]
internal sealed class ClydeAudioHeadless : IClydeAudio, IClydeAudioInternal
{
public bool InitializePostWindowing()
{
return true;
}
public void FrameProcess(FrameEventArgs eventArgs)
{
}
public void Shutdown()
{
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, channels, name);
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
public void SetMasterVolume(float newVolume)
{
// Nada.
}
public void DisposeAllAudio()
{
// Nada.
}
public void StopAllAudio()
{
// Nada.
}
}
}

View File

@@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's ClydeAudio.AudioSource's evil twin brother!
/// </summary>
[Virtual]
internal class DummyAudioSource : IClydeAudioSource
{
public static DummyAudioSource Instance { get; } = new();
public bool IsPlaying => default;
public bool IsLooping { get; set; }
public void Dispose()
{
// Nada.
}
public void StartPlaying()
{
// Nada.
}
public void StopPlaying()
{
// Nada.
}
public bool IsGlobal { get; }
public bool SetPosition(Vector2 position)
{
return true;
}
public void SetPitch(float pitch)
{
// Nada.
}
public void SetGlobal()
{
// Nada.
}
public void SetVolume(float decibels)
{
// Nada.
}
public void SetVolumeDirect(float gain)
{
// Nada.
}
public void SetMaxDistance(float maxDistance)
{
// Nada.
}
public void SetRolloffFactor(float rolloffFactor)
{
// Nada.
}
public void SetReferenceDistance(float refDistance)
{
// Nada.
}
public void SetOcclusion(float blocks)
{
// Nada.
}
public void SetPlaybackPosition(float seconds)
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Shared.Timing;
using Robust.Shared.IoC;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// For "start ss14 with no audio devices" Smugleaf
/// </summary>
[UsedImplicitly]
internal sealed class FallbackProxyClydeAudio : ProxyClydeAudio
{
[Dependency] private readonly IDependencyCollection _deps = default!;
public override bool InitializePostWindowing()
{
// Deliberate lack of base call here (see base implementation for comments as to why there even is a base)
ActualImplementation = new ClydeAudio();
_deps.InjectDependencies(ActualImplementation, true);
if (ActualImplementation.InitializePostWindowing())
return true;
// If we get here, that failed, so use the fallback
ActualImplementation = new ClydeAudioHeadless();
_deps.InjectDependencies(ActualImplementation, true);
return ActualImplementation.InitializePostWindowing();
}
}
}

View File

@@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// For "start ss14 with no audio devices" Smugleaf
/// </summary>
[UsedImplicitly]
internal abstract class ProxyClydeAudio : IClydeAudio, IClydeAudioInternal
{
protected IClydeAudioInternal ActualImplementation = default!;
public virtual bool InitializePostWindowing()
{
// This particular implementation exists to be overridden because removing this method causes C# to complain
return ActualImplementation.InitializePostWindowing();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
ActualImplementation.FrameProcess(eventArgs);
}
public void Shutdown()
{
ActualImplementation.Shutdown();
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
return ActualImplementation.LoadAudioOggVorbis(stream, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
return ActualImplementation.LoadAudioWav(stream, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
return ActualImplementation.LoadAudioRaw(samples, channels, sampleRate, name);
}
public IClydeAudioSource? CreateAudioSource(AudioStream stream)
{
return ActualImplementation.CreateAudioSource(stream);
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return ActualImplementation.CreateBufferedAudioSource(buffers, floatAudio);
}
public void SetMasterVolume(float newVolume)
{
ActualImplementation.SetMasterVolume(newVolume);
}
public void DisposeAllAudio()
{
ActualImplementation.DisposeAllAudio();
}
public void StopAllAudio()
{
ActualImplementation.StopAllAudio();
}
}
}

View File

@@ -260,14 +260,14 @@ namespace Robust.Client.Graphics.Clyde
yield break;
}
foreach (var file in _resourceCache.ContentFindFiles(_windowIconPath))
foreach (var file in _resManager.ContentFindFiles(_windowIconPath))
{
if (file.Extension != "png")
{
continue;
}
using var stream = _resourceCache.ContentFileRead(file);
using var stream = _resManager.ContentFileRead(file);
yield return Image.Load<Rgba32>(stream);
}
}

View File

@@ -11,6 +11,7 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
@@ -36,6 +37,7 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;

View File

@@ -292,123 +292,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
[Virtual]
private class DummyAudioSource : IClydeAudioSource
{
public static DummyAudioSource Instance { get; } = new();
public bool IsPlaying => default;
public bool IsLooping { get; set; }
public void Dispose()
{
// Nada.
}
public void StartPlaying()
{
// Nada.
}
public void StopPlaying()
{
// Nada.
}
public bool IsGlobal { get; }
public bool SetPosition(Vector2 position)
{
return true;
}
public void SetPitch(float pitch)
{
// Nada.
}
public void SetGlobal()
{
// Nada.
}
public void SetVolume(float decibels)
{
// Nada.
}
public void SetVolumeDirect(float gain)
{
// Nada.
}
public void SetMaxDistance(float maxDistance)
{
// Nada.
}
public void SetRolloffFactor(float rolloffFactor)
{
// Nada.
}
public void SetReferenceDistance(float refDistance)
{
// Nada.
}
public void SetOcclusion(float blocks)
{
// Nada.
}
public void SetPlaybackPosition(float seconds)
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
}
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
{
public new static DummyBufferedAudioSource Instance { get; } = new();
public int SampleRate { get; set; } = 0;
public void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
// Nada.
}
public void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
// Nada.
}
public void QueueBuffers(ReadOnlySpan<int> handles)
{
// Nada.
}
public void EmptyBuffers()
{
// Nada.
}
public void GetBuffersProcessed(Span<int> handles)
{
// Nada.
}
public int GetNumberOfBuffersProcessed()
{
return 0;
}
}
private sealed class DummyTexture : OwnedTexture
{
public DummyTexture(Vector2i size) : base(size)

View File

@@ -1,54 +1,54 @@
using System;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics
namespace Robust.Client.Graphics;
internal readonly struct ClydeHandle : IEquatable<ClydeHandle>, IClydeHandle
{
internal struct ClydeHandle : IEquatable<ClydeHandle>
public ClydeHandle(long value)
{
public ClydeHandle(long value)
{
Value = value;
}
Value = value;
}
public readonly long Value;
public long Value { get; }
public static explicit operator ClydeHandle(long x)
{
return new(x);
}
public static explicit operator ClydeHandle(long x)
{
return new(x);
}
public static explicit operator long(ClydeHandle h)
{
return h.Value;
}
public static explicit operator long(ClydeHandle h)
{
return h.Value;
}
public bool Equals(ClydeHandle other)
{
return Value == other.Value;
}
public bool Equals(ClydeHandle other)
{
return Value == other.Value;
}
public override bool Equals(object? obj)
{
return obj is ClydeHandle other && Equals(other);
}
public override bool Equals(object? obj)
{
return obj is ClydeHandle other && Equals(other);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator ==(ClydeHandle left, ClydeHandle right)
{
return left.Value == right.Value;
}
public static bool operator ==(ClydeHandle left, ClydeHandle right)
{
return left.Value == right.Value;
}
public static bool operator !=(ClydeHandle left, ClydeHandle right)
{
return left.Value != right.Value;
}
public static bool operator !=(ClydeHandle left, ClydeHandle right)
{
return left.Value != right.Value;
}
public override string ToString()
{
return $"ClydeHandle {Value}";
}
public override string ToString()
{
return $"ClydeHandle {Value}";
}
}

View File

@@ -114,9 +114,10 @@ namespace Robust.Client.Graphics
{
if (rune == new Rune('\n'))
{
baseLine.X = 0f;
baseLine.Y += lineHeight;
advanceTotal.Y += lineHeight;
advanceTotal.X = Math.Max(advanceTotal.X, baseLine.X);
baseLine.X = 0f;
continue;
}
@@ -126,7 +127,6 @@ namespace Robust.Client.Graphics
continue;
var advance = metrics.Value.Advance;
advanceTotal.X += advance;
baseLine += new Vector2(advance, 0);
}

View File

@@ -1,23 +0,0 @@
using System;
using System.IO;
using Robust.Client.Audio;
namespace Robust.Client.Graphics
{
public interface IClydeAudio
{
// AUDIO SYSTEM DOWN BELOW.
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
AudioStream LoadAudioWav(Stream stream, string? name = null);
AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null);
void SetMasterVolume(float newVolume);
void DisposeAllAudio();
void StopAllAudio();
IClydeAudioSource? CreateAudioSource(AudioStream stream);
IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false);
}
}

View File

@@ -1,16 +0,0 @@
using System;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
{
internal interface IClydeAudioInternal : IClydeAudio
{
bool InitializePostWindowing();
void FrameProcess(FrameEventArgs eventArgs);
void Shutdown();
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
public interface IClydeAudioSource : IDisposable
{
void StartPlaying();
void StopPlaying();
bool IsPlaying { get; }
bool IsLooping { get; set; }
bool IsGlobal { get; }
[MustUseReturnValue]
bool SetPosition(Vector2 position);
void SetPitch(float pitch);
void SetGlobal();
void SetVolume(float decibels);
void SetVolumeDirect(float gain);
void SetMaxDistance(float maxDistance);
void SetRolloffFactor(float rolloffFactor);
void SetReferenceDistance(float refDistance);
void SetOcclusion(float blocks);
void SetPlaybackPosition(float seconds);
void SetVelocity(Vector2 velocity);
}
}

View File

@@ -1,15 +0,0 @@
using System;
namespace Robust.Client.Graphics
{
public interface IClydeBufferedAudioSource : IClydeAudioSource
{
int SampleRate { get; set; }
int GetNumberOfBuffersProcessed();
void GetBuffersProcessed(Span<int> handles);
void WriteBuffer(int handle, ReadOnlySpan<ushort> data);
void WriteBuffer(int handle, ReadOnlySpan<float> data);
void QueueBuffers(ReadOnlySpan<int> handles);
void EmptyBuffers();
}
}

View File

@@ -4,32 +4,29 @@ using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
namespace Robust.Client.Graphics;
[PublicAPI]
public interface IOverlayManager
{
bool AddOverlay(Overlay overlay);
[PublicAPI]
public interface IOverlayManager
{
bool AddOverlay(Overlay overlay);
bool RemoveOverlay(Overlay overlay);
bool RemoveOverlay(Type overlayClass);
bool RemoveOverlay<T>() where T : Overlay;
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
bool RemoveOverlay(Overlay overlay);
bool RemoveOverlay(Type overlayClass);
bool RemoveOverlay<T>() where T : Overlay;
Overlay GetOverlay(Type overlayClass);
T GetOverlay<T>() where T : Overlay;
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
bool HasOverlay(Type overlayClass);
bool HasOverlay<T>() where T : Overlay;
Overlay GetOverlay(Type overlayClass);
T GetOverlay<T>() where T : Overlay;
bool HasOverlay(Type overlayClass);
bool HasOverlay<T>() where T : Overlay;
IEnumerable<Overlay> AllOverlays { get; }
}
internal interface IOverlayManagerInternal : IOverlayManager
{
void FrameUpdate(FrameEventArgs args);
}
IEnumerable<Overlay> AllOverlays { get; }
}
internal interface IOverlayManagerInternal : IOverlayManager
{
void FrameUpdate(FrameEventArgs args);
}

View File

@@ -6,107 +6,106 @@ using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Graphics
namespace Robust.Client.Graphics;
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
{
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
[Dependency] private readonly ILogManager _logMan = default!;
[ViewVariables]
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
private ISawmill _logger = default!;
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public void FrameUpdate(FrameEventArgs args)
{
[Dependency] private readonly ILogManager _logMan = default!;
[ViewVariables]
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
private ISawmill _logger = default!;
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public void FrameUpdate(FrameEventArgs args)
foreach (var overlay in _overlays.Values)
{
foreach (var overlay in _overlays.Values)
{
overlay.FrameUpdate(args);
}
overlay.FrameUpdate(args);
}
}
public bool AddOverlay(Overlay overlay)
public bool AddOverlay(Overlay overlay)
{
if (_overlays.ContainsKey(overlay.GetType()))
return false;
_overlays.Add(overlay.GetType(), overlay);
return true;
}
public bool RemoveOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
if (_overlays.ContainsKey(overlay.GetType()))
return false;
_overlays.Add(overlay.GetType(), overlay);
return true;
}
public bool RemoveOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"RemoveOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
return _overlays.Remove(overlayClass);
}
public bool RemoveOverlay<T>() where T : Overlay
{
return RemoveOverlay(typeof(T));
}
public bool RemoveOverlay(Overlay overlay)
{
return _overlays.Remove(overlay.GetType());
}
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
{
overlay = null;
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"TryGetOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
return _overlays.TryGetValue(overlayClass, out overlay);
}
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay
{
overlay = null;
if (_overlays.TryGetValue(typeof(T), out Overlay? toReturn))
{
overlay = (T)toReturn;
return true;
}
_logger.Error($"RemoveOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
public Overlay GetOverlay(Type overlayClass)
return _overlays.Remove(overlayClass);
}
public bool RemoveOverlay<T>() where T : Overlay
{
return RemoveOverlay(typeof(T));
}
public bool RemoveOverlay(Overlay overlay)
{
return _overlays.Remove(overlay.GetType());
}
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
{
overlay = null;
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
return _overlays[overlayClass];
_logger.Error($"TryGetOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
public T GetOverlay<T>() where T : Overlay
return _overlays.TryGetValue(overlayClass, out overlay);
}
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay
{
overlay = null;
if (_overlays.TryGetValue(typeof(T), out Overlay? toReturn))
{
return (T)_overlays[typeof(T)];
overlay = (T)toReturn;
return true;
}
public bool HasOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"HasOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
}
return false;
}
return _overlays.ContainsKey(overlayClass);
public Overlay GetOverlay(Type overlayClass)
{
return _overlays[overlayClass];
}
public T GetOverlay<T>() where T : Overlay
{
return (T)_overlays[typeof(T)];
}
public bool HasOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"HasOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
}
public bool HasOverlay<T>() where T : Overlay
{
return _overlays.ContainsKey(typeof(T));
}
return _overlays.ContainsKey(overlayClass);
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("overlay");
}
public bool HasOverlay<T>() where T : Overlay
{
return _overlays.ContainsKey(typeof(T));
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("overlay");
}
}

View File

@@ -7,6 +7,7 @@ using Robust.Client.Map;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
@@ -22,7 +23,7 @@ namespace Robust.Client.Map
{
internal sealed class ClydeTileDefinitionManager : TileDefinitionManager, IClydeTileDefinitionManager
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceManager _manager = default!;
private Texture? _tileTextureAtlas;
@@ -86,7 +87,7 @@ namespace Robust.Client.Map
0, (h - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
Image<Rgba32> image;
using (var stream = _resourceCache.ContentFileRead("/Textures/noTile.png"))
using (var stream = _manager.ContentFileRead("/Textures/noTile.png"))
{
image = Image.Load<Rgba32>(stream);
}
@@ -110,7 +111,7 @@ namespace Robust.Client.Map
// Already know it's not null above
var path = def.Sprite!.Value;
using (var stream = _resourceCache.ContentFileRead(path))
using (var stream = _manager.ContentFileRead(path))
{
image = Image.Load<Rgba32>(stream);
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Configuration;
using Robust.Client.GameObjects;
@@ -10,6 +11,7 @@ using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Client.Upload;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -28,7 +30,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IMidiManager _midi = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly IAudioInternal _clydeAudio = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IClientNetManager _netMan = default!;
[Dependency] private readonly IComponentFactory _factory = default!;

View File

@@ -1,36 +1,34 @@
using System;
using System;
using System.Threading;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
namespace Robust.Client.ResourceManagement;
/// <summary>
/// Base resource for the cache.
/// </summary>
public abstract class BaseResource : IDisposable
{
/// <summary>
/// Base resource for the cache.
/// Fallback resource path if this one does not exist.
/// </summary>
public abstract class BaseResource : IDisposable
public virtual ResPath? Fallback => null;
/// <summary>
/// Disposes this resource.
/// </summary>
public virtual void Dispose()
{
/// <summary>
/// Fallback resource path if this one does not exist.
/// </summary>
public virtual ResPath? Fallback => null;
}
/// <summary>
/// Disposes this resource.
/// </summary>
public virtual void Dispose()
{
}
/// <summary>
/// Deserializes the resource from the VFS.
/// </summary>
public abstract void Load(IDependencyCollection dependencies, ResPath path);
/// <summary>
/// Deserializes the resource from the VFS.
/// </summary>
/// <param name="cache">ResourceCache this resource is being loaded into.</param>
/// <param name="path">Path of the resource requested on the VFS.</param>
public abstract void Load(IResourceCache cache, ResPath path);
public virtual void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
{
public virtual void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
{
}
}
}

View File

@@ -1,49 +1,51 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
namespace Robust.Client.ResourceManagement;
/// <summary>
/// Handles caching of <see cref="BaseResource"/>
/// </summary>
public interface IResourceCache : IResourceManager
{
public interface IResourceCache : IResourceManager
{
T GetResource<T>(string path, bool useFallback = true)
where T : BaseResource, new();
T GetResource<T>(string path, bool useFallback = true)
where T : BaseResource, new();
T GetResource<T>(ResPath path, bool useFallback = true)
where T : BaseResource, new();
T GetResource<T>(ResPath path, bool useFallback = true)
where T : BaseResource, new();
bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource)
where T : BaseResource, new();
bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource)
where T : BaseResource, new();
bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource)
where T : BaseResource, new();
bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource)
where T : BaseResource, new();
void ReloadResource<T>(string path)
where T : BaseResource, new();
void ReloadResource<T>(string path)
where T : BaseResource, new();
void ReloadResource<T>(ResPath path)
where T : BaseResource, new();
void ReloadResource<T>(ResPath path)
where T : BaseResource, new();
void CacheResource<T>(string path, T resource)
where T : BaseResource, new();
void CacheResource<T>(string path, T resource)
where T : BaseResource, new();
void CacheResource<T>(ResPath path, T resource)
where T : BaseResource, new();
void CacheResource<T>(ResPath path, T resource)
where T : BaseResource, new();
T GetFallback<T>()
where T : BaseResource, new();
T GetFallback<T>()
where T : BaseResource, new();
IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new();
IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new();
// Resource load callbacks so content can hook stuff like click maps.
event Action<TextureLoadedEventArgs> OnRawTextureLoaded;
event Action<RsiLoadedEventArgs> OnRsiLoaded;
IClyde Clyde { get; }
IClydeAudio ClydeAudio { get; }
IFontManager FontManager { get; }
}
// Resource load callbacks so content can hook stuff like click maps.
event Action<TextureLoadedEventArgs> OnRawTextureLoaded;
event Action<RsiLoadedEventArgs> OnRsiLoaded;
IClyde Clyde { get; }
IFontManager FontManager { get; }
}

View File

@@ -2,14 +2,14 @@
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
{
internal interface IResourceCacheInternal : IResourceCache, IResourceManagerInternal
{
void TextureLoaded(TextureLoadedEventArgs eventArgs);
void RsiLoaded(RsiLoadedEventArgs eventArgs);
namespace Robust.Client.ResourceManagement;
void MountLoaderApi(IFileApi api, string apiPrefix, ResPath? prefix=null);
void PreloadTextures();
}
/// <inheritdoc />
internal interface IResourceCacheInternal : IResourceCache
{
void TextureLoaded(TextureLoadedEventArgs eventArgs);
void RsiLoaded(RsiLoadedEventArgs eventArgs);
void PreloadTextures();
void MountLoaderApi(IResourceManager manager, IFileApi api, string apiPrefix, ResPath? prefix = null);
}

View File

@@ -9,6 +9,13 @@ namespace Robust.Client.ResourceManagement
{
internal partial class ResourceCache
{
public void MountLoaderApi(IResourceManager manager, IFileApi api, string apiPrefix, ResPath? prefix = null)
{
prefix ??= ResPath.Root;
var root = new LoaderApiLoader(api, apiPrefix);
manager.AddRoot(prefix.Value, root);
}
private sealed class LoaderApiLoader : IContentRoot
{
private readonly IFileApi _api;

View File

@@ -3,10 +3,13 @@ using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
@@ -19,7 +22,8 @@ namespace Robust.Client.ResourceManagement
internal partial class ResourceCache
{
[field: Dependency] public IClyde Clyde { get; } = default!;
[field: Dependency] public IClydeAudio ClydeAudio { get; } = default!;
[field: Dependency] public IAudioInternal ClydeAudio { get; } = default!;
[Dependency] private readonly IResourceManager _manager = default!;
[field: Dependency] public IFontManager FontManager { get; } = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
@@ -44,7 +48,7 @@ namespace Robust.Client.ResourceManagement
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<TextureResource>();
var texList = ContentFindFiles("/Textures/")
var texList = _manager.ContentFindFiles("/Textures/")
// Skip PNG files inside RSIs.
.Where(p => p.Extension == "png" && !p.ToString().Contains(".rsi/") && !resList.ContainsKey(p))
.Select(p => new TextureResource.LoadStepData {Path = p})
@@ -54,7 +58,7 @@ namespace Robust.Client.ResourceManagement
{
try
{
TextureResource.LoadPreTexture(this, data);
TextureResource.LoadPreTexture(_manager, data);
}
catch (Exception e)
{
@@ -116,7 +120,7 @@ namespace Robust.Client.ResourceManagement
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<RSIResource>();
var rsiList = ContentFindFiles("/Textures/")
var rsiList = _manager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
.Select(c => c.Directory)
.Where(p => !resList.ContainsKey(p))
@@ -127,7 +131,7 @@ namespace Robust.Client.ResourceManagement
{
try
{
RSIResource.LoadPreTexture(this, data);
RSIResource.LoadPreTexture(_manager, data);
}
catch (Exception e)
{

View File

@@ -1,221 +1,219 @@
using Robust.Shared.ContentPack;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.LoaderApi;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
namespace Robust.Client.ResourceManagement;
/// <summary>
/// Handles caching of <see cref="BaseResource"/>
/// </summary>
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
{
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> _cachedResources =
new();
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
{
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> CachedResources =
new();
return GetResource<T>(new ResPath(path), useFallback);
}
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
public T GetResource<T>(ResPath path, bool useFallback = true) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
{
return GetResource<T>(new ResPath(path), useFallback);
return (T) cached;
}
public T GetResource<T>(ResPath path, bool useFallback = true) where T : BaseResource, new()
var resource = new T();
try
{
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
{
return (T) cached;
}
var _resource = new T();
try
{
_resource.Load(this, path);
cache[path] = _resource;
return _resource;
}
catch (Exception e)
{
if (useFallback && _resource.Fallback != null)
{
Logger.Error(
$"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}");
return GetResource<T>(_resource.Fallback.Value, false);
}
else
{
Logger.Error(
$"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}");
throw;
}
}
var dependencies = IoCManager.Instance!;
resource.Load(dependencies, path);
cache[path] = resource;
return resource;
}
public bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
catch (Exception e)
{
return TryGetResource(new ResPath(path), out resource);
}
public bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
if (useFallback && resource.Fallback != null)
{
resource = (T) cached;
return true;
Logger.Error(
$"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}");
return GetResource<T>(resource.Fallback.Value, false);
}
var _resource = new T();
try
else
{
_resource.Load(this, path);
resource = _resource;
cache[path] = resource;
return true;
}
catch
{
resource = null;
return false;
}
}
public void ReloadResource<T>(string path) where T : BaseResource, new()
{
ReloadResource<T>(new ResPath(path));
}
public void ReloadResource<T>(ResPath path) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
if (!cache.TryGetValue(path, out var res))
{
return;
}
try
{
res.Reload(this, path);
}
catch (Exception e)
{
Logger.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
Logger.Error(
$"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}");
throw;
}
}
}
public bool HasResource<T>(string path) where T : BaseResource, new()
public bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
{
return TryGetResource(new ResPath(path), out resource);
}
public bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
{
return HasResource<T>(new ResPath(path));
resource = (T) cached;
return true;
}
public bool HasResource<T>(ResPath path) where T : BaseResource, new()
var _resource = new T();
try
{
return TryGetResource<T>(path, out var _);
var dependencies = IoCManager.Instance!;
_resource.Load(dependencies, path);
resource = _resource;
cache[path] = resource;
return true;
}
catch
{
resource = null;
return false;
}
}
public void ReloadResource<T>(string path) where T : BaseResource, new()
{
ReloadResource<T>(new ResPath(path));
}
public void ReloadResource<T>(ResPath path) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
if (!cache.TryGetValue(path, out var res))
{
return;
}
public void CacheResource<T>(string path, T resource) where T : BaseResource, new()
try
{
CacheResource(new ResPath(path), resource);
var dependencies = IoCManager.Instance!;
res.Reload(dependencies, path);
}
public void CacheResource<T>(ResPath path, T resource) where T : BaseResource, new()
catch (Exception e)
{
GetTypeDict<T>()[path] = resource;
Logger.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
throw;
}
}
public T GetFallback<T>() where T : BaseResource, new()
public bool HasResource<T>(string path) where T : BaseResource, new()
{
return HasResource<T>(new ResPath(path));
}
public bool HasResource<T>(ResPath path) where T : BaseResource, new()
{
return TryGetResource<T>(path, out var _);
}
public void CacheResource<T>(string path, T resource) where T : BaseResource, new()
{
CacheResource(new ResPath(path), resource);
}
public void CacheResource<T>(ResPath path, T resource) where T : BaseResource, new()
{
GetTypeDict<T>()[path] = resource;
}
public T GetFallback<T>() where T : BaseResource, new()
{
if (_fallbacks.TryGetValue(typeof(T), out var fallback))
{
if (_fallbacks.TryGetValue(typeof(T), out var fallback))
{
return (T) fallback;
}
var res = new T();
if (res.Fallback == null)
{
throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback.");
}
fallback = GetResource<T>(res.Fallback.Value, useFallback: false);
_fallbacks.Add(typeof(T), fallback);
return (T) fallback;
}
public IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new()
var res = new T();
if (res.Fallback == null)
{
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback.");
}
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
public event Action<RsiLoadedEventArgs>? OnRsiLoaded;
fallback = GetResource<T>(res.Fallback.Value, useFallback: false);
_fallbacks.Add(typeof(T), fallback);
return (T) fallback;
}
#region IDisposable Members
public IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new()
{
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
}
private bool disposed = false;
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
public event Action<RsiLoadedEventArgs>? OnRsiLoaded;
public void Dispose()
#region IDisposable Members
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
return;
}
private void Dispose(bool disposing)
if (disposing)
{
if (disposed)
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Values))
{
return;
res.Dispose();
}
if (disposing)
{
foreach (var res in CachedResources.Values.SelectMany(dict => dict.Values))
{
res.Dispose();
}
}
disposed = true;
}
~ResourceCache()
disposed = true;
}
~ResourceCache()
{
Dispose(false);
}
#endregion IDisposable Members
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<ResPath, BaseResource> GetTypeDict<T>()
{
if (!_cachedResources.TryGetValue(typeof(T), out var ret))
{
Dispose(false);
ret = new Dictionary<ResPath, BaseResource>();
_cachedResources.Add(typeof(T), ret);
}
#endregion IDisposable Members
return ret;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Dictionary<ResPath, BaseResource> GetTypeDict<T>()
{
if (!CachedResources.TryGetValue(typeof(T), out var ret))
{
ret = new Dictionary<ResPath, BaseResource>();
CachedResources.Add(typeof(T), ret);
}
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
{
OnRawTextureLoaded?.Invoke(eventArgs);
}
return ret;
}
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
{
OnRawTextureLoaded?.Invoke(eventArgs);
}
public void RsiLoaded(RsiLoadedEventArgs eventArgs)
{
OnRsiLoaded?.Invoke(eventArgs);
}
public void MountLoaderApi(IFileApi api, string apiPrefix, ResPath? prefix=null)
{
prefix ??= ResPath.Root;
var root = new LoaderApiLoader(api, apiPrefix);
AddRoot(prefix.Value, root);
}
public void RsiLoaded(RsiLoadedEventArgs eventArgs)
{
OnRsiLoaded?.Invoke(eventArgs);
}
}

View File

@@ -1,43 +1,46 @@
using System;
using Robust.Client.Audio;
using Robust.Shared.Utility;
using System;
using System.IO;
using Robust.Client.Graphics;
using Robust.Client.Audio;
using Robust.Shared.Audio;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
namespace Robust.Client.ResourceManagement;
public sealed class AudioResource : BaseResource
{
public sealed class AudioResource : BaseResource
public AudioStream AudioStream { get; private set; } = default!;
public override void Load(IDependencyCollection dependencies, ResPath path)
{
public AudioStream AudioStream { get; private set; } = default!;
var cache = dependencies.Resolve<IResourceManager>();
public override void Load(IResourceCache cache, ResPath path)
if (!cache.ContentFileExists(path))
{
if (!cache.ContentFileExists(path))
{
throw new FileNotFoundException("Content file does not exist for audio sample.");
}
using (var fileStream = cache.ContentFileRead(path))
{
if (path.Extension == "ogg")
{
AudioStream = cache.ClydeAudio.LoadAudioOggVorbis(fileStream, path.ToString());
}
else if (path.Extension == "wav")
{
AudioStream = cache.ClydeAudio.LoadAudioWav(fileStream, path.ToString());
}
else
{
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
}
}
throw new FileNotFoundException("Content file does not exist for audio sample.");
}
public static implicit operator AudioStream(AudioResource res)
using (var fileStream = cache.ContentFileRead(path))
{
return res.AudioStream;
var audioManager = dependencies.Resolve<IAudioInternal>();
if (path.Extension == "ogg")
{
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
}
else if (path.Extension == "wav")
{
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
}
else
{
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
}
}
}
public static implicit operator AudioStream(AudioResource res)
{
return res.AudioStream;
}
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using Robust.Client.Graphics;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
@@ -9,16 +10,17 @@ namespace Robust.Client.ResourceManagement
{
internal IFontFaceHandle FontFaceHandle { get; private set; } = default!;
public override void Load(IResourceCache cache, ResPath path)
public override void Load(IDependencyCollection dependencies, ResPath path)
{
if (!cache.TryContentFileRead(path, out var stream))
if (!dependencies.Resolve<IResourceManager>().TryContentFileRead(path, out var stream))
{
throw new FileNotFoundException("Content file does not exist for font");
}
using (stream)
{
FontFaceHandle = ((IFontManagerInternal)cache.FontManager).Load(stream);
FontFaceHandle = dependencies.Resolve<IFontManagerInternal>().Load(stream);
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
@@ -34,26 +35,27 @@ namespace Robust.Client.ResourceManagement
/// </summary>
public const uint MAXIMUM_RSI_VERSION = RsiLoading.MAXIMUM_RSI_VERSION;
public override void Load(IResourceCache cache, ResPath path)
public override void Load(IDependencyCollection dependencies, ResPath path)
{
var loadStepData = new LoadStepData {Path = path};
LoadPreTexture(cache, loadStepData);
var manager = dependencies.Resolve<IResourceManager>();
LoadPreTexture(manager, loadStepData);
loadStepData.AtlasTexture = cache.Clyde.LoadTextureFromImage(
loadStepData.AtlasTexture = dependencies.Resolve<IClyde>().LoadTextureFromImage(
loadStepData.AtlasSheet,
loadStepData.Path.ToString());
LoadPostTexture(loadStepData);
LoadFinish(cache, loadStepData);
LoadFinish(dependencies.Resolve<IResourceCacheInternal>(), loadStepData);
loadStepData.AtlasSheet.Dispose();
}
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
{
var manifestPath = data.Path / "meta.json";
RsiLoading.RsiMetadata metadata;
using (var manifestFile = cache.ContentFileRead(manifestPath))
using (var manifestFile = manager.ContentFileRead(manifestPath))
{
metadata = RsiLoading.LoadRsiMetadata(manifestFile);
}
@@ -86,7 +88,7 @@ namespace Robust.Client.ResourceManagement
var stateObject = metadata.States[index];
// Load image from disk.
var texPath = data.Path / (stateObject.StateId + ".png");
using (var stream = cache.ContentFileRead(texPath))
using (var stream = manager.ContentFileRead(texPath))
{
reg.Src = Image.Load<Rgba32>(stream);
}
@@ -212,14 +214,10 @@ namespace Robust.Client.ResourceManagement
}
}
internal void LoadFinish(IResourceCache cache, LoadStepData data)
internal void LoadFinish(IResourceCacheInternal cache, LoadStepData data)
{
RSI = data.Rsi;
if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
}
cache.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
}
/// <summary>

View File

@@ -20,28 +20,31 @@ namespace Robust.Client.ResourceManagement
[ViewVariables]
internal ParsedShader ParsedShader { get; private set; } = default!;
public override void Load(IResourceCache cache, ResPath path)
public override void Load(IDependencyCollection dependencies, ResPath path)
{
using (var stream = cache.ContentFileRead(path))
var manager = dependencies.Resolve<IResourceManager>();
using (var stream = manager.ContentFileRead(path))
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
{
ParsedShader = ShaderParser.Parse(reader, cache);
ParsedShader = ShaderParser.Parse(reader, manager);
}
ClydeHandle = ((IClydeInternal)cache.Clyde).LoadShader(ParsedShader, path.ToString());
ClydeHandle = dependencies.Resolve<IClydeInternal>().LoadShader(ParsedShader, path.ToString());
}
public override void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
{
var manager = dependencies.Resolve<IResourceManager>();
ct = ct != default ? ct : new CancellationTokenSource(30000).Token;
for (;;)
{
try
{
using var stream = cache.ContentFileRead(path);
using var stream = manager.ContentFileRead(path);
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
ParsedShader = ShaderParser.Parse(reader, cache);
ParsedShader = ShaderParser.Parse(reader, manager);
break;
}
catch (IOException ioe)
@@ -57,7 +60,7 @@ namespace Robust.Client.ResourceManagement
}
}
((IClydeInternal)cache.Clyde).ReloadShader(ClydeHandle, ParsedShader);
dependencies.Resolve<IClydeInternal>().ReloadShader(ClydeHandle, ParsedShader);
}
}
}

View File

@@ -1,6 +1,7 @@
using System.IO;
using System.Threading;
using Robust.Client.Graphics;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -19,7 +20,7 @@ namespace Robust.Client.ResourceManagement
public Texture Texture => _texture;
public override void Load(IResourceCache cache, ResPath path)
public override void Load(IDependencyCollection dependencies, ResPath path)
{
if (path.Directory.Filename.EndsWith(".rsi"))
{
@@ -31,12 +32,12 @@ namespace Robust.Client.ResourceManagement
var data = new LoadStepData {Path = path};
LoadPreTexture(cache, data);
LoadTexture(cache.Clyde, data);
LoadFinish(cache, data);
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
LoadTexture(dependencies.Resolve<IClyde>(), data);
LoadFinish(dependencies.Resolve<IResourceCache>(), data);
}
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
{
using (var stream = cache.ContentFileRead(data.Path))
{
@@ -63,7 +64,7 @@ namespace Robust.Client.ResourceManagement
data.Image.Dispose();
}
private static TextureLoadParameters? TryLoadTextureParameters(IResourceCache cache, ResPath path)
private static TextureLoadParameters? TryLoadTextureParameters(IResourceManager cache, ResPath path)
{
var metaPath = path.WithName(path.Filename + ".yml");
if (cache.TryContentFileRead(metaPath, out var stream))
@@ -90,12 +91,11 @@ namespace Robust.Client.ResourceManagement
return null;
}
public override void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
{
var clyde = IoCManager.Resolve<IClyde>();
var data = new LoadStepData {Path = path};
LoadPreTexture(cache, data);
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
{
@@ -106,7 +106,7 @@ namespace Robust.Client.ResourceManagement
{
// Dimensions do not match, make new texture.
_texture.Dispose();
LoadTexture(clyde, data);
LoadTexture(dependencies.Resolve<IClyde>(), data);
_texture = data.Texture;
}

View File

@@ -16,12 +16,12 @@
<PackageReference Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.2" Condition="'$(UseSystemSqlite)' == 'True'" PrivateAssets="compile" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.2" Condition="'$(UseSystemSqlite)' != 'True'" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.NFluidsynth" Version="0.1.1" PrivateAssets="compile" />
<PackageReference Include="NVorbis" Version="0.10.1" PrivateAssets="compile" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" PrivateAssets="compile" />
<PackageReference Include="OpenTK.OpenAL" Version="4.7.5" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" PrivateAssets="compile" />
<PackageReference Include="Robust.Natives" Version="0.1.1" />
<PackageReference Include="System.Numerics.Vectors" Version="4.4.0" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-rc2" PrivateAssets="compile" />
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" Version="1.2.0" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.Sodium" Version="0.2.1" PrivateAssets="compile" />

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -37,10 +38,11 @@ public sealed class UITheme : IPrototype
public Dictionary<string, Color>? Colors { get; }
public ResPath Path => _path == default ? new ResPath(DefaultPath+"/"+ID) : _path;
private void ValidateFilePath(IResourceCache resourceCache)
private void ValidateFilePath(IResourceManager manager)
{
var foundFolders = resourceCache.ContentFindFiles(Path.ToRootedPath());
if (!foundFolders.Any()) throw new Exception("UITheme: "+ID+" not found in resources!");
var foundFolders = manager.ContentFindFiles(Path.ToRootedPath());
if (!foundFolders.Any())
throw new Exception("UITheme: "+ID+" not found in resources!");
}
public Texture ResolveTexture(string texturePath)

View File

@@ -0,0 +1,83 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Robust.Shared.Audio;
using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Serialization;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Robust.Packaging.AssetProcessing.Passes;
/// <summary>
/// Strips out audio files and writes them to a metadata .yml
/// Used for server packaging to avoid bundling entire audio files on the server.
/// </summary>
public sealed class AssetPassAudioMetadata : AssetPass
{
private readonly List<AudioMetadataPrototype> _audioMetadata = new();
private readonly string _metadataPath;
public AssetPassAudioMetadata(string metadataPath = "Prototypes/_audio_metadata.yml")
{
_metadataPath = metadataPath;
}
protected override AssetFileAcceptResult AcceptFile(AssetFile file)
{
if (!AudioLoader.IsLoadableAudioFile(file.Path))
return AssetFileAcceptResult.Pass;
using var stream = file.Open();
var metadata = AudioLoader.LoadAudioMetadata(stream, file.Path);
lock (_audioMetadata)
{
_audioMetadata.Add(new AudioMetadataPrototype()
{
ID = "/" + file.Path,
Length = metadata.Length,
});
}
return AssetFileAcceptResult.Consumed;
}
[SuppressMessage("ReSharper", "InconsistentlySynchronizedField")]
protected override void AcceptFinished()
{
if (_audioMetadata.Count == 0)
{
Logger?.Debug("Have no audio metadata, not writing anything");
return;
}
Logger?.Debug("Writing audio metadata for {0} audio files", _audioMetadata.Count);
// ReSharper disable once InconsistentlySynchronizedField
var root = new YamlSequenceNode();
var document = new YamlDocument(root);
foreach (var prototype in _audioMetadata)
{
// TODO: I know but sermanager and please get me out of this hell.
var jaml = new YamlMappingNode
{
{ "type", AudioMetadataPrototype.ProtoName },
{ "id", new YamlScalarNode(prototype.ID) },
{ "length", new YamlScalarNode(prototype.Length.TotalSeconds.ToString(CultureInfo.InvariantCulture)) }
};
root.Add(jaml);
}
RunJob(() =>
{
using var memory = new MemoryStream();
using var writer = new StreamWriter(memory);
var yamlStream = new YamlStream(document);
yamlStream.Save(new YamlNoDocEndDotsFix(new YamlMappingFix(new Emitter(writer))), false);
writer.Flush();
var result = new AssetFileMemory(_metadataPath, memory.ToArray());
SendFile(result);
});
}
}

View File

@@ -0,0 +1,28 @@
namespace Robust.Packaging.AssetProcessing.Passes;
/// <summary>
/// Appends a prefix to file paths of passed-through files.
/// </summary>
public sealed class AssetPassPrefix : AssetPass
{
public string Prefix { get; set; }
public AssetPassPrefix(string prefix)
{
Prefix = prefix;
}
protected override AssetFileAcceptResult AcceptFile(AssetFile file)
{
var newPath = Prefix + file.Path;
var newFile = file switch
{
AssetFileDisk disk => (AssetFile) new AssetFileDisk(newPath, disk.DiskPath),
AssetFileMemory memory => new AssetFileMemory(newPath, memory.Memory),
_ => throw new ArgumentOutOfRangeException(nameof(file))
};
SendFile(newFile);
return AssetFileAcceptResult.Consumed;
}
}

View File

@@ -10,5 +10,9 @@
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NVorbis" Version="0.10.5" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Properties.targets" />
</Project>

View File

@@ -21,6 +21,7 @@ public sealed class RobustClientAssetGraph
/// </summary>
public IReadOnlyCollection<AssetPass> AllPasses { get; }
/// <param name="parallel">Should inputs be run in parallel. Should only be turned off for debugging.</param>
public RobustClientAssetGraph(bool parallel = true)
{
// The code injecting the list of source files is assumed to be pretty single-threaded.
@@ -40,7 +41,7 @@ public sealed class RobustClientAssetGraph
Input,
PresetPasses,
Output,
NormalizeText
NormalizeText,
};
}
}

View File

@@ -0,0 +1,129 @@
using Robust.Packaging.AssetProcessing;
using Robust.Packaging.AssetProcessing.Passes;
namespace Robust.Packaging;
/// <summary>
/// Standard asset graph for packaging server files. Extend by wiring things up to <see cref="InputCore"/>, <see cref="InputResources"/>, and <see cref="Output"/>.
/// </summary>
/// <remarks>
/// <para>
/// This graph has two inputs: one for "core" server files such as the main engine executable, and another for resource files.
/// </para>
/// <para>
/// If you want to add extra passes to run before preset passes, depend them on the relevant input pass, with a before of the relevant preset pass.
/// </para>
/// <para>
/// See the following graph (Mermaid syntax) to understand this asset graph:
/// </para>
/// <code>
/// flowchart LR
/// InputCore --> PresetPassesCore
/// PresetPassesCore --1--> NormalizeTextCore
/// NormalizeTextCore --> Output
/// PresetPassesCore --2--> Output
/// InputResources --> PresetPassesResources
/// PresetPassesResources --1--> AudioMetadata
/// PresetPassesResources --2--> NormalizeTextResources
/// PresetPassesResources --3--> PrefixResources
/// AudioMetadata --> PrefixResources
/// NormalizeTextResources --> PrefixResources
/// PrefixResources --> Output
/// </code>
/// </remarks>
public sealed class RobustServerAssetGraph
{
public AssetPassPipe Output { get; }
/// <summary>
/// Input pass for core server files, such as <c>Robust.Server.exe</c>.
/// </summary>
/// <seealso cref="InputResources"/>
public AssetPassPipe InputCore { get; }
public AssetPassPipe PresetPassesCore { get; }
/// <summary>
/// Normalizes text files in core files.
/// </summary>
public AssetPassNormalizeText NormalizeTextCore { get; }
/// <summary>
/// Input pass for server resource files. Everything that will go into <c>Resources/</c>.
/// </summary>
/// <remarks>
/// Do not prefix file paths with <c>Resources/</c>, the asset pass will automatically remap them.
/// </remarks>
/// <seealso cref="InputCore"/>
public AssetPassPipe InputResources { get; }
public AssetPassPipe PresetPassesResources { get; }
public AssetPassAudioMetadata AudioMetadata { get; }
/// <summary>
/// Normalizes text files in resources.
/// </summary>
public AssetPassNormalizeText NormalizeTextResources { get; }
/// <summary>
/// Responsible for putting resources into the "<c>Resources/</c>" folder.
/// </summary>
public AssetPassPrefix PrefixResources { get; }
/// <summary>
/// Collection of all passes in this preset graph.
/// </summary>
public IReadOnlyCollection<AssetPass> AllPasses { get; }
/// <param name="parallel">Should inputs be run in parallel. Should only be turned off for debugging.</param>
public RobustServerAssetGraph(bool parallel = true)
{
Output = new AssetPassPipe { Name = "RobustServerAssetGraphOutput", CheckDuplicates = true };
//
// Core files
//
// The code injecting the list of source files is assumed to be pretty single-threaded.
// We use a parallelizing input to break out all the work on files coming in onto multiple threads.
InputCore = new AssetPassPipe { Name = "RobustServerAssetGraphInputCore", Parallelize = parallel };
PresetPassesCore = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesCore" };
NormalizeTextCore = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextCore" };
PresetPassesCore.AddDependency(InputCore);
NormalizeTextCore.AddDependency(PresetPassesCore).AddBefore(Output);
Output.AddDependency(PresetPassesCore);
Output.AddDependency(NormalizeTextCore);
//
// Resource files
//
// Ditto about parallelizing
InputResources = new AssetPassPipe { Name = "RobustServerAssetGraphInputResources", Parallelize = parallel };
PresetPassesResources = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesResources" };
NormalizeTextResources = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextResources" };
AudioMetadata = new AssetPassAudioMetadata { Name = "RobustServerAssetGraphAudioMetadata" };
PrefixResources = new AssetPassPrefix("Resources/") { Name = "RobustServerAssetGraphPrefixResources" };
PresetPassesResources.AddDependency(InputResources);
AudioMetadata.AddDependency(PresetPassesResources).AddBefore(NormalizeTextResources);
NormalizeTextResources.AddDependency(PresetPassesResources).AddBefore(PrefixResources);
PrefixResources.AddDependency(PresetPassesResources);
PrefixResources.AddDependency(AudioMetadata);
PrefixResources.AddDependency(NormalizeTextResources);
Output.AddDependency(PrefixResources);
AllPasses = new AssetPass[]
{
Output,
InputCore,
PresetPassesCore,
NormalizeTextCore,
InputResources,
PresetPassesResources,
NormalizeTextResources,
AudioMetadata,
PrefixResources,
};
}
}

View File

@@ -6,7 +6,6 @@ public sealed class RobustServerPackaging
{
public static IReadOnlySet<string> ServerIgnoresResources { get; } = new HashSet<string>
{
"Audio",
"Textures",
"Fonts",
"Shaders",
@@ -19,15 +18,16 @@ public sealed class RobustServerPackaging
{
var ignoreSet = ServerIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"),
await RobustSharedPackaging.DoResourceCopy(
Path.Combine(contentDir, "Resources"),
pass,
ignoreSet,
"Resources",
cancel);
await RobustSharedPackaging.DoResourceCopy(Path.Combine("RobustToolbox", "Resources"),
cancel: cancel);
await RobustSharedPackaging.DoResourceCopy(
Path.Combine("RobustToolbox", "Resources"),
pass,
ignoreSet,
"Resources",
cancel);
cancel: cancel);
}
}

View File

@@ -0,0 +1,79 @@
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
namespace Robust.Server.Audio;
public sealed partial class AudioSystem
{
protected override void InitializeEffect()
{
base.InitializeEffect();
SubscribeLocalEvent<AudioEffectComponent, ComponentAdd>(OnEffectAdd);
SubscribeLocalEvent<AudioAuxiliaryComponent, ComponentAdd>(OnAuxiliaryAdd);
}
private void ShutdownEffect()
{
}
/// <summary>
/// Reloads all <see cref="AudioPresetPrototype"/> entities.
/// </summary>
public void ReloadPresets()
{
var query = AllEntityQuery<AudioPresetComponent>();
var toDelete = new ValueList<EntityUid>();
while (query.MoveNext(out var uid, out _))
{
toDelete.Add(uid);
}
foreach (var ent in toDelete)
{
Del(ent);
}
foreach (var proto in ProtoMan.EnumeratePrototypes<AudioPresetPrototype>())
{
if (!proto.CreateAuxiliary)
continue;
var effect = CreateEffect();
var aux = CreateAuxiliary();
SetEffectPreset(effect.Entity, effect.Component, proto);
SetEffect(aux.Entity, aux.Component, effect.Entity);
var preset = AddComp<AudioPresetComponent>(aux.Entity);
_auxiliaries.Remove(preset.Preset);
preset.Preset = proto.ID;
_auxiliaries[preset.Preset] = aux.Entity;
}
}
private void OnEffectAdd(EntityUid uid, AudioEffectComponent component, ComponentAdd args)
{
component.Effect = new DummyAudioEffect();
}
private void OnAuxiliaryAdd(EntityUid uid, AudioAuxiliaryComponent component, ComponentAdd args)
{
component.Auxiliary = new DummyAuxiliaryAudio();
}
public override (EntityUid Entity, AudioAuxiliaryComponent Component) CreateAuxiliary()
{
var (ent, comp) = base.CreateAuxiliary();
_pvs.AddGlobalOverride(GetNetEntity(ent));
return (ent, comp);
}
public override (EntityUid Entity, AudioEffectComponent Component) CreateEffect()
{
var (ent, comp) = base.CreateEffect();
_pvs.AddGlobalOverride(GetNetEntity(ent));
return (ent, comp);
}
}

View File

@@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Audio;
using Robust.Shared.Audio.AudioLoading;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.Audio;
public sealed partial class AudioSystem : SharedAudioSystem
{
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
private readonly Dictionary<string, TimeSpan> _cachedAudioLengths = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AudioComponent, ComponentStartup>(OnAudioStartup);
}
public override void Shutdown()
{
base.Shutdown();
ShutdownEffect();
}
private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args)
{
component.Source = new DummyAudioSource();
}
private void AddAudioFilter(EntityUid uid, AudioComponent component, Filter filter)
{
var count = filter.Count;
if (count == 0)
return;
var nent = GetNetEntity(uid);
_pvs.AddSessionOverrides(nent, filter);
var ents = new HashSet<EntityUid>(count);
foreach (var session in filter.Recipients)
{
var ent = session.AttachedEntity;
if (ent == null)
continue;
ents.Add(ent.Value);
}
DebugTools.Assert(component.IncludedEntities == null);
component.IncludedEntities = ents;
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
var entity = Spawn("Audio", MapCoordinates.Nullspace);
var audio = SetupAudio(entity, filename, audioParams);
AddAudioFilter(entity, audio, playerFilter);
return (entity, audio);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
if (!Exists(uid))
return null;
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
AddAudioFilter(entity, audio, playerFilter);
return (entity, audio);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
{
if (!Exists(uid))
return null;
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
return (entity, audio);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
if (!coordinates.IsValid(EntityManager))
return null;
var entity = Spawn("Audio", coordinates);
var audio = SetupAudio(entity, filename, audioParams);
AddAudioFilter(entity, audio, playerFilter);
return (entity, audio);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
if (!coordinates.IsValid(EntityManager))
return null;
var entity = Spawn("Audio", coordinates);
var audio = SetupAudio(entity, filename, audioParams);
return (entity, audio);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (sound == null)
return null;
var audio = PlayPvs(GetSound(sound), source, audioParams ?? sound.Params);
if (audio == null)
return null;
audio.Value.Component.ExcludedEntity = user;
return audio;
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
{
if (sound == null)
return null;
var audio = PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
if (audio == null)
return null;
audio.Value.Component.ExcludedEntity = user;
return audio;
}
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayGlobal(filename, actor.PlayerSession, audioParams);
return null;
}
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
return null;
}
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);
return null;
}
protected override TimeSpan GetAudioLengthImpl(string filename)
{
// Check shipped metadata from packaging.
if (ProtoMan.TryIndex(filename, out AudioMetadataPrototype? metadata))
return metadata.Length;
// Try loading audio files directly.
// This is necessary in development and environments,
// and when working with audio files uploaded dynamically at runtime.
if (_cachedAudioLengths.TryGetValue(filename, out var length))
return length;
if (!_resourceManager.TryContentFileRead(filename, out var stream))
throw new FileNotFoundException($"Unable to find metadata for audio file {filename}");
using (stream)
{
var loadedMetadata = AudioLoader.LoadAudioMetadata(stream, filename);
_cachedAudioLengths.Add(filename, loadedMetadata.Length);
return loadedMetadata.Length;
}
}
}

View File

@@ -1,176 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects;
[UsedImplicitly]
public sealed class AudioSystem : SharedAudioSystem
{
[Dependency] private readonly TransformSystem _transform = default!;
private uint _streamIndex;
private sealed class AudioSourceServer : IPlayingAudioStream
{
private readonly uint _id;
private readonly AudioSystem _audioSystem;
private readonly IEnumerable<ICommonSession>? _sessions;
internal AudioSourceServer(AudioSystem parent, uint identifier, IEnumerable<ICommonSession>? sessions = null)
{
_audioSystem = parent;
_id = identifier;
_sessions = sessions;
}
public void Stop()
{
_audioSystem.InternalStop(_id, _sessions);
}
}
private void InternalStop(uint id, IEnumerable<ICommonSession>? sessions = null)
{
var msg = new StopAudioMessageClient
{
Identifier = id
};
if (sessions == null)
RaiseNetworkEvent(msg);
else
{
foreach (var session in sessions)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
}
}
}
private uint CacheIdentifier()
{
return unchecked(_streamIndex++);
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
var id = CacheIdentifier();
var msg = new PlayAudioGlobalMessage
{
FileName = filename,
AudioParams = audioParams ?? AudioParams.Default,
Identifier = id
};
RaiseNetworkEvent(msg, playerFilter, recordReplay);
return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray());
}
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
if(!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
return null;
var id = CacheIdentifier();
var fallbackCoordinates = GetFallbackCoordinates(transform.MapPosition);
var msg = new PlayAudioEntityMessage
{
FileName = filename,
Coordinates = GetNetCoordinates(transform.Coordinates),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
NetEntity = GetNetEntity(uid),
AudioParams = audioParams ?? AudioParams.Default,
Identifier = id,
};
RaiseNetworkEvent(msg, playerFilter, recordReplay);
return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray());
}
/// <inheritdoc />
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
var id = CacheIdentifier();
var fallbackCoordinates = GetFallbackCoordinates(coordinates.ToMap(EntityManager, _transform));
var msg = new PlayAudioPositionalMessage
{
FileName = filename,
Coordinates = GetNetCoordinates(coordinates),
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
AudioParams = audioParams ?? AudioParams.Default,
Identifier = id
};
RaiseNetworkEvent(msg, playerFilter, recordReplay);
return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray());
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (sound == null)
return null;
var filter = Filter.Pvs(source, entityManager: EntityManager, playerManager: PlayerManager, cfgManager: CfgManager).RemoveWhereAttachedEntity(e => e == user);
return Play(sound, filter, source, true, audioParams);
}
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user,
AudioParams? audioParams = null)
{
if (sound == null)
return null;
var filter = Filter.Pvs(coordinates, entityMan: EntityManager, playerMan: PlayerManager).RemoveWhereAttachedEntity(e => e == user);
return Play(sound, filter, coordinates, true, audioParams);
}
public override IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
}
public override IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayGlobal(filename, actor.PlayerSession, audioParams);
return null;
}
public override IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return Play(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
}
public override IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
return null;
}
public override IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return Play(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
}
public override IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);
return null;
}
}

View File

@@ -16,30 +16,38 @@ public sealed class PvsOverrideSystem : EntitySystem
/// </summary>
/// <param name="removeExistingOverride">Whether or not to supersede existing overrides.</param>
/// <param name="recursive">If true, this will also recursively send any children of the given index.</param>
public void AddGlobalOverride(EntityUid uid, bool removeExistingOverride = true, bool recursive = false)
public void AddGlobalOverride(NetEntity entity, bool removeExistingOverride = true, bool recursive = false)
{
_pvs.EntityPVSCollection.AddGlobalOverride(GetNetEntity(uid), removeExistingOverride, recursive);
_pvs.EntityPVSCollection.AddGlobalOverride(entity, removeExistingOverride, recursive);
}
/// <summary>
/// Used to ensure that an entity is always sent to a specific client. By default this overrides any global or pre-existing
/// client-specific overrides. Unlike global overrides, this is always recursive.
/// Used to ensure that an entity is always sent to a specific client. Overrides any global or pre-existing
/// client-specific overrides.
/// </summary>
/// <param name="removeExistingOverride">Whether or not to supersede existing overrides.</param>
/// <param name="recursive">If true, this will also recursively send any children of the given index.</param>
public void AddSessionOverride(EntityUid uid, ICommonSession session, bool removeExistingOverride = true)
public void AddSessionOverride(NetEntity entity, ICommonSession session, bool removeExistingOverride = true)
{
_pvs.EntityPVSCollection.AddSessionOverride(GetNetEntity(uid), session, removeExistingOverride);
_pvs.EntityPVSCollection.AddSessionOverride(entity, session, removeExistingOverride);
}
// 'placeholder'
public void AddSessionOverrides(NetEntity entity, Filter filter, bool removeExistingOverride = true)
{
foreach (var player in filter.Recipients)
{
AddSessionOverride(entity, player, removeExistingOverride);
}
}
/// <summary>
/// Removes any global or client-specific overrides.
/// </summary>
public void ClearOverride(EntityUid uid, TransformComponent? xform = null)
public void ClearOverride(NetEntity entity, TransformComponent? xform = null)
{
if (!Resolve(uid, ref xform))
if (!TryGetEntity(entity, out var uid) || !Resolve(uid.Value, ref xform))
return;
_pvs.EntityPVSCollection.UpdateIndex(GetNetEntity(uid), xform.Coordinates, true);
_pvs.EntityPVSCollection.UpdateIndex(entity, xform.Coordinates, true);
}
}

View File

@@ -0,0 +1,8 @@
using Robust.Shared.Graphics;
namespace Robust.Server.Graphics;
public struct ClydeHandle : IClydeHandle
{
public long Value => -1;
}

View File

@@ -87,6 +87,8 @@ namespace Robust.Shared.Maths
get => (TopRight - BottomLeft) * 0.5f;
}
public static Box2 Empty = new Box2();
/// <summary>
/// A 1x1 unit box with the origin centered.
/// </summary>

View File

@@ -0,0 +1,36 @@
using Robust.Shared.Audio.Systems;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Shared.Audio;
internal sealed class AudioDebugCommands : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
public override string Command => "audio_length";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError(LocalizationManager.GetString("cmd-invalid-arg-number-error"));
return;
}
var audioSystem = _entitySystem.GetEntitySystem<SharedAudioSystem>();
var length = audioSystem.GetAudioLength(args[0]);
shell.WriteLine(length.ToString());
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHint(LocalizationManager.GetString("cmd-audio_length-arg-file-name"));
}
return CompletionResult.Empty;
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.IO;
namespace Robust.Shared.Audio.AudioLoading;
/// <summary>
/// Implements functionality for loading audio files.
/// </summary>
/// <seealso cref="AudioLoaderOgg"/>
/// <seealso cref="AudioLoaderWav"/>
internal static class AudioLoader
{
/// <summary>
/// Test if the given file name is something that we can load.
/// </summary>
/// <remarks>
/// This is detected based on file extension.
/// </remarks>
public static bool IsLoadableAudioFile(ReadOnlySpan<char> filename)
{
var extension = Path.GetExtension(filename);
return extension is ".wav" or ".ogg";
}
/// <summary>
/// Load metadata about an audio file. Can handle all supported audio file types.
/// </summary>
/// <param name="stream">Stream containing audio file data to load.</param>
/// <param name="filename">File name of the audio file. Used to detect which file type it is.</param>
public static AudioMetadata LoadAudioMetadata(Stream stream, ReadOnlySpan<char> filename)
{
var extension = Path.GetExtension(filename);
if (extension is ".ogg")
{
return AudioLoaderOgg.LoadAudioMetadata(stream);
}
else if (extension is ".wav")
{
return AudioLoaderWav.LoadAudioMetadata(stream);
}
else
{
throw new ArgumentException($"Unknown file type: {extension}");
}
}
}
/// <summary>
/// Contains basic metadata of an audio file.
/// </summary>
/// <seealso cref="AudioLoader"/>
internal record AudioMetadata(TimeSpan Length, int ChannelCount, string? Title = null, string? Artist = null);

View File

@@ -0,0 +1,71 @@
using System;
using System.IO;
using NVorbis;
namespace Robust.Shared.Audio.AudioLoading;
/// <summary>
/// Implements functionality for loading ogg audio files.
/// </summary>
/// <seealso cref="AudioLoaderOgg"/>
internal static class AudioLoaderOgg
{
/// <summary>
/// Load metadata for an ogg audio file.
/// </summary>
/// <param name="stream">Audio file stream to load.</param>
public static AudioMetadata LoadAudioMetadata(Stream stream)
{
using var reader = new VorbisReader(stream);
return new AudioMetadata(reader.TotalTime, reader.Channels, reader.Tags.Title, reader.Tags.Artist);
}
/// <summary>
/// Load an ogg file into raw samples and metadata.
/// </summary>
/// <param name="stream">Audio file stream to load.</param>
public static OggVorbisData LoadAudioData(Stream stream)
{
using var vorbis = new NVorbis.VorbisReader(stream, false);
var sampleRate = vorbis.SampleRate;
var channels = vorbis.Channels;
var totalSamples = vorbis.TotalSamples;
var readSamples = 0;
var buffer = new float[totalSamples * channels];
while (readSamples < totalSamples)
{
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
if (read == 0)
{
break;
}
readSamples += read;
}
return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
}
internal readonly struct OggVorbisData
{
public readonly long TotalSamples;
public readonly long SampleRate;
public readonly long Channels;
public readonly ReadOnlyMemory<float> Data;
public readonly string Title;
public readonly string Artist;
public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory<float> data, string title, string artist)
{
TotalSamples = totalSamples;
SampleRate = sampleRate;
Channels = channels;
Data = data;
Title = title;
Artist = artist;
}
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.IO;
using Robust.Shared.Utility;
namespace Robust.Shared.Audio.AudioLoading;
/// <summary>
/// Implements functionality for loading wav audio files.
/// </summary>
/// <seealso cref="AudioLoaderOgg"/>
internal static class AudioLoaderWav
{
/// <summary>
/// Load metadata for a wav audio file.
/// </summary>
/// <param name="stream">Audio file stream to load.</param>
public static AudioMetadata LoadAudioMetadata(Stream stream)
{
// TODO: Don't load entire WAV file just to extract metadata.
var data = LoadAudioData(stream);
var length = TimeSpan.FromSeconds(data.Data.Length / (double) data.BlockAlign / data.SampleRate);
return new AudioMetadata(length, data.NumChannels);
}
/// <summary>
/// Load a wav file into raw samples and metadata.
/// </summary>
/// <param name="stream">Audio file stream to load.</param>
public static WavData LoadAudioData(Stream stream)
{
var reader = new BinaryReader(stream, EncodingHelpers.UTF8, true);
// Read outer most chunks.
Span<byte> fourCc = stackalloc byte[4];
while (true)
{
ReadFourCC(reader, fourCc);
if (!fourCc.SequenceEqual("RIFF"u8))
{
SkipChunk(reader);
continue;
}
return ReadRiffChunk(reader);
}
}
private static void SkipChunk(BinaryReader reader)
{
var length = reader.ReadUInt32();
reader.BaseStream.Position += length;
}
private static void ReadFourCC(BinaryReader reader, Span<byte> fourCc)
{
fourCc[0] = reader.ReadByte();
fourCc[1] = reader.ReadByte();
fourCc[2] = reader.ReadByte();
fourCc[3] = reader.ReadByte();
}
private static WavData ReadRiffChunk(BinaryReader reader)
{
Span<byte> format = stackalloc byte[4];
reader.ReadUInt32();
ReadFourCC(reader, format);
if (!format.SequenceEqual("WAVE"u8))
{
throw new InvalidDataException("File is not a WAVE file.");
}
ReadFourCC(reader, format);
if (!format.SequenceEqual("fmt "u8))
{
throw new InvalidDataException("Expected fmt chunk.");
}
// Read fmt chunk.
var size = reader.ReadInt32();
var afterFmtPos = reader.BaseStream.Position + size;
var audioType = (WavAudioFormatType)reader.ReadInt16();
var channels = reader.ReadInt16();
var sampleRate = reader.ReadInt32();
var byteRate = reader.ReadInt32();
var blockAlign = reader.ReadInt16();
var bitsPerSample = reader.ReadInt16();
if (audioType != WavAudioFormatType.PCM)
{
throw new NotImplementedException("Unable to support audio types other than PCM.");
}
DebugTools.Assert(byteRate == sampleRate * channels * bitsPerSample / 8);
// Fmt is not of guaranteed size, so use the size header to skip to the end.
reader.BaseStream.Position = afterFmtPos;
while (true)
{
ReadFourCC(reader, format);
if (!format.SequenceEqual("data"u8))
{
SkipChunk(reader);
continue;
}
break;
}
// We are in the data chunk.
size = reader.ReadInt32();
var data = reader.ReadBytes(size);
return new WavData(audioType, channels, sampleRate, byteRate, blockAlign, bitsPerSample, data);
}
/// <summary>
/// See http://soundfile.sapp.org/doc/WaveFormat/ for reference.
/// </summary>
internal readonly struct WavData
{
public readonly WavAudioFormatType AudioType;
public readonly short NumChannels;
public readonly int SampleRate;
public readonly int ByteRate;
public readonly short BlockAlign;
public readonly short BitsPerSample;
public readonly ReadOnlyMemory<byte> Data;
public WavData(WavAudioFormatType audioType, short numChannels, int sampleRate, int byteRate,
short blockAlign, short bitsPerSample, ReadOnlyMemory<byte> data)
{
AudioType = audioType;
NumChannels = numChannels;
SampleRate = sampleRate;
ByteRate = byteRate;
BlockAlign = blockAlign;
BitsPerSample = bitsPerSample;
Data = data;
}
}
internal enum WavAudioFormatType : short
{
Unknown = 0,
PCM = 1,
// There's a bunch of other types, those are all unsupported.
}
}

View File

@@ -0,0 +1,21 @@
using System;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Audio;
/// <summary>
/// Stores server-side metadata about an audio file.
/// These prototypes get automatically generated when packaging the server,
/// to allow the server to know audio lengths without shipping the large audio files themselves.
/// </summary>
[Prototype(ProtoName)]
public sealed class AudioMetadataPrototype : IPrototype
{
public const string ProtoName = "audioMetadata";
[IdDataField] public string ID { get; set; } = string.Empty;
[DataField]
public TimeSpan Length;
}

View File

@@ -1,6 +1,7 @@
using Robust.Shared.Serialization;
using System;
using System.Diagnostics.Contracts;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.GameObjects;
@@ -11,10 +12,7 @@ namespace Robust.Shared.Audio
{
// https://hackage.haskell.org/package/OpenAL-1.7.0.5/docs/Sound-OpenAL-AL-Attenuation.html
/// <summary>
/// Default to the overall attenuation. If set project-wide will use InverseDistanceClamped. This is what you typically want for an audio source.
/// </summary>
Default = 0,
Invalid = 0,
NoAttenuation = 1 << 0,
InverseDistance = 1 << 1,
InverseDistanceClamped = 1 << 2,
@@ -34,54 +32,56 @@ namespace Robust.Shared.Audio
/// <summary>
/// The DistanceModel to use for this specific source.
/// </summary>
[DataField("attenuation")]
public Attenuation Attenuation { get; set; } = Default.Attenuation;
[DataField]
public Attenuation Attenuation { get; set; } = Attenuation.LinearDistanceClamped;
/// <summary>
/// Base volume to play the audio at, in dB.
/// </summary>
[DataField("volume")]
[DataField]
public float Volume { get; set; } = Default.Volume;
/// <summary>
/// Scale for the audio pitch.
/// </summary>
[DataField("pitchscale")]
public float PitchScale { get; set; } = Default.PitchScale;
[DataField]
public float Pitch { get; set; } = Default.Pitch;
/// <summary>
/// Audio bus to play on.
/// </summary>
[DataField("busname")]
[DataField]
public string BusName { get; set; } = Default.BusName;
/// <summary>
/// Only applies to positional audio.
/// The maximum distance from which the audio is hearable.
/// </summary>
[DataField("maxdistance")]
[DataField]
public float MaxDistance { get; set; } = Default.MaxDistance;
/// <summary>
/// Used for distance attenuation calculations. Set to 0f to make a sound exempt from distance attenuation.
/// </summary>
[DataField("rolloffFactor")]
[DataField]
public float RolloffFactor { get; set; } = Default.RolloffFactor;
/// <summary>
/// Equivalent of the minimum distance to use for an audio source.
/// </summary>
[DataField("referenceDistance")]
[DataField]
public float ReferenceDistance { get; set; } = Default.ReferenceDistance;
[DataField("loop")] public bool Loop { get; set; } = Default.Loop;
[DataField]
public bool Loop { get; set; } = Default.Loop;
[DataField("playoffset")] public float PlayOffsetSeconds { get; set; } = Default.PlayOffsetSeconds;
[DataField]
public float PlayOffsetSeconds { get; set; } = Default.PlayOffsetSeconds;
/// <summary>
/// If not null, this will randomly modify the pitch scale by adding a number drawn from a normal distribution with this deviation.
/// </summary>
[DataField("variation")]
[DataField]
public float? Variation { get; set; } = null;
// For the max distance value: it's 2000 in Godot, but I assume that's PIXELS due to the 2D positioning,
@@ -96,21 +96,21 @@ namespace Robust.Shared.Audio
public AudioParams(
float volume,
float pitchScale,
float pitch,
string busName,
float maxDistance,
float refDistance,
bool loop,
float playOffsetSeconds,
float? variation = null)
: this(volume, pitchScale, busName, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
: this(volume, pitch, busName, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
{
}
public AudioParams(float volume, float pitchScale, string busName, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
public AudioParams(float volume, float pitch, string busName, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
{
Volume = volume;
PitchScale = pitchScale;
Pitch = pitch;
BusName = busName;
MaxDistance = maxDistance;
RolloffFactor = rolloffFactor;
@@ -163,7 +163,7 @@ namespace Robust.Shared.Audio
public readonly AudioParams WithPitchScale(float pitch)
{
var me = this;
me.PitchScale = pitch;
me.Pitch = pitch;
return me;
}

View File

@@ -0,0 +1,91 @@
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Audio;
/// <summary>
/// Contains audio defaults to set for sounds.
/// This can be used by <see cref="Content.Shared.Audio.SharedContentAudioSystem"/> to apply an audio preset.
/// </summary>
[Prototype("audioPreset")]
public sealed class AudioPresetPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = default!;
/// <summary>
/// Should the engine automatically create an auxiliary audio effect slot for this.
/// </summary>
[DataField]
public bool CreateAuxiliary;
[DataField]
public float Density;
[DataField]
public float Diffusion;
[DataField]
public float Gain;
[DataField("gainHf")]
public float GainHF;
[DataField("gainLf")]
public float GainLF;
[DataField]
public float DecayTime;
[DataField("decayHfRatio")]
public float DecayHFRatio;
[DataField("decayLfRatio")]
public float DecayLFRatio;
[DataField]
public float ReflectionsGain;
[DataField]
public float ReflectionsDelay;
[DataField]
public Vector3 ReflectionsPan;
[DataField]
public float LateReverbGain;
[DataField]
public float LateReverbDelay;
[DataField]
public Vector3 LateReverbPan;
[DataField]
public float EchoTime;
[DataField]
public float EchoDepth;
[DataField]
public float ModulationTime;
[DataField]
public float ModulationDepth;
[DataField("airAbsorptionGainHf")]
public float AirAbsorptionGainHF;
[DataField("hfReference")]
public float HFReference;
[DataField("lfReference")]
public float LFReference;
[DataField]
public float RoomRolloffFactor;
[DataField("decayHfLimit")]
public int DecayHFLimit;
}

View File

@@ -0,0 +1,24 @@
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Audio.Components;
/// <summary>
/// Can have Audio passed to it to apply effects or filters.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedAudioSystem))]
public sealed partial class AudioAuxiliaryComponent : Component
{
/// <summary>
/// Audio effect to attach to this auxiliary audio slot.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Effect;
[ViewVariables]
internal IAuxiliaryAudio Auxiliary = new DummyAuxiliaryAudio();
}

View File

@@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Audio.Components;
/// <summary>
/// Stores the audio data for an audio entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedAudioSystem))]
public sealed partial class AudioComponent : Component, IAudioSource
{
#region Filter
public override bool SessionSpecific => true;
/// <summary>
/// Used for synchronising audio on client that comes into PVS range.
/// </summary>
[DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
public TimeSpan AudioStart;
#region Filters
// Don't need to network these as client doesn't care.
/// <summary>
/// If this sound was predicted do we exclude it from a specific entity.
/// Useful for predicted audio.
/// </summary>
[DataField]
public EntityUid? ExcludedEntity;
/// <summary>
/// If the sound was filtered what entities were included.
/// </summary>
[DataField]
public HashSet<EntityUid>? IncludedEntities;
#endregion
#endregion
// We can't just start playing on audio creation as we don't have the correct position yet.
// As such we'll wait for FrameUpdate before we start playing to avoid the position being cooked.
public bool Started = false;
[AutoNetworkedField]
[DataField(required: true)]
public string FileName = string.Empty;
/// <summary>
/// Audio params. Set this if you want to adjust default volume, max distance, etc.
/// </summary>
[AutoNetworkedField]
[DataField]
public AudioParams Params = AudioParams.Default;
/// <summary>
/// Audio source that interacts with OpenAL.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
internal IAudioSource Source = default!;
/// <summary>
/// Auxiliary entity to pass audio to.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Auxiliary;
/*
* Values for IAudioSource stored on the component and sent to IAudioSource as applicable.
* Most of these aren't networked as they double AudioParams data and these just interact with IAudioSource.
*/
#region Source
public void Pause() => Source.Pause();
/// <inheritdoc />
public void StartPlaying() => Source.StartPlaying();
/// <inheritdoc />
public void StopPlaying() => Source.StopPlaying();
/// <summary>
/// <see cref="IAudioSource.Playing"/>
/// </summary>
[ViewVariables]
public bool Playing
{
get => Source.Playing;
set => Source.Playing = value;
}
/// <summary>
/// <see cref="IAudioSource.Looping"/>
/// </summary>
[ViewVariables]
public bool Looping
{
get => Source.Looping;
set => Source.Looping = value;
}
/// <summary>
/// <see cref="IAudioSource.Global"/>
/// </summary>
[AutoNetworkedField]
public bool Global { get; set; }
/// <summary>
/// <see cref="IAudioSource.Pitch"/>
/// </summary>
public float Pitch
{
get => Source.Pitch;
set => Source.Pitch = value;
}
/// <summary>
/// <see cref="IAudioSource.MaxDistance"/>
/// </summary>
public float MaxDistance
{
get => Source.MaxDistance;
set => Source.MaxDistance = value;
}
/// <summary>
/// <see cref="IAudioSource.RolloffFactor"/>
/// </summary>
public float RolloffFactor
{
get => Source.RolloffFactor;
set => Source.RolloffFactor = value;
}
/// <summary>
/// <see cref="IAudioSource.ReferenceDistance"/>
/// </summary>
public float ReferenceDistance
{
get => Source.ReferenceDistance;
set => Source.ReferenceDistance = value;
}
/// <summary>
/// <see cref="IAudioSource.Position"/>
/// </summary>
/// <remarks>
/// Not replicated as audio always tracks the entity's position.
/// </remarks>
[ViewVariables]
public Vector2 Position
{
get => Source.Position;
set => Source.Position = value;
}
/// <summary>
/// <see cref="IAudioSource.Volume"/>
/// </summary>
[ViewVariables]
[Access(Other = AccessPermissions.ReadWriteExecute)]
public float Volume
{
get => Source.Volume;
set => Source.Volume = value;
}
/// <summary>
/// <see cref="IAudioSource.Gain"/>
/// </summary>
[ViewVariables]
[Access(Other = AccessPermissions.ReadWriteExecute)]
public float Gain
{
get => Source.Gain;
set => Source.Gain = value;
}
/// <summary>
/// <see cref="IAudioSource.Occlusion"/>
/// </summary>
[ViewVariables]
[Access(Other = AccessPermissions.ReadWriteExecute)]
public float Occlusion
{
get => Source.Occlusion;
set => Source.Occlusion = value;
}
/// <summary>
/// <see cref="IAudioSource.PlaybackPosition"/>
/// </summary>
[ViewVariables]
public float PlaybackPosition
{
get => Source.PlaybackPosition;
set => Source.PlaybackPosition = value;
}
/// <summary>
/// <see cref="IAudioSource.Velocity"/>
/// </summary>
/// <remarks>
/// Not replicated.
/// </remarks>
[ViewVariables]
public Vector2 Velocity
{
get => Source.Velocity;
set => Source.Velocity = value;
}
void IAudioSource.SetAuxiliary(IAuxiliaryAudio? audio)
{
Source.SetAuxiliary(audio);
}
#endregion
public void Dispose()
{
Source.Dispose();
}
}

View File

@@ -0,0 +1,204 @@
using System;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Audio.Components;
/// <summary>
/// Stores OpenAL audio effect data that can be bound to an <see cref="AudioAuxiliaryComponent"/>.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedAudioSystem)), AutoGenerateComponentState]
public sealed partial class AudioEffectComponent : Component, IAudioEffect
{
[ViewVariables]
internal IAudioEffect Effect = new DummyAudioEffect();
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float Density
{
get => Effect.Density;
set => Effect.Density = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float Diffusion
{
get => Effect.Diffusion;
set => Effect.Diffusion = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float Gain
{
get => Effect.Gain;
set => Effect.Gain = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float GainHF
{
get => Effect.GainHF;
set => Effect.GainHF = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float GainLF
{
get => Effect.GainLF;
set => Effect.GainLF = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float DecayTime
{
get => Effect.DecayTime;
set => Effect.DecayTime = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float DecayHFRatio
{
get => Effect.DecayHFRatio;
set => Effect.DecayHFRatio = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float DecayLFRatio
{
get => Effect.DecayLFRatio;
set => Effect.DecayLFRatio = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float ReflectionsGain
{
get => Effect.ReflectionsGain;
set => Effect.ReflectionsGain = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float ReflectionsDelay
{
get => Effect.ReflectionsDelay;
set => Effect.ReflectionsDelay = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public Vector3 ReflectionsPan
{
get => Effect.ReflectionsPan;
set => Effect.ReflectionsPan = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float LateReverbGain
{
get => Effect.LateReverbGain;
set => Effect.LateReverbGain = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float LateReverbDelay
{
get => Effect.LateReverbDelay;
set => Effect.LateReverbDelay = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public Vector3 LateReverbPan
{
get => Effect.LateReverbPan;
set => Effect.LateReverbPan = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float EchoTime
{
get => Effect.EchoTime;
set => Effect.EchoTime = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float EchoDepth
{
get => Effect.EchoDepth;
set => Effect.EchoDepth = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float ModulationTime
{
get => Effect.ModulationTime;
set => Effect.ModulationTime = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float ModulationDepth
{
get => Effect.ModulationDepth;
set => Effect.ModulationDepth = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float AirAbsorptionGainHF
{
get => Effect.AirAbsorptionGainHF;
set => Effect.AirAbsorptionGainHF = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float HFReference
{
get => Effect.HFReference;
set => Effect.HFReference = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float LFReference
{
get => Effect.LFReference;
set => Effect.LFReference = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public float RoomRolloffFactor
{
get => Effect.RoomRolloffFactor;
set => Effect.RoomRolloffFactor = value;
}
/// <inheritdoc />
[DataField, AutoNetworkedField]
public int DecayHFLimit
{
get => Effect.DecayHFLimit;
set => Effect.DecayHFLimit = value;
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
namespace Robust.Shared.Audio.Components;
/// <summary>
/// Marks this entity as being spawned for audio presets in case we need to reload.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedAudioSystem))]
public sealed partial class AudioPresetComponent : Component
{
[AutoNetworkedField]
public string Preset = string.Empty;
}

View File

@@ -0,0 +1,81 @@
using Robust.Shared.Audio.Components;
using Robust.Shared.Maths;
namespace Robust.Shared.Audio.Effects;
/// <inheritdoc />
internal sealed class DummyAudioEffect : IAudioEffect
{
public void Dispose()
{
}
/// <inheritdoc />
public float Density { get; set; }
/// <inheritdoc />
public float Diffusion { get; set; }
/// <inheritdoc />
public float Gain { get; set; }
/// <inheritdoc />
public float GainHF { get; set; }
/// <inheritdoc />
public float GainLF { get; set; }
/// <inheritdoc />
public float DecayTime { get; set; }
/// <inheritdoc />
public float DecayHFRatio { get; set; }
/// <inheritdoc />
public float DecayLFRatio { get; set; }
/// <inheritdoc />
public float ReflectionsGain { get; set; }
/// <inheritdoc />
public float ReflectionsDelay { get; set; }
/// <inheritdoc />
public Vector3 ReflectionsPan { get; set; }
/// <inheritdoc />
public float LateReverbGain { get; set; }
/// <inheritdoc />
public float LateReverbDelay { get; set; }
/// <inheritdoc />
public Vector3 LateReverbPan { get; set; }
/// <inheritdoc />
public float EchoTime { get; set; }
/// <inheritdoc />
public float EchoDepth { get; set; }
/// <inheritdoc />
public float ModulationTime { get; set; }
/// <inheritdoc />
public float ModulationDepth { get; set; }
/// <inheritdoc />
public float AirAbsorptionGainHF { get; set; }
/// <inheritdoc />
public float HFReference { get; set; }
/// <inheritdoc />
public float LFReference { get; set; }
/// <inheritdoc />
public float RoomRolloffFactor { get; set; }
/// <inheritdoc />
public int DecayHFLimit { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Robust.Shared.Audio.Effects;
/// <inheritdoc />
internal sealed class DummyAuxiliaryAudio : IAuxiliaryAudio
{
public void Dispose()
{
}
/// <inheritdoc />
public void SetEffect(IAudioEffect? effect)
{
}
}

View File

@@ -0,0 +1,122 @@
using System;
using Robust.Shared.Maths;
namespace Robust.Shared.Audio.Effects;
internal interface IAudioEffect
{
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDensity"/>.
/// </summary>
public float Density { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDiffusion "/>.
/// </summary>
public float Diffusion { get; set; }
/// <summary>
/// Gets the preset value for <ReverbGainsee cref="EffectFloat.ReverbGain"/>.
/// </summary>
public float Gain { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbGainHF"/>.
/// </summary>
public float GainHF { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbGainLF"/>.
/// </summary>
public float GainLF { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDecayTime"/>.
/// </summary>
public float DecayTime { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDecayHFRatio"/>.
/// </summary>
public float DecayHFRatio { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbDecayLFRatio"/>.
/// </summary>
public float DecayLFRatio { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbReflectionsGain"/>.
/// </summary>
public float ReflectionsGain { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbReflectionsDelay"/>.
/// </summary>
public float ReflectionsDelay { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectVector3.EaxReverbReflectionsPan"/>.
/// </summary>
public Vector3 ReflectionsPan { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbLateReverbGain"/>.
/// </summary>
public float LateReverbGain { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbLateReverbDelay"/>.
/// </summary>
public float LateReverbDelay { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectVector3.EaxReverbLateReverbPan"/>.
/// </summary>
public Vector3 LateReverbPan { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbEchoTime"/>.
/// </summary>
public float EchoTime { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbEchoDepth"/>.
/// </summary>
public float EchoDepth { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbModulationTime"/>.
/// </summary>
public float ModulationTime { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbModulationDepth"/>.
/// </summary>
public float ModulationDepth { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbAirAbsorptionGainHF"/>.
/// </summary>
public float AirAbsorptionGainHF { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbHFReference"/>.
/// </summary>
public float HFReference { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbLFReference"/>.
/// </summary>
public float LFReference { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbRoomRolloffFactor"/>.
/// </summary>
public float RoomRolloffFactor { get; set; }
/// <summary>
/// Gets the preset value for <see cref="EffectInteger.ReverbDecayHFLimit"/>.
/// </summary>
public int DecayHFLimit { get; set; }
}

View File

@@ -0,0 +1,11 @@
using System;
namespace Robust.Shared.Audio.Effects;
internal interface IAuxiliaryAudio : IDisposable
{
/// <summary>
/// Sets the audio effect for this auxiliary audio slot.
/// </summary>
void SetEffect(IAudioEffect? effect);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
//
// ReverbProperties.cs
//
// Copyright (C) 2019 OpenTK
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
//
using Robust.Shared.Maths;
namespace Robust.Shared.Audio.Effects;
/// <summary>
/// Defines a set of predefined reverb properties.
/// </summary>
public record struct ReverbProperties
{
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDensity"/>.
/// </summary>
public float Density;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDiffusion "/>.
/// </summary>
public float Diffusion;
/// <summary>
/// Gets the preset value for <ReverbGainsee cref="EffectFloat.ReverbGain"/>.
/// </summary>
public float Gain;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbGainHF"/>.
/// </summary>
public float GainHF;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbGainLF"/>.
/// </summary>
public float GainLF;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDecayTime"/>.
/// </summary>
public float DecayTime;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbDecayHFRatio"/>.
/// </summary>
public float DecayHFRatio;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbDecayLFRatio"/>.
/// </summary>
public float DecayLFRatio;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbReflectionsGain"/>.
/// </summary>
public float ReflectionsGain;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbReflectionsDelay"/>.
/// </summary>
public float ReflectionsDelay;
/// <summary>
/// Gets the preset value for <see cref="EffectVector3.EaxReverbReflectionsPan"/>.
/// </summary>
public Vector3 ReflectionsPan;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbLateReverbGain"/>.
/// </summary>
public float LateReverbGain;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbLateReverbDelay"/>.
/// </summary>
public float LateReverbDelay;
/// <summary>
/// Gets the preset value for <see cref="EffectVector3.EaxReverbLateReverbPan"/>.
/// </summary>
public Vector3 LateReverbPan;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbEchoTime"/>.
/// </summary>
public float EchoTime;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbEchoDepth"/>.
/// </summary>
public float EchoDepth;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbModulationTime"/>.
/// </summary>
public float ModulationTime;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbModulationDepth"/>.
/// </summary>
public float ModulationDepth;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbAirAbsorptionGainHF"/>.
/// </summary>
public float AirAbsorptionGainHF;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbHFReference"/>.
/// </summary>
public float HFReference;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.EaxReverbLFReference"/>.
/// </summary>
public float LFReference;
/// <summary>
/// Gets the preset value for <see cref="EffectFloat.ReverbRoomRolloffFactor"/>.
/// </summary>
public float RoomRolloffFactor;
/// <summary>
/// Gets the preset value for <see cref="EffectInteger.ReverbDecayHFLimit"/>.
/// </summary>
public int DecayHFLimit;
/// <summary>
/// Initializes a new instance of the <see cref="ReverbProperties"/> struct.
/// </summary>
/// <param name="density">See <see cref="Density"/>.</param>
/// <param name="diffusion">See <see cref="Diffusion"/>.</param>
/// <param name="gain">See <see cref="Gain"/>.</param>
/// <param name="gainHF">See <see cref="GainHF"/>.</param>
/// <param name="gainLF">See <see cref="GainLF"/>.</param>
/// <param name="decayTime">See <see cref="DecayTime"/>.</param>
/// <param name="decayHFRatio">See <see cref="DecayHFRatio"/>.</param>
/// <param name="decayLFRatio">See <see cref="DecayLFRatio"/>.</param>
/// <param name="reflectionsGain">See <see cref="ReflectionsGain"/>.</param>
/// <param name="reflectionsDelay">See <see cref="ReflectionsDelay"/>.</param>
/// <param name="reflectionsPan">See <see cref="ReflectionsPan"/>.</param>
/// <param name="lateReverbGain">See <see cref="LateReverbGain"/>.</param>
/// <param name="lateReverbDelay">See <see cref="LateReverbDelay"/>.</param>
/// <param name="lateReverbPan">See <see cref="LateReverbPan"/>.</param>
/// <param name="echoTime">See <see cref="EchoTime"/>.</param>
/// <param name="echoDepth">See <see cref="EchoDepth"/>.</param>
/// <param name="modulationTime">See <see cref="ModulationTime"/>.</param>
/// <param name="modulationDepth">See <see cref="ModulationDepth"/>.</param>
/// <param name="airAbsorptionGainHF">See <see cref="AirAbsorptionGainHF"/>.</param>
/// <param name="hfReference">See <see cref="HFReference"/>.</param>
/// <param name="lfReference">See <see cref="LFReference"/>.</param>
/// <param name="roomRolloffFactor">See <see cref="RoomRolloffFactor"/>.</param>
/// <param name="decayHFLimit">See <see cref="DecayHFLimit"/>.</param>
public ReverbProperties
(
float density,
float diffusion,
float gain,
float gainHF,
float gainLF,
float decayTime,
float decayHFRatio,
float decayLFRatio,
float reflectionsGain,
float reflectionsDelay,
Vector3 reflectionsPan,
float lateReverbGain,
float lateReverbDelay,
Vector3 lateReverbPan,
float echoTime,
float echoDepth,
float modulationTime,
float modulationDepth,
float airAbsorptionGainHF,
float hfReference,
float lfReference,
float roomRolloffFactor,
int decayHFLimit
)
{
Density = density;
Diffusion = diffusion;
Gain = gain;
GainHF = gainHF;
GainLF = gainLF;
DecayTime = decayTime;
DecayHFRatio = decayHFRatio;
DecayLFRatio = decayLFRatio;
ReflectionsGain = reflectionsGain;
ReflectionsDelay = reflectionsDelay;
ReflectionsPan = reflectionsPan;
LateReverbGain = lateReverbGain;
LateReverbDelay = lateReverbDelay;
LateReverbPan = lateReverbPan;
EchoTime = echoTime;
EchoDepth = echoDepth;
ModulationTime = modulationTime;
ModulationDepth = modulationDepth;
AirAbsorptionGainHF = airAbsorptionGainHF;
HFReference = hfReference;
LFReference = lfReference;
RoomRolloffFactor = roomRolloffFactor;
DecayHFLimit = decayHFLimit;
}
}

View File

@@ -1,5 +0,0 @@
namespace Robust.Shared.Audio;
public interface IPlayingAudioStream
{
void Stop();
}

View File

@@ -1,69 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using System;
namespace Robust.Shared.Audio
{
/// <summary>
/// A static proxy class for interfacing with the AudioSystem.
/// </summary>
public static class SoundSystem
{
// These functions are obsolete and I CBF adding new arguments to them.
private static bool _recordReplay = false;
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="audioParams">Audio parameters to apply when playing the sound.</param>
[Obsolete("Use SharedAudioSystem.PlayGlobal()")]
public static IPlayingAudioStream? Play(string filename, Filter playerFilter, AudioParams? audioParams = null)
{
var entSystMan = IoCManager.Resolve<IEntitySystemManager>();
// Some timers try to play audio after the system has shut down?
entSystMan.TryGetEntitySystem(out SharedAudioSystem? audio);
return audio?.PlayGlobal(filename, playerFilter, _recordReplay, audioParams);
}
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
/// <param name="audioParams">Audio parameters to apply when playing the sound.</param>
[Obsolete("Use SharedAudioSystem")]
public static IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid uid,
AudioParams? audioParams = null)
{
var entSystMan = IoCManager.Resolve<IEntitySystemManager>();
// Some timers try to play audio after the system has shut down?
entSystMan.TryGetEntitySystem(out SharedAudioSystem? audio);
return audio?.Play(filename, playerFilter, uid, _recordReplay, audioParams);
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams">Audio parameters to apply when playing the sound.</param>
[Obsolete("Use SharedAudioSystem")]
public static IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
var entSystMan = IoCManager.Resolve<IEntitySystemManager>();
// Some timers try to play audio after the system has shut down?
entSystMan.TryGetEntitySystem(out SharedAudioSystem? audio);
return audio?.Play(filename, playerFilter, coordinates, _recordReplay, audioParams);
}
}
}

View File

@@ -0,0 +1,83 @@
using System.Numerics;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Audio.Sources;
/// <summary>
/// Hey look, it's AudioSource's evil twin brother!
/// </summary>
[Virtual]
[DataDefinition]
internal partial class DummyAudioSource : IAudioSource
{
public static DummyAudioSource Instance { get; } = new();
public void Dispose()
{
}
/// <inheritdoc />
public void Pause()
{
}
/// <inheritdoc />
public void StartPlaying()
{
}
/// <inheritdoc />
public void StopPlaying()
{
}
/// <inheritdoc />
public bool Playing { get; set; }
/// <inheritdoc />
[DataField]
public bool Looping { get; set; }
/// <inheritdoc />
[DataField]
public bool Global { get; set; }
/// <inheritdoc />
public Vector2 Position { get; set; }
/// <inheritdoc />
[DataField]
public float Pitch { get; set; }
/// <inheritdoc />
public float Volume { get; set; }
/// <inheritdoc />
public float Gain { get; set; }
/// <inheritdoc />
[DataField]
public float MaxDistance { get; set; }
/// <inheritdoc />
[DataField]
public float RolloffFactor { get; set; }
/// <inheritdoc />
[DataField]
public float ReferenceDistance { get; set; }
/// <inheritdoc />
public float Occlusion { get; set; }
/// <inheritdoc />
public float PlaybackPosition { get; set; }
/// <inheritdoc />
public Vector2 Velocity { get; set; }
public void SetAuxiliary(IAuxiliaryAudio? audio)
{
}
}

View File

@@ -1,24 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
namespace Robust.Shared.Audio.Sources
{
/// <summary>
/// Hey look, it's ClydeAudio.BufferedAudioSource's evil twin brother!
/// Hey look, it's Audio.BufferedAudioSource's evil twin brother!
/// </summary>
internal sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
internal sealed class DummyBufferedAudioSource : DummyAudioSource, IBufferedAudioSource
{
public new static DummyBufferedAudioSource Instance { get; } = new();
public int SampleRate { get; set; } = 0;

View File

@@ -0,0 +1,82 @@
using System;
using System.Numerics;
using Robust.Shared.Audio.Effects;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Audio.Sources;
/// <summary>
/// Engine audio source that directly interacts with OpenAL.
/// </summary>
/// <remarks>
/// This just exists so client can interact with OpenAL and server can interact with nothing.
/// </remarks>
internal interface IAudioSource : IDisposable
{
void Pause();
/// <summary>
/// Tries to start playing the audio if not already playing.
/// </summary>
void StartPlaying();
/// <summary>
/// Stops playing a source if it is currently playing.
/// </summary>
void StopPlaying();
/// <summary>
/// Is the audio source currently playing.
/// </summary>
bool Playing { get; set; }
/// <summary>
/// Will the audio source loop when finished playing?
/// </summary>
bool Looping { get; set; }
/// <summary>
/// Is the audio source relative to the listener or to the world (global or local).
/// </summary>
bool Global { get; set; }
/// <summary>
/// Position of the audio relative to listener.
/// </summary>
Vector2 Position { get; set; }
float Pitch { get; set; }
/// <summary>
/// Decibels of the audio. Converted to gain when setting.
/// </summary>
float Volume { get; set; }
/// <summary>
/// Direct gain for audio.
/// </summary>
float Gain { get; set; }
float MaxDistance { get; set; }
float RolloffFactor { get; set; }
float ReferenceDistance { get; set; }
float Occlusion { get; set; }
/// <summary>
/// Current playback position.
/// </summary>
float PlaybackPosition { get; set; }
/// <summary>
/// Audio source velocity. Used for the doppler effect.
/// </summary>
Vector2 Velocity { get; set; }
/// <summary>
/// Set the auxiliary sendfilter for this audio source.
/// </summary>
void SetAuxiliary(IAuxiliaryAudio? audio);
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Robust.Shared.Audio.Sources;
internal interface IBufferedAudioSource : IAudioSource
{
int SampleRate { get; set; }
int GetNumberOfBuffersProcessed();
void GetBuffersProcessed(Span<int> handles);
void WriteBuffer(int handle, ReadOnlySpan<ushort> data);
void WriteBuffer(int handle, ReadOnlySpan<float> data);
void QueueBuffers(ReadOnlySpan<int> handles);
void EmptyBuffers();
}

View File

@@ -0,0 +1,150 @@
using System.Collections.Generic;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Effects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Robust.Shared.Audio.Systems;
public abstract partial class SharedAudioSystem
{
/*
* This is somewhat limiting but also is the easiest way to expose it to content for now.
*/
/// <summary>
/// Pre-calculated auxiliary effect slots for audio presets.
/// </summary>
public IReadOnlyDictionary<string, EntityUid> Auxiliaries => _auxiliaries;
protected readonly Dictionary<string, EntityUid> _auxiliaries = new();
protected virtual void InitializeEffect()
{
SubscribeLocalEvent<AudioPresetComponent, ComponentStartup>(OnPresetStartup);
SubscribeLocalEvent<AudioPresetComponent, ComponentShutdown>(OnPresetShutdown);
}
private void OnPresetStartup(EntityUid uid, AudioPresetComponent component, ComponentStartup args)
{
_auxiliaries[component.Preset] = uid;
}
private void OnPresetShutdown(EntityUid uid, AudioPresetComponent component, ComponentShutdown args)
{
_auxiliaries.Remove(component.Preset);
}
/// <summary>
/// Creates an auxiliary audio slot that can have an audio source or audio effect applied to it.
/// </summary>
public virtual (EntityUid Entity, AudioAuxiliaryComponent Component) CreateAuxiliary()
{
var ent = Spawn(null, MapCoordinates.Nullspace);
var comp = AddComp<AudioAuxiliaryComponent>(ent);
return (ent, comp);
}
/// <summary>
/// Creates an audio effect that can be used with an auxiliary audio slot.
/// </summary>
public virtual (EntityUid Entity, AudioEffectComponent Component) CreateEffect()
{
var ent = Spawn(null, MapCoordinates.Nullspace);
var comp = AddComp<AudioEffectComponent>(ent);
return (ent, comp);
}
/// <summary>
/// Sets the auxiliary effect slot for a specified audio source.
/// </summary>
public virtual void SetAuxiliary(EntityUid uid, AudioComponent audio, EntityUid? auxUid)
{
DebugTools.Assert(auxUid == null || HasComp<AudioAuxiliaryComponent>(auxUid));
audio.Auxiliary = auxUid;
Dirty(uid, audio);
}
/// <summary>
/// Sets the audio effect for a specified auxiliary effect slot.
/// </summary>
public virtual void SetEffect(EntityUid auxUid, AudioAuxiliaryComponent aux, EntityUid? effectUid)
{
DebugTools.Assert(effectUid == null || HasComp<AudioEffectComponent>(effectUid));
aux.Effect = effectUid;
Dirty(auxUid, aux);
}
public void SetEffect(EntityUid? audioUid, AudioComponent? component, string effectProto)
{
if (audioUid == null || component == null)
return;
SetAuxiliary(audioUid.Value, component, _auxiliaries[effectProto]);
}
/// <summary>
/// Applies an audio preset prototype to an audio effect entity.
/// </summary>
public void SetEffectPreset(EntityUid effectUid, AudioEffectComponent effectComp, AudioPresetPrototype preset)
{
effectComp.Density = preset.Density;
effectComp.Diffusion = preset.Diffusion;
effectComp.Gain = preset.Gain;
effectComp.GainHF = preset.GainHF;
effectComp.GainLF = preset.GainLF;
effectComp.DecayTime = preset.DecayTime;
effectComp.DecayHFRatio = preset.DecayHFRatio;
effectComp.DecayLFRatio = preset.DecayLFRatio;
effectComp.ReflectionsGain = preset.ReflectionsGain;
effectComp.ReflectionsDelay = preset.ReflectionsDelay;
effectComp.ReflectionsPan = preset.ReflectionsPan;
effectComp.LateReverbGain = preset.LateReverbGain;
effectComp.LateReverbDelay = preset.LateReverbDelay;
effectComp.LateReverbPan = preset.LateReverbPan;
effectComp.EchoTime = preset.EchoTime;
effectComp.EchoDepth = preset.EchoDepth;
effectComp.ModulationTime = preset.ModulationTime;
effectComp.ModulationDepth = preset.ModulationDepth;
effectComp.AirAbsorptionGainHF = preset.AirAbsorptionGainHF;
effectComp.HFReference = preset.HFReference;
effectComp.LFReference = preset.LFReference;
effectComp.RoomRolloffFactor = preset.RoomRolloffFactor;
effectComp.DecayHFLimit = preset.DecayHFLimit;
Dirty(effectUid, effectComp);
}
/// <summary>
/// Applies an EAX reverb effect preset to an audio effect.
/// </summary>
public void SetEffectPreset(EntityUid effectUid, AudioEffectComponent effectComp, ReverbProperties preset)
{
effectComp.Density = preset.Density;
effectComp.Diffusion = preset.Diffusion;
effectComp.Gain = preset.Gain;
effectComp.GainHF = preset.GainHF;
effectComp.GainLF = preset.GainLF;
effectComp.DecayTime = preset.DecayTime;
effectComp.DecayHFRatio = preset.DecayHFRatio;
effectComp.DecayLFRatio = preset.DecayLFRatio;
effectComp.ReflectionsGain = preset.ReflectionsGain;
effectComp.ReflectionsDelay = preset.ReflectionsDelay;
effectComp.ReflectionsPan = preset.ReflectionsPan;
effectComp.LateReverbGain = preset.LateReverbGain;
effectComp.LateReverbDelay = preset.LateReverbDelay;
effectComp.LateReverbPan = preset.LateReverbPan;
effectComp.EchoTime = preset.EchoTime;
effectComp.EchoDepth = preset.EchoDepth;
effectComp.ModulationTime = preset.ModulationTime;
effectComp.ModulationDepth = preset.ModulationDepth;
effectComp.AirAbsorptionGainHF = preset.AirAbsorptionGainHF;
effectComp.HFReference = preset.HFReference;
effectComp.LFReference = preset.LFReference;
effectComp.RoomRolloffFactor = preset.RoomRolloffFactor;
effectComp.DecayHFLimit = preset.DecayHFLimit;
Dirty(effectUid, effectComp);
}
}

View File

@@ -0,0 +1,491 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Spawners;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Audio.Systems;
/// <summary>
/// Handles audio for robust toolbox inside of the sim.
/// </summary>
/// <remarks>
/// Interacts with AudioManager internally.
/// </remarks>
public abstract partial class SharedAudioSystem : EntitySystem
{
[Dependency] protected readonly IConfigurationManager CfgManager = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
[Dependency] protected readonly IRobustRandom RandMan = default!;
/// <summary>
/// Default max range at which the sound can be heard.
/// </summary>
public const float DefaultSoundRange = 20;
/// <summary>
/// Used in the PAS to designate the physics collision mask of occluders.
/// </summary>
public int OcclusionCollisionMask { get; set; }
public float ZOffset;
public override void Initialize()
{
base.Initialize();
InitializeEffect();
ZOffset = CfgManager.GetCVar(CVars.AudioZOffset);
CfgManager.OnValueChanged(CVars.AudioZOffset, SetZOffset);
SubscribeLocalEvent<AudioComponent, ComponentGetStateAttemptEvent>(OnAudioGetStateAttempt);
SubscribeLocalEvent<AudioComponent, EntityUnpausedEvent>(OnAudioUnpaused);
}
public override void Shutdown()
{
base.Shutdown();
CfgManager.UnsubValueChanged(CVars.AudioZOffset, SetZOffset);
}
protected virtual void SetZOffset(float value)
{
var query = AllEntityQuery<AudioComponent>();
var oldZOffset = ZOffset;
ZOffset = value;
while (query.MoveNext(out var uid, out var audio))
{
// Pythagoras back to normal then adjust.
var maxDistance = MathF.Pow(audio.Params.MaxDistance, 2) - oldZOffset;
var refDistance = MathF.Pow(audio.Params.ReferenceDistance, 2) - oldZOffset;
audio.Params.MaxDistance = maxDistance;
audio.Params.ReferenceDistance = refDistance;
audio.Params = GetAdjustedParams(audio.Params);
Dirty(uid, audio);
}
}
protected virtual void OnAudioUnpaused(EntityUid uid, AudioComponent component, ref EntityUnpausedEvent args)
{
component.AudioStart += args.PausedTime;
}
private void OnAudioGetStateAttempt(EntityUid uid, AudioComponent component, ref ComponentGetStateAttemptEvent args)
{
var playerEnt = args.Player?.AttachedEntity;
if ((component.ExcludedEntity != null && playerEnt == component.ExcludedEntity) ||
(playerEnt != null && component.IncludedEntities != null && !component.IncludedEntities.Contains(playerEnt.Value)))
{
args.Cancelled = true;
}
}
/// <summary>
/// Considers Z-offset for audio and gets the adjusted distance.
/// </summary>
/// <remarks>
/// Really it's just doing pythagoras for you.
/// </remarks>
public float GetAudioDistance(float length)
{
return MathF.Sqrt(MathF.Pow(length, 2) + MathF.Pow(ZOffset, 2));
}
/// <summary>
/// Resolves the filepath to a sound file.
/// </summary>
public string GetSound(SoundSpecifier specifier)
{
switch (specifier)
{
case SoundPathSpecifier path:
return path.Path == default ? string.Empty : path.Path.ToString();
case SoundCollectionSpecifier collection:
{
if (collection.Collection == null)
return string.Empty;
var soundCollection = ProtoMan.Index<SoundCollectionPrototype>(collection.Collection);
return RandMan.Pick(soundCollection.PickFiles).ToString();
}
}
return string.Empty;
}
#region AudioParams
protected AudioComponent SetupAudio(EntityUid uid, string fileName, AudioParams? audioParams)
{
DebugTools.Assert(!string.IsNullOrEmpty(fileName));
audioParams ??= AudioParams.Default;
var comp = AddComp<Components.AudioComponent>(uid);
comp.FileName = fileName;
comp.Params = GetAdjustedParams(audioParams.Value);
comp.AudioStart = Timing.CurTime;
if (!audioParams.Value.Loop)
{
var length = GetAudioLength(fileName);
var despawn = AddComp<TimedDespawnComponent>(uid);
// Don't want to clip audio too short due to imprecision.
despawn.Lifetime = (float) length.TotalSeconds + 0.01f;
}
return comp;
}
/// <summary>
/// Accounts for ZOffset on audio distance.
/// </summary>
private AudioParams GetAdjustedParams(AudioParams audioParams)
{
var maxDistance = GetAudioDistance(audioParams.MaxDistance);
var refDistance = GetAudioDistance(audioParams.ReferenceDistance);
return audioParams
.WithMaxDistance(maxDistance)
.WithReferenceDistance(refDistance);
}
/// <summary>
/// Sets the audio params volume for an entity.
/// </summary>
public void SetVolume(EntityUid? entity, float value, Components.AudioComponent? component = null)
{
if (entity == null || !Resolve(entity.Value, ref component))
return;
if (component.Params.Volume.Equals(value))
return;
component.Params.Volume = value;
Dirty(entity.Value, component);
}
#endregion
/// <summary>
/// Gets the timespan of the specified audio.
/// </summary>
public TimeSpan GetAudioLength(string filename)
{
if (!filename.StartsWith("/"))
throw new ArgumentException("Path must be rooted");
return GetAudioLengthImpl(filename);
}
protected abstract TimeSpan GetAudioLengthImpl(string filename);
/// <summary>
/// Stops the specified audio entity from playing.
/// </summary>
/// <remarks>
/// Returns null so you can inline the call.
/// </remarks>
public EntityUid? Stop(EntityUid? uid, Components.AudioComponent? component = null)
{
// One morbillion warnings for logging missing.
if (uid == null || !Resolve(uid.Value, ref component, false))
return null;
if (!Timing.IsFirstTimePredicted || (_netManager.IsClient && !IsClientSide(uid.Value)))
return null;
QueueDel(uid);
return null;
}
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), playerFilter, recordReplay, sound.Params);
}
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="recipient">The player that will hear the sound.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, ICommonSession recipient)
{
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, sound.Params);
}
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="recipient">The player that will hear the sound.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, EntityUid recipient, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, sound.Params);
}
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file following an entity.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file following an entity for every entity in PVS range.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayPvs(GetSound(sound), uid, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file at the specified EntityCoordinates for every entity in PVS range.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file at the specified EntityCoordinates for every entity in PVS range.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string filename,
EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity for every entity in PVS range.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string filename, EntityUid uid,
AudioParams? audioParams = null);
/// <summary>
/// Plays a predicted sound following an entity. The server will send the sound to every player in PVS range,
/// unless that player is attached to the "user" entity that initiated the sound. The client-side system plays
/// this sound as normal
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="source">The UID of the entity "emitting" the audio.</param>
/// <param name="user">The UID of the user that initiated this sound. This is usually some player's controlled entity.</param>
[return: NotNullIfNotNull("sound")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null);
/// <summary>
/// Plays a predicted sound following an EntityCoordinates. The server will send the sound to every player in PVS range,
/// unless that player is attached to the "user" entity that initiated the sound. The client-side system plays
/// this sound as normal
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="coordinates">The entitycoordinates "emitting" the audio</param>
/// <param name="user">The UID of the user that initiated this sound. This is usually some player's controlled entity.</param>
[return: NotNullIfNotNull("sound")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
[return: NotNullIfNotNull("filename")]
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), playerFilter, coordinates, recordReplay);
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file at a static position.
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
[return: NotNullIfNotNull("sound")]
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
}
// These are just here for replays now.
// We don't actually need them in shared, or netserializable, but this makes net serialization
// and replays happy
// TODO: This is quite bandwidth intensive.
// Sending bus names and file names as strings is expensive and can be optimized.
// Also there's redundant fields in AudioParams in most cases.
[NetSerializable, Serializable]
protected abstract class AudioMessage : EntityEventArgs
{
public string FileName = string.Empty;
public AudioParams AudioParams;
}
[NetSerializable, Serializable]
protected sealed class PlayAudioGlobalMessage : AudioMessage
{
}
[NetSerializable, Serializable]
protected sealed class PlayAudioPositionalMessage : AudioMessage
{
public NetCoordinates Coordinates;
}
[NetSerializable, Serializable]
protected sealed class PlayAudioEntityMessage : AudioMessage
{
public NetEntity NetEntity;
}
}

Some files were not shown because too many files have changed in this diff Show More