Gameplay/UnderwaterAudioManager.cs

Component that detects when the camera is underwater and mutes/muffles scene audio. It scales volume and pitch of SoundPointComponent instances and currently playing SoundHandle objects while underwater, and restores their previous values on surfacing.

NetworkingFile Access
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;
	}
}