Game/PumpMusic.cs

Component that plays a looping background music track for the game, respects a music volume setting and speaker mute by stopping playback, and handles fade-in and restart when the loop ends.

Native Interop
namespace DesertPump;

/// <summary>
/// Keeps the background track playing on a loop, and takes the speaker button seriously -
/// muting stops the music rather than just silencing the effects.
/// </summary>
[Title( "Desert Pump Music" )]
[Category( "Desert Pump" )]
[Icon( "music_note" )]
public sealed class PumpMusic : Component
{
	[Property]
	public SoundEvent Track { get; set; }

	/// <summary>
	/// Ceiling for the track. The player's music slider scales this, so it stays
	/// background even at full volume.
	/// </summary>
	[Property, Range( 0f, 1f )]
	public float Volume { get; set; } = 0.6f;

	/// <summary>What the track should actually play at, once settings are applied.</summary>
	public float TargetVolume => Volume * PumpSettings.Current.EffectiveMusic;

	/// <summary>Seconds to ease in, so it doesn't slam in at full level on load.</summary>
	[Property, Range( 0f, 8f )]
	public float FadeIn { get; set; } = 2.5f;

	SoundHandle handle;

	/// <summary>Whether the track is actually rolling right now.</summary>
	public bool IsPlaying => handle.IsValid() && handle.IsPlaying;

	protected override void OnEnabled()
	{
		Start();
	}

	protected override void OnDisabled()
	{
		Stop();
	}

	protected override void OnUpdate()
	{
		// Muting, or dragging the music slider to zero, stops the track rather than
		// leaving it running silently.
		if ( TargetVolume <= 0f )
		{
			Stop();
			return;
		}

		// The track is a fixed-length loop, so restart it as soon as it runs out.
		if ( !handle.IsValid() || !handle.IsPlaying )
		{
			Start();
			return;
		}

		handle.Volume = TargetVolume;
	}

	void Start()
	{
		if ( Track is null || TargetVolume <= 0f )
			return;

		if ( handle.IsValid() && handle.IsPlaying )
			return;

		handle = Sound.Play( Track, (Sandbox.Audio.Mixer)null );

		if ( !handle.IsValid() )
			return;

		handle.Volume = TargetVolume;
		handle.Fadein = FadeIn;
	}

	void Stop()
	{
		if ( handle.IsValid() )
		{
			handle.Stop( 0.4f );
		}

		handle = null;
	}
}