feat: nisps/core foundation — perf, types, concepts, buffers, rng, math
Greenfield C++20 core for the unified firmware+WASM rewrite (architecture.md
streams, meml-dn7). Header-only, platform-agnostic, no heap, no virtual
dispatch.
Components:
- perf.hpp memory section + inlining macros, RP2040/RP2350-aware,
inert on host/Emscripten
- types.hpp stereosample_t (mirrors firmware AudioDriver API),
sample_t/param_t aliases, DriverConfig negotiation struct
- concepts.hpp MLEngine, AudioEngine, Mode (architecture §4.1-4.3)
- fixed_buffer.hpp std::array-backed cursor; replaces std::vector in hot paths
- ring_buffer.hpp SPSC lock-free FIFO, power-of-two capacity, atomic
head/tail; replaces pico/util/queue in core
- rng.hpp xoshiro256+ with splitmix64 seeding, uniform/signed/
gaussian-via-3-uniforms (matches legacy MoveWeights shape)
- math.hpp clamp01, fast_sigmoid (tanh-Padé, ~1.2% max err on [-6,6]),
exact_sigmoid, fast_exp, named Curve catalog (linear/exp/
log/square/sqrt/sigmoid/cubic) — TypeScript twin lives in
playground/src/output/curves.ts (stream 5)
Performance discipline (Chris's rules):
- No heap, no std::vector, no malloc/new in core
- All float literals carry .f suffix
- Memory section attrs syntactically present, inert on non-firmware builds
2026-04-29 14:21:52 +02:00
|
|
|
// nisps/core/math.hpp — small math helpers used by the audio path, the ML
|
|
|
|
|
// activations and the parameter curve mapping.
|
|
|
|
|
//
|
|
|
|
|
// CONTRACT WITH TYPESCRIPT (stream 5)
|
|
|
|
|
// The `Curve` enum below is mirrored in `playground/src/output/curves.ts`. The
|
|
|
|
|
// numeric meaning of each variant must stay identical so a parameter routed
|
|
|
|
|
// through firmware and a parameter routed through the browser produce the
|
|
|
|
|
// same value. JSON schemas reference curves by string name, so the source of
|
|
|
|
|
// truth for the *names* lives in the schema validator, but the *math* lives
|
|
|
|
|
// here. Golden-vector tests in `tests/cpp/parity_check.cpp` (stream 11)
|
|
|
|
|
// will pin this down.
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
|
|
|
|
namespace nisps {
|
|
|
|
|
|
|
|
|
|
inline float clamp01(float x) noexcept {
|
|
|
|
|
if (x < 0.f) return 0.f;
|
|
|
|
|
if (x > 1.f) return 1.f;
|
|
|
|
|
return x;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline float clamp(float x, float lo, float hi) noexcept {
|
|
|
|
|
if (x < lo) return lo;
|
|
|
|
|
if (x > hi) return hi;
|
|
|
|
|
return x;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fast_sigmoid: 3rd-order rational approximation via the Padé-approximant of
|
|
|
|
|
// tanh. We use the identity sigmoid(x) = 0.5 + 0.5 * tanh(x/2), and approx
|
|
|
|
|
// tanh(u) ≈ u * (27 + u²) / (27 + 9u²)
|
|
|
|
|
// which is the (3,2)-Padé expansion of tanh around 0.
|
|
|
|
|
//
|
|
|
|
|
// Max error vs true logistic 1/(1+e^-x) over x ∈ [-6, 6]: ≈ 0.012 (~1.2%).
|
|
|
|
|
// Outside that range the approximation drifts, so we clamp to [0, 1].
|
|
|
|
|
//
|
|
|
|
|
// For the ML output activation we need monotonic, smooth, bounded — NOT
|
|
|
|
|
// log-likelihood-grade accuracy. The MLP can opt into either fast_sigmoid
|
|
|
|
|
// or exp-based exact_sigmoid; both are provided.
|
|
|
|
|
inline float fast_sigmoid(float x) noexcept {
|
|
|
|
|
const float u = x * 0.5f;
|
|
|
|
|
const float u2 = u * u;
|
|
|
|
|
const float t = u * (27.f + u2) / (27.f + 9.f * u2);
|
|
|
|
|
const float y = 0.5f + 0.5f * t;
|
|
|
|
|
if (y < 0.f) return 0.f;
|
|
|
|
|
if (y > 1.f) return 1.f;
|
|
|
|
|
return y;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// True sigmoid via std::exp. Exact, slower; provided so callers that need
|
|
|
|
|
// gradient-correct activations have a path that avoids the ~1.8% bias.
|
|
|
|
|
inline float exact_sigmoid(float x) noexcept {
|
|
|
|
|
// Guard against extreme inputs to avoid expf overflow / underflow noise.
|
|
|
|
|
if (x > 40.f) return 1.f;
|
|
|
|
|
if (x < -40.f) return 0.f;
|
|
|
|
|
return 1.f / (1.f + std::exp(-x));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Approximate exp via the limit (1 + x/n)^n with n=256. Good to ~0.5% over
|
|
|
|
|
// x∈[-4, 4]; falls apart outside that range. Only use where exp is on the
|
|
|
|
|
// hot path AND inputs are bounded — otherwise reach for std::exp.
|
|
|
|
|
inline float fast_exp(float x) noexcept {
|
|
|
|
|
float r = 1.f + x * (1.f / 256.f);
|
|
|
|
|
r *= r; r *= r; r *= r; r *= r;
|
|
|
|
|
r *= r; r *= r; r *= r; r *= r; // 8 squarings ⇒ ^256
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Curve catalog. Each curve maps [0,1] → [0,1], monotone increasing, with
|
|
|
|
|
// the endpoints fixed at 0 and 1. The caller is responsible for clamping the
|
|
|
|
|
// input — apply_curve assumes x is already in range.
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
enum class Curve : int {
|
|
|
|
|
linear = 0,
|
|
|
|
|
exp = 1,
|
|
|
|
|
log = 2,
|
|
|
|
|
square = 3,
|
|
|
|
|
sqrt = 4,
|
|
|
|
|
sigmoid = 5,
|
|
|
|
|
cubic = 6,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
inline float apply_curve(Curve c, float x) noexcept {
|
|
|
|
|
switch (c) {
|
|
|
|
|
case Curve::linear: return x;
|
|
|
|
|
case Curve::exp: // (e^x - 1) / (e - 1) — concave-up, slow start
|
|
|
|
|
return (std::exp(x) - 1.f) * (1.f / 1.71828182845904523536f);
|
|
|
|
|
case Curve::log: // log(1 + (e-1) x) — concave-down, fast start
|
|
|
|
|
return std::log(1.f + 1.71828182845904523536f * x);
|
|
|
|
|
case Curve::square: return x * x;
|
|
|
|
|
case Curve::sqrt: return std::sqrt(x);
|
|
|
|
|
case Curve::sigmoid: {
|
|
|
|
|
// S-curve through (0,0) and (1,1). Stretch logistic and rescale.
|
|
|
|
|
const float k = 6.f; // slope at midpoint
|
|
|
|
|
const float s = exact_sigmoid(k * (x - 0.5f));
|
|
|
|
|
const float s0 = exact_sigmoid(-k * 0.5f);
|
|
|
|
|
const float s1 = exact_sigmoid( k * 0.5f);
|
|
|
|
|
return (s - s0) / (s1 - s0);
|
|
|
|
|
}
|
|
|
|
|
case Curve::cubic: return x * x * x;
|
|
|
|
|
}
|
|
|
|
|
return x; // unreachable, silences -Wreturn-type
|
|
|
|
|
}
|
|
|
|
|
|
feat(pipeline)!: P4 core — input/output chains + curve catalog in nisps/
- nisps/pipeline/input_chain.hpp: faithful f32 port of the manifold input
pipeline (invert→deadzone→circular clamp→momentum-modulated zoom→centred
power→EMA→momentum update). Caller-supplied dt, internal accumulated
clock (no wall clock — deterministic; matches the P1 fixtures' clock
contract). Fixed-capacity velocity ring; serialisable state.
- nisps/pipeline/output_chain.hpp: curve→EMA→slew→freeze(+per-output mask)
chain, capacity-templated (browser cap 4096; firmware would use NOut).
- nisps/core/math.hpp: + centered_power(x, exponent) (both chains use it).
- bindings: nisps_pipeline_create/destroy, nisps_input_set_config(15-float
wire layout)/process/reset, nisps_output_set_config/set_freeze_mask/
process/reset, pipeline state save/load, nisps_curve_apply(+batch)
(ids 0-6 = Curve enum, 7 = centred power).
- parity v5 Stage 7: rational (transcendental-free) traces through both
chains (2 configs each) + full curve catalog — 1273 floats PASS, the
pipeline floats bit-identical native↔WASM.
- ctest test_pipeline.cpp: deadzone remap, circular clamp, zoom+freeze,
sticky anchor, frame-rate-independent EMA, momentum zoom-out/recovery,
state round-trip, slew, freeze gate/mask, reseed-on-length-change,
centred-power endpoints.
Part of one-core-engine-refactor P4; the manifold TS switch follows.
2026-07-18 11:52:53 +02:00
|
|
|
// Centred power curve — pivots around 0.5 instead of 0 (from the legacy
|
|
|
|
|
// input/output pipelines; both nisps/pipeline chains use it):
|
|
|
|
|
// exponent < 1 → push toward the extremes
|
|
|
|
|
// exponent = 1 → identity
|
|
|
|
|
// exponent > 1 → pull toward the centre
|
|
|
|
|
// Parameterised, so it lives beside the Curve enum rather than inside it
|
|
|
|
|
// (schema param curves don't carry a parameter).
|
|
|
|
|
inline float centered_power(float x, float exponent) noexcept {
|
|
|
|
|
if (exponent == 1.f) return clamp01(x);
|
|
|
|
|
const float offset = x - 0.5f;
|
|
|
|
|
const float sign = (offset < 0.f) ? -1.f : 1.f;
|
|
|
|
|
// Range [-0.5, 0.5] → [-1, 1] for the power op, then halve back.
|
|
|
|
|
const float shaped =
|
|
|
|
|
sign * std::pow(std::fabs(offset) * 2.f, exponent) * 0.5f;
|
|
|
|
|
return clamp01(shaped + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
|
feat: nisps/core foundation — perf, types, concepts, buffers, rng, math
Greenfield C++20 core for the unified firmware+WASM rewrite (architecture.md
streams, meml-dn7). Header-only, platform-agnostic, no heap, no virtual
dispatch.
Components:
- perf.hpp memory section + inlining macros, RP2040/RP2350-aware,
inert on host/Emscripten
- types.hpp stereosample_t (mirrors firmware AudioDriver API),
sample_t/param_t aliases, DriverConfig negotiation struct
- concepts.hpp MLEngine, AudioEngine, Mode (architecture §4.1-4.3)
- fixed_buffer.hpp std::array-backed cursor; replaces std::vector in hot paths
- ring_buffer.hpp SPSC lock-free FIFO, power-of-two capacity, atomic
head/tail; replaces pico/util/queue in core
- rng.hpp xoshiro256+ with splitmix64 seeding, uniform/signed/
gaussian-via-3-uniforms (matches legacy MoveWeights shape)
- math.hpp clamp01, fast_sigmoid (tanh-Padé, ~1.2% max err on [-6,6]),
exact_sigmoid, fast_exp, named Curve catalog (linear/exp/
log/square/sqrt/sigmoid/cubic) — TypeScript twin lives in
playground/src/output/curves.ts (stream 5)
Performance discipline (Chris's rules):
- No heap, no std::vector, no malloc/new in core
- All float literals carry .f suffix
- Memory section attrs syntactically present, inert on non-firmware builds
2026-04-29 14:21:52 +02:00
|
|
|
} // namespace nisps
|