Component that mutes/muffles scene audio when the camera is underwater. It records original SoundPointComponent and SoundHandle volume/pitch and sound override state, scales them while underwater, and restores them on surfacing.
using System.Collections.Generic;
using Sandbox;
using Sandbox.Audio;
namespace RedSnail.WaterTool2;
/// <summary>
/// Muffles the world's sounds while the camera is under the water: every sound point and playing sound gets its volume
/// and pitch scaled down, and is put back exactly as it was on surfacing.
/// </summary>
[Title("Underwater Audio Manager")]
[Category("Water Tool 2")]
[Icon("hearing")]
public sealed class UnderwaterAudioManager : Component
{
private sealed class SoundState
{
public bool SoundOverride;
public float Volume;
public float Pitch;
}
private bool m_IsUnderwater;
private readonly Dictionary<SoundPointComponent, SoundState> m_SoundPoints = new();
private readonly Dictionary<SoundHandle, SoundState> m_SoundHandles = new();
private readonly List<SoundHandle> m_Active = new();
[Property, Range(0, 1)] private float UnderwaterVolumeMultiplier { get; set; } = 0.8f;
[Property, Range(0, 2)] private float UnderwaterPitchMultiplier { get; set; } = 0.5f;
protected override void OnUpdate()
{
if (Scene.Camera is not CameraComponent camera)
return;
bool isUnderwater = WaterNetwork.IsPositionInsideAny(camera.WorldPosition);
// Under water, every sound that starts playing is caught and muffled as it turns up.
if (isUnderwater)
{
var ui = Mixer.FindMixerByName("ui");
foreach (var point in Scene.GetAllComponents<SoundPointComponent>())
{
if (point.TargetMixer == ui || m_SoundPoints.ContainsKey(point))
continue;
m_SoundPoints[point] = new SoundState { SoundOverride = point.SoundOverride, Volume = point.Volume, Pitch = point.Pitch };
point.SoundOverride = true;
point.Volume *= UnderwaterVolumeMultiplier;
point.Pitch *= UnderwaterPitchMultiplier;
}
m_Active.Clear();
SoundHandle.GetActive(m_Active);
foreach (var handle in m_Active)
{
if (handle.TargetMixer == ui || m_SoundHandles.ContainsKey(handle))
continue;
m_SoundHandles[handle] = new SoundState { Volume = handle.Volume, Pitch = handle.Pitch };
handle.Volume *= UnderwaterVolumeMultiplier;
handle.Pitch *= UnderwaterPitchMultiplier;
}
}
if (isUnderwater == m_IsUnderwater)
return;
// Surfacing: everything goes back to how it was.
if (!isUnderwater)
{
foreach (var (point, state) in m_SoundPoints)
{
if (!point.IsValid())
continue;
point.SoundOverride = state.SoundOverride;
point.Volume = state.Volume;
point.Pitch = state.Pitch;
}
foreach (var (handle, state) in m_SoundHandles)
{
if (!handle.IsValid())
continue;
handle.Volume = state.Volume;
handle.Pitch = state.Pitch;
}
m_SoundPoints.Clear();
m_SoundHandles.Clear();
}
m_IsUnderwater = isUnderwater;
}
}