SoundBoardManager.cs

Manager for playing short sound pads. Tracks active SoundHandle entries, plays pads, supports looping replay, updates status text and exposes events for UI updates.

NetworkingFile AccessNative Interop
namespace SSoundboard;

public static class SoundBoardManager
{
	public static bool LoopEnabled { get; private set; }
	public static int ActiveVoiceCount => _voices.Count;
	public static string StatusText { get; private set; } = "READY · PRESS ANY BUTTON";
	public static string LastPlayedLabel { get; private set; } = "";

	static readonly List<VoiceEntry> _voices = new();
	static int _lastVoiceCount;
	static RealTimeUntil _loopReplayCooldown;

	public static event Action StateChanged;

	public static bool IsPadPlaying( string soundId )
	{
		if ( string.IsNullOrWhiteSpace( soundId ) )
			return false;

		for ( var i = 0; i < _voices.Count; i++ )
		{
			if ( _voices[i].SoundId == soundId && _voices[i].Handle.IsValid() )
				return true;
		}

		return false;
	}

	public static void Play( SoundPad pad )
	{
		if ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )
			return;

		var handle = Sound.Play( ToSoundEvent( pad.SoundId ) );
		if ( !handle.IsValid() )
		{
			SetStatus( $"MISSING · {pad.Label}" );
			StateChanged?.Invoke();
			return;
		}

		handle.Volume = 0.9f * pad.Volume;
		handle.Pitch = 0.82f + Game.Random.Float( 0f, 0.36f );

		_voices.Add( new VoiceEntry
		{
			Handle = handle,
			SoundId = pad.SoundId,
			Pad = pad
		} );

		LastPlayedLabel = pad.Label;
		SoundPlayTracker.Record( pad.SoundId );
		UpdateStatus();
		StateChanged?.Invoke();
	}

	public static void ToggleLoop()
	{
		LoopEnabled = !LoopEnabled;
		SetStatus( LoopEnabled ? "LOOP ON · CHAOS MODE ENGAGED" : "LOOP OFF · ONE-SHOT MODE" );
		StateChanged?.Invoke();
	}

	public static void KillAll()
	{
		var voices = _voices.ToArray();
		_voices.Clear();
		_lastVoiceCount = 0;

		for ( var i = 0; i < voices.Length; i++ )
		{
			var handle = voices[i].Handle;
			if ( !handle.IsValid() )
				continue;

			handle.Stop( 0f );
		}

		_loopReplayCooldown = 0.2f;
		SetStatus( "☠ SILENCED · KILL ALL" );
		StateChanged?.Invoke();
	}

	public static void Tick()
	{
		var changed = false;

		for ( var i = _voices.Count - 1; i >= 0; i-- )
		{
			var voice = _voices[i];
			if ( voice.Handle.IsValid() )
				continue;

			if ( LoopEnabled && voice.Pad is not null && TryReplay( voice.Pad, out var replay ) )
			{
				voice.Handle = replay;
				changed = true;
				continue;
			}

			_voices.RemoveAt( i );
			changed = true;
		}

		if ( changed || _voices.Count != _lastVoiceCount )
		{
			_lastVoiceCount = _voices.Count;

			if ( _voices.Count == 0 )
				SetStatus( "READY · PRESS ANY BUTTON" );
			else
				UpdateStatus();

			StateChanged?.Invoke();
		}
	}

	static void UpdateStatus()
	{
		if ( _voices.Count >= 6 )
		{
			SetStatus( $"CACOPHONY · {_voices.Count} LAYERS DEEP" );
			return;
		}

		if ( _voices.Count == 1 )
		{
			SetStatus( $"▶ {LastPlayedLabel}" );
			return;
		}

		if ( _voices.Count > 1 )
		{
			SetStatus( $"▶ {_voices.Count} FX · {LastPlayedLabel}" );
			return;
		}

		SetStatus( "READY · PRESS ANY BUTTON" );
	}

	static void SetStatus( string text ) => StatusText = text;

	static string ToSoundEvent( string soundId )
	{
		if ( string.IsNullOrWhiteSpace( soundId ) )
			return soundId;

		return soundId.EndsWith( ".sound", StringComparison.OrdinalIgnoreCase )
			? soundId
			: $"{soundId}.sound";
	}

	static bool TryReplay( SoundPad pad, out SoundHandle handle )
	{
		if ( !_loopReplayCooldown )
		{
			handle = default;
			return false;
		}

		handle = Sound.Play( ToSoundEvent( pad.SoundId ) );
		if ( !handle.IsValid() )
			return false;

		handle.Volume = 0.9f * pad.Volume;
		handle.Pitch = 0.82f + Game.Random.Float( 0f, 0.36f );
		return true;
	}

	sealed class VoiceEntry
	{
		public SoundHandle Handle;
		public string SoundId;
		public SoundPad Pad;
	}
}