memlnaut-nisps/nisps/dsp/seq_clock.hpp
monkey-w1n5t0n 96737a3d42 refactor(engines): extract the shared sequencer machinery
Phase 3 (L8). Pure statement-for-statement relocation into
dsp/ratio_seq.hpp, dsp/seq_clock.hpp and core/event_queue.hpp.

AUDIT CORRECTION: L8 says "breakor and elysiamorf duplicate ratio_seq". That is
false — ElysiamorfEngine has no ratio_seq at all; it triggers continuously via
FM operators. The real duplicate pair is BreakOrEngine and MEMLCeliumEngine,
whose copies are byte-for-byte identical. Elysiamorf did share the clock and
event-queue machinery, so it uses those. memlcelium now includes the shared
ratio_seq too, which is what actually closes this finding.

Deliberately NOT folded into core/ring_buffer.hpp: RingBuffer is an
atomics-based cross-core SPSC channel (its header says so), whereas the engines'
event queue is produced and drained on one thread. Reusing it would have meant
paying for atomics to serve a single-threaded FIFO. The distinction is now
recorded in MAP.md so the next audit does not read them as duplicates.

Bit-exactness: verified the MIDI-clock tick and bar-phasor tick preserve the
original operation order with no floating-point re-association, and that
EventQueue keeps the original `% N` indexing rather than adopting RingBuffer's
bitmask. The golden suite (nisps_golden_tests) and the native<->WASM parity blob
both pass unchanged — they are the check, and they were not re-baselined.
2026-07-21 14:02:23 +02:00

94 lines
3.5 KiB
C++

// nisps/dsp/seq_clock.hpp — shared bar-phasor + 24-PPQN MIDI-clock phasor +
// control-rate sample counter for sequencer engines.
//
// Extracted from the byte-for-byte-identical bar/MIDI-clock/counter/
// update_bpm member blocks previously duplicated in nisps/engines/breakor.hpp
// and nisps/engines/elysiamorf.hpp (2026-07 simplification audit, finding
// L8). The one place the two engines differ is the control-rate divisor
// (breakor: 400 samples/tick, elysiamorf: 500) — SeqClock takes that as a
// constructor argument instead of baking it in, so it stays a per-engine
// choice.
//
// Call shape (matches the original inlined code exactly, just moved behind
// two named methods):
// process() per sample:
// if (clock.tick_midi_clock()) { emit Clock event }
// if (clock.tick_bar()) { use clock.bar_phasor() to drive tracks }
// setup() / set_playing(false):
// clock.reset();
// whenever bpm changes:
// clock.update_bpm(bpm, sample_rate);
#pragma once
#include <cstddef>
#include "../core/perf.hpp"
namespace nisps {
class SeqClock {
public:
explicit SeqClock(std::size_t seq_sample_div) noexcept
: seq_sample_div_(seq_sample_div) {}
// Resets phase/counter state. Does NOT touch bpm_/the *_inc_ rates —
// callers re-derive those via update_bpm() on setup(), matching the
// original engines (which called update_bpm(90.f) once in setup()).
void reset() noexcept {
bar_phasor_ = 0.f;
midi_clock_phasor_ = 0.f;
sample_counter_ = 0u;
}
void update_bpm(float bpm, float sample_rate) noexcept {
bpm_ = bpm;
const float beat_seconds = 60.f / bpm;
const float bar_seconds = beat_seconds * 4.f;
const float bar_samples = bar_seconds * (sample_rate / static_cast<float>(seq_sample_div_));
bar_phasor_inc_ = 1.f / bar_samples;
const float clock_seconds = beat_seconds / 24.f;
midi_clock_phasor_inc_ = 1.f / (clock_seconds * sample_rate);
}
// Advances the MIDI-clock phasor by one sample. Returns true exactly on
// the sample the phasor wraps (caller emits a Clock event then).
NISPS_FORCE_INLINE bool tick_midi_clock() noexcept {
midi_clock_phasor_ += midi_clock_phasor_inc_;
if (midi_clock_phasor_ >= 1.f) {
midi_clock_phasor_ -= 1.f;
return true;
}
return false;
}
// Advances the control-rate sample counter by one sample, advancing (and
// wrapping) the bar phasor exactly when the counter was at 0 — i.e. once
// every `seq_sample_div_` samples. Returns whether the bar phasor
// advanced this call (caller should re-evaluate tracks against
// bar_phasor() when true).
NISPS_FORCE_INLINE bool tick_bar() noexcept {
bool fired = false;
if (sample_counter_ == 0u) {
bar_phasor_ += bar_phasor_inc_;
if (bar_phasor_ >= 1.f) bar_phasor_ -= 1.f;
fired = true;
}
++sample_counter_;
if (sample_counter_ >= seq_sample_div_) sample_counter_ = 0u;
return fired;
}
float bar_phasor() const noexcept { return bar_phasor_; }
private:
std::size_t seq_sample_div_;
float bpm_ = 90.f;
float bar_phasor_ = 0.f;
float bar_phasor_inc_ = 0.f;
float midi_clock_phasor_ = 0.f;
float midi_clock_phasor_inc_ = 0.f;
std::size_t sample_counter_ = 0u;
};
} // namespace nisps