feat(nisps-core,vcv): complete Phase 0 + Phase 1
Phase 0 — spread-aware API ported to nisps-core C++: - MLP::DrawWeightsSpread(T spread) — interpolate uniform↔Xavier per layer - MLP::MoveWeightsSpread(T speed, T spread) — per-layer noise + weight decay - IML::randomise_weights(Float spread) and IML::move_weights(speed, spread) - 5 unit tests (10/10 total pass) Phase 1 — VCV Rack 2 plugin skeleton: - Makefile with C++20, nisps-core include path - plugin.json manifest - Empty MEMLNaut module: 2 inputs, 12 outputs, placeholder SVG panel - static_assert verifies nisps-core headers resolve - C++20 confirmed working in VCV SDK (8 existing plugins use it)
This commit is contained in:
parent
9040886e16
commit
d0ba1faaea
11 changed files with 459 additions and 0 deletions
|
|
@ -46,6 +46,14 @@ public:
|
|||
void clear_dataset();
|
||||
void randomise_weights();
|
||||
|
||||
// Spread-aware weight randomization
|
||||
// spread: 0 = uniform [-1,1], 1 = Xavier-scaled per layer
|
||||
void randomise_weights(Float spread);
|
||||
|
||||
// Spread-aware weight perturbation (for RL exploration)
|
||||
// speed: noise magnitude, spread: 0 = flat noise, 1 = Xavier-scaled + weight decay
|
||||
void move_weights(Float speed, Float spread);
|
||||
|
||||
// Optional logging
|
||||
void set_logger(LogFn fn) { log_fn_ = fn; }
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,33 @@ void IML<Float>::randomise_weights() {
|
|||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::randomise_weights(Float spread) {
|
||||
if (mode_ == Mode::Training) {
|
||||
stored_weights_ = mlp_->GetWeights();
|
||||
mlp_->DrawWeightsSpread(spread);
|
||||
weights_randomised_ = true;
|
||||
|
||||
// Run inference to show effect
|
||||
std::vector<Float> input_with_bias = input_state_;
|
||||
input_with_bias.push_back(static_cast<Float>(1.0));
|
||||
std::vector<Float> output(n_outputs_);
|
||||
mlp_->GetOutput(input_with_bias, &output);
|
||||
output_state_ = output;
|
||||
|
||||
log("Weights randomised (spread).");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::move_weights(Float speed, Float spread) {
|
||||
mlp_->MoveWeightsSpread(speed, spread);
|
||||
|
||||
// Run inference to show effect of perturbation
|
||||
input_updated_ = true;
|
||||
process();
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::train() {
|
||||
// Restore weights if they were randomised
|
||||
|
|
|
|||
|
|
@ -229,6 +229,17 @@ public:
|
|||
[[deprecated]]
|
||||
void DrawWeights(float scale=1.f);
|
||||
|
||||
/**
|
||||
* @brief Randomize weights with spread-controlled scaling
|
||||
* @param spread 0 = uniform [-1,1] (polarised outputs), 1 = Xavier-scaled (centered outputs)
|
||||
*
|
||||
* Interpolates weight scale between uniform and Xavier initialization per layer.
|
||||
* At spread=0: weights are uniform [-1,1] (original behavior).
|
||||
* At spread=1: weights are scaled by 1/sqrt(fan_in) per layer (Xavier).
|
||||
* Biases are set to 0.
|
||||
*/
|
||||
void DrawWeightsSpread(T spread);
|
||||
|
||||
void RandomiseWeightsAndBiasesLin(T weightMin, T weightMax, T biasMin, T biasMax);
|
||||
|
||||
void InitXavier();
|
||||
|
|
@ -239,6 +250,17 @@ public:
|
|||
*/
|
||||
void MoveWeights(T speed);
|
||||
|
||||
/**
|
||||
* @brief Add Gaussian noise to weights with spread-controlled scaling and decay
|
||||
* @param speed Base noise standard deviation
|
||||
* @param spread 0 = flat noise (original), 1 = Xavier-scaled noise with weight decay
|
||||
*
|
||||
* At spread=0: noise is uniform across all layers, no weight decay (original behavior).
|
||||
* At spread=1: noise is scaled by 1/sqrt(fan_in) per layer, weights decay 10% per call.
|
||||
* Weight decay prevents unbounded magnitude drift from repeated perturbation.
|
||||
*/
|
||||
void MoveWeightsSpread(T speed, T spread);
|
||||
|
||||
/**
|
||||
* @brief Enable/disable caching of layer outputs
|
||||
*
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
// #define SAFE_MODE
|
||||
|
|
@ -807,6 +808,24 @@ void MLP<T>::DrawWeights(float scale)
|
|||
// assert(m_layers[0].m_nodes[0].m_weights[0] != before);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::DrawWeightsSpread(T spread) {
|
||||
utils::gen_rand<T> gen;
|
||||
|
||||
for (size_t n = 0; n < m_layers.size(); n++) {
|
||||
const size_t fanIn = m_layers_nodes[n];
|
||||
const T xavierScale = static_cast<T>(1.0) / std::sqrt(static_cast<T>(fanIn));
|
||||
const T scale = static_cast<T>(1.0) * (static_cast<T>(1.0) - spread) + xavierScale * spread;
|
||||
|
||||
for (size_t k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
m_layers[n].m_nodes[k].m_weights[j] = gen() * scale;
|
||||
}
|
||||
m_layers[n].m_nodes[k].m_bias = static_cast<T>(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::MoveWeights(T speed)
|
||||
{
|
||||
|
|
@ -830,6 +849,32 @@ void MLP<T>::MoveWeights(T speed)
|
|||
assert(m_layers[0].m_nodes[0].m_weights[0] != before);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::MoveWeightsSpread(T speed, T spread) {
|
||||
const T decay = static_cast<T>(1.0) - static_cast<T>(0.1) * spread;
|
||||
// spread=0 → decay=1.0 (no decay), spread=1 → decay=0.9
|
||||
|
||||
for (size_t n = 0; n < m_layers.size(); n++) {
|
||||
const size_t fanIn = m_layers_nodes[n];
|
||||
const T xavierScale = static_cast<T>(1.0) / std::sqrt(static_cast<T>(fanIn));
|
||||
const T layerScale = static_cast<T>(1.0) * (static_cast<T>(1.0) - spread) + xavierScale * spread;
|
||||
|
||||
for (size_t k = 0; k < m_layers[n].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < m_layers[n].m_nodes[k].m_weights.size(); j++) {
|
||||
// Decay toward zero
|
||||
m_layers[n].m_nodes[k].m_weights[j] *= decay;
|
||||
|
||||
// Sum of 3 uniform randoms × kN_times(3) × speed × layerScale
|
||||
T accum = static_cast<T>(0);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
accum += static_cast<T>(rand()) / static_cast<T>(RAND_MAX) * static_cast<T>(2) - static_cast<T>(1);
|
||||
}
|
||||
m_layers[n].m_nodes[k].m_weights[j] += static_cast<T>(3) * accum * speed * layerScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void MLP<T>::InitXavier() {
|
||||
for(auto & layer : m_layers) {
|
||||
|
|
|
|||
|
|
@ -217,6 +217,244 @@ bool test_multi_output_training() {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool test_draw_weights_spread_zero() {
|
||||
std::cout << "--- Test: DrawWeightsSpread(0) — uniform [-1, 1] ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
mlp.DrawWeightsSpread(0.0f);
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
// Check bias is 0
|
||||
if (std::abs(mlp.m_layers[l].m_nodes[k].m_bias) > 1e-6f) {
|
||||
std::cerr << "FAIL: Bias not zero at layer " << l << " node " << k
|
||||
<< " (got " << mlp.m_layers[l].m_nodes[k].m_bias << ")\n";
|
||||
return false;
|
||||
}
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::isnan(w) || std::isinf(w)) {
|
||||
std::cerr << "FAIL: NaN/Inf weight at layer " << l << " node " << k << " weight " << j << "\n";
|
||||
return false;
|
||||
}
|
||||
if (w < -1.0f || w > 1.0f) {
|
||||
std::cerr << "FAIL: Weight " << w << " outside [-1, 1] at layer " << l
|
||||
<< " node " << k << " weight " << j << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_draw_weights_spread_one() {
|
||||
std::cout << "--- Test: DrawWeightsSpread(1) — Xavier-scaled weights ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
mlp.DrawWeightsSpread(1.0f);
|
||||
|
||||
// Layer 0: fan_in=3, xavier=1/sqrt(3)≈0.577
|
||||
// Layer 1: fan_in=8, xavier=1/sqrt(8)≈0.354
|
||||
float expected_xavier[] = {
|
||||
1.0f / std::sqrt(3.0f), // layer 0
|
||||
1.0f / std::sqrt(8.0f) // layer 1
|
||||
};
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
float max_abs = 0.0f;
|
||||
float xavier = expected_xavier[l];
|
||||
float limit = xavier * 1.1f;
|
||||
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
// Check bias is 0
|
||||
if (std::abs(mlp.m_layers[l].m_nodes[k].m_bias) > 1e-6f) {
|
||||
std::cerr << "FAIL: Bias not zero at layer " << l << " node " << k << "\n";
|
||||
return false;
|
||||
}
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
float aw = std::abs(w);
|
||||
if (aw > max_abs) max_abs = aw;
|
||||
if (aw > limit) {
|
||||
std::cerr << "FAIL: Weight " << w << " exceeds xavier limit " << limit
|
||||
<< " at layer " << l << " node " << k << " weight " << j << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " Layer " << l << ": xavier=" << xavier
|
||||
<< ", limit=" << limit << ", max|w|=" << max_abs << "\n";
|
||||
}
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_move_weights_spread_decay() {
|
||||
std::cout << "--- Test: MoveWeightsSpread decay (speed=0, spread=1) ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
|
||||
// Set all weights to 1.0 via SetWeights
|
||||
auto weights = mlp.GetWeights();
|
||||
for (auto& layer_w : weights) {
|
||||
for (auto& node_w : layer_w) {
|
||||
for (auto& w : node_w) {
|
||||
w = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
mlp.SetWeights(weights);
|
||||
|
||||
// Call with speed=0 (no noise), spread=1 (full decay: multiply by 0.9)
|
||||
mlp.MoveWeightsSpread(0.0f, 1.0f);
|
||||
|
||||
// Verify weights are approximately 0.9
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::abs(w - 0.9f) > 0.01f) {
|
||||
std::cerr << "FAIL: After first decay, weight=" << w
|
||||
<< " (expected ~0.9) at layer " << l << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " After 1st call: weights ~0.9 (OK)\n";
|
||||
|
||||
// Call again: 0.9 * 0.9 = 0.81
|
||||
mlp.MoveWeightsSpread(0.0f, 1.0f);
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::abs(w - 0.81f) > 0.01f) {
|
||||
std::cerr << "FAIL: After second decay, weight=" << w
|
||||
<< " (expected ~0.81) at layer " << l << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " After 2nd call: weights ~0.81 (OK)\n";
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_move_weights_spread_no_decay() {
|
||||
std::cout << "--- Test: MoveWeightsSpread no decay (speed=0, spread=0) ---\n";
|
||||
|
||||
std::vector<size_t> layers = {3, 8, 4};
|
||||
std::vector<nisps::ACTIVATION_FUNCTIONS> activs = {
|
||||
nisps::ACTIVATION_FUNCTIONS::RELU,
|
||||
nisps::ACTIVATION_FUNCTIONS::SIGMOID
|
||||
};
|
||||
nisps::MLP<float> mlp(layers, activs);
|
||||
|
||||
// Set all weights to 1.0
|
||||
auto weights = mlp.GetWeights();
|
||||
for (auto& layer_w : weights) {
|
||||
for (auto& node_w : layer_w) {
|
||||
for (auto& w : node_w) {
|
||||
w = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
mlp.SetWeights(weights);
|
||||
|
||||
// speed=0, spread=0 → no noise, no decay
|
||||
mlp.MoveWeightsSpread(0.0f, 0.0f);
|
||||
|
||||
for (size_t l = 0; l < mlp.m_layers.size(); l++) {
|
||||
for (size_t k = 0; k < mlp.m_layers[l].m_nodes.size(); k++) {
|
||||
for (size_t j = 0; j < mlp.m_layers[l].m_nodes[k].m_weights.size(); j++) {
|
||||
float w = mlp.m_layers[l].m_nodes[k].m_weights[j];
|
||||
if (std::abs(w - 1.0f) > 1e-6f) {
|
||||
std::cerr << "FAIL: Weight changed to " << w
|
||||
<< " (expected 1.0) at layer " << l << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << " All weights still 1.0 (OK)\n";
|
||||
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool test_iml_spread_api() {
|
||||
std::cout << "--- Test: IML spread API (randomise_weights / move_weights) ---\n";
|
||||
|
||||
nisps::IML<float> iml(2, 4, {8});
|
||||
iml.set_logger(log_callback);
|
||||
iml.set_mode(nisps::IML<float>::Mode::Training);
|
||||
|
||||
// randomise_weights with spread should not crash
|
||||
iml.randomise_weights(0.5f);
|
||||
|
||||
// Set inputs and process
|
||||
iml.set_input(0, 0.3f);
|
||||
iml.set_input(1, 0.7f);
|
||||
iml.process();
|
||||
|
||||
const float* out_before = iml.get_outputs();
|
||||
float saved[4];
|
||||
for (int i = 0; i < 4; i++) saved[i] = out_before[i];
|
||||
|
||||
// move_weights with spread should not crash and should change outputs
|
||||
iml.move_weights(0.1f, 0.5f);
|
||||
iml.process();
|
||||
|
||||
const float* out_after = iml.get_outputs();
|
||||
|
||||
bool any_changed = false;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (std::isnan(out_after[i]) || std::isinf(out_after[i])) {
|
||||
std::cerr << "FAIL: Output " << i << " is NaN/Inf\n";
|
||||
return false;
|
||||
}
|
||||
if (out_after[i] < 0.0f || out_after[i] > 1.0f) {
|
||||
std::cerr << "FAIL: Output " << i << " = " << out_after[i] << " outside [0, 1]\n";
|
||||
return false;
|
||||
}
|
||||
if (std::abs(out_after[i] - saved[i]) > 1e-6f) {
|
||||
any_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!any_changed) {
|
||||
std::cerr << "FAIL: move_weights did not change any outputs\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << " Outputs valid and changed after move_weights\n";
|
||||
std::cout << "PASS\n\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "\n=== NISPS Core Test Suite ===\n\n";
|
||||
|
||||
|
|
@ -230,6 +468,11 @@ int main() {
|
|||
run(test_add_example_api());
|
||||
run(test_training_convergence());
|
||||
run(test_multi_output_training());
|
||||
run(test_draw_weights_spread_zero());
|
||||
run(test_draw_weights_spread_one());
|
||||
run(test_move_weights_spread_decay());
|
||||
run(test_move_weights_spread_no_decay());
|
||||
run(test_iml_spread_api());
|
||||
|
||||
std::cout << "=== Results: " << passed << " passed, " << failed << " failed ===\n\n";
|
||||
|
||||
|
|
|
|||
15
vcv/Makefile
Normal file
15
vcv/Makefile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
RACK_DIR ?= $(HOME)/.local/share/Rack2/Rack-SDK
|
||||
|
||||
FLAGS += -std=c++20
|
||||
FLAGS += -I$(RACK_DIR)/include -I$(RACK_DIR)/dep/include
|
||||
FLAGS += -I../nisps-core/include
|
||||
|
||||
SOURCES += src/plugin.cpp
|
||||
SOURCES += src/MEMLNaut.cpp
|
||||
|
||||
DISTRIBUTABLES += res
|
||||
|
||||
include $(RACK_DIR)/plugin.mk
|
||||
|
||||
# Remove SDK's default c++11 to avoid conflicting flags
|
||||
CXXFLAGS := $(filter-out -std=c++11,$(CXXFLAGS))
|
||||
19
vcv/plugin.json
Normal file
19
vcv/plugin.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"slug": "MEMLNaut",
|
||||
"name": "MEMLNaut",
|
||||
"version": "0.1.0",
|
||||
"license": "proprietary",
|
||||
"brand": "MEMLNaut",
|
||||
"author": "MEML",
|
||||
"authorUrl": "https://musicallyembodiedml.github.io",
|
||||
"pluginUrl": "https://github.com/MusicallyEmbodiedML/MEMLNaut-NISPS",
|
||||
"sourceUrl": "https://github.com/MusicallyEmbodiedML/MEMLNaut-NISPS",
|
||||
"modules": [
|
||||
{
|
||||
"slug": "MEMLNaut",
|
||||
"name": "MEMLNaut",
|
||||
"description": "Neural Interactive Shaping of Parameter Spaces — ML-powered CV mapper with RL feedback",
|
||||
"tags": ["Controller", "Utility", "Random"]
|
||||
}
|
||||
]
|
||||
}
|
||||
5
vcv/res/MEMLNaut.svg
Normal file
5
vcv/res/MEMLNaut.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="203.2mm" height="128.5mm" viewBox="0 0 203.2 128.5">
|
||||
<rect width="203.2" height="128.5" fill="#1a1a2e" />
|
||||
<text x="101.6" y="12" text-anchor="middle" fill="#e0e0e0" font-family="monospace" font-size="6">MEMLNaut</text>
|
||||
<text x="101.6" y="120" text-anchor="middle" fill="#808080" font-family="monospace" font-size="3">NISPS v0.1</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 395 B |
58
vcv/src/MEMLNaut.cpp
Normal file
58
vcv/src/MEMLNaut.cpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#include "plugin.hpp"
|
||||
#include <nisps/nisps.hpp>
|
||||
|
||||
struct MEMLNaut : Module {
|
||||
enum ParamId {
|
||||
PARAMS_LEN
|
||||
};
|
||||
enum InputId {
|
||||
INPUT_X,
|
||||
INPUT_Y,
|
||||
INPUTS_LEN
|
||||
};
|
||||
enum OutputId {
|
||||
OUTPUT_1, OUTPUT_2, OUTPUT_3, OUTPUT_4,
|
||||
OUTPUT_5, OUTPUT_6, OUTPUT_7, OUTPUT_8,
|
||||
OUTPUT_9, OUTPUT_10, OUTPUT_11, OUTPUT_12,
|
||||
OUTPUTS_LEN
|
||||
};
|
||||
enum LightId {
|
||||
LIGHTS_LEN
|
||||
};
|
||||
|
||||
MEMLNaut() {
|
||||
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
|
||||
configInput(INPUT_X, "X");
|
||||
configInput(INPUT_Y, "Y");
|
||||
for (int i = 0; i < 12; 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");
|
||||
}
|
||||
|
||||
void process(const ProcessArgs& args) override {
|
||||
// Empty — Phase 2 will wire up IML inference
|
||||
}
|
||||
};
|
||||
|
||||
struct MEMLNautWidget : ModuleWidget {
|
||||
MEMLNautWidget(MEMLNaut* module) {
|
||||
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));
|
||||
|
||||
// Outputs (right side, 2 columns of 6)
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Model* modelMEMLNaut = createModel<MEMLNaut, MEMLNautWidget>("MEMLNaut");
|
||||
11
vcv/src/plugin.cpp
Normal file
11
vcv/src/plugin.cpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#include "plugin.hpp"
|
||||
|
||||
Plugin* pluginInstance;
|
||||
|
||||
extern Model* modelMEMLNaut;
|
||||
|
||||
void init(Plugin* p) {
|
||||
pluginInstance = p;
|
||||
|
||||
p->addModel(modelMEMLNaut);
|
||||
}
|
||||
6
vcv/src/plugin.hpp
Normal file
6
vcv/src/plugin.hpp
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
#include <rack.hpp>
|
||||
|
||||
using namespace rack;
|
||||
|
||||
extern Plugin* pluginInstance;
|
||||
Loading…
Reference in a new issue