mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
Audio rework unrevert + audio packaging (#4555)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
5
Resources/EnginePrototypes/Audio/audio_entities.yml
Normal file
5
Resources/EnginePrototypes/Audio/audio_entities.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
- type: entity
|
||||
id: Audio
|
||||
name: Audio
|
||||
description: Audio entity used by engine
|
||||
save: false
|
||||
3076
Resources/EnginePrototypes/Audio/audio_presets.yml
Normal file
3076
Resources/EnginePrototypes/Audio/audio_presets.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
58
Robust.Client/Audio/AudioManager.ALDisposeQueues.cs
Normal file
58
Robust.Client/Audio/AudioManager.ALDisposeQueues.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
339
Robust.Client/Audio/AudioManager.Public.cs
Normal file
339
Robust.Client/Audio/AudioManager.Public.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
161
Robust.Client/Audio/AudioManager.cs
Normal file
161
Robust.Client/Audio/AudioManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Robust.Client/Audio/AudioOverlay.cs
Normal file
89
Robust.Client/Audio/AudioOverlay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
76
Robust.Client/Audio/AudioSystem.Effects.cs
Normal file
76
Robust.Client/Audio/AudioSystem.Effects.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
600
Robust.Client/Audio/AudioSystem.cs
Normal file
600
Robust.Client/Audio/AudioSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
455
Robust.Client/Audio/Effects/AudioEffect.cs
Normal file
455
Robust.Client/Audio/Effects/AudioEffect.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Robust.Client/Audio/Effects/AuxiliaryAudio.cs
Normal file
32
Robust.Client/Audio/Effects/AuxiliaryAudio.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Robust.Client/Audio/HeadlessAudioManager.cs
Normal file
106
Robust.Client/Audio/HeadlessAudioManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
63
Robust.Client/Audio/IAudioInternal.cs
Normal file
63
Robust.Client/Audio/IAudioInternal.cs
Normal 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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
34
Robust.Client/Audio/ShowAudioCommand.cs
Normal file
34
Robust.Client/Audio/ShowAudioCommand.cs
Normal 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>()));
|
||||
}
|
||||
}
|
||||
90
Robust.Client/Audio/Sources/AudioSource.cs
Normal file
90
Robust.Client/Audio/Sources/AudioSource.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
390
Robust.Client/Audio/Sources/BaseAudioSource.cs
Normal file
390
Robust.Client/Audio/Sources/BaseAudioSource.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
225
Robust.Client/Audio/Sources/BufferedAudioSource.cs
Normal file
225
Robust.Client/Audio/Sources/BufferedAudioSource.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
28
Robust.Packaging/AssetProcessing/Passes/AssetPassPrefix.cs
Normal file
28
Robust.Packaging/AssetProcessing/Passes/AssetPassPrefix.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
129
Robust.Packaging/RobustServerAssetGraph.cs
Normal file
129
Robust.Packaging/RobustServerAssetGraph.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
79
Robust.Server/Audio/AudioSystem.Effects.cs
Normal file
79
Robust.Server/Audio/AudioSystem.Effects.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
223
Robust.Server/Audio/AudioSystem.cs
Normal file
223
Robust.Server/Audio/AudioSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
8
Robust.Server/Graphics/ClydeHandle.cs
Normal file
8
Robust.Server/Graphics/ClydeHandle.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.Graphics;
|
||||
|
||||
namespace Robust.Server.Graphics;
|
||||
|
||||
public struct ClydeHandle : IClydeHandle
|
||||
{
|
||||
public long Value => -1;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
36
Robust.Shared/Audio/AudioDebugCommands.cs
Normal file
36
Robust.Shared/Audio/AudioDebugCommands.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
52
Robust.Shared/Audio/AudioLoading/AudioLoader.cs
Normal file
52
Robust.Shared/Audio/AudioLoading/AudioLoader.cs
Normal 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);
|
||||
71
Robust.Shared/Audio/AudioLoading/AudioLoaderOgg.cs
Normal file
71
Robust.Shared/Audio/AudioLoading/AudioLoaderOgg.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Robust.Shared/Audio/AudioLoading/AudioLoaderWav.cs
Normal file
152
Robust.Shared/Audio/AudioLoading/AudioLoaderWav.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
21
Robust.Shared/Audio/AudioMetadataPrototype.cs
Normal file
21
Robust.Shared/Audio/AudioMetadataPrototype.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
91
Robust.Shared/Audio/AudioPresetPrototype.cs
Normal file
91
Robust.Shared/Audio/AudioPresetPrototype.cs
Normal 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;
|
||||
}
|
||||
24
Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs
Normal file
24
Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs
Normal 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();
|
||||
}
|
||||
236
Robust.Shared/Audio/Components/AudioComponent.cs
Normal file
236
Robust.Shared/Audio/Components/AudioComponent.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
204
Robust.Shared/Audio/Components/AudioEffectComponent.cs
Normal file
204
Robust.Shared/Audio/Components/AudioEffectComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
15
Robust.Shared/Audio/Components/AudioPresetComponent.cs
Normal file
15
Robust.Shared/Audio/Components/AudioPresetComponent.cs
Normal 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;
|
||||
}
|
||||
81
Robust.Shared/Audio/Effects/DummyAudioEffect.cs
Normal file
81
Robust.Shared/Audio/Effects/DummyAudioEffect.cs
Normal 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; }
|
||||
}
|
||||
14
Robust.Shared/Audio/Effects/DummyAuxiliaryAudio.cs
Normal file
14
Robust.Shared/Audio/Effects/DummyAuxiliaryAudio.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
122
Robust.Shared/Audio/Effects/IAudioEffect.cs
Normal file
122
Robust.Shared/Audio/Effects/IAudioEffect.cs
Normal 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; }
|
||||
}
|
||||
11
Robust.Shared/Audio/Effects/IAuxiliaryAudio.cs
Normal file
11
Robust.Shared/Audio/Effects/IAuxiliaryAudio.cs
Normal 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);
|
||||
}
|
||||
3435
Robust.Shared/Audio/Effects/ReverbPresets.cs
Normal file
3435
Robust.Shared/Audio/Effects/ReverbPresets.cs
Normal file
File diff suppressed because it is too large
Load Diff
211
Robust.Shared/Audio/Effects/ReverbProperties.cs
Normal file
211
Robust.Shared/Audio/Effects/ReverbProperties.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Robust.Shared.Audio;
|
||||
public interface IPlayingAudioStream
|
||||
{
|
||||
void Stop();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
Robust.Shared/Audio/Sources/DummyAudioSource.cs
Normal file
83
Robust.Shared/Audio/Sources/DummyAudioSource.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
82
Robust.Shared/Audio/Sources/IAudioSource.cs
Normal file
82
Robust.Shared/Audio/Sources/IAudioSource.cs
Normal 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);
|
||||
}
|
||||
14
Robust.Shared/Audio/Sources/IBufferedAudioSource.cs
Normal file
14
Robust.Shared/Audio/Sources/IBufferedAudioSource.cs
Normal 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();
|
||||
}
|
||||
150
Robust.Shared/Audio/Systems/SharedAudioSystem.Effects.cs
Normal file
150
Robust.Shared/Audio/Systems/SharedAudioSystem.Effects.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
491
Robust.Shared/Audio/Systems/SharedAudioSystem.cs
Normal file
491
Robust.Shared/Audio/Systems/SharedAudioSystem.cs
Normal 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
Reference in New Issue
Block a user