Ambient sound system (#4552)

* Ambient sound system

Client-side system that plays audio from nearby objects that are randomly sampled.

* Decent

* Tweaks

* Tweaks

* Comment this out for now

* reduce VM sound

* Fix rolloff

* Fixes

* Volume tweak
This commit is contained in:
metalgearsloth
2021-09-07 18:47:23 +10:00
committed by GitHub
parent 97af34f70a
commit ff40a7665d
25 changed files with 361 additions and 6 deletions

View File

@@ -0,0 +1,178 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Client.Audio
{
/// <summary>
/// Samples nearby <see cref="AmbientSoundComponent"/> and plays audio.
/// </summary>
public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
{
[Dependency] private IEntityLookup _lookup = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private int _maxAmbientCount;
private float _maxAmbientRange;
private float _cooldown;
private float _accumulator;
/// <summary>
/// How many times we can be playing 1 particular sound at once.
/// </summary>
private int _maxSingleSound = 3;
private Dictionary<AmbientSoundComponent, (IPlayingAudioStream? Stream, string Sound)> _playingSounds = new();
private const float RangeBuffer = 0.5f;
public override void Initialize()
{
base.Initialize();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CCVars.AmbientCooldown, SetCooldown, true);
configManager.OnValueChanged(CCVars.MaxAmbientSources, SetAmbientCount, true);
configManager.OnValueChanged(CCVars.AmbientRange, SetAmbientRange, true);
}
private void SetCooldown(float value) => _cooldown = value;
private void SetAmbientCount(int value) => _maxAmbientCount = value;
private void SetAmbientRange(float value) => _maxAmbientRange = value;
public override void Shutdown()
{
base.Shutdown();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CCVars.AmbientCooldown, SetCooldown);
configManager.UnsubValueChanged(CCVars.MaxAmbientSources, SetAmbientCount);
configManager.UnsubValueChanged(CCVars.AmbientRange, SetAmbientRange);
}
private int PlayingCount(string countSound)
{
var count = 0;
foreach (var (_, (_, sound)) in _playingSounds)
{
if (sound.Equals(countSound)) count++;
}
return count;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_cooldown <= 0f)
{
_accumulator = 0f;
return;
}
_accumulator += frameTime;
if (_accumulator < _cooldown) return;
_accumulator -= _cooldown;
var player = _playerManager.LocalPlayer?.ControlledEntity;
if (player == null)
{
ClearSounds();
return;
}
var coordinates = player.Transform.Coordinates;
foreach (var (comp, (stream, _)) in _playingSounds.ToArray())
{
if (!comp.Deleted && comp.Enabled && comp.Owner.Transform.Coordinates.TryDistance(EntityManager, coordinates, out var range) &&
range <= comp.Range)
{
continue;
}
stream?.Stop();
_playingSounds.Remove(comp);
}
if (_playingSounds.Count >= _maxAmbientCount) return;
SampleNearby(coordinates);
}
private void ClearSounds()
{
foreach (var (_, (stream, _)) in _playingSounds)
{
stream?.Stop();
}
_playingSounds.Clear();
}
/// <summary>
/// Get a list of ambient components in range and determine which ones to start playing.
/// </summary>
private void SampleNearby(EntityCoordinates coordinates)
{
var compsInRange = new List<AmbientSoundComponent>();
foreach (var entity in _lookup.GetEntitiesInRange(coordinates, _maxAmbientRange,
LookupFlags.Approximate | LookupFlags.IncludeAnchored))
{
if (!entity.TryGetComponent(out AmbientSoundComponent? ambientComp) ||
_playingSounds.ContainsKey(ambientComp) ||
!ambientComp.Enabled ||
// We'll also do this crude distance check because it's what we're doing in the active loop above.
!entity.Transform.Coordinates.TryDistance(EntityManager, coordinates, out var range) ||
range > ambientComp.Range - RangeBuffer)
{
continue;
}
compsInRange.Add(ambientComp);
}
while (_playingSounds.Count < _maxAmbientCount)
{
if (compsInRange.Count == 0) break;
var comp = _random.PickAndTake(compsInRange);
var sound = comp.Sound.GetSound();
if (PlayingCount(sound) >= _maxSingleSound) continue;
var audioParams = AudioHelpers
.WithVariation(0.01f)
.WithVolume(comp.Volume)
.WithLoop(true)
.WithAttenuation(Attenuation.LinearDistance)
// Randomise start so 2 sources don't increase their volume.
.WithPlayOffset(_random.NextFloat())
.WithMaxDistance(comp.Range);
var stream = SoundSystem.Play(
Filter.Local(),
sound,
comp.Owner,
audioParams);
if (stream == null) continue;
_playingSounds[comp] = (stream, sound);
}
}
}
}

View File

@@ -29,8 +29,8 @@ namespace Content.Client.Audio
private SoundCollectionPrototype _ambientCollection = default!;
private AudioParams _ambientParams = new(-10f, 1, "Master", 0, 0, true, 0f);
private AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, true, 0f);
private AudioParams _ambientParams = new(-10f, 1, "Master", 0, 0, 0, true, 0f);
private AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, 0, true, 0f);
private IPlayingAudioStream? _ambientStream;
private IPlayingAudioStream? _lobbyStream;

View File

@@ -272,7 +272,8 @@ namespace Content.Client.Entry
"Advertise",
"PowerNetworkBattery",
"BatteryCharger",
"SpawnItemsOnUse"
"SpawnItemsOnUse",
"AmbientOnPowered",
};
}
}

View File

@@ -0,0 +1,14 @@
using Content.Shared.Audio;
using Robust.Shared.GameObjects;
namespace Content.Server.Audio
{
/// <summary>
/// Toggles <see cref="AmbientSoundComponent"/> on when powered and off when not powered.
/// </summary>
[RegisterComponent]
public class AmbientOnPoweredComponent : Component
{
public override string Name => "AmbientOnPowered";
}
}

View File

@@ -0,0 +1,23 @@
using Content.Server.Power.Components;
using Content.Shared.Audio;
using Robust.Shared.GameObjects;
namespace Content.Server.Audio
{
public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AmbientOnPoweredComponent, PowerChangedEvent>(HandlePowerChange);
}
private void HandlePowerChange(EntityUid uid, AmbientOnPoweredComponent component, PowerChangedEvent args)
{
if (!ComponentManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambientSound)) return;
if (ambientSound.Enabled == args.Powered) return;
ambientSound.Enabled = args.Powered;
ambientSound.Dirty();
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.Acts;
using Content.Shared.Audio;
using Content.Shared.Gravity;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
@@ -137,6 +138,7 @@ namespace Content.Server.Gravity
private void MakeBroken()
{
_status = GravityGeneratorStatus.Broken;
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, false);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
@@ -145,6 +147,7 @@ namespace Content.Server.Gravity
private void MakeUnpowered()
{
_status = GravityGeneratorStatus.Unpowered;
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, false);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
@@ -153,6 +156,7 @@ namespace Content.Server.Gravity
private void MakeOff()
{
_status = GravityGeneratorStatus.Off;
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, false);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
@@ -161,6 +165,7 @@ namespace Content.Server.Gravity
private void MakeOn()
{
_status = GravityGeneratorStatus.On;
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, true);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, true);

View File

@@ -4,6 +4,7 @@ using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
@@ -226,7 +227,7 @@ namespace Content.Server.Light.Components
if (LightBulb == null) // No light bulb.
{
_currentLit = false;
SetLight(false);
powerReceiver.Load = 0;
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Empty);
return;
@@ -267,6 +268,7 @@ namespace Content.Server.Light.Components
private void SetLight(bool value, Color? color = null)
{
_currentLit = value;
EntitySystem.Get<SharedAmbientSoundSystem>().SetAmbience(Owner.Uid, value);
if (!Owner.TryGetComponent(out PointLightComponent? pointLight)) return;
pointLight.Enabled = value;
@@ -327,6 +329,6 @@ namespace Content.Server.Light.Components
UpdateLight();
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using Content.Shared.Sound;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Audio
{
[RegisterComponent]
[NetworkedComponent]
public sealed class AmbientSoundComponent : Component
{
public override string Name => "AmbientSound";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enabled")]
public bool Enabled { get; set; } = true;
[DataField("sound")]
public SoundSpecifier Sound = default!;
/// <summary>
/// How far away this ambient sound can potentially be heard.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("range")]
public float Range = 2f;
/// <summary>
/// Applies this volume to the sound being played.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("volume")]
public float Volume = -10f;
}
[Serializable, NetSerializable]
public sealed class AmbientSoundComponentState : ComponentState
{
public bool Enabled { get; init; }
public float Range { get; init; }
public float Volume { get; init; }
}
}

View File

@@ -0,0 +1,45 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
namespace Content.Shared.Audio
{
public abstract class SharedAmbientSoundSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AmbientSoundComponent, ComponentGetState>(GetCompState);
SubscribeLocalEvent<AmbientSoundComponent, ComponentHandleState>(HandleCompState);
}
public void SetAmbience(EntityUid uid, bool value)
{
// Reason I didn't make this eventbus for the callers is because it seemed a bit silly
// trying to account for damageable + powered + toggle, plus we can't just check if it's powered.
// So we'll just call it directly for whatever.
if (!ComponentManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambience) ||
ambience.Enabled == value) return;
ambience.Enabled = value;
ambience.Dirty();
}
private void HandleCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentHandleState args)
{
if (args.Current is not AmbientSoundComponentState state) return;
component.Enabled = state.Enabled;
component.Range = state.Range;
component.Volume = state.Volume;
}
private void GetCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentGetState args)
{
args.State = new AmbientSoundComponentState
{
Enabled = component.Enabled,
Range = component.Range,
Volume = component.Volume,
};
}
}
}

View File

@@ -7,6 +7,22 @@ namespace Content.Shared.CCVar
[CVarDefs]
public sealed class CCVars : CVars
{
/*
* Ambience
*/
/// <summary>
/// How long we'll wait until re-sampling nearby objects for ambience.
/// </summary>
public static readonly CVarDef<float> AmbientCooldown =
CVarDef.Create("ambience.cooldown", 0.5f, CVar.REPLICATED | CVar.SERVER);
public static readonly CVarDef<float> AmbientRange =
CVarDef.Create("ambience.range", 5f, CVar.REPLICATED | CVar.SERVER);
public static readonly CVarDef<int> MaxAmbientSources =
CVarDef.Create("ambience.max_sounds", 6, CVar.REPLICATED | CVar.SERVER);
/*
* Status
*/

View File

@@ -29,7 +29,7 @@ namespace Content.Shared.Light.Component
[NetworkedComponent]
public abstract class SharedExpendableLightComponent: Robust.Shared.GameObjects.Component
{
public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, true, 0.3f);
public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, 1, true, 0.3f);
public sealed override string Name => "ExpendableLight";

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,6 +6,10 @@
placement:
mode: AlignTileAny
components:
- type: AmbientSound
range: 7
sound:
path: /Audio/Ambience/Objects/gravity_gen_hum.ogg
- type: Sprite
netsync: false
sprite: Structures/Machines/gravity_generator.rsi

View File

@@ -5,6 +5,12 @@
description: Just add capitalism!
abstract: true
components:
- type: AmbientOnPowered
- type: AmbientSound
volume: -15
range: 3
sound:
path: /Audio/Ambience/Objects/vending_machine_hum.ogg
- type: Sprite
sprite: Structures/Machines/VendingMachines/empty.rsi
netsync: false

View File

@@ -6,6 +6,10 @@
placement:
mode: SnapgridCenter
components:
#- type: AmbientSound
# range: 5
# sound:
# path: /Audio/Ambience/Objects/engine_hum.ogg
- type: Clickable
- type: InteractionOutline
- type: Physics

View File

@@ -46,6 +46,9 @@
placement:
mode: SnapgridCenter
components:
- type: AmbientSound
sound:
path: /Audio/Ambience/Objects/hdd_buzz.ogg
- type: Clickable
- type: AccessReader
access: [["Engineering"]]

View File

@@ -7,6 +7,10 @@
placement:
mode: SnapgridCenter
components:
- type: AmbientSound
range: 3
sound:
path: /Audio/Ambience/Objects/periodic_beep.ogg
- type: Sprite
netsync: false
sprite: Structures/Power/smes.rsi

View File

@@ -4,6 +4,10 @@
description: "An unpowered light."
suffix: Unpowered
components:
- type: AmbientSound
volume: -12
sound:
path: /Audio/Ambience/Objects/light_hum.ogg
- type: Clickable
- type: InteractionOutline
- type: Construction