Data model and lookup utilities for a soundboard. Defines SoundPad and SoundBoardTab types, a catalog of tabs, methods to get pads by tab id, build unique/trending pools, and search/filter pads by text.
using System;
using System.Collections.Generic;
using System.Linq;
namespace SSoundboard;
public sealed class SoundPad
{
public string Label { get; init; }
public string SoundId { get; init; }
public string Color { get; init; }
public float Volume { get; init; } = 1f;
}
public sealed class SoundBoardTab
{
public string Id { get; init; }
public string Label { get; init; }
public string Icon { get; init; }
public List<SoundPad> Pads { get; init; } = new();
}
/// <summary>
/// Tab catalog + pad routing. Pad lists live in SoundBoardPads.generated.cs
/// (chunked builders so C# static-ctor IL limits never break publish).
/// Source of truth for rebuild: Assets/sounds/pads-*.json via publish/rebuild_tabs.py.
/// </summary>
public static partial class SoundBoardData
{
// Keep in sync with publish/pad_tags.py CATEGORY_TABS (+ arnold/meme).
static readonly IReadOnlyList<SoundBoardTab> TabCatalog = new List<SoundBoardTab>
{
new() { Id = "arnold", Label = "ARNOLD", Icon = "💪" },
new() { Id = "gas", Label = "GAS", Icon = "💨" },
new() { Id = "reaction", Label = "REACTIONS", Icon = "😂" },
new() { Id = "game", Label = "VIDEO GAMES", Icon = "🎮" },
new() { Id = "anime", Label = "ANIME", Icon = "🎌" },
new() { Id = "movie", Label = "MOVIES", Icon = "🎬" },
new() { Id = "cartoon", Label = "CARTOONS", Icon = "📺" },
new() { Id = "tv", Label = "TV", Icon = "📡" },
new() { Id = "horror", Label = "HORROR", Icon = "👻" },
new() { Id = "sfx", Label = "SFX", Icon = "🔊" },
new() { Id = "sports", Label = "SPORTS", Icon = "⚽" },
new() { Id = "vehicles", Label = "VEHICLES", Icon = "🚗" },
new() { Id = "weapons", Label = "WEAPONS", Icon = "🔫" },
new() { Id = "politics", Label = "POLITICS", Icon = "🏛️" },
new() { Id = "brainrot", Label = "BRAINROT", Icon = "🧠" },
new() { Id = "uwu", Label = "UWU", Icon = "🥺" },
new() { Id = "music", Label = "MUSIC", Icon = "🎵" },
new() { Id = "app", Label = "APP", Icon = "📱" },
new() { Id = "voice", Label = "VOICE", Icon = "🗣️" },
new() { Id = "nature", Label = "NATURE", Icon = "🌿" },
new() { Id = "animals", Label = "ANIMALS", Icon = "🐾" },
new() { Id = "meme", Label = "MEME", Icon = "🤡" },
};
/// <summary>Warm set for first screen (not every Arnold pad).</summary>
public static IEnumerable<string> StartupSoundIds =>
ArnoldPads.Select( pad => pad.SoundId ).Take( 48 );
public static IReadOnlyList<SoundBoardTab> Boards => TabCatalog;
public static IReadOnlyList<SoundPad> GetPads( string boardId )
{
if ( string.IsNullOrWhiteSpace( boardId ) )
return Array.Empty<SoundPad>();
return boardId.ToLowerInvariant() switch
{
"new" => NewPads,
"gas" => GasPads,
"reaction" or "reactions" => ReactionPads,
"game" or "games" => GamesPads,
"anime" => AnimePads,
"movie" or "movies" => MoviePads,
"cartoon" or "cartoons" => CartoonPads,
"tv" => TvPads,
"horror" => HorrorPads,
"sfx" or "sound-effect" => SfxPads,
"sports" => SportsPads,
"vehicles" or "vehicle" => VehiclePads,
"weapons" or "weapon" => WeaponPads,
"politics" => PoliticsPads,
"brainrot" => BrainrotPads,
"uwu" => UwuPads,
"music" => MusicPads,
"app" => AppPads,
"voice" => VoicePads,
"nature" => NaturePads,
"animals" => AnimalsPads,
"meme" => MemePads,
"arnold" => ArnoldPads,
"trending" => SoundPlayTracker.BuildTrendingPads( TrendingPool, TrendingFallbackPads ),
_ => Array.Empty<SoundPad>()
};
}
static IReadOnlyList<SoundPad> TrendingPool => _trendingPool ??= BuildTrendingPool();
static IReadOnlyList<SoundPad> _trendingPool;
/// <summary>Every unique pad across all boards (for global search).</summary>
public static IReadOnlyList<SoundPad> AllUniquePads => _allUniquePads ??= BuildAllUniquePads();
static IReadOnlyList<SoundPad> _allUniquePads;
static IReadOnlyList<SoundPad> BuildTrendingPool()
{
var pads = new Dictionary<string, SoundPad>( StringComparer.OrdinalIgnoreCase );
AddPads( pads, NewPads );
AddPads( pads, MemePads );
AddPads( pads, ArnoldPads );
AddPads( pads, TrendingFallbackPads );
return pads.Values.ToList();
}
static IReadOnlyList<SoundPad> BuildAllUniquePads()
{
var pads = new Dictionary<string, SoundPad>( StringComparer.OrdinalIgnoreCase );
AddPads( pads, NewPads );
AddPads( pads, MemePads );
AddPads( pads, ArnoldPads );
AddPads( pads, TrendingFallbackPads );
foreach ( var tab in TabCatalog )
AddPads( pads, GetPads( tab.Id ) );
return pads.Values.ToList();
}
static void AddPads( Dictionary<string, SoundPad> pads, IEnumerable<SoundPad> source )
{
if ( source is null )
return;
foreach ( var pad in source )
{
if ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )
continue;
pads[pad.SoundId] = pad;
}
}
/// <summary>
/// Global search across every board. Comma-separated terms are OR
/// (e.g. "dolphin, ocean" matches either word in label/id).
/// </summary>
public static IReadOnlyList<SoundPad> SearchAll( string query )
{
var terms = ParseSearchTerms( query );
if ( terms.Count == 0 )
return Array.Empty<SoundPad>();
var all = AllUniquePads;
var matches = new List<SoundPad>( 64 );
for ( var i = 0; i < all.Count; i++ )
{
var pad = all[i];
if ( PadMatchesAnyTerm( pad, terms ) )
matches.Add( pad );
}
matches.Sort( static ( a, b ) =>
string.Compare( a.Label, b.Label, StringComparison.OrdinalIgnoreCase ) );
return matches;
}
public static List<string> ParseSearchTerms( string query )
{
var terms = new List<string>();
if ( string.IsNullOrWhiteSpace( query ) )
return terms;
// Split on comma (and optional semicolon) — "dolphin, ocean" → OR search.
var parts = query.Split( new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries );
foreach ( var part in parts )
{
var t = part.Trim();
if ( t.Length > 0 )
terms.Add( t );
}
// No commas: one term (may contain spaces, e.g. "half life").
if ( terms.Count == 0 )
{
var single = query.Trim();
if ( single.Length > 0 )
terms.Add( single );
}
return terms;
}
public static bool PadMatchesAnyTerm( SoundPad pad, List<string> terms )
{
if ( pad is null || terms is null || terms.Count == 0 )
return false;
for ( var i = 0; i < terms.Count; i++ )
{
if ( PadMatchesTerm( pad, terms[i] ) )
return true;
}
return false;
}
public static bool PadMatchesTerm( SoundPad pad, string term )
{
if ( pad is null || string.IsNullOrWhiteSpace( term ) )
return false;
term = term.Trim();
if ( !string.IsNullOrEmpty( pad.Label )
&& pad.Label.Contains( term, StringComparison.OrdinalIgnoreCase ) )
return true;
if ( string.IsNullOrEmpty( pad.SoundId ) )
return false;
var slash = pad.SoundId.LastIndexOf( '/' );
var stem = slash >= 0 ? pad.SoundId[(slash + 1)..] : pad.SoundId;
if ( stem.Contains( term, StringComparison.OrdinalIgnoreCase ) )
return true;
var spaced = stem.Replace( '-', ' ' ).Replace( '_', ' ' );
if ( spaced.Contains( term, StringComparison.OrdinalIgnoreCase ) )
return true;
var compactTerm = term.Replace( " ", "" ).Replace( "-", "" ).Replace( "_", "" );
if ( compactTerm.Length >= 2 )
{
var compactStem = stem.Replace( "-", "" ).Replace( "_", "" );
if ( compactStem.Contains( compactTerm, StringComparison.OrdinalIgnoreCase ) )
return true;
}
return false;
}
// Used by SoundBoardPads.generated.cs builders.
internal static SoundPad Pad( string soundId, string color, string label )
=> Pad( soundId, color, 1f, label );
internal static SoundPad Pad( string soundId, string color, float volume = 1f, string label = null )
{
return new SoundPad
{
Label = (label ?? soundId.Split( '/' ).Last().Replace( '-', ' ' ).Replace( '_', ' ' )).ToUpperInvariant(),
SoundId = soundId,
Color = color,
Volume = volume
};
}
}