diff --git a/vcv/BUILDING.md b/vcv/BUILDING.md index 97fff09..4e159d8 100644 --- a/vcv/BUILDING.md +++ b/vcv/BUILDING.md @@ -3,15 +3,13 @@ > **Distribution & cross-platform builds:** for packaging `.vcvplugin` files, > the cross-platform CI matrix, and publishing to `/next/vcv`, see > [DISTRIBUTION.md](DISTRIBUTION.md). The official `make dist` target (from the -> SDK's `plugin.mk`) produces `dist/--.vcvplugin`; the -> legacy `Makefile.dist` (zip-based, hard-coded 0.1.0) is superseded by it. +> SDK's `plugin.mk`) produces `dist/--.vcvplugin`. ## Prerequisites - **VCV Rack 2 SDK** — download from https://vcvrack.com/manual/PluginDevelopmentTutorial or build from source - **C++20 compiler** — GCC 10+, Clang 11+, or MSVC 19.29+ (required by nisps-core for `std::span` and concepts) - **GNU Make** -- **zip** (for distribution packaging only) ## Build Steps @@ -44,16 +42,13 @@ After installing, restart VCV Rack (or use the module browser refresh if availab ## Distribution Packaging ```bash -# Build and package in one step -make -f Makefile.dist dist +# Build and package a .vcvplugin (SDK plugin.mk target) +make dist -# Or package an already-built plugin -make -f Makefile.dist package-only - -# Output: dist/MEMLNaut-0.1.0-Linux-x86_64.zip (platform name varies) +# Output: dist/MEMLNaut--.vcvplugin ``` -The zip contains a `MEMLNaut/` directory with `plugin.so` (or `.dylib`/`.dll`), `plugin.json`, and `res/`. Users extract this into their VCV Rack plugins directory. +See [DISTRIBUTION.md](DISTRIBUTION.md) for the full packaging, CI matrix, and publishing workflow. ## Cross-Compilation diff --git a/vcv/Makefile.dist b/vcv/Makefile.dist deleted file mode 100644 index 8e01378..0000000 --- a/vcv/Makefile.dist +++ /dev/null @@ -1,35 +0,0 @@ -# Distribution packaging for MEMLNaut VCV plugin -# -# Usage: -# make -f Makefile.dist dist # Build plugin then package -# make -f Makefile.dist package-only # Package without rebuilding -# make -f Makefile.dist clean-dist # Remove dist/ directory - -PLUGIN_NAME = MEMLNaut -VERSION = 0.1.0 -PLATFORM = $(shell uname -s)-$(shell uname -m) - -# Delegate build to the main Makefile (which includes the VCV SDK plugin.mk) -all: - $(MAKE) -f Makefile - -# Build then package -dist: all package-only - -# Package the already-built plugin (no rebuild) -package-only: - @echo "Packaging $(PLUGIN_NAME) v$(VERSION) for $(PLATFORM)..." - mkdir -p dist/$(PLUGIN_NAME) - cp plugin.so dist/$(PLUGIN_NAME)/ 2>/dev/null || true - cp plugin.dylib dist/$(PLUGIN_NAME)/ 2>/dev/null || true - cp plugin.dll dist/$(PLUGIN_NAME)/ 2>/dev/null || true - cp plugin.json dist/$(PLUGIN_NAME)/ - cp -r res dist/$(PLUGIN_NAME)/ - cd dist && zip -r $(PLUGIN_NAME)-$(VERSION)-$(PLATFORM).zip $(PLUGIN_NAME) - rm -rf dist/$(PLUGIN_NAME) - @echo "Created dist/$(PLUGIN_NAME)-$(VERSION)-$(PLATFORM).zip" - -clean-dist: - rm -rf dist - -.PHONY: all dist package-only clean-dist diff --git a/vcv/src/osc_server.hpp b/vcv/src/osc_server.hpp index 2352cbf..aac39d9 100644 --- a/vcv/src/osc_server.hpp +++ b/vcv/src/osc_server.hpp @@ -269,19 +269,10 @@ private: void recvLoop() { uint8_t buf[65536]; while (!shouldStop_.load()) { - sockaddr_in from{}; - socklen_t fromLen = sizeof(from); ssize_t n = recvfrom(recvSock_, reinterpret_cast(buf), sizeof(buf), 0, - reinterpret_cast(&from), &fromLen); + nullptr, nullptr); if (n <= 0) continue; // timeout or error - // Remember sender for replies - { - std::lock_guard lock(sendMutex_); - lastSender_ = from; - hasLastSender_ = true; - } - parseMessage(buf, static_cast(n)); } } @@ -338,19 +329,14 @@ private: target.sin_family = AF_INET; if (sendTargetDirty_ || !hasExplicitTarget_) { - // Use explicit target if set, otherwise reply to last sender if (!sendHost_.empty()) { inet_pton(AF_INET, sendHost_.c_str(), &target.sin_addr); target.sin_port = htons(static_cast(sendPort_)); hasExplicitTarget_ = true; sendTargetDirty_ = false; sendAddr_ = target; - } else if (hasLastSender_) { - target = lastSender_; - target.sin_port = htons(static_cast(sendPort_)); - sendAddr_ = target; } else { - return; // nobody to send to + return; // no target configured } } @@ -380,8 +366,6 @@ private: int sendPort_ = 9001; bool sendTargetDirty_ = false; bool hasExplicitTarget_ = false; - bool hasLastSender_ = false; - sockaddr_in lastSender_{}; sockaddr_in sendAddr_{}; #ifdef _WIN32 diff --git a/vcv/test/smoke_test b/vcv/test/smoke_test deleted file mode 100755 index 2244f04..0000000 Binary files a/vcv/test/smoke_test and /dev/null differ diff --git a/vcv/test/smoke_test.cpp b/vcv/test/smoke_test.cpp deleted file mode 100644 index e5ca5ca..0000000 --- a/vcv/test/smoke_test.cpp +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Standalone smoke test for MEMLNaut VCV module logic. - * Tests the IML inference pipeline without the VCV Rack runtime. - * Simulates what process() does: input CVs → IML → output CVs. - */ -#include -#include -#include -#include -#include -#include -#include - -static constexpr int NUM_INPUTS = 2; -static constexpr int NUM_OUTPUTS = 12; - -struct SmokeTestResults { - int passed = 0; - int failed = 0; - void check(const char* name, bool ok) { - if (ok) { passed++; std::cout << " PASS: " << name << "\n"; } - else { failed++; std::cerr << " FAIL: " << name << "\n"; } - } -}; - -// Test 1: Outputs are valid after inference -bool test_basic_inference(SmokeTestResults& r) { - std::cout << "\n--- Test: Basic inference pipeline ---\n"; - nisps::IML iml(NUM_INPUTS, NUM_OUTPUTS, {16, 24, 16}); - - // Simulate input: X=0.5, Y=0.5 (center position) - iml.set_input(0, 0.5f); - iml.set_input(1, 0.5f); - iml.process(); - - const float* outs = iml.get_outputs(); - bool all_valid = true; - for (int i = 0; i < NUM_OUTPUTS; i++) { - if (std::isnan(outs[i]) || std::isinf(outs[i]) || outs[i] < 0.f || outs[i] > 1.f) { - std::cerr << " Output " << i << " = " << outs[i] << " (invalid!)\n"; - all_valid = false; - } - } - r.check("All 12 outputs in [0,1]", all_valid); - return all_valid; -} - -// Test 2: Outputs change when inputs change -bool test_outputs_respond_to_inputs(SmokeTestResults& r) { - std::cout << "\n--- Test: Outputs respond to input changes ---\n"; - nisps::IML iml(NUM_INPUTS, NUM_OUTPUTS, {16, 24, 16}); - - // Get outputs at (0.1, 0.1) - iml.set_input(0, 0.1f); - iml.set_input(1, 0.1f); - iml.process(); - const float* outs1 = iml.get_outputs(); - std::vector snapshot1(outs1, outs1 + NUM_OUTPUTS); - - // Get outputs at (0.9, 0.9) - iml.set_input(0, 0.9f); - iml.set_input(1, 0.9f); - iml.process(); - const float* outs2 = iml.get_outputs(); - - // At least some outputs should differ - int changed = 0; - for (int i = 0; i < NUM_OUTPUTS; i++) { - if (std::abs(outs2[i] - snapshot1[i]) > 0.001f) changed++; - } - std::cout << " " << changed << "/" << NUM_OUTPUTS << " outputs changed\n"; - r.check("At least 6 outputs changed between (0.1,0.1) and (0.9,0.9)", changed >= 6); - return changed >= 6; -} - -// Test 3: Randomize produces different mappings -bool test_randomize_changes_outputs(SmokeTestResults& r) { - std::cout << "\n--- Test: Randomize produces different mappings ---\n"; - nisps::IML iml(NUM_INPUTS, NUM_OUTPUTS, {16, 24, 16}); - - iml.set_input(0, 0.5f); - iml.set_input(1, 0.5f); - iml.process(); - const float* outs1 = iml.get_outputs(); - std::vector before(outs1, outs1 + NUM_OUTPUTS); - - // Randomize with spread=0.6 — this internally re-runs inference - iml.set_mode(nisps::IML::Mode::Training); - iml.randomise_weights(0.6f); - iml.set_mode(nisps::IML::Mode::Inference); - - // get_outputs() now reflects the new weights (randomise_weights runs inference internally) - const float* outs2 = iml.get_outputs(); - - int changed = 0; - for (int i = 0; i < NUM_OUTPUTS; i++) { - if (std::abs(outs2[i] - before[i]) > 0.001f) changed++; - } - std::cout << " " << changed << "/" << NUM_OUTPUTS << " outputs changed after randomize\n"; - r.check("Randomize changes most outputs", changed >= 8); - return changed >= 8; -} - -// Test 4: Spread=0 vs spread=1 produce different weight distributions -bool test_spread_affects_distribution(SmokeTestResults& r) { - std::cout << "\n--- Test: Spread parameter affects output distribution ---\n"; - - auto get_output_stats = [](float spread, int trials) { - float total_extreme = 0; - int total_samples = 0; - for (int t = 0; t < trials; t++) { - nisps::IML iml(NUM_INPUTS, NUM_OUTPUTS, {16, 24, 16}); - iml.set_mode(nisps::IML::Mode::Training); - iml.randomise_weights(spread); - iml.set_mode(nisps::IML::Mode::Inference); - - for (float x = 0.1f; x <= 0.9f; x += 0.2f) { - for (float y = 0.1f; y <= 0.9f; y += 0.2f) { - iml.set_input(0, x); - iml.set_input(1, y); - iml.process(); - const float* outs = iml.get_outputs(); - for (int i = 0; i < NUM_OUTPUTS; i++) { - if (outs[i] < 0.1f || outs[i] > 0.9f) total_extreme++; - total_samples++; - } - } - } - } - return total_extreme / total_samples; - }; - - float extreme_ratio_spread0 = get_output_stats(0.0f, 10); - float extreme_ratio_spread1 = get_output_stats(1.0f, 10); - - std::cout << " Spread=0 extreme ratio: " << extreme_ratio_spread0 << "\n"; - std::cout << " Spread=1 extreme ratio: " << extreme_ratio_spread1 << "\n"; - - // Spread parameter should produce measurably different distributions. - // Note: with small networks [16,24,16], spread=0 doesn't necessarily produce - // MORE extreme outputs (fan-in too small to saturate sigmoid). The saturation - // effect is architecture-dependent. Just verify the distributions differ. - float diff = std::abs(extreme_ratio_spread0 - extreme_ratio_spread1); - std::cout << " Distribution difference: " << diff << "\n"; - r.check("Spread=0 and spread=1 produce different distributions (diff > 0.01)", diff > 0.01f); - return diff > 0.01f; -} - -// Test 5: Network expressiveness — different input regions produce different output patterns -bool test_network_expressiveness(SmokeTestResults& r) { - std::cout << "\n--- Test: Network [16,24,16] is expressive for 12 outputs ---\n"; - nisps::IML iml(NUM_INPUTS, NUM_OUTPUTS, {16, 24, 16}); - iml.set_mode(nisps::IML::Mode::Training); - iml.randomise_weights(0.6f); - iml.set_mode(nisps::IML::Mode::Inference); - - // Sample 4 corners of input space - float corners[4][2] = {{0.1f, 0.1f}, {0.9f, 0.1f}, {0.1f, 0.9f}, {0.9f, 0.9f}}; - std::vector> corner_outputs(4); - - for (int c = 0; c < 4; c++) { - iml.set_input(0, corners[c][0]); - iml.set_input(1, corners[c][1]); - iml.process(); - const float* outs = iml.get_outputs(); - corner_outputs[c].assign(outs, outs + NUM_OUTPUTS); - } - - // Check pairwise distances — all corner pairs should produce distinct output vectors - int distinct_pairs = 0; - for (int a = 0; a < 4; a++) { - for (int b = a + 1; b < 4; b++) { - float dist = 0; - for (int i = 0; i < NUM_OUTPUTS; i++) { - float d = corner_outputs[a][i] - corner_outputs[b][i]; - dist += d * d; - } - dist = std::sqrt(dist); - if (dist > 0.1f) distinct_pairs++; - } - } - std::cout << " " << distinct_pairs << "/6 corner pairs are distinct (L2 > 0.1)\n"; - r.check("At least 4/6 corner pairs produce distinct outputs", distinct_pairs >= 4); - - // Check output range utilization — how much of [0,1] do the outputs cover? - float min_out = 1.f, max_out = 0.f; - for (auto& co : corner_outputs) { - for (float v : co) { - min_out = std::min(min_out, v); - max_out = std::max(max_out, v); - } - } - float range = max_out - min_out; - std::cout << " Output range utilization: " << range << " (min=" << min_out << " max=" << max_out << ")\n"; - r.check("Output range > 0.3 (sufficient variety)", range > 0.3f); - - return distinct_pairs >= 4 && range > 0.3f; -} - -// Test 6: Continuous sweep — outputs vary smoothly, not just binary -bool test_smooth_variation(SmokeTestResults& r) { - std::cout << "\n--- Test: Outputs vary smoothly across input sweep ---\n"; - nisps::IML iml(NUM_INPUTS, NUM_OUTPUTS, {16, 24, 16}); - iml.set_mode(nisps::IML::Mode::Training); - iml.randomise_weights(0.6f); - iml.set_mode(nisps::IML::Mode::Inference); - - // Sweep X from 0 to 1, fixed Y=0.5 - std::vector prev_outs(NUM_OUTPUTS, 0.f); - int smooth_steps = 0; - int total_steps = 0; - - for (float x = 0.f; x <= 1.f; x += 0.05f) { - iml.set_input(0, x); - iml.set_input(1, 0.5f); - iml.process(); - const float* outs = iml.get_outputs(); - - if (x > 0.f) { - float max_jump = 0; - for (int i = 0; i < NUM_OUTPUTS; i++) { - max_jump = std::max(max_jump, std::abs(outs[i] - prev_outs[i])); - } - // For a 0.05 input step, output jumps should be < 0.5 (smooth, not binary) - if (max_jump < 0.5f) smooth_steps++; - total_steps++; - } - for (int i = 0; i < NUM_OUTPUTS; i++) prev_outs[i] = outs[i]; - } - - float smooth_ratio = (float)smooth_steps / total_steps; - std::cout << " " << smooth_steps << "/" << total_steps << " steps were smooth (max jump < 0.5)\n"; - r.check("At least 80% of steps are smooth", smooth_ratio >= 0.8f); - return smooth_ratio >= 0.8f; -} - -int main() { - std::cout << "=== MEMLNaut VCV Module Smoke Test ===\n"; - std::cout << "Network: [" << NUM_INPUTS << "+bias, 16, 24, 16, " << NUM_OUTPUTS << "]\n"; - - SmokeTestResults r; - - test_basic_inference(r); - test_outputs_respond_to_inputs(r); - test_randomize_changes_outputs(r); - test_spread_affects_distribution(r); - test_network_expressiveness(r); - test_smooth_variation(r); - - std::cout << "\n=== Results: " << r.passed << " passed, " << r.failed << " failed ===\n"; - return r.failed > 0 ? 1 : 0; -}