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
85 lines
4.1 KiB
C++
85 lines
4.1 KiB
C++
// nisps/core/concepts.hpp — C++20 concepts that pin down the shape of the
|
|
// three central types in the rewrite: ML engines, audio engines and modes.
|
|
//
|
|
// These are CONCEPTS, not interfaces — there is no virtual dispatch in the
|
|
// hot paths. Concrete types are templated/satisfied at compile time on
|
|
// firmware; the WASM bindings layer flattens to a C API for the browser.
|
|
//
|
|
// See architecture.md §4.1, §4.2, §4.3.
|
|
|
|
#pragma once
|
|
|
|
#include <concepts>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <span>
|
|
#include <string_view>
|
|
|
|
#include "types.hpp"
|
|
|
|
namespace nisps {
|
|
|
|
// Forward declaration. Concrete `ParamSchema` lives with the codegen output
|
|
// (stream 5). Modes only need to expose a constexpr reference to it.
|
|
struct ParamSchema;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MLEngine — interactive ML inference + training + RL state perturbation.
|
|
// Implementations: nisps::MLP<NIn, NHidden..., NOut> in stream 2.
|
|
// ---------------------------------------------------------------------------
|
|
template <typename T>
|
|
concept MLEngine = requires(T e,
|
|
std::span<const float> in,
|
|
std::span<float> out,
|
|
std::size_t idx,
|
|
float fv,
|
|
std::uint64_t seed) {
|
|
{ e.set_input(idx, fv) } -> std::same_as<void>;
|
|
{ e.process() } -> std::same_as<void>;
|
|
{ e.outputs() } -> std::same_as<std::span<const float>>;
|
|
{ e.add_example(in, in) } -> std::same_as<void>;
|
|
{ e.train() } -> std::same_as<float>; // returns final loss
|
|
{ e.move_weights(fv, fv) } -> std::same_as<void>; // (speed, spread)
|
|
{ e.draw_weights(fv) } -> std::same_as<void>; // (spread)
|
|
{ e.reset() } -> std::same_as<void>;
|
|
{ e.seed(seed) } -> std::same_as<void>;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioEngine — per-sample stereo processor. Sample-rate is negotiated at
|
|
// setup; engines should NOT assume 48 kHz.
|
|
// ---------------------------------------------------------------------------
|
|
template <typename T>
|
|
concept AudioEngine = requires(T e,
|
|
stereosample_t s,
|
|
std::span<const float> params,
|
|
float sr) {
|
|
{ T::param_count() } -> std::convertible_to<std::size_t>; // constexpr
|
|
{ T::engine_id() } -> std::same_as<std::string_view>; // constexpr
|
|
{ e.setup(sr) } -> std::same_as<void>;
|
|
{ e.set_params(params) } -> std::same_as<void>; // non-RT
|
|
{ e.process(s) } -> std::same_as<stereosample_t>; // RT, per-sample
|
|
{ e.driver_config() } -> std::convertible_to<DriverConfig>;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mode — binds an ML config + audio engine + voice space + I/O channel
|
|
// mapping. Hardware bindings live OUTSIDE this concept (firmware/glue,
|
|
// playground/src/modes); modes are platform-agnostic.
|
|
// ---------------------------------------------------------------------------
|
|
template <typename T>
|
|
concept Mode = requires(T m, std::size_t idx, float v, stereosample_t s, float sr) {
|
|
{ T::mode_id() } -> std::same_as<std::string_view>; // constexpr
|
|
{ T::input_channel_count() } -> std::convertible_to<std::size_t>; // constexpr
|
|
{ T::param_schema() } -> std::same_as<const ParamSchema&>; // constexpr ref
|
|
{ m.setup(sr) } -> std::same_as<void>;
|
|
{ m.set_input(idx, v) } -> std::same_as<void>;
|
|
{ m.tick_control() } -> std::same_as<void>; // non-RT
|
|
{ m.process(s) } -> std::same_as<stereosample_t>; // RT
|
|
// engine() / ml() return references; we only assert they are callable.
|
|
// (`auto&` in concept signatures is awkward; require non-void.)
|
|
{ m.engine() };
|
|
{ m.ml() };
|
|
};
|
|
|
|
} // namespace nisps
|