Code/Demos/BeatPad/SequencerClock.cs
using System;
using System.Collections.Generic;

namespace Sandbox.BeatPad;

// Transport + playhead for the step sequencer. Advances absolute monotonic time and
// reports which steps were crossed this tick (swing applied to odd steps on playback).
// No engine types -- compiles headless for unit tests.
public sealed class SequencerClock
{
    float _bpm = 120f;
    float _swing;     // 0..MaxSwing, applied to odd steps
    float _t;         // absolute time since Start(), seconds
    bool _playing;

    public bool Playing => _playing;

    public void SetBpm(float bpm) => _bpm = QuantizeMath.SafeBpm(bpm);
    public void SetSwing(float swingAmount) => _swing = swingAmount < 0f ? 0f : swingAmount;

    public void Start()
    {
        _t = 0f;
        _playing = true;
    }

    public void Stop() => _playing = false;

    // 0..1 position within the current bar (for the playhead visual).
    public float PlayheadFraction
    {
        get
        {
            float bar = QuantizeMath.BarDuration(_bpm);
            float inBar = _t - MathF.Floor(_t / bar) * bar;
            return inBar / bar;
        }
    }

    // Advance time by dt and append the step indices whose trigger time fell in
    // the half-open window [from, to). No allocation.
    public void Advance(float dt, List<int> fired)
    {
        if (!_playing || dt <= 0f) return;

        float bar = QuantizeMath.BarDuration(_bpm);
        float step = QuantizeMath.StepDuration(_bpm);
        float from = _t;
        float to = _t + dt;

        int kStart = (int)MathF.Floor(from / bar);
        int kEnd = (int)MathF.Floor(to / bar);
        for (int k = kStart; k <= kEnd; k++)
        {
            for (int s = 0; s < QuantizeMath.StepsPerBar; s++)
            {
                float trig = k * bar + s * step + QuantizeMath.SwingOffset(s, _swing, _bpm);
                if (trig >= from && trig < to) fired.Add(s);
            }
        }

        _t = to;
    }
}