Code/Demos/BeatPad/BeatPadAudio.cs
using Sandbox.Audio;

namespace Sandbox;

// Thin wrapper over Sound.Play for the beat pad. Plays a .sound event, applies the
// master volume/pitch from the knobs, routes through a shared filter+reverb mixer,
// and resolves a lit-decay halflife from the sample's duration where the engine allows.
internal static class BeatPadAudio
{
    // only hard-coded decay number; remove once SoundFile.Duration is confirmed and routed through ResolveHalflife.
    public const float FallbackHalflife = 0.06f;

    const string MixerName = "Goo.BeatPad";
    const string ReverbPreset = "room.diffuse.small";

    static Mixer? _mixer;
    static LowPassProcessor? _lowPass;
    static HighPassProcessor? _highPass;
    static DspProcessor? _reverb;

    // cutoffs/mix from KnobMath; FLT is bipolar (one filter open while the other engages); falls back to dry default mixer if unavailable.
    public static void Play(string soundName, float volume, float pitch,
        float lowPassCutoff, float highPassCutoff, float reverbMix)
    {
        var handle = Sound.Play(soundName);
        if (!handle.IsValid()) return;
        handle.Volume = volume;
        handle.Pitch = pitch;
        handle.ListenLocal = true; // UI sound: no 3D attenuation

        var mixer = EnsureMixer();
        if (mixer is null) return;
        handle.TargetMixer = mixer;
        if (_lowPass is not null) _lowPass.Cutoff = lowPassCutoff;
        if (_highPass is not null) _highPass.Cutoff = highPassCutoff;
        if (_reverb is not null) _reverb.Mix = reverbMix;
    }

    // Lazily build (once) a child mixer carrying low-pass + high-pass + reverb processors.
    // Re-finds by name so a hot-reload reuses the bus; any failure degrades to a dry,
    // unrouted sound.
    static Mixer? EnsureMixer()
    {
        if (_mixer is not null) return _mixer;
        try
        {
            var mixer = Mixer.FindMixerByName(MixerName);
            if (mixer is null)
            {
                var root = Mixer.Master;
                if (root is null) return null;
                mixer = root.AddChild();
                mixer.Name = MixerName;
            }

            _lowPass = mixer.GetProcessor<LowPassProcessor>();
            if (_lowPass is null) { _lowPass = new LowPassProcessor(); mixer.AddProcessor(_lowPass); }

            _highPass = mixer.GetProcessor<HighPassProcessor>();
            if (_highPass is null) { _highPass = new HighPassProcessor(); mixer.AddProcessor(_highPass); }

            _reverb = mixer.GetProcessor<DspProcessor>();
            if (_reverb is null) { _reverb = new DspProcessor(ReverbPreset); mixer.AddProcessor(_reverb); }

            _mixer = mixer;
            return _mixer;
        }
        catch
        {
            // Mixer / AudioProcessor surface unavailable on this build; play dry.
            return null;
        }
    }

    // Returns a DecayFloat halflife driving the lit-pad fade. Tries the sample's real
    // duration; falls back to a fixed snap if unavailable. A halflife of duration/5
    // makes the glow visually last roughly the sample length.
    public static float ResolveHalflife(string soundName)
    {
        try
        {
            var file = SoundFile.Load(soundName);
            if (file != null && file.Duration > 0.01f)
                return System.MathF.Max(0.02f, file.Duration / 5f);
        }
        catch
        {
            // SoundFile.Load may reject a .sound event path on this build; fall through.
        }
        return FallbackHalflife;
    }
}