From d4b1c76d50e29462f67ac93256a4494567db7294 Mon Sep 17 00:00:00 2001
From: stalengd
Date: Sun, 8 Dec 2024 21:57:20 +0300
Subject: [PATCH] Audio mixers
---
Robust.Client/Audio/AudioSystem.Mixers.cs | 97 ++++++++
Robust.Client/Audio/AudioSystem.cs | 21 +-
Robust.Client/Audio/Midi/IMidiManager.cs | 6 +
Robust.Client/Audio/Midi/IMidiRenderer.cs | 5 +
Robust.Client/Audio/Midi/MidiManager.cs | 35 ++-
Robust.Client/Audio/Midi/MidiRenderer.cs | 4 +
Robust.Client/Audio/Mixers/AudioMixer.cs | 125 ++++++++++
.../Audio/Mixers/AudioMixersManager.cs | 77 ++++++
.../Audio/Mixers/IAudioMixersManager.cs | 14 ++
.../Audio/Sources/MixableAudioSource.cs | 172 +++++++++++++
Robust.Client/ClientIoC.cs | 2 +
Robust.Server/Audio/AudioSystem.Mixers.cs | 22 ++
Robust.Server/Audio/AudioSystem.cs | 2 +-
Robust.Shared/Audio/AudioParams.cs | 21 +-
.../Audio/Components/AudioComponent.cs | 16 +-
.../Audio/Components/AudioMixerComponent.cs | 81 ++++++
.../Audio/Mixers/AudioMixerPrototype.cs | 43 ++++
Robust.Shared/Audio/Mixers/DummyAudioMixer.cs | 36 +++
Robust.Shared/Audio/Mixers/IAudioMixer.cs | 50 ++++
.../Audio/Mixers/IAudioMixerSubscriber.cs | 12 +
.../Audio/Sources/DummyMixableAudioSource.cs | 12 +
.../Audio/Sources/IMixableAudioSource.cs | 11 +
.../Audio/Systems/SharedAudioSystem.Mixers.cs | 234 ++++++++++++++++++
.../Audio/Systems/SharedAudioSystem.cs | 10 +
24 files changed, 1091 insertions(+), 17 deletions(-)
create mode 100644 Robust.Client/Audio/AudioSystem.Mixers.cs
create mode 100644 Robust.Client/Audio/Mixers/AudioMixer.cs
create mode 100644 Robust.Client/Audio/Mixers/AudioMixersManager.cs
create mode 100644 Robust.Client/Audio/Mixers/IAudioMixersManager.cs
create mode 100644 Robust.Client/Audio/Sources/MixableAudioSource.cs
create mode 100644 Robust.Server/Audio/AudioSystem.Mixers.cs
create mode 100644 Robust.Shared/Audio/Components/AudioMixerComponent.cs
create mode 100644 Robust.Shared/Audio/Mixers/AudioMixerPrototype.cs
create mode 100644 Robust.Shared/Audio/Mixers/DummyAudioMixer.cs
create mode 100644 Robust.Shared/Audio/Mixers/IAudioMixer.cs
create mode 100644 Robust.Shared/Audio/Mixers/IAudioMixerSubscriber.cs
create mode 100644 Robust.Shared/Audio/Sources/DummyMixableAudioSource.cs
create mode 100644 Robust.Shared/Audio/Sources/IMixableAudioSource.cs
create mode 100644 Robust.Shared/Audio/Systems/SharedAudioSystem.Mixers.cs
diff --git a/Robust.Client/Audio/AudioSystem.Mixers.cs b/Robust.Client/Audio/AudioSystem.Mixers.cs
new file mode 100644
index 00000000000..a94f58c9b61
--- /dev/null
+++ b/Robust.Client/Audio/AudioSystem.Mixers.cs
@@ -0,0 +1,97 @@
+using Robust.Client.Audio.Mixers;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+
+namespace Robust.Client.Audio;
+
+public sealed partial class AudioSystem
+{
+ [Dependency] private readonly IAudioMixersManager _audioMixersManager = default!;
+
+ protected override void InitializeMixers()
+ {
+ base.InitializeMixers();
+
+ SubscribeLocalEvent(OnMixerAdd);
+ }
+
+ public override Entity CreateMixer(Entity? outMixer)
+ {
+ var mixerEntity = base.CreateMixer(outMixer);
+ if (outMixer is { } outMixerValue)
+ {
+ mixerEntity.Comp.Mixer.SetOut(outMixerValue.Comp.Mixer);
+ }
+ return mixerEntity;
+ }
+
+ public override void SetMixerGain(Entity mixer, float gain)
+ {
+ base.SetMixerGain(mixer, gain);
+ if (mixer.Comp.Mixer.GainCVar is { } cvar)
+ {
+ CfgManager.SetCVar(cvar, gain);
+ }
+ else
+ {
+ mixer.Comp.Mixer.SelfGain = gain;
+ }
+ }
+
+ public override void SetMixerGainCVar(Entity mixer, string? name)
+ {
+ base.SetMixerGainCVar(mixer, name);
+ _audioMixersManager.SetMixerGainCVar(mixer.Comp.Mixer, name);
+ }
+
+ protected override Entity SpawnMixerForPrototype(ProtoId mixerProtoId)
+ {
+ var mixer = base.SpawnMixerForPrototype(mixerProtoId);
+ mixer.Comp.Mixer = _audioMixersManager.GetMixer(mixerProtoId) ?? mixer.Comp.Mixer;
+ return mixer;
+ }
+
+ private void OnMixerAdd(Entity mixer, ref ComponentAdd args)
+ {
+ mixer.Comp.Mixer = _audioMixersManager.CreateMixer();
+ }
+
+ protected override void OnMixerShutdown(Entity mixer, ref ComponentShutdown args)
+ {
+ base.OnMixerShutdown(mixer, ref args);
+ DisposeMixer(mixer.Comp.Mixer);
+ }
+
+ protected override void OnHandleState(Entity mixer, ref ComponentHandleState args)
+ {
+ base.OnHandleState(mixer, ref args);
+
+ if (mixer.Comp.ProtoId is { } protoId
+ && protoId != mixer.Comp.Mixer.ProtoId
+ && _audioMixersManager.GetMixer(protoId) is { } newMixer)
+ {
+ DisposeMixer(mixer.Comp.Mixer);
+ mixer.Comp.Mixer = newMixer;
+ }
+ SetMixerGainCVar(mixer, mixer.Comp.GainCVar);
+ if (mixer.Comp.ProtoId is null)
+ {
+ Entity? outMixer = mixer.Comp.OutEntity is { } outMixerOwner
+ && TryComp(outMixerOwner, out var outMixerComponent)
+ ? new Entity(outMixerOwner, outMixerComponent) : null;
+ SetMixerOut(mixer, outMixer);
+ }
+ }
+
+ private void DisposeMixer(IAudioMixer mixer)
+ {
+ // We don't want to dispose mixers from prototypes cos they are supposed to be re-used.
+ if (mixer.ProtoId is { })
+ return;
+ mixer.Dispose();
+ }
+}
diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs
index 5dfd954bb4c..ed8582d8cbf 100644
--- a/Robust.Client/Audio/AudioSystem.cs
+++ b/Robust.Client/Audio/AudioSystem.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
+using Robust.Client.Audio.Sources;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -134,6 +135,15 @@ private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAuto
component.Source.SetAuxiliary(null);
}
+ if (TryComp(component.Mixer, out var mixerComp))
+ {
+ component.Source.SetMixer(mixerComp.Mixer);
+ }
+ else
+ {
+ ApplyAudioParamsMixer((uid, component), component.Params);
+ }
+
switch (component.State)
{
case AudioState.Playing:
@@ -219,12 +229,20 @@ private void SetupSource(Entity entity, AudioResource audioResou
}
else
{
- component.Source = newSource;
+ component.Source = new MixableAudioSource(newSource);
}
}
// Need to set all initial data for first frame.
ApplyAudioParams(component.Params, component);
+ if (TryComp(component.Mixer, out var mixerComp))
+ {
+ component.Source.SetMixer(mixerComp.Mixer);
+ }
+ else
+ {
+ ApplyAudioParamsMixer(entity, component.Params);
+ }
component.Source.Global = component.Global;
// Don't play until first frame so occlusion etc. are correct.
@@ -245,6 +263,7 @@ private void OnAudioShutdown(EntityUid uid, AudioComponent component, ComponentS
{
// Breaks with prediction?
component.Source.Dispose();
+ ClearMixer((uid, component));
RemoveAudioLimit(component.FileName);
}
diff --git a/Robust.Client/Audio/Midi/IMidiManager.cs b/Robust.Client/Audio/Midi/IMidiManager.cs
index a171564cc2a..89ca292a05d 100644
--- a/Robust.Client/Audio/Midi/IMidiManager.cs
+++ b/Robust.Client/Audio/Midi/IMidiManager.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using NFluidsynth;
using Robust.Shared.Audio.Midi;
+using Robust.Shared.Audio.Mixers;
namespace Robust.Client.Audio.Midi;
@@ -21,6 +22,11 @@ public interface IMidiManager
///
float Gain { get; set; }
+ ///
+ /// Audio mixer to play with.
+ ///
+ IAudioMixer? Mixer { get; set; }
+
///
/// This method tries to return a midi renderer ready to be used.
/// You only need to set the afterwards.
diff --git a/Robust.Client/Audio/Midi/IMidiRenderer.cs b/Robust.Client/Audio/Midi/IMidiRenderer.cs
index 483694faa7c..287ff525a1b 100644
--- a/Robust.Client/Audio/Midi/IMidiRenderer.cs
+++ b/Robust.Client/Audio/Midi/IMidiRenderer.cs
@@ -23,6 +23,11 @@ public interface IMidiRenderer : IDisposable
///
internal IBufferedAudioSource Source { get; }
+ ///
+ /// Mixable audio source reference to apply mixing.
+ ///
+ internal IMixableAudioSource MixableSource { get; }
+
///
/// Whether this renderer has been disposed or not.
///
diff --git a/Robust.Client/Audio/Midi/MidiManager.cs b/Robust.Client/Audio/Midi/MidiManager.cs
index 8aa9cafee69..8da825b32b5 100644
--- a/Robust.Client/Audio/Midi/MidiManager.cs
+++ b/Robust.Client/Audio/Midi/MidiManager.cs
@@ -9,6 +9,7 @@
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio.Midi;
+using Robust.Shared.Audio.Mixers;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
@@ -81,6 +82,7 @@ public bool IsAvailable
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _gain = 0f;
+ private IAudioMixer? _audioMixer;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
@@ -95,7 +97,21 @@ public float Gain
if (MathHelper.CloseToPercent(_gain, clamped))
return;
- _cfgMan.SetCVar(CVars.MidiVolume, clamped);
+ _gain = value;
+ _volumeDirty = true;
+ }
+ }
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public IAudioMixer? Mixer
+ {
+ get => _audioMixer;
+ set
+ {
+ if (_audioMixer == value)
+ return;
+
+ _audioMixer = value;
_volumeDirty = true;
}
}
@@ -145,12 +161,6 @@ private void InitializeFluidsynth()
{
if (FluidsynthInitialized || _failedInitialize) return;
- _cfgMan.OnValueChanged(CVars.MidiVolume, value =>
- {
- _gain = value;
- _volumeDirty = true;
- }, true);
-
_midiSawmill = _logger.GetSawmill("midi");
#if DEBUG
_midiSawmill.Level = LogLevel.Debug;
@@ -396,7 +406,8 @@ private void UpdateRenderer(IMidiRenderer renderer, MapCoordinates listener)
if (_volumeDirty)
{
- renderer.Source.Gain = Gain;
+ renderer.MixableSource.Gain = Gain;
+ renderer.MixableSource.SetMixer(_audioMixer);
}
if (!renderer.Mono)
@@ -429,14 +440,14 @@ private void UpdateRenderer(IMidiRenderer renderer, MapCoordinates listener)
// If it's on a different map then just mute it, not pause.
if (mapPos.MapId == MapId.Nullspace || mapPos.MapId != listener.MapId)
{
- renderer.Source.Gain = 0f;
+ renderer.MixableSource.Gain = 0f;
return;
}
// Was previously muted maybe so try unmuting it?
- if (renderer.Source.Gain == 0f)
+ if (renderer.MixableSource.Gain == 0f)
{
- renderer.Source.Gain = Gain;
+ renderer.MixableSource.Gain = Gain;
}
var worldPos = mapPos.Position;
@@ -448,7 +459,7 @@ private void UpdateRenderer(IMidiRenderer renderer, MapCoordinates listener)
if (distance > renderer.Source.MaxDistance)
{
// Still keeps the source playing, just with no volume.
- renderer.Source.Gain = 0f;
+ renderer.MixableSource.Gain = 0f;
return;
}
diff --git a/Robust.Client/Audio/Midi/MidiRenderer.cs b/Robust.Client/Audio/Midi/MidiRenderer.cs
index bed5537c38d..f38143bace6 100644
--- a/Robust.Client/Audio/Midi/MidiRenderer.cs
+++ b/Robust.Client/Audio/Midi/MidiRenderer.cs
@@ -2,6 +2,8 @@
using System.Collections;
using JetBrains.Annotations;
using NFluidsynth;
+
+using Robust.Client.Audio.Sources;
using Robust.Client.Graphics;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
@@ -56,6 +58,7 @@ internal sealed class MidiRenderer : IMidiRenderer
private IMidiRenderer? _master;
public MidiRendererState RendererState => _rendererState;
public IBufferedAudioSource Source { get; set; }
+ public IMixableAudioSource MixableSource { get; set; }
IBufferedAudioSource IMidiRenderer.Source => Source;
[ViewVariables]
@@ -263,6 +266,7 @@ internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool m
_midiSawmill = midiSawmill;
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true) ?? DummyBufferedAudioSource.Instance;
+ MixableSource = new MixableAudioSource(Source);
Source.SampleRate = SampleRate;
_settings = settings;
_soundFontLoader = soundFontLoader;
diff --git a/Robust.Client/Audio/Mixers/AudioMixer.cs b/Robust.Client/Audio/Mixers/AudioMixer.cs
new file mode 100644
index 00000000000..f7a9f7f9b7b
--- /dev/null
+++ b/Robust.Client/Audio/Mixers/AudioMixer.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.Prototypes;
+
+namespace Robust.Client.Audio.Mixers;
+
+public sealed class AudioMixer : IAudioMixer
+{
+ public IAudioMixer? Out => _out;
+
+ public float SelfGain
+ {
+ get => _selfGain;
+ set
+ {
+ if (_isDisposed) return;
+ _selfGain = value;
+ _selfGain = Math.Max(_selfGain, 0);
+ RecalculateGain();
+ }
+ }
+ public float Gain { get; private set; }
+
+ public ProtoId? ProtoId { get; }
+ string? IAudioMixer.GainCVar
+ {
+ get => _gainCVar;
+ set => _gainCVar = _isDisposed ? null : value;
+ }
+
+ private readonly AudioMixersManager _manager;
+ private readonly HashSet _subscribers = new();
+ private IAudioMixer? _out;
+ private float _selfGain = 1f;
+ private string? _gainCVar;
+ private bool _isDisposed = false;
+ private bool _isNotifyingSubscribers = false;
+
+ internal AudioMixer(ProtoId? protoId, AudioMixersManager manager)
+ {
+ ProtoId = protoId;
+ _manager = manager;
+ Recalculate();
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+ SetDefaults();
+ SetOut(null);
+ _subscribers.Clear();
+ _manager.SetMixerGainCVar(this, null);
+ _isDisposed = true;
+ }
+
+ public void Subscribe(IAudioMixerSubscriber subscriber)
+ {
+ if (_isDisposed) return;
+ _subscribers.Add(subscriber);
+ }
+
+ public void Unsubscribe(IAudioMixerSubscriber subscriber)
+ {
+ if (_isDisposed) return;
+ _subscribers.Remove(subscriber);
+ }
+
+ public void SetOut(IAudioMixer? outMixer)
+ {
+ if (_out == outMixer || outMixer == this || _isDisposed) return;
+ if (_out is { })
+ {
+ _out.Unsubscribe(this);
+ }
+ _out = outMixer;
+ if (outMixer is { })
+ {
+ outMixer.Subscribe(this);
+ }
+ Recalculate();
+ }
+
+ void IAudioMixerSubscriber.OnMixerGainChanged(float mixerGain)
+ {
+ RecalculateGain();
+ }
+
+ void IAudioMixer.OnGainCVarChanged(float value)
+ {
+ SelfGain = value;
+ }
+
+ private void Recalculate()
+ {
+ RecalculateGain();
+ }
+
+ private void RecalculateGain()
+ {
+ if (_isNotifyingSubscribers)
+ {
+ _manager.Sawmill.Error($"Audio mixer {ToString()} has a circular output.");
+ return;
+ }
+ var gain = (_out?.Gain ?? 1f) * _selfGain;
+ Gain = gain;
+ _isNotifyingSubscribers = true;
+ foreach (var subscriber in _subscribers)
+ {
+ subscriber.OnMixerGainChanged(gain);
+ }
+ _isNotifyingSubscribers = false;
+ }
+
+ private void SetDefaults()
+ {
+ _selfGain = 1f;
+ }
+
+ public override string ToString()
+ {
+ return $"{{ Proto: {ProtoId} GainCVar: {_gainCVar} }}";
+ }
+}
diff --git a/Robust.Client/Audio/Mixers/AudioMixersManager.cs b/Robust.Client/Audio/Mixers/AudioMixersManager.cs
new file mode 100644
index 00000000000..faf2af2b081
--- /dev/null
+++ b/Robust.Client/Audio/Mixers/AudioMixersManager.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.Configuration;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Prototypes;
+
+namespace Robust.Client.Audio.Mixers;
+
+public sealed class AudioMixersManager : IAudioMixersManager
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
+ internal ISawmill Sawmill => _sawmill ??= _logManager.GetSawmill("audiomixers");
+
+ private readonly Dictionary, IAudioMixer> _audioMixers = new();
+ private ISawmill? _sawmill;
+
+ public IAudioMixer CreateMixer()
+ {
+ return CreateMixer(null);
+ }
+
+ public IAudioMixer? GetMixer(ProtoId? mixerProtoIdMaybe)
+ {
+ return GetMixer(mixerProtoIdMaybe, mixerProtoIdMaybe);
+ }
+
+ public void SetMixerGainCVar(IAudioMixer mixer, string? name)
+ {
+ if (mixer.GainCVar is { })
+ {
+ _configurationManager.UnsubValueChanged(mixer.GainCVar, mixer.OnGainCVarChanged);
+ }
+ mixer.GainCVar = name;
+ if (mixer.GainCVar is { })
+ {
+ _configurationManager.OnValueChanged(mixer.GainCVar, mixer.OnGainCVarChanged, true);
+ }
+ }
+
+ private IAudioMixer CreateMixer(ProtoId? mixerProtoIdMaybe)
+ {
+ return new AudioMixer(mixerProtoIdMaybe, this);
+ }
+
+ private IAudioMixer? GetMixer(ProtoId? mixerProtoIdMaybe, ProtoId? originProtoId)
+ {
+ if (mixerProtoIdMaybe is not { } protoId)
+ {
+ return null;
+ }
+ if (_audioMixers.TryGetValue(protoId, out var mixer))
+ {
+ return mixer;
+ }
+ if (!_prototypeManager.TryIndex(protoId, out var proto))
+ {
+ return null;
+ }
+ mixer = CreateMixer(mixerProtoIdMaybe);
+ _audioMixers[protoId] = mixer;
+ if (proto.Out == originProtoId)
+ {
+ Sawmill.Error($"Audio mixer prototype {originProtoId} has a circular output.");
+ }
+ else if (GetMixer(proto.Out, originProtoId) is { } outMixer)
+ {
+ mixer.SetOut(outMixer);
+ }
+ mixer.SelfGain = proto.Gain;
+ SetMixerGainCVar(mixer, proto.GainCVar);
+ return mixer;
+ }
+}
diff --git a/Robust.Client/Audio/Mixers/IAudioMixersManager.cs b/Robust.Client/Audio/Mixers/IAudioMixersManager.cs
new file mode 100644
index 00000000000..721427a03d1
--- /dev/null
+++ b/Robust.Client/Audio/Mixers/IAudioMixersManager.cs
@@ -0,0 +1,14 @@
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.Prototypes;
+
+namespace Robust.Client.Audio.Mixers;
+
+///
+/// Public API to manipulate on raw objects.
+///
+public interface IAudioMixersManager
+{
+ IAudioMixer CreateMixer();
+ IAudioMixer? GetMixer(ProtoId? mixerProtoIdMaybe);
+ void SetMixerGainCVar(IAudioMixer mixer, string? name);
+}
diff --git a/Robust.Client/Audio/Sources/MixableAudioSource.cs b/Robust.Client/Audio/Sources/MixableAudioSource.cs
new file mode 100644
index 00000000000..ce98a20a747
--- /dev/null
+++ b/Robust.Client/Audio/Sources/MixableAudioSource.cs
@@ -0,0 +1,172 @@
+using System.Numerics;
+using Robust.Shared.Audio.Effects;
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.Audio.Sources;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.ViewVariables;
+
+namespace Robust.Client.Audio.Sources;
+
+public sealed class MixableAudioSource : IMixableAudioSource, IAudioMixerSubscriber
+{
+ [ViewVariables(VVAccess.ReadOnly)]
+ private readonly IAudioSource _innerSource;
+ [ViewVariables(VVAccess.ReadOnly)]
+ private IAudioMixer? _mixer;
+ [ViewVariables]
+ private float _selfGain = 1f;
+
+ private bool _isDisposed = false;
+
+ public MixableAudioSource(IAudioSource innerSource)
+ {
+ _innerSource = innerSource;
+ _selfGain = innerSource.Gain;
+ }
+
+ public bool Playing
+ {
+ get => _innerSource.Playing;
+ set => _innerSource.Playing = value;
+ }
+
+ public bool Looping
+ {
+ get => _innerSource.Looping;
+ set => _innerSource.Looping = value;
+ }
+
+ public bool Global
+ {
+ get => _innerSource.Global;
+ set => _innerSource.Global = value;
+ }
+
+ public Vector2 Position
+ {
+ get => _innerSource.Position;
+ set => _innerSource.Position = value;
+ }
+
+ public float Pitch
+ {
+ get => _innerSource.Pitch;
+ set => _innerSource.Pitch = value;
+ }
+
+ public float Volume
+ {
+ get
+ {
+ var gain = Gain;
+ var volume = SharedAudioSystem.GainToVolume(gain);
+ return volume;
+ }
+ set => Gain = SharedAudioSystem.VolumeToGain(value);
+ }
+
+ public float Gain
+ {
+ get => _selfGain;
+ set
+ {
+ _selfGain = value;
+ RecalculateGain();
+ }
+ }
+
+ public float MaxDistance
+ {
+ get => _innerSource.MaxDistance;
+ set => _innerSource.MaxDistance = value;
+ }
+
+ public float RolloffFactor
+ {
+ get => _innerSource.RolloffFactor;
+ set => _innerSource.RolloffFactor = value;
+ }
+
+ public float ReferenceDistance
+ {
+ get => _innerSource.ReferenceDistance;
+ set => _innerSource.ReferenceDistance = value;
+ }
+
+ public float Occlusion
+ {
+ get => _innerSource.Occlusion;
+ set => _innerSource.Occlusion = value;
+ }
+
+ public float PlaybackPosition
+ {
+ get => _innerSource.PlaybackPosition;
+ set => _innerSource.PlaybackPosition = value;
+ }
+
+ public Vector2 Velocity
+ {
+ get => _innerSource.Velocity;
+ set => _innerSource.Velocity = value;
+ }
+
+ public void Pause()
+ {
+ _innerSource.Pause();
+ }
+
+ public void StartPlaying()
+ {
+ _innerSource.StartPlaying();
+ }
+
+ public void StopPlaying()
+ {
+ _innerSource.StopPlaying();
+ }
+
+ public void Restart()
+ {
+ _innerSource.Restart();
+ }
+
+ public void Dispose()
+ {
+ _isDisposed = true;
+ _mixer?.Unsubscribe(this);
+ _innerSource.Dispose();
+ }
+
+ public void SetAuxiliary(IAuxiliaryAudio? audio)
+ {
+ _innerSource.SetAuxiliary(audio);
+ }
+
+ public void SetMixer(IAudioMixer? mixer)
+ {
+ if (_mixer == mixer || _isDisposed)
+ {
+ return;
+ }
+ _mixer?.Unsubscribe(this);
+ _mixer = mixer;
+ _mixer?.Subscribe(this);
+ Recalculate();
+ }
+
+ public void OnMixerGainChanged(float mixerGain)
+ {
+ RecalculateGain();
+ }
+
+ private void Recalculate()
+ {
+ RecalculateGain();
+ }
+
+ private void RecalculateGain()
+ {
+ _innerSource.Gain = _selfGain * (_mixer?.Gain ?? 1f);
+ }
+}
diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs
index e197f433fb7..2526be40b21 100644
--- a/Robust.Client/ClientIoC.cs
+++ b/Robust.Client/ClientIoC.cs
@@ -1,6 +1,7 @@
using System;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
+using Robust.Client.Audio.Mixers;
using Robust.Client.Configuration;
using Robust.Client.Console;
using Robust.Client.Debugging;
@@ -102,6 +103,7 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle
deps.Register();
deps.Register();
deps.Register();
+ deps.Register();
switch (mode)
{
diff --git a/Robust.Server/Audio/AudioSystem.Mixers.cs b/Robust.Server/Audio/AudioSystem.Mixers.cs
new file mode 100644
index 00000000000..6c464c00116
--- /dev/null
+++ b/Robust.Server/Audio/AudioSystem.Mixers.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.GameObjects;
+
+namespace Robust.Server.Audio;
+
+public partial class AudioSystem
+{
+ public override Entity CreateMixerEntity(IAudioMixer mixer)
+ {
+ var mixerEntity = base.CreateMixerEntity(mixer);
+ _pvs.AddGlobalOverride(mixerEntity);
+ return mixerEntity;
+ }
+
+ public override Entity CreateMixer(Entity? outMixer)
+ {
+ var mixerEntity = base.CreateMixer(outMixer);
+ _pvs.AddGlobalOverride(mixerEntity);
+ return mixerEntity;
+ }
+}
diff --git a/Robust.Server/Audio/AudioSystem.cs b/Robust.Server/Audio/AudioSystem.cs
index 981c01e770e..1c955755cd6 100644
--- a/Robust.Server/Audio/AudioSystem.cs
+++ b/Robust.Server/Audio/AudioSystem.cs
@@ -39,7 +39,7 @@ public override void Shutdown()
private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args)
{
- component.Source = new DummyAudioSource();
+ component.Source = new DummyMixableAudioSource();
}
public override void SetGridAudio(Entity? entity)
diff --git a/Robust.Shared/Audio/AudioParams.cs b/Robust.Shared/Audio/AudioParams.cs
index fa7557dece7..432271d66fb 100644
--- a/Robust.Shared/Audio/AudioParams.cs
+++ b/Robust.Shared/Audio/AudioParams.cs
@@ -1,10 +1,12 @@
-using Robust.Shared.Serialization;
+using Robust.Shared.Serialization;
using System;
using System.Diagnostics.Contracts;
+using Robust.Shared.Audio.Mixers;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
namespace Robust.Shared.Audio
{
@@ -78,6 +80,12 @@ public float Pitch
[DataField]
public float? Variation { get; set; } = null;
+ ///
+ /// Output mixer to use, if any.
+ ///
+ [DataField]
+ public ProtoId? MixerProto { get; set; }
+
// For the max distance value: it's 2000 in Godot, but I assume that's PIXELS due to the 2D positioning,
// so that's divided by 32 (EyeManager.PIXELSPERMETER).
///
@@ -214,5 +222,16 @@ public readonly AudioParams WithPlayOffset(float offset)
me.PlayOffsetSeconds = offset;
return me;
}
+
+ ///
+ /// Returns a copy of this instance with a mixer set, for easy chaining.
+ ///
+ [Pure]
+ public readonly AudioParams WithMixer(ProtoId? mixerProto)
+ {
+ var me = this;
+ me.MixerProto = mixerProto;
+ return me;
+ }
}
}
diff --git a/Robust.Shared/Audio/Components/AudioComponent.cs b/Robust.Shared/Audio/Components/AudioComponent.cs
index 374075c84fe..ee2c87cc0ad 100644
--- a/Robust.Shared/Audio/Components/AudioComponent.cs
+++ b/Robust.Shared/Audio/Components/AudioComponent.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Audio.Effects;
+using Robust.Shared.Audio.Mixers;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameObjects;
@@ -17,7 +18,7 @@ namespace Robust.Shared.Audio.Components;
/// Stores the audio data for an audio entity.
///
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedAudioSystem))]
-public sealed partial class AudioComponent : Component, IAudioSource
+public sealed partial class AudioComponent : Component, IMixableAudioSource
{
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField, DataField, Access(Other = AccessPermissions.ReadWriteExecute)]
public AudioFlags Flags = AudioFlags.None;
@@ -70,7 +71,7 @@ public sealed partial class AudioComponent : Component, IAudioSource
/// Audio source that interacts with OpenAL.
///
[ViewVariables(VVAccess.ReadOnly)]
- internal IAudioSource Source = new DummyAudioSource();
+ internal IMixableAudioSource Source = DummyMixableAudioSource.Instance;
///
/// Auxiliary entity to pass audio to.
@@ -78,6 +79,12 @@ public sealed partial class AudioComponent : Component, IAudioSource
[DataField, AutoNetworkedField]
public EntityUid? Auxiliary;
+ ///
+ /// Audio mixer entity to pass audio to.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityUid? Mixer;
+
/*
* 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.
@@ -247,6 +254,11 @@ void IAudioSource.SetAuxiliary(IAuxiliaryAudio? audio)
Source.SetAuxiliary(audio);
}
+ void IMixableAudioSource.SetMixer(IAudioMixer? mixer)
+ {
+ Source.SetMixer(mixer);
+ }
+
#endregion
public void Dispose()
diff --git a/Robust.Shared/Audio/Components/AudioMixerComponent.cs b/Robust.Shared/Audio/Components/AudioMixerComponent.cs
new file mode 100644
index 00000000000..26fac0587ef
--- /dev/null
+++ b/Robust.Shared/Audio/Components/AudioMixerComponent.cs
@@ -0,0 +1,81 @@
+using System;
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Robust.Shared.Audio.Components;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedAudioSystem))]
+public sealed partial class AudioMixerComponent : Component, IAudioMixer
+{
+ public IAudioMixer? Out => Mixer.Out;
+
+ public EntityUid? OutEntity { get; set; }
+
+ public ProtoId? ProtoId { get; set; }
+
+ public float SelfGain
+ {
+ get => Mixer.SelfGain;
+ set => Mixer.SelfGain = value;
+ }
+ public float Gain => Mixer.Gain;
+
+ ///
+ /// Set if you want to control gain of the mixer from the server side.
+ ///
+ [Access(Other = AccessPermissions.ReadWrite)]
+ public bool IsGainSynced { get; set; } = false;
+
+ public string? GainCVar
+ {
+ get => Mixer.GainCVar;
+ set => Mixer.GainCVar = value;
+ }
+
+ [ViewVariables]
+ internal IAudioMixer Mixer = new DummyAudioMixer();
+
+ internal bool IsInitiallySynced { get; set; } = false;
+
+ public void Dispose() { }
+
+ public void Subscribe(IAudioMixerSubscriber subscriber)
+ {
+ Mixer.Subscribe(subscriber);
+ }
+
+ public void Unsubscribe(IAudioMixerSubscriber subscriber)
+ {
+ Mixer.Unsubscribe(subscriber);
+ }
+
+ public void SetOut(IAudioMixer? outMixer)
+ {
+ Mixer.SetOut(outMixer);
+ }
+
+ public void OnMixerGainChanged(float mixerGain)
+ {
+ Mixer.OnMixerGainChanged(mixerGain);
+ }
+
+ void IAudioMixer.OnGainCVarChanged(float value)
+ {
+ Mixer.OnGainCVarChanged(value);
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class AudioMixerComponentState : IComponentState
+{
+ public NetEntity? OutEntity;
+ public ProtoId? ProtoId;
+ public float SelfGain;
+ public bool IsGainSynced;
+ public string? GainCVar;
+}
diff --git a/Robust.Shared/Audio/Mixers/AudioMixerPrototype.cs b/Robust.Shared/Audio/Mixers/AudioMixerPrototype.cs
new file mode 100644
index 00000000000..46225500221
--- /dev/null
+++ b/Robust.Shared/Audio/Mixers/AudioMixerPrototype.cs
@@ -0,0 +1,43 @@
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Robust.Shared.Audio.Mixers;
+
+///
+/// Preset for creating s from.
+///
+[Prototype("audioMixer")]
+public sealed class AudioMixerPrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// Name of the CVar bound to this mixer to store gain value.
+ ///
+ [DataField]
+ public string? GainCVar;
+
+ ///
+ /// Mixer to pass signal to.
+ ///
+ [DataField]
+ public ProtoId? Out;
+
+ ///
+ /// Default volume of the mixer, if no is specified.
+ ///
+ [DataField]
+ public float Volume
+ {
+ get => SharedAudioSystem.GainToVolume(Gain);
+ set => Gain = SharedAudioSystem.VolumeToGain(value);
+ }
+
+ ///
+ /// Default gain of the mixer, if no is specified.
+ ///
+ [DataField]
+ public float Gain = 1f;
+}
diff --git a/Robust.Shared/Audio/Mixers/DummyAudioMixer.cs b/Robust.Shared/Audio/Mixers/DummyAudioMixer.cs
new file mode 100644
index 00000000000..115cff23806
--- /dev/null
+++ b/Robust.Shared/Audio/Mixers/DummyAudioMixer.cs
@@ -0,0 +1,36 @@
+using Robust.Shared.Prototypes;
+
+namespace Robust.Shared.Audio.Mixers;
+
+internal sealed class DummyAudioMixer : IAudioMixer
+{
+ public float SelfGain { get; set; } = 1f;
+ public float Gain => SelfGain;
+ public IAudioMixer? Out { get; }
+ public ProtoId? ProtoId { get; }
+ string? IAudioMixer.GainCVar { get; set; }
+
+ public void Subscribe(IAudioMixerSubscriber subscriber)
+ {
+ }
+
+ public void Unsubscribe(IAudioMixerSubscriber subscriber)
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public void SetOut(IAudioMixer? outMixer)
+ {
+ }
+
+ public void OnMixerGainChanged(float mixerGain)
+ {
+ }
+
+ void IAudioMixer.OnGainCVarChanged(float value)
+ {
+ }
+}
diff --git a/Robust.Shared/Audio/Mixers/IAudioMixer.cs b/Robust.Shared/Audio/Mixers/IAudioMixer.cs
new file mode 100644
index 00000000000..187cc33670f
--- /dev/null
+++ b/Robust.Shared/Audio/Mixers/IAudioMixer.cs
@@ -0,0 +1,50 @@
+using System;
+
+using Robust.Shared.Prototypes;
+
+namespace Robust.Shared.Audio.Mixers;
+
+///
+/// Controls the parameters of the audio sources to which this mixer is assigned.
+/// Mixers can also output signal to other mixers, creating a hierarchy.
+///
+public interface IAudioMixer : IAudioMixerSubscriber, IDisposable
+{
+ ///
+ /// Mixer to pass signal to.
+ ///
+ IAudioMixer? Out { get; }
+ ///
+ /// Audio mixer prototype id that is associated with this mixer.
+ ///
+ ProtoId? ProtoId { get; }
+ ///
+ /// Gain assigned to this mixer before passing to any output mixers.
+ ///
+ float SelfGain { get; set; }
+ ///
+ /// Gain of this mixer after passing through all output chain.
+ ///
+ float Gain { get; }
+ ///
+ /// Name of the CVar bound to this mixer to store gain value.
+ ///
+ internal string? GainCVar { get; set; }
+
+ ///
+ /// Subscribes to this mixer instance.
+ ///
+ void Subscribe(IAudioMixerSubscriber subscriber);
+ ///
+ /// Unsubscribes from this mixer instance.
+ ///
+ void Unsubscribe(IAudioMixerSubscriber subscriber);
+ ///
+ /// Set specified mixer as an output for this mixer, pass to set as root mixer.
+ ///
+ void SetOut(IAudioMixer? outMixer);
+ ///
+ /// Called when the value of the gain CVar is changed.
+ ///
+ internal void OnGainCVarChanged(float value);
+}
diff --git a/Robust.Shared/Audio/Mixers/IAudioMixerSubscriber.cs b/Robust.Shared/Audio/Mixers/IAudioMixerSubscriber.cs
new file mode 100644
index 00000000000..adcd5eeb16f
--- /dev/null
+++ b/Robust.Shared/Audio/Mixers/IAudioMixerSubscriber.cs
@@ -0,0 +1,12 @@
+namespace Robust.Shared.Audio.Mixers;
+
+///
+/// Implement this to be able to subscribe to .
+///
+public interface IAudioMixerSubscriber
+{
+ ///
+ /// This is called from subscribed mixer when its gain is changed.
+ ///
+ void OnMixerGainChanged(float mixerGain);
+}
diff --git a/Robust.Shared/Audio/Sources/DummyMixableAudioSource.cs b/Robust.Shared/Audio/Sources/DummyMixableAudioSource.cs
new file mode 100644
index 00000000000..27d4c6312c2
--- /dev/null
+++ b/Robust.Shared/Audio/Sources/DummyMixableAudioSource.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.Audio.Mixers;
+
+namespace Robust.Shared.Audio.Sources;
+
+internal sealed class DummyMixableAudioSource : DummyAudioSource, IMixableAudioSource
+{
+ public static new DummyMixableAudioSource Instance { get; } = new();
+
+ public void SetMixer(IAudioMixer? mixer)
+ {
+ }
+}
diff --git a/Robust.Shared/Audio/Sources/IMixableAudioSource.cs b/Robust.Shared/Audio/Sources/IMixableAudioSource.cs
new file mode 100644
index 00000000000..8f5bae83312
--- /dev/null
+++ b/Robust.Shared/Audio/Sources/IMixableAudioSource.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.Audio.Mixers;
+
+namespace Robust.Shared.Audio.Sources;
+
+///
+/// with support for .
+///
+public interface IMixableAudioSource : IAudioSource
+{
+ void SetMixer(IAudioMixer? mixer);
+}
diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.Mixers.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.Mixers.cs
new file mode 100644
index 00000000000..e05387eafcd
--- /dev/null
+++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.Mixers.cs
@@ -0,0 +1,234 @@
+using System;
+using System.Collections.Generic;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Audio.Mixers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Robust.Shared.Audio.Systems;
+
+public abstract partial class SharedAudioSystem
+{
+ ///
+ /// Mixer prototype id that will be used on audio sources without specified mixer.
+ ///
+ public ProtoId? DefaultMixer { get; set; }
+
+ private readonly Dictionary, Entity> _audioMixers = new();
+ private bool _isMixersStarted;
+
+ protected virtual void InitializeMixers()
+ {
+ SubscribeLocalEvent(OnMixerShutdown);
+ SubscribeLocalEvent(OnGetState);
+ SubscribeLocalEvent(OnHandleState);
+ EntityManager.AfterEntityFlush += LoadPrototypedMixers;
+ }
+
+ protected virtual void UpdateMixers()
+ {
+ // Ahhhh, I find this the best way to know when game is ready to spawn entities on startup.
+ if (!_isMixersStarted)
+ {
+ StartMixers();
+ }
+ }
+
+ private void StartMixers()
+ {
+ _isMixersStarted = true;
+ LoadPrototypedMixers();
+ }
+
+ ///
+ /// Creates audio mixer entity wrapper from raw .
+ ///
+ public virtual Entity CreateMixerEntity(IAudioMixer mixer)
+ {
+ var ent = Spawn(null, MapCoordinates.Nullspace);
+ var comp = AddComp(ent);
+ comp.Mixer = mixer;
+ return (ent, comp);
+ }
+
+ ///
+ /// Creates audio mixer.
+ ///
+ /// Mixer to set as out for created mixer.
+ public virtual Entity CreateMixer(Entity? outMixer)
+ {
+ var ent = Spawn(null, MapCoordinates.Nullspace);
+ var comp = AddComp(ent);
+ SetMixerOut((ent, comp), outMixer);
+ return (ent, comp);
+ }
+
+ ///
+ /// Assigns audio mixer to specified audio source.
+ ///
+ public void SetMixer(Entity audio, Entity? mixerOrNone)
+ {
+ if (mixerOrNone is { } mixer)
+ {
+ SetMixer(audio, mixer);
+ }
+ else
+ {
+ ClearMixer(audio);
+ }
+ }
+
+ ///
+ /// Assigns audio mixer to specified audio source.
+ ///
+ public virtual void SetMixer(Entity audio, Entity mixer)
+ {
+ audio.Comp.Mixer = mixer;
+ audio.Comp.Params.MixerProto = mixer.Comp.ProtoId;
+ audio.Comp.Source.SetMixer(mixer.Comp.Mixer);
+ Dirty(audio);
+ }
+
+ ///
+ /// Clears audio mixer from specified audio source.
+ ///
+ public virtual void ClearMixer(Entity audio)
+ {
+ audio.Comp.Mixer = null;
+ audio.Comp.Params.MixerProto = null;
+ audio.Comp.Source.SetMixer(null);
+ Dirty(audio);
+ }
+
+ ///
+ /// Returns audio mixer associated with provided mixer prototype id.
+ ///
+ public Entity? GetMixer(ProtoId? mixerProtoId)
+ {
+ return mixerProtoId.HasValue ? GetMixer(mixerProtoId.Value) : null;
+ }
+
+ ///
+ /// Returns audio mixer associated with provided mixer prototype id.
+ ///
+ public virtual Entity? GetMixer(ProtoId mixerProtoId)
+ {
+ return _audioMixers.TryGetValue(mixerProtoId, out var mixer) ? mixer : null;
+ }
+
+ ///
+ /// Set gain value to the audio mixer.
+ ///
+ public virtual void SetMixerGain(Entity mixer, float gain)
+ {
+ gain = Math.Max(gain, 0);
+ mixer.Comp.SelfGain = gain;
+ if (mixer.Comp.IsGainSynced)
+ Dirty(mixer);
+ }
+
+ ///
+ /// Assigns CVar to store mixer gain value. Pass to clear.
+ ///
+ public virtual void SetMixerGainCVar(Entity mixer, string? name)
+ {
+ mixer.Comp.GainCVar = name;
+ Dirty(mixer);
+ }
+
+ ///
+ /// Set specified mixer as an output for this mixer, pass to set as root mixer.
+ ///
+ public virtual void SetMixerOut(Entity mixer, Entity? outMixerOrNone)
+ {
+ if (outMixerOrNone is { } outMixer && !TerminatingOrDeleted(outMixer))
+ {
+ mixer.Comp.OutEntity = outMixer;
+ mixer.Comp.Mixer.SetOut(outMixer.Comp.Mixer);
+ }
+ else
+ {
+ mixer.Comp.OutEntity = null;
+ mixer.Comp.Mixer.SetOut(null);
+ }
+ Dirty(mixer);
+ }
+
+ protected virtual Entity SpawnMixerForPrototype(ProtoId mixerProtoId)
+ {
+ var mixer = CreateMixer(null);
+ mixer.Comp.ProtoId = mixerProtoId;
+ return mixer;
+ }
+
+ protected void ApplyAudioParamsMixer(Entity audio, AudioParams audioParams)
+ {
+ SetMixer(audio, GetMixer(audioParams.MixerProto));
+ }
+
+ protected virtual void OnMixerShutdown(Entity mixer, ref ComponentShutdown args)
+ {
+ // It is too hard to store all the subscribers to unsubscribe here, so we do this
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var audio))
+ {
+ if (audio.Mixer != mixer.Owner)
+ continue;
+ audio.Mixer = null;
+ audio.Params.MixerProto = null;
+ }
+ }
+
+ private void OnGetState(Entity mixer, ref ComponentGetState args)
+ {
+ args.State = new AudioMixerComponentState
+ {
+ OutEntity = GetNetEntity(mixer.Comp.OutEntity),
+ ProtoId = mixer.Comp.ProtoId,
+ IsGainSynced = mixer.Comp.IsGainSynced,
+ SelfGain = mixer.Comp.SelfGain,
+ GainCVar = mixer.Comp.GainCVar,
+ };
+ }
+
+ protected virtual void OnHandleState(Entity mixer, ref ComponentHandleState args)
+ {
+ if (args.Current is not AudioMixerComponentState state)
+ return;
+
+ mixer.Comp.OutEntity = EnsureEntity(state.OutEntity, mixer);
+ mixer.Comp.ProtoId = state.ProtoId;
+ mixer.Comp.IsGainSynced = state.IsGainSynced;
+ if (mixer.Comp.IsGainSynced || !mixer.Comp.IsInitiallySynced)
+ mixer.Comp.SelfGain = state.SelfGain;
+ mixer.Comp.GainCVar = state.GainCVar;
+
+ mixer.Comp.IsInitiallySynced = true;
+ }
+
+ private void LoadPrototypedMixers()
+ {
+ if (EntityManager.ShuttingDown)
+ {
+ return;
+ }
+ // Initialization
+ foreach (var proto in ProtoMan.EnumeratePrototypes())
+ {
+ var mixer = SpawnMixerForPrototype(proto.ID);
+ _audioMixers[proto.ID] = mixer;
+ SetMixerGain(mixer, proto.Gain);
+ SetMixerGainCVar(mixer, proto.GainCVar);
+ }
+ // Out setup
+ foreach (var proto in ProtoMan.EnumeratePrototypes())
+ {
+ if (proto.Out is { } outId)
+ {
+ SetMixerOut(_audioMixers[proto.ID], _audioMixers.TryGetValue(outId, out var mixer) ? mixer : null);
+ }
+ }
+ }
+}
diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
index 4d79ff0e3d0..664dec8cfc9 100644
--- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
+++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
@@ -53,12 +53,19 @@ public override void Initialize()
{
base.Initialize();
InitializeEffect();
+ InitializeMixers();
ZOffset = CfgManager.GetCVar(CVars.AudioZOffset);
Subs.CVar(CfgManager, CVars.AudioZOffset, SetZOffset);
SubscribeLocalEvent(OnAudioGetStateAttempt);
SubscribeLocalEvent(OnAudioUnpaused);
}
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ UpdateMixers();
+ }
+
///
/// Sets the playback position of audio to the specified spot.
///
@@ -301,6 +308,7 @@ protected Entity SetupAudio(string? fileName, AudioParams? audio
DebugTools.Assert(!string.IsNullOrEmpty(fileName) || length is not null);
MetadataSys.SetEntityName(uid, $"Audio ({fileName})", raiseEvents: false);
audioParams ??= AudioParams.Default;
+ audioParams = audioParams.Value.WithMixer(audioParams.Value.MixerProto ?? DefaultMixer);
var comp = AddComp(uid);
comp.FileName = fileName ?? string.Empty;
comp.Params = audioParams.Value;
@@ -320,6 +328,8 @@ protected Entity SetupAudio(string? fileName, AudioParams? audio
comp.Params.Pitch *= (float) RandMan.NextGaussian(1, comp.Params.Variation.Value);
}
+ ApplyAudioParamsMixer((uid, comp), audioParams.Value);
+
if (initialize)
{
EntityManager.InitializeAndStartEntity(uid);