Add ambient music (#16829)

This commit is contained in:
metalgearsloth
2023-05-29 10:44:11 +10:00
committed by GitHub
parent f35fcff23f
commit 0c83642c5a
84 changed files with 1252 additions and 338 deletions

View File

@@ -11,6 +11,7 @@ using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
using Robust.Client.GameObjects;
namespace Content.Client.Audio
{
@@ -106,7 +107,19 @@ namespace Content.Client.Audio
_playingCount.Remove(sound.Sound);
}
private void SetAmbienceVolume(float value) => _ambienceVolume = value;
private void SetAmbienceVolume(float value)
{
_ambienceVolume = value;
foreach (var (comp, values) in _playingSounds)
{
if (values.Stream == null)
continue;
var stream = (AudioSystem.PlayingStream) values.Stream;
stream.Volume = _params.Volume + comp.Volume + _ambienceVolume;
}
}
private void SetCooldown(float value) => _cooldown = value;
private void SetAmbientCount(int value) => _maxAmbientCount = value;
private void SetAmbientRange(float value) => _maxAmbientRange = value;

View File

@@ -1,21 +1,12 @@
using System.Threading;
using Content.Client.Gameplay;
using Content.Client.GameTicking.Managers;
using Content.Client.Lobby;
using Content.Shared.CCVar;
using JetBrains.Annotations;
using Robust.Client;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Audio;
@@ -26,57 +17,18 @@ public sealed class BackgroundAudioSystem : EntitySystem
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly ClientGameTicker _gameTicker = default!;
[Dependency] private readonly IPlayerManager _playMan = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly AudioParams _ambientParams = new(-10f, 1, "Master", 0, 0, 0, true, 0f);
private readonly AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, 0, true, 0f);
private IPlayingAudioStream? _ambientStream;
private IPlayingAudioStream? _lobbyStream;
/// <summary>
/// What is currently playing.
/// </summary>
private SoundCollectionPrototype? _playingCollection;
/// <summary>
/// What the ambience has been set to.
/// </summary>
private SoundCollectionPrototype? _currentCollection;
private CancellationTokenSource _timerCancelTokenSource = new();
private SoundCollectionPrototype _spaceAmbience = default!;
private SoundCollectionPrototype _stationAmbience = default!;
public override void Initialize()
{
base.Initialize();
_stationAmbience = _prototypeManager.Index<SoundCollectionPrototype>("StationAmbienceBase");
_spaceAmbience = _prototypeManager.Index<SoundCollectionPrototype>("SpaceAmbienceBase");
_currentCollection = _stationAmbience;
// TODO: Ideally audio loading streamed better / we have more robust audio but this is quite annoying
var cache = IoCManager.Resolve<IResourceCache>();
foreach (var audio in _spaceAmbience.PickFiles)
{
cache.GetResource<AudioResource>(audio.ToString());
}
_configManager.OnValueChanged(CCVars.AmbienceVolume, AmbienceCVarChanged);
_configManager.OnValueChanged(CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged);
_configManager.OnValueChanged(CCVars.LobbyMusicVolume, LobbyMusicVolumeCVarChanged);
_configManager.OnValueChanged(CCVars.StationAmbienceEnabled, StationAmbienceCVarChanged);
_configManager.OnValueChanged(CCVars.SpaceAmbienceEnabled, SpaceAmbienceCVarChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<EntParentChangedMessage>(EntParentChanged);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
_stateManager.OnStateChanged += StateManagerOnStateChanged;
@@ -85,28 +37,12 @@ public sealed class BackgroundAudioSystem : EntitySystem
_gameTicker.LobbyStatusUpdated += LobbySongReceived;
}
private void OnPlayerAttached(PlayerAttachedEvent ev)
{
if (!TryComp<TransformComponent>(ev.Entity, out var xform))
return;
CheckAmbience(xform);
}
private void OnPlayerDetached(PlayerDetachedEvent ev)
{
EndAmbience();
}
public override void Shutdown()
{
base.Shutdown();
_configManager.UnsubValueChanged(CCVars.AmbienceVolume, AmbienceCVarChanged);
_configManager.UnsubValueChanged(CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged);
_configManager.UnsubValueChanged(CCVars.LobbyMusicVolume, LobbyMusicVolumeCVarChanged);
_configManager.UnsubValueChanged(CCVars.StationAmbienceEnabled, StationAmbienceCVarChanged);
_configManager.UnsubValueChanged(CCVars.SpaceAmbienceEnabled, SpaceAmbienceCVarChanged);
_stateManager.OnStateChanged -= StateManagerOnStateChanged;
@@ -114,61 +50,17 @@ public sealed class BackgroundAudioSystem : EntitySystem
_gameTicker.LobbyStatusUpdated -= LobbySongReceived;
EndAmbience();
EndLobbyMusic();
}
private void CheckAmbience(TransformComponent xform)
{
if (xform.GridUid != null)
ChangeAmbience(_stationAmbience);
else
ChangeAmbience(_spaceAmbience);
}
private void EntParentChanged(ref EntParentChangedMessage message)
{
if (_playMan.LocalPlayer is null
|| _playMan.LocalPlayer.ControlledEntity != message.Entity
|| !(_timing.IsFirstTimePredicted || _timing.ApplyingState))
return;
// Check if we traversed to grid.
CheckAmbience(message.Transform);
}
private void ChangeAmbience(SoundCollectionPrototype newAmbience)
{
if (_currentCollection == newAmbience)
return;
_timerCancelTokenSource.Cancel();
_currentCollection = newAmbience;
_timerCancelTokenSource = new CancellationTokenSource();
Timer.Spawn(1500, () =>
{
// If we traverse a few times then don't interrupt an existing song.
// If we are not in gameplay, don't call StartAmbience because of player movement
if (_playingCollection == _currentCollection || _stateManager.CurrentState is not GameplayState)
return;
StartAmbience();
}, _timerCancelTokenSource.Token);
}
private void StateManagerOnStateChanged(StateChangedEventArgs args)
{
switch (args.NewState)
{
case LobbyState:
EndAmbience();
StartLobbyMusic();
break;
case GameplayState:
EndLobbyMusic();
StartAmbience();
break;
default:
EndAmbience();
EndLobbyMusic();
break;
}
@@ -176,80 +68,9 @@ public sealed class BackgroundAudioSystem : EntitySystem
private void OnLeave(object? sender, PlayerEventArgs args)
{
EndAmbience();
EndLobbyMusic();
}
private void AmbienceCVarChanged(float volume)
{
if (_stateManager.CurrentState is GameplayState)
{
StartAmbience();
}
else
{
EndAmbience();
}
}
private void StartAmbience()
{
EndAmbience();
if (_currentCollection == null || !CanPlayCollection(_currentCollection))
return;
_playingCollection = _currentCollection;
var file = _robustRandom.Pick(_currentCollection.PickFiles).ToString();
_ambientStream = _audio.PlayGlobal(file, Filter.Local(), false,
_ambientParams.WithVolume(_ambientParams.Volume + _configManager.GetCVar(CCVars.AmbienceVolume)));
}
private void EndAmbience()
{
_playingCollection = null;
_ambientStream?.Stop();
_ambientStream = null;
}
private bool CanPlayCollection(SoundCollectionPrototype collection)
{
if (collection.ID == _spaceAmbience.ID)
return _configManager.GetCVar(CCVars.SpaceAmbienceEnabled);
if (collection.ID == _stationAmbience.ID)
return _configManager.GetCVar(CCVars.StationAmbienceEnabled);
return true;
}
private void StationAmbienceCVarChanged(bool enabled)
{
if (_currentCollection == null)
return;
if (enabled && _stateManager.CurrentState is GameplayState && _currentCollection.ID == _stationAmbience.ID)
{
StartAmbience();
}
else if (_currentCollection.ID == _stationAmbience.ID)
{
EndAmbience();
}
}
private void SpaceAmbienceCVarChanged(bool enabled)
{
if (_currentCollection == null)
return;
if (enabled && _stateManager.CurrentState is GameplayState && _currentCollection.ID == _spaceAmbience.ID)
{
StartAmbience();
}
else if (_currentCollection.ID == _spaceAmbience.ID)
{
EndAmbience();
}
}
private void LobbyMusicVolumeCVarChanged(float volume)
{
if (_stateManager.CurrentState is LobbyState)

View File

@@ -0,0 +1,262 @@
using System.Linq;
using Content.Client.Gameplay;
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Random;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Audio;
public sealed partial class ContentAudioSystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly RulesSystem _rules = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
private readonly TimeSpan _minAmbienceTime = TimeSpan.FromSeconds(30);
private readonly TimeSpan _maxAmbienceTime = TimeSpan.FromSeconds(60);
private const float AmbientMusicFadeTime = 10f;
private static float _volumeSlider;
// Don't need to worry about this being serializable or pauseable as it doesn't affect the sim.
private TimeSpan _nextAudio;
private AudioSystem.PlayingStream? _ambientMusicStream;
private AmbientMusicPrototype? _musicProto;
/// <summary>
/// If we find a better ambient music proto can we interrupt this one.
/// </summary>
private bool _interruptable;
/// <summary>
/// Track what ambient sounds we've played. This is so they all get played an even
/// number of times.
/// When we get to the end of the list we'll re-shuffle
/// </summary>
private readonly Dictionary<string, List<ResPath>> _ambientSounds = new();
private ISawmill _sawmill = default!;
private void InitializeAmbientMusic()
{
// TODO: Shitty preload
foreach (var audio in _proto.Index<SoundCollectionPrototype>("AmbienceSpace").PickFiles)
{
_resource.GetResource<AudioResource>(audio.ToString());
}
_configManager.OnValueChanged(CCVars.AmbientMusicVolume, AmbienceCVarChanged, true);
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("audio.ambience");
// Reset audio
_nextAudio = TimeSpan.MaxValue;
SetupAmbientSounds();
_proto.PrototypesReloaded += OnProtoReload;
_state.OnStateChanged += OnStateChange;
// On round end summary OR lobby cut audio.
SubscribeNetworkEvent<RoundEndMessageEvent>(OnRoundEndMessage);
}
private void AmbienceCVarChanged(float obj)
{
_volumeSlider = obj;
if (_ambientMusicStream != null && _musicProto != null)
{
_ambientMusicStream.Volume = _musicProto.Sound.Params.Volume + _volumeSlider;
}
}
private void ShutdownAmbientMusic()
{
_configManager.UnsubValueChanged(CCVars.AmbientMusicVolume, AmbienceCVarChanged);
_proto.PrototypesReloaded -= OnProtoReload;
_state.OnStateChanged -= OnStateChange;
_ambientMusicStream?.Stop();
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
if (!obj.ByType.ContainsKey(typeof(AmbientMusicPrototype)) &&
!obj.ByType.ContainsKey(typeof(RulesPrototype)))
{
return;
}
_ambientSounds.Clear();
SetupAmbientSounds();
}
private void OnStateChange(StateChangedEventArgs obj)
{
if (obj.NewState is not GameplayState)
return;
// If they go to game then reset the ambience timer.
_nextAudio = _timing.CurTime + _random.Next(_minAmbienceTime, _maxAmbienceTime);
}
private void SetupAmbientSounds()
{
foreach (var ambience in _proto.EnumeratePrototypes<AmbientMusicPrototype>())
{
var tracks = _ambientSounds.GetOrNew(ambience.ID);
RefreshTracks(ambience.Sound, tracks, null);
_random.Shuffle(tracks);
}
}
private void OnRoundEndMessage(RoundEndMessageEvent ev)
{
// If scoreboard shows then just stop the music
_ambientMusicStream?.Stop();
_ambientMusicStream = null;
_nextAudio = TimeSpan.FromMinutes(3);
}
private void RefreshTracks(SoundSpecifier sound, List<ResPath> tracks, ResPath? lastPlayed)
{
DebugTools.Assert(tracks.Count == 0);
switch (sound)
{
case SoundCollectionSpecifier collection:
if (collection.Collection == null)
break;
var slothCud = _proto.Index<SoundCollectionPrototype>(collection.Collection);
tracks.AddRange(slothCud.PickFiles);
break;
case SoundPathSpecifier path:
tracks.Add(path.Path);
break;
}
// Just so the same track doesn't play twice
if (tracks.Count > 1 && tracks[^1] == lastPlayed)
{
(tracks[0], tracks[^1]) = (tracks[^1], tracks[0]);
}
}
private void UpdateAmbientMusic()
{
// Update still runs in lobby so just ignore it.
if (_state.CurrentState is not GameplayState)
{
FadeOut(_ambientMusicStream);
_ambientMusicStream = null;
_musicProto = null;
return;
}
var isDone = _ambientMusicStream?.Done;
if (_interruptable)
{
var player = _player.LocalPlayer?.ControlledEntity;
if (player == null || _musicProto == null || !_rules.IsTrue(player.Value, _proto.Index<RulesPrototype>(_musicProto.Rules)))
{
FadeOut(_ambientMusicStream, AmbientMusicFadeTime);
_musicProto = null;
_interruptable = false;
isDone = true;
}
}
// Still running existing ambience
if (isDone == false)
return;
// If ambience finished reset the CD (this also means if we have long ambience it won't clip)
if (isDone == true)
{
// Also don't need to worry about rounding here as it doesn't affect the sim
_nextAudio = _timing.CurTime + _random.Next(_minAmbienceTime, _maxAmbienceTime);
}
_ambientMusicStream = null;
if (_nextAudio > _timing.CurTime)
return;
_musicProto = GetAmbience();
if (_musicProto == null)
{
_interruptable = false;
return;
}
_interruptable = _musicProto.Interruptable;
var tracks = _ambientSounds[_musicProto.ID];
var track = tracks[^1];
tracks.RemoveAt(tracks.Count - 1);
var strim = _audio.PlayGlobal(
track.ToString(),
Filter.Local(),
false,
AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider));
if (strim != null)
{
_ambientMusicStream = (AudioSystem.PlayingStream) strim;
if (_musicProto.FadeIn)
{
FadeIn(_ambientMusicStream, AmbientMusicFadeTime);
}
}
// Refresh the list
if (tracks.Count == 0)
{
RefreshTracks(_musicProto.Sound, tracks, track);
}
}
private AmbientMusicPrototype? GetAmbience()
{
var player = _player.LocalPlayer?.ControlledEntity;
if (player == null)
return null;
var ambiences = _proto.EnumeratePrototypes<AmbientMusicPrototype>().ToList();
ambiences.Sort((x, y) => y.Priority.CompareTo(x.Priority));
foreach (var amb in ambiences)
{
if (!_rules.IsTrue(player.Value, _proto.Index<RulesPrototype>(amb.Rules)))
continue;
return amb;
}
_sawmill.Warning($"Unable to find fallback ambience track");
return null;
}
}

View File

@@ -1,8 +1,124 @@
using Content.Shared.Audio;
using Robust.Client.GameObjects;
namespace Content.Client.Audio;
public sealed class ContentAudioSystem : SharedContentAudioSystem
public sealed partial class ContentAudioSystem : SharedContentAudioSystem
{
// Need how much volume to change per tick and just remove it when it drops below "0"
private readonly Dictionary<AudioSystem.PlayingStream, float> _fadingOut = new();
// Need volume change per tick + target volume.
private readonly Dictionary<AudioSystem.PlayingStream, (float VolumeChange, float TargetVolume)> _fadingIn = new();
private readonly List<AudioSystem.PlayingStream> _fadeToRemove = new();
private const float MinVolume = -32f;
private const float DefaultDuration = 2f;
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
InitializeAmbientMusic();
}
public override void Shutdown()
{
base.Shutdown();
ShutdownAmbientMusic();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
UpdateAmbientMusic();
UpdateFades(frameTime);
}
#region Fades
public void FadeOut(AudioSystem.PlayingStream? stream, float duration = DefaultDuration)
{
if (stream == null || duration <= 0f)
return;
// Just in case
// TODO: Maybe handle the removals by making it seamless?
_fadingIn.Remove(stream);
var diff = stream.Volume - MinVolume;
_fadingOut.Add(stream, diff / duration);
}
public void FadeIn(AudioSystem.PlayingStream? stream, float duration = DefaultDuration)
{
if (stream == null || duration <= 0f || stream.Volume < MinVolume)
return;
_fadingOut.Remove(stream);
var curVolume = stream.Volume;
var change = (curVolume - MinVolume) / duration;
_fadingIn.Add(stream, (change, stream.Volume));
stream.Volume = MinVolume;
}
private void UpdateFades(float frameTime)
{
_fadeToRemove.Clear();
foreach (var (stream, change) in _fadingOut)
{
// Cancelled elsewhere
if (stream.Done)
{
_fadeToRemove.Add(stream);
continue;
}
var volume = stream.Volume - change * frameTime;
stream.Volume = MathF.Max(MinVolume, volume);
if (stream.Volume.Equals(MinVolume))
{
stream.Stop();
_fadeToRemove.Add(stream);
}
}
foreach (var stream in _fadeToRemove)
{
_fadingOut.Remove(stream);
}
_fadeToRemove.Clear();
foreach (var (stream, (change, target)) in _fadingIn)
{
// Cancelled elsewhere
if (stream.Done)
{
_fadeToRemove.Add(stream);
continue;
}
var volume = stream.Volume + change * frameTime;
stream.Volume = MathF.Min(target, volume);
if (stream.Volume.Equals(target))
{
_fadeToRemove.Add(stream);
}
}
foreach (var stream in _fadeToRemove)
{
_fadingIn.Remove(stream);
}
}
#endregion
}