feat(vcv): complete Phase 2 — core engine wired up

- IML<float> with [16,24,16] hidden layers as module member
- CV inputs read, clamped 0-10V, normalized to [0,1]
- MLP inference in process(), 12 outputs scaled to 0-10V
- SPREAD knob (0-1, default 0.6) controls weight initialization
- RAND button randomizes weights using current spread value
- Panel: knob + button + 2 inputs + 12 outputs in 2x6 grid
This commit is contained in:
w1n5t0n 2026-03-28 00:38:58 +02:00
parent d0ba1faaea
commit e52b800a92

View file

@ -1,8 +1,13 @@
#include "plugin.hpp"
#include <nisps/nisps.hpp>
static constexpr int NUM_ML_INPUTS = 2;
static constexpr int NUM_ML_OUTPUTS = 12;
struct MEMLNaut : Module {
enum ParamId {
PARAM_SPREAD,
PARAM_RAND,
PARAMS_LEN
};
enum InputId {
@ -20,21 +25,47 @@ struct MEMLNaut : Module {
LIGHTS_LEN
};
nisps::IML<float> iml{NUM_ML_INPUTS, NUM_ML_OUTPUTS, {16, 24, 16}};
dsp::BooleanTrigger randTrigger;
MEMLNaut() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
configParam(PARAM_SPREAD, 0.f, 1.f, 0.6f, "Spread", "%", 0.f, 100.f);
configButton(PARAM_RAND, "Randomize weights");
configInput(INPUT_X, "X");
configInput(INPUT_Y, "Y");
for (int i = 0; i < 12; i++) {
for (int i = 0; i < NUM_ML_OUTPUTS; i++) {
configOutput(OUTPUT_1 + i, string::f("Out %d", i + 1));
}
// Verify nisps-core headers integrate correctly
// (actual IML instance will be added in Phase 2)
static_assert(sizeof(nisps::IML<float>) > 0, "nisps::IML must be a complete type");
// Initial randomization with default spread
iml.set_mode(nisps::IML<float>::Mode::Training);
iml.randomise_weights(0.6f);
iml.set_mode(nisps::IML<float>::Mode::Inference);
}
void process(const ProcessArgs& args) override {
// Empty — Phase 2 will wire up IML inference
// Handle RAND button
if (randTrigger.process(params[PARAM_RAND].getValue() > 0.f)) {
float spread = params[PARAM_SPREAD].getValue();
iml.set_mode(nisps::IML<float>::Mode::Training);
iml.randomise_weights(spread);
iml.set_mode(nisps::IML<float>::Mode::Inference);
}
// Read inputs, normalize 0-10V → [0,1], clamp
float x = clamp(inputs[INPUT_X].getVoltage() / 10.f, 0.f, 1.f);
float y = clamp(inputs[INPUT_Y].getVoltage() / 10.f, 0.f, 1.f);
iml.set_input(0, x);
iml.set_input(1, y);
iml.process();
// Write outputs: sigmoid [0,1] → 0-10V
const float* outs = iml.get_outputs();
for (int i = 0; i < NUM_ML_OUTPUTS; i++) {
outputs[OUTPUT_1 + i].setVoltage(outs[i] * 10.f);
}
}
};
@ -43,14 +74,18 @@ struct MEMLNautWidget : ModuleWidget {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/MEMLNaut.svg")));
// Inputs (left side)
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.0, 20.0)), module, MEMLNaut::INPUT_X));
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.0, 32.0)), module, MEMLNaut::INPUT_Y));
// Knobs
addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(12.0, 20.0)), module, MEMLNaut::PARAM_SPREAD));
addParam(createParamCentered<VCVButton>(mm2px(Vec(28.0, 20.0)), module, MEMLNaut::PARAM_RAND));
// Outputs (right side, 2 columns of 6)
// Inputs (left side)
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.0, 38.0)), module, MEMLNaut::INPUT_X));
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.0, 50.0)), module, MEMLNaut::INPUT_Y));
// Outputs (2 columns of 6, below inputs)
for (int i = 0; i < 6; i++) {
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(20.0, 20.0 + i * 12.0)), module, MEMLNaut::OUTPUT_1 + i));
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(32.0, 20.0 + i * 12.0)), module, MEMLNaut::OUTPUT_1 + 6 + i));
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(12.0, 62.0 + i * 10.0)), module, MEMLNaut::OUTPUT_1 + i));
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(28.0, 62.0 + i * 10.0)), module, MEMLNaut::OUTPUT_1 + 6 + i));
}
}
};