480 lines
16 KiB
C++
480 lines
16 KiB
C++
|
|
// UGen tests: phasor, lfo, slew, one-pole, env-follow, sah, noise, toggle, count
|
|||
|
|
//
|
|||
|
|
// Tests exercise state-slot allocation, cross-tick accumulation, keyword
|
|||
|
|
// parsing, and alias resolution.
|
|||
|
|
|
|||
|
|
#define CATCH_CONFIG_MAIN
|
|||
|
|
#include "../catch.hpp"
|
|||
|
|
|
|||
|
|
#include "src/signal_engine/signal_engine.h"
|
|||
|
|
#include <cmath>
|
|||
|
|
#include <cstring>
|
|||
|
|
#include <string>
|
|||
|
|
#include <vector>
|
|||
|
|
#include <initializer_list>
|
|||
|
|
|
|||
|
|
using namespace sig;
|
|||
|
|
|
|||
|
|
#ifndef M_PI
|
|||
|
|
#define M_PI 3.14159265358979323846
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
namespace {
|
|||
|
|
|
|||
|
|
struct GoldenHarness {
|
|||
|
|
SignalEngine engine;
|
|||
|
|
double cell_values[MAX_CELLS] = {};
|
|||
|
|
double hw_inputs[32] = {};
|
|||
|
|
double outputs[MAX_OUTPUTS] = {};
|
|||
|
|
double workspace[MAX_TOTAL_NODES] = {};
|
|||
|
|
double prev_t = 0.0;
|
|||
|
|
double last_dt = 0.0;
|
|||
|
|
bool has_ticked = false;
|
|||
|
|
bool state_committed_this_step = false;
|
|||
|
|
|
|||
|
|
explicit GoldenHarness(double bpm = 120.0, int beats_per_bar = 4) {
|
|||
|
|
engine.init_defaults(bpm, beats_per_bar);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EvalResult eval_result(const std::string& code) {
|
|||
|
|
return eval_cold(code.c_str(), static_cast<uint32_t>(code.size()), engine);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void eval_ok(const std::string& code) {
|
|||
|
|
EvalResult r = eval_result(code);
|
|||
|
|
INFO("code: " << code);
|
|||
|
|
if (r.kind == EvalResult::Error && r.diagnostic_count > 0) {
|
|||
|
|
INFO("diagnostic: " << (r.diagnostics[0].message ? r.diagnostics[0].message : ""));
|
|||
|
|
INFO("suggestion: " << (r.diagnostics[0].suggestion ? r.diagnostics[0].suggestion : ""));
|
|||
|
|
}
|
|||
|
|
REQUIRE(r.kind != EvalResult::Error);
|
|||
|
|
engine.pool.rebuild_execution_order();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void assign_ok(const char* output, const char* expr) {
|
|||
|
|
eval_ok(std::string("(") + output + " " + expr + ")");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uint16_t output_index(const char* output_name) {
|
|||
|
|
SymbolID sym = internSymbol(output_name);
|
|||
|
|
uint16_t idx = GraphBuilder::resolve_output_index(sym);
|
|||
|
|
REQUIRE(idx != NODE_NONE);
|
|||
|
|
return idx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double sample(const char* output_name, double t) {
|
|||
|
|
std::memset(outputs, 0, sizeof(outputs));
|
|||
|
|
std::memset(workspace, 0, sizeof(workspace));
|
|||
|
|
engine.cells.snapshot_values(cell_values, MAX_CELLS);
|
|||
|
|
|
|||
|
|
last_dt = t - prev_t;
|
|||
|
|
|
|||
|
|
if (engine.pool.state_slot_count > 0 &&
|
|||
|
|
has_ticked && t != prev_t && !state_committed_this_step)
|
|||
|
|
{
|
|||
|
|
ExecutionContext state_ctx;
|
|||
|
|
state_ctx.t = t;
|
|||
|
|
state_ctx.dt = last_dt;
|
|||
|
|
state_ctx.cell_values = cell_values;
|
|||
|
|
state_ctx.hw_inputs = hw_inputs;
|
|||
|
|
state_ctx.data_pool = engine.cells.data_pool;
|
|||
|
|
state_ctx.data_offsets = engine.cells.data_offsets;
|
|||
|
|
state_ctx.data_lengths = engine.cells.data_lengths;
|
|||
|
|
state_ctx.prev_outputs = engine.pool.prev_output_values;
|
|||
|
|
state_ctx.output_values = outputs;
|
|||
|
|
state_ctx.workspace = workspace;
|
|||
|
|
execute_all_outputs(engine.pool, state_ctx);
|
|||
|
|
commit_state(engine.pool, workspace);
|
|||
|
|
state_committed_this_step = true;
|
|||
|
|
std::memset(workspace, 0, sizeof(workspace));
|
|||
|
|
std::memset(outputs, 0, sizeof(outputs));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ExecutionContext ctx;
|
|||
|
|
ctx.t = t;
|
|||
|
|
ctx.dt = last_dt;
|
|||
|
|
ctx.cell_values = cell_values;
|
|||
|
|
ctx.hw_inputs = hw_inputs;
|
|||
|
|
ctx.data_pool = engine.cells.data_pool;
|
|||
|
|
ctx.data_offsets = engine.cells.data_offsets;
|
|||
|
|
ctx.data_lengths = engine.cells.data_lengths;
|
|||
|
|
ctx.prev_outputs = engine.pool.prev_output_values;
|
|||
|
|
ctx.output_values = outputs;
|
|||
|
|
ctx.workspace = workspace;
|
|||
|
|
execute_all_outputs(engine.pool, ctx);
|
|||
|
|
|
|||
|
|
return outputs[output_index(output_name)];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double tick(const char* output_name, double t) {
|
|||
|
|
if (t != prev_t) state_committed_this_step = false;
|
|||
|
|
double value = sample(output_name, t);
|
|||
|
|
commit_outputs(engine.pool, outputs);
|
|||
|
|
has_ticked = true;
|
|||
|
|
prev_t = t;
|
|||
|
|
return value;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} // anonymous namespace
|
|||
|
|
|
|||
|
|
// ── phasor ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: phasor produces phase ramp [0,1)", "[ugens][phasor]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(phasor 1.0)");
|
|||
|
|
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(0.1));
|
|||
|
|
REQUIRE(h.tick("a1", 0.2) == Approx(0.2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: phasor wraps at 1.0", "[ugens][phasor]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(phasor 1.0)");
|
|||
|
|
|
|||
|
|
// Tick through one full cycle (10 × dt=0.1 = 1.0s)
|
|||
|
|
for (int i = 0; i < 10; i++) {
|
|||
|
|
h.tick("a1", i * 0.1);
|
|||
|
|
}
|
|||
|
|
// After 1.0s: phase = frac(1.0) = 0.0
|
|||
|
|
REQUIRE(h.tick("a1", 1.0) == Approx(0.0).margin(1e-10));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: phasor with :phase init", "[ugens][phasor]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(phasor 1.0 :phase 0.5)");
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.5));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: phasor at 2Hz", "[ugens][phasor]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(phasor 2.0)");
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
// dt=0.1, state = frac(0 + 2.0*0.1) = 0.2
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(0.2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── lfo ─────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo default wave is sine", "[ugens][lfo]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo 1.0)");
|
|||
|
|
|
|||
|
|
// usin(0) = (sin(0)+1)/2 = 0.5
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.5));
|
|||
|
|
|
|||
|
|
// After dt=0.25: phase = 0.25, usin(0.25) = (sin(2π*0.25)+1)/2 = 1.0
|
|||
|
|
REQUIRE(h.tick("a1", 0.25) == Approx(1.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo :wave :saw produces phasor output", "[ugens][lfo]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo 1.0 :wave :saw)");
|
|||
|
|
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(0.1));
|
|||
|
|
REQUIRE(h.tick("a1", 0.2) == Approx(0.2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo :wave :tri produces triangle", "[ugens][lfo]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo 1.0 :wave :tri)");
|
|||
|
|
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // tri(0) = 0
|
|||
|
|
// After dt=0.25: phase=0.25, tri(0.25) = 1 - |2*0.25 - 1| = 0.5
|
|||
|
|
REQUIRE(h.tick("a1", 0.25) == Approx(0.5));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo :wave :sqr produces square", "[ugens][lfo]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo 1.0 :wave :sqr)");
|
|||
|
|
|
|||
|
|
// phase=0 → frac(0)=0 < 0.5 → 1
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(1.0));
|
|||
|
|
// phase=0.1 → < 0.5 → 1
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(1.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo :sqr with :pw", "[ugens][lfo]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo 1.0 :wave :sqr :pw 0.25)");
|
|||
|
|
|
|||
|
|
// phase=0 < 0.25 → high
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(1.0));
|
|||
|
|
// phase=0.1 < 0.25 → still high
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(1.0));
|
|||
|
|
// phase=0.3 > 0.25 → low
|
|||
|
|
h.tick("a1", 0.2);
|
|||
|
|
REQUIRE(h.tick("a1", 0.3) == Approx(0.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── namespaced lfo waveforms ────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo/sin is a unipolar sine lfo", "[ugens][lfo][namespace]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo/sin 1.0)");
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.5)); // usin(0) = 0.5
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo/saw is a saw lfo", "[ugens][lfo][namespace]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo/saw 1.0)");
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(0.1));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo/tri is a triangle lfo", "[ugens][lfo][namespace]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo/tri 1.0)");
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // tri(0) = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: lfo/sqr is a square lfo", "[ugens][lfo][namespace]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(lfo/sqr 1.0)");
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(1.0)); // phase < 0.5 → 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── slew ────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: slew limits rate of change", "[ugens][slew]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
// Use ain1 (bare symbol, not function call) as input
|
|||
|
|
h.assign_ok("a1", "(slew ain1 10.0)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 0.0; // ain1 = INP_AI1 (index 8)
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
|
|||
|
|
// Jump target to 1.0, dt=0.1, max_step = 10*0.1 = 1.0
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(1.0)); // delta=1.0 ≤ step=1.0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: slew clamps when target changes too fast", "[ugens][slew]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(slew ain1 1.0)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
|
|||
|
|
// Target jumps to 10, max_step = 1.0*0.1 = 0.1
|
|||
|
|
h.hw_inputs[8] = 10.0;
|
|||
|
|
double val = h.tick("a1", 0.1);
|
|||
|
|
REQUIRE(val == Approx(0.1));
|
|||
|
|
|
|||
|
|
val = h.tick("a1", 0.2);
|
|||
|
|
REQUIRE(val == Approx(0.2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── one-pole ────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: one-pole low-pass filter", "[ugens][one-pole]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(one-pole ain1 10.0)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // initial state = 0
|
|||
|
|
|
|||
|
|
// dt=0.01, alpha = min(1, 2*pi*10*0.01) ≈ 0.628
|
|||
|
|
double val = h.tick("a1", 0.01);
|
|||
|
|
double alpha = std::min(1.0, 2.0 * M_PI * 10.0 * 0.01);
|
|||
|
|
REQUIRE(val == Approx(alpha).margin(0.01));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── env-follow ──────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: env-follow tracks absolute input", "[ugens][env-follow]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(env-follow ain1 100.0 10.0)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
|
|||
|
|
// dt=0.01, attack=100, a_coeff = min(1, 100*0.01) = 1.0
|
|||
|
|
// diff = 1.0 - 0 = 1.0, new = 0 + 1.0*1.0 = 1.0
|
|||
|
|
double val = h.tick("a1", 0.01);
|
|||
|
|
REQUIRE(val == Approx(1.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: envelope-follower is alias", "[ugens][env-follow][alias]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(envelope-follower ain1 100.0 10.0)");
|
|||
|
|
h.hw_inputs[8] = 0.5;
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── sah (sample-and-hold) ───────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: sah samples on rising edge", "[ugens][sah]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(sah ain1 ain2)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 0.42; // input
|
|||
|
|
h.hw_inputs[9] = 0.0; // trigger low
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // initial = 0, no trigger
|
|||
|
|
|
|||
|
|
// Trigger rises: ain2 goes from 0 → 1 (> 0.5)
|
|||
|
|
h.hw_inputs[9] = 1.0;
|
|||
|
|
double val = h.tick("a1", 0.1);
|
|||
|
|
REQUIRE(val == Approx(0.42)); // sampled the input
|
|||
|
|
|
|||
|
|
// Trigger stays high, input changes — should hold
|
|||
|
|
h.hw_inputs[8] = 0.99;
|
|||
|
|
val = h.tick("a1", 0.2);
|
|||
|
|
REQUIRE(val == Approx(0.42)); // still holding
|
|||
|
|
|
|||
|
|
// Trigger goes low, then high again with new value
|
|||
|
|
h.hw_inputs[9] = 0.0;
|
|||
|
|
h.tick("a1", 0.3);
|
|||
|
|
h.hw_inputs[8] = 0.77;
|
|||
|
|
h.hw_inputs[9] = 1.0;
|
|||
|
|
val = h.tick("a1", 0.4);
|
|||
|
|
REQUIRE(val == Approx(0.77)); // sampled new value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: latch is alias for sah", "[ugens][sah][alias]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(latch ain1 ain2)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 0.5;
|
|||
|
|
h.hw_inputs[9] = 0.0;
|
|||
|
|
h.tick("a1", 0.0);
|
|||
|
|
|
|||
|
|
h.hw_inputs[9] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(0.5));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── noise ───────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: noise produces values in [-1,1]", "[ugens][noise]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(noise)");
|
|||
|
|
|
|||
|
|
bool found_positive = false;
|
|||
|
|
bool found_negative = false;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < 20; i++) {
|
|||
|
|
double val = h.tick("a1", i * 0.01);
|
|||
|
|
REQUIRE(val >= -1.0);
|
|||
|
|
REQUIRE(val <= 1.0);
|
|||
|
|
if (val > 0.1) found_positive = true;
|
|||
|
|
if (val < -0.1) found_negative = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
REQUIRE(found_positive);
|
|||
|
|
REQUIRE(found_negative);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: noise is deterministic", "[ugens][noise]") {
|
|||
|
|
GoldenHarness h1, h2;
|
|||
|
|
h1.assign_ok("a1", "(noise)");
|
|||
|
|
h2.assign_ok("a1", "(noise)");
|
|||
|
|
|
|||
|
|
for (int i = 0; i < 10; i++) {
|
|||
|
|
double v1 = h1.tick("a1", i * 0.01);
|
|||
|
|
double v2 = h2.tick("a1", i * 0.01);
|
|||
|
|
REQUIRE(v1 == Approx(v2));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── toggle ──────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: toggle flips on rising edge", "[ugens][toggle]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(toggle ain1)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // initial = 0
|
|||
|
|
|
|||
|
|
// Trigger rises → toggle: 0 → 1
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(1.0));
|
|||
|
|
|
|||
|
|
// Trigger stays high → no change
|
|||
|
|
REQUIRE(h.tick("a1", 0.2) == Approx(1.0));
|
|||
|
|
|
|||
|
|
// Trigger goes low → no change
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.3) == Approx(1.0));
|
|||
|
|
|
|||
|
|
// Trigger rises again → toggle: 1 → 0
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.4) == Approx(0.0));
|
|||
|
|
|
|||
|
|
// And again → 0 → 1
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
h.tick("a1", 0.5);
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.6) == Approx(1.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── count ───────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: count increments on trigger", "[ugens][count]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(count ain1)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.0) == Approx(0.0));
|
|||
|
|
|
|||
|
|
// Rising edge → count = 1
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.1) == Approx(1.0));
|
|||
|
|
|
|||
|
|
// Still high → no increment
|
|||
|
|
REQUIRE(h.tick("a1", 0.2) == Approx(1.0));
|
|||
|
|
|
|||
|
|
// Low → no increment
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.3) == Approx(1.0));
|
|||
|
|
|
|||
|
|
// Rising again → count = 2
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.4) == Approx(2.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TEST_CASE("UGen: count with :reset", "[ugens][count]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(count ain1 :reset ain2)");
|
|||
|
|
|
|||
|
|
h.hw_inputs[8] = 0.0; // trigger
|
|||
|
|
h.hw_inputs[9] = 0.0; // reset
|
|||
|
|
h.tick("a1", 0.0);
|
|||
|
|
|
|||
|
|
// Count up: 2 triggers
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
h.tick("a1", 0.1);
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
h.tick("a1", 0.2);
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.3) == Approx(2.0));
|
|||
|
|
|
|||
|
|
// Reset with rising edge on ain2
|
|||
|
|
h.hw_inputs[9] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.4) == Approx(0.0)); // count reset to 0
|
|||
|
|
|
|||
|
|
// Count again after reset: trigger goes low then high
|
|||
|
|
h.hw_inputs[8] = 0.0;
|
|||
|
|
h.hw_inputs[9] = 0.0;
|
|||
|
|
h.tick("a1", 0.5); // trigger low, reset low
|
|||
|
|
h.hw_inputs[8] = 1.0;
|
|||
|
|
REQUIRE(h.tick("a1", 0.6) == Approx(1.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Classification ──────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
TEST_CASE("Classification: UGens are stateful", "[ugens][classification]") {
|
|||
|
|
GoldenHarness h;
|
|||
|
|
h.assign_ok("a1", "(phasor 1.0)");
|
|||
|
|
REQUIRE(h.engine.pool.output_class[h.output_index("a1")] == OutputClass::Stateful);
|
|||
|
|
|
|||
|
|
GoldenHarness h2;
|
|||
|
|
h2.assign_ok("a1", "(lfo 1.0)");
|
|||
|
|
REQUIRE(h2.engine.pool.output_class[h2.output_index("a1")] == OutputClass::Stateful);
|
|||
|
|
|
|||
|
|
GoldenHarness h3;
|
|||
|
|
h3.assign_ok("a1", "(noise)");
|
|||
|
|
REQUIRE(h3.engine.pool.output_class[h3.output_index("a1")] == OutputClass::Stateful);
|
|||
|
|
}
|