Demos/BeatPad/StepPattern.cs
using System.Collections.Generic;
namespace Sandbox.BeatPad;
// The recorded pattern: 16 steps, each holding a set of pad indices. No timing
// knowledge -- the clock tells it which step is playing. Backed by a dense bool
// grid so there is no per-frame allocation. Pad count tracks the kit (16).
public sealed class StepPattern
{
public const int Steps = QuantizeMath.StepsPerBar; // 16
public static readonly int Pads = BeatPadKits.PadCount; // 16
readonly bool[,] _hits = new bool[Steps, BeatPadKits.PadCount];
static bool InRange(int step, int pad)
=> step >= 0 && step < Steps && pad >= 0 && pad < BeatPadKits.PadCount;
public void Record(int step, int pad)
{
if (InRange(step, pad)) _hits[step, pad] = true;
}
// Flip the cell at (step, pad) for click-to-edit. Returns the new state
// (true = now on). Out-of-range is ignored and returns false.
public bool Toggle(int step, int pad)
{
if (!InRange(step, pad)) return false;
bool now = !_hits[step, pad];
_hits[step, pad] = now;
return now;
}
public bool Has(int step, int pad) => InRange(step, pad) && _hits[step, pad];
public bool HasAnyAt(int step)
{
if (step < 0 || step >= Steps) return false;
for (int pad = 0; pad < BeatPadKits.PadCount; pad++)
if (_hits[step, pad]) return true;
return false;
}
// Append the pad indices recorded at this step (ascending) into `into`. Caller
// clears/reuses the list to avoid allocation in the playback hot path.
public void CollectPadsAt(int step, List<int> into)
{
into.Clear();
if (step < 0 || step >= Steps) return;
for (int pad = 0; pad < BeatPadKits.PadCount; pad++)
if (_hits[step, pad]) into.Add(pad);
}
public void Erase(int pad)
{
if (pad < 0 || pad >= BeatPadKits.PadCount) return;
for (int step = 0; step < Steps; step++) _hits[step, pad] = false;
}
public void ClearAll()
{
for (int step = 0; step < Steps; step++)
for (int pad = 0; pad < BeatPadKits.PadCount; pad++)
_hits[step, pad] = false;
}
}