feat(modes): ModeBase input-pin API for single/dual joystick
Platform-agnostic neutral-pin mechanism (set_input_pinned/pin_value) on ModeBase, lifted from ebb953a. Consumed by the firmware Joystick Dual/Single settings menu; the browser fixed-MLP<4> half of ebb953a is superseded by main's MLP<32> N-D input work and intentionally dropped.
This commit is contained in:
parent
c955e94a7a
commit
2f9cfd1de2
2 changed files with 90 additions and 3 deletions
|
|
@ -141,7 +141,7 @@ class ModeBase {
|
|||
// Run an inference at default inputs so engine has params on first
|
||||
// process() call, even if no input has been touched.
|
||||
for (std::size_t i = 0u; i < NInputs; ++i) {
|
||||
ml_.set_input(i, input_channels_[i]);
|
||||
ml_.set_input(i, effective_input(i));
|
||||
}
|
||||
ml_.process();
|
||||
if constexpr (kRouteOutputsToEngine) {
|
||||
|
|
@ -160,13 +160,42 @@ class ModeBase {
|
|||
input_dirty_ = true;
|
||||
}
|
||||
|
||||
// ---- Input neutralization (single/double controller toggle) ----
|
||||
//
|
||||
// A pinned channel feeds `pin_value_` (neutral, default 0.5) to the MLP
|
||||
// instead of its live value, without rebuilding/resizing the network.
|
||||
// Glue toggles which channels are pinned (e.g. single-joystick mode pins
|
||||
// the second 2D controller's two channels). The stored live value is left
|
||||
// untouched, so unpinning resumes from the controller's current position.
|
||||
NISPS_FORCE_INLINE void set_input_pinned(std::size_t idx, bool pinned) noexcept {
|
||||
if (idx >= NInputs) return;
|
||||
input_pinned_[idx] = pinned;
|
||||
input_dirty_ = true;
|
||||
}
|
||||
NISPS_FORCE_INLINE bool is_input_pinned(std::size_t idx) const noexcept {
|
||||
return idx < NInputs && input_pinned_[idx];
|
||||
}
|
||||
NISPS_FORCE_INLINE void set_pin_value(float v) noexcept {
|
||||
if (v < 0.f) v = 0.f;
|
||||
else if (v > 1.f) v = 1.f;
|
||||
pin_value_ = v;
|
||||
input_dirty_ = true;
|
||||
}
|
||||
float pin_value() const noexcept { return pin_value_; }
|
||||
|
||||
// Effective value fed to the MLP for channel i (pin override applied).
|
||||
NISPS_FORCE_INLINE float effective_input(std::size_t i) const noexcept {
|
||||
return input_pinned_[i] ? pin_value_ : input_channels_[i];
|
||||
}
|
||||
|
||||
NISPS_HOT void tick_control() noexcept {
|
||||
if constexpr (requires(Derived& d) { d.on_pre_inference(); }) {
|
||||
static_cast<Derived&>(*this).on_pre_inference();
|
||||
}
|
||||
// Forward (possibly Derived-mutated) channels into the MLP.
|
||||
// Forward (possibly Derived-mutated) channels into the MLP, applying
|
||||
// the per-channel pin override.
|
||||
for (std::size_t i = 0u; i < NInputs; ++i) {
|
||||
ml_.set_input(i, input_channels_[i]);
|
||||
ml_.set_input(i, effective_input(i));
|
||||
}
|
||||
ml_.process();
|
||||
if constexpr (kRouteOutputsToEngine) {
|
||||
|
|
@ -235,6 +264,8 @@ class ModeBase {
|
|||
EngineT engine_{};
|
||||
MLPType ml_;
|
||||
std::array<float, NInputs> input_channels_{};
|
||||
std::array<bool, NInputs> input_pinned_{}; // false => live
|
||||
float pin_value_ = 0.5f; // neutral
|
||||
bool input_dirty_ = false;
|
||||
std::size_t voice_space_idx_ = 0u;
|
||||
RingBuffer<ControlEvent, kModeEventBufferSize> events_{};
|
||||
|
|
|
|||
|
|
@ -88,6 +88,62 @@ NISPS_TEST(paf_synth_mode_input_clamping) {
|
|||
m.set_input(99u, 0.5f);
|
||||
}
|
||||
|
||||
NISPS_TEST(paf_synth_mode_pinned_inputs_neutralized) {
|
||||
// Single-joystick mode pins the second 2D controller's channels (2,3).
|
||||
// A pinned channel must feed the neutral pin value (default 0.5) to the
|
||||
// MLP regardless of its live value, and pinning must not resize the model.
|
||||
PAFSynthMode m;
|
||||
m.setup(48000.f);
|
||||
NISPS_EXPECT(m.pin_value() == 0.5f);
|
||||
|
||||
// Reference: channels 2,3 driven live to 0.5 (the neutral value).
|
||||
m.set_input(0, 0.2f);
|
||||
m.set_input(1, 0.8f);
|
||||
m.set_input(2, 0.5f);
|
||||
m.set_input(3, 0.5f);
|
||||
m.tick_control();
|
||||
std::array<float, 33> ref{};
|
||||
{
|
||||
const auto outs = m.ml().outputs();
|
||||
for (std::size_t i = 0u; i < outs.size(); ++i) ref[i] = outs[i];
|
||||
}
|
||||
|
||||
// Pin channels 2,3, then drive them to arbitrary values. Output must match
|
||||
// the reference (they are neutralized to 0.5).
|
||||
m.set_input_pinned(2, true);
|
||||
m.set_input_pinned(3, true);
|
||||
NISPS_EXPECT(m.is_input_pinned(2));
|
||||
m.set_input(2, 0.0f);
|
||||
m.set_input(3, 1.0f);
|
||||
m.tick_control();
|
||||
for (std::size_t i = 0u; i < 33u; ++i) {
|
||||
NISPS_EXPECT(std::fabs(m.ml().outputs()[i] - ref[i]) < 1e-6f);
|
||||
}
|
||||
|
||||
// The active controller (channels 0,1) still affects the output.
|
||||
m.set_input(0, 0.9f);
|
||||
m.tick_control();
|
||||
bool changed = false;
|
||||
for (std::size_t i = 0u; i < 33u; ++i) {
|
||||
if (std::fabs(m.ml().outputs()[i] - ref[i]) > 1e-6f) { changed = true; break; }
|
||||
}
|
||||
NISPS_EXPECT(changed);
|
||||
|
||||
// Unpinning restores sensitivity on channels 2,3.
|
||||
m.set_input_pinned(2, false);
|
||||
m.set_input_pinned(3, false);
|
||||
m.set_input(0, 0.2f); // restore active channels to the reference pose
|
||||
m.set_input(1, 0.8f);
|
||||
m.set_input(2, 0.0f);
|
||||
m.set_input(3, 1.0f);
|
||||
m.tick_control();
|
||||
bool differs = false;
|
||||
for (std::size_t i = 0u; i < 33u; ++i) {
|
||||
if (std::fabs(m.ml().outputs()[i] - ref[i]) > 1e-6f) { differs = true; break; }
|
||||
}
|
||||
NISPS_EXPECT(differs);
|
||||
}
|
||||
|
||||
NISPS_TEST(paf_synth_mode_engine_and_ml_accessors) {
|
||||
PAFSynthMode m;
|
||||
m.setup(48000.f);
|
||||
|
|
|
|||
Loading…
Reference in a new issue