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;
}
}