memlnaut-nisps/nisps/engines/breakor.hpp
monkey-w1n5t0n ea588e79cd refactor(nisps): delete dead engine/mode mass
Phase 1 group 3 (L3, L9, L4, L5, L6, ST1).

- L3: nisps/modes/voice_space.hpp — entirely dead, no includes anywhere.
- L4: deleted SawOsc and SquareOsc. KEPT SineOsc — a verifier caught that the
  original reviewer's grep missed its live consumer (the firmware selftest);
  re-confirmed here before touching the file.
- L9: removed the no-op VoiceSpace enum/table/setter boilerplate from the five
  engines with no real voice spaces; kept it on PAFSynth, VerbFX and
  ChannelStrip, which have real ones. Every engine member was checked against
  nisps/wasm/bindings.cpp, firmware/ and tests/ for callers first.
- L5: ModeBase::input_dirty_ was write-only state — deleted the flag rather
  than making it gate inference, which would have been a behaviour change.
- L6: VerbFXEngine's delay_to_verb_ (computed 12x per block, never read),
  enable_reverb_, enable_delay_to_reverb_ and the unused set_enable_* setters.
- ST1: rewrote the four engine header comment blocks that described
  implementations which do not exist.

L7 (MEMLCeliumEngine's inert feedback path) is deliberately NOT done — tracing
git history showed feedbackGain went 0.1f (live) -> "0; //0.1f" (explicitly
muted, value preserved) -> dropped entirely in the port. That is a muted
feature, not dead weight, and deleting it would silently lose it. Left intact
pending an operator decision; see the phase report.

Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:48:50 +02:00

224 lines
8.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// nisps/engines/breakor.hpp — 8-track ratio-sequencer.
//
// `process()` returns silence directly; the engine's job is to emit
// NoteOn/NoteOff/Clock events on each tick via `pop_events()`. BreakOrMode
// (mode layer) drains these into its ControlEvent ring for platform glue
// (firmware MIDI/I2C, browser WebMIDI) to forward.
//
// Param layout: 8 tracks × 7 ratio-seq params each = 56 params.
// per track: [ratio0, ratio1, ratio2, phasorMul, phaseOff, ampRatio0, ampRatio1]
//
// Default MIDI notes: {36,37,38,39,40,42,43,45} (kick/snare/toms/hats etc.).
#pragma once
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string_view>
#include "../core/concepts.hpp"
#include "../core/perf.hpp"
#include "../core/types.hpp"
namespace nisps {
class BreakOrEngine {
public:
static constexpr std::size_t kNSequences = 8u;
static constexpr std::size_t kSeqParamsEach = 7u;
static constexpr std::size_t kNParams = kNSequences * kSeqParamsEach; // = 56
static constexpr std::size_t kEventBufferSize = 64u;
static constexpr std::size_t param_count() noexcept { return kNParams; }
static constexpr std::string_view engine_id() noexcept { return "breakor"; }
enum class EventKind : std::uint8_t { NoteOn, NoteOff, Clock };
struct Event {
EventKind kind;
std::uint8_t track;
std::uint8_t midi_note;
std::uint8_t velocity;
};
void setup(float sample_rate) noexcept {
sample_rate_ = sample_rate;
static const std::uint8_t default_notes[kNSequences] = {36u,37u,38u,39u,40u,42u,43u,45u};
for (std::size_t i = 0u; i < kNSequences; ++i) {
tracks_[i].midi_note = default_notes[i];
tracks_[i].last_trig = false;
}
bar_phasor_ = 0.f;
midi_clock_phasor_ = 0.f;
sequencing_sample_counter_ = 0u;
update_bpm(90.f);
}
void set_params(std::span<const float> params) noexcept {
if (params.size() < kNParams) return;
std::size_t i = 0u;
for (auto& t : tracks_) {
float sum = 0.f;
for (std::size_t r = 0u; r < 3u; ++r) {
t.ratios[r] = static_cast<float>(static_cast<int>(params[i++] * 3.f)) + 1.f;
sum += t.ratios[r];
}
t.ratio_sum = sum;
static const float muls[4] = {1.f, 2.f, 4.f, 8.f};
t.phasor_mul = muls[static_cast<int>(params[i++] * 3.999999f) & 3];
t.phase_off = static_cast<float>(static_cast<int>(params[i++] * 4.f)) * 0.25f;
sum = 0.f;
for (std::size_t r = 0u; r < 2u; ++r) {
t.amp_ratios[r] = static_cast<float>(static_cast<int>(params[i++] * 3.f)) + 1.f;
sum += t.amp_ratios[r];
}
t.amp_ratio_sum = sum;
}
}
NISPS_HOT NISPS_FORCE_INLINE stereosample_t process(stereosample_t /*x*/) noexcept {
if (!playing_) return {0.f, 0.f};
// MIDI clock: 24 PPQN — emit on clock-phasor wrap.
midi_clock_phasor_ += midi_clock_phasor_inc_;
if (midi_clock_phasor_ >= 1.f) {
midi_clock_phasor_ -= 1.f;
push_event({EventKind::Clock, 0u, 0u, 0u});
}
// Sequencer ticks at sample-rate / kSequencingSampleDiv.
if (sequencing_sample_counter_ == 0u) {
bar_phasor_ += bar_phasor_inc_;
if (bar_phasor_ >= 1.f) bar_phasor_ -= 1.f;
for (std::size_t i = 0u; i < kNSequences; ++i) {
auto& t = tracks_[i];
float seq_phasor = bar_phasor_ * t.phasor_mul;
seq_phasor = std::fmod(seq_phasor + t.phase_off, 1.f);
const bool trig = ratio_seq_3(seq_phasor, t.ratio_sum, t.ratios, 0.5f);
const bool high_amp = ratio_seq_2(seq_phasor, t.amp_ratio_sum, t.amp_ratios, 0.5f);
if (trig && !t.last_trig) {
const std::uint8_t v = high_amp ? 127u : 64u;
push_event({EventKind::NoteOn, static_cast<std::uint8_t>(i), t.midi_note, v});
} else if (!trig && t.last_trig) {
push_event({EventKind::NoteOff, static_cast<std::uint8_t>(i), t.midi_note, 0u});
}
t.last_trig = trig;
}
}
++sequencing_sample_counter_;
if (sequencing_sample_counter_ >= kSequencingSampleDiv) sequencing_sample_counter_ = 0u;
return {0.f, 0.f};
}
DriverConfig driver_config() const noexcept { return {}; }
// Event interface — drains `out` with up to `out.size()` queued events.
// Returns how many were copied.
std::size_t pop_events(std::span<Event> out) noexcept {
std::size_t n = 0u;
while (n < out.size() && event_count_ > 0u) {
out[n++] = events_[event_read_];
event_read_ = (event_read_ + 1u) % kEventBufferSize;
--event_count_;
}
return n;
}
void update_bpm(float bpm) 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>(kSequencingSampleDiv));
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_);
}
void set_playing(bool playing) noexcept {
playing_ = playing;
if (!playing) {
bar_phasor_ = 0.f;
midi_clock_phasor_ = 0.f;
sequencing_sample_counter_ = 0u;
for (auto& t : tracks_) {
if (t.last_trig) {
push_event({EventKind::NoteOff, 0u, t.midi_note, 0u});
}
t.last_trig = false;
}
}
}
void set_track_note(std::size_t track, std::uint8_t note) noexcept {
if (track < kNSequences) tracks_[track].midi_note = note;
}
private:
static constexpr std::size_t kSequencingSampleDiv = 400u;
struct Track {
std::array<float, 3> ratios{1.f, 1.f, 1.f};
std::array<float, 2> amp_ratios{1.f, 1.f};
float ratio_sum = 3.f;
float amp_ratio_sum = 2.f;
float phasor_mul = 1.f;
float phase_off = 0.f;
std::uint8_t midi_note = 36u;
bool last_trig = false;
};
template <std::size_t N>
static bool ratio_seq(float phasor, float ratio_sum,
const std::array<float, N>& ratios,
float pulse_width) noexcept {
float offset_phase = phasor;
if (offset_phase >= 1.f) offset_phase -= 1.f;
const float phase_adj = ratio_sum * offset_phase;
float accum = 0.f, last = 0.f;
for (std::size_t i = 0u; i < N; ++i) {
accum += ratios[i];
if (phase_adj <= accum) {
const float beat_phase = (phase_adj - last) / (accum - last);
return beat_phase <= pulse_width;
}
last = accum;
}
return false;
}
static bool ratio_seq_3(float p, float s, const std::array<float, 3>& r, float pw) noexcept {
return ratio_seq<3>(p, s, r, pw);
}
static bool ratio_seq_2(float p, float s, const std::array<float, 2>& r, float pw) noexcept {
return ratio_seq<2>(p, s, r, pw);
}
NISPS_FORCE_INLINE void push_event(const Event& e) noexcept {
if (event_count_ >= kEventBufferSize) return; // drop on overflow
events_[event_write_] = e;
event_write_ = (event_write_ + 1u) % kEventBufferSize;
++event_count_;
}
float sample_rate_ = 48000.f;
float bpm_ = 90.f;
bool playing_ = true;
std::array<Track, kNSequences> tracks_;
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 sequencing_sample_counter_ = 0u;
std::array<Event, kEventBufferSize> events_{};
std::size_t event_read_ = 0u;
std::size_t event_write_ = 0u;
std::size_t event_count_ = 0u;
};
static_assert(AudioEngine<BreakOrEngine>, "BreakOrEngine must satisfy AudioEngine");
} // namespace nisps