268 lines
12 KiB
Markdown
268 lines
12 KiB
Markdown
|
|
# MEMLNaut for VCV Rack
|
||
|
|
|
||
|
|
MEMLNaut is a CV-to-CV mapper powered by a neural network that you train in real time using reinforcement learning. Patch any CV sources into its 2 inputs, connect its 12 outputs to your synth parameters, and shape the mapping by giving thumbs-up/thumbs-down feedback. The module learns your preferences, producing complex, nonlinear modulation that evolves with your taste. It does not generate sound itself -- it generates control voltages.
|
||
|
|
|
||
|
|
## Installation
|
||
|
|
|
||
|
|
MEMLNaut is built from source against the VCV Rack 2 SDK.
|
||
|
|
|
||
|
|
### Requirements
|
||
|
|
|
||
|
|
- VCV Rack 2 (Community Edition or Pro)
|
||
|
|
- VCV Rack SDK (v2.x)
|
||
|
|
- C++20 compiler (GCC 10+, Clang 12+, or MSVC 2019+)
|
||
|
|
- GNU Make
|
||
|
|
|
||
|
|
### Build
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git clone --recursive https://github.com/MusicallyEmbodiedML/MEMLNaut-NISPS.git
|
||
|
|
cd MEMLNaut-NISPS/vcv
|
||
|
|
|
||
|
|
# Point to your Rack SDK (or set in environment)
|
||
|
|
export RACK_DIR=/path/to/Rack-SDK
|
||
|
|
|
||
|
|
make
|
||
|
|
make install # copies plugin to your VCV Rack plugins directory
|
||
|
|
```
|
||
|
|
|
||
|
|
The default `RACK_DIR` is `~/.local/share/Rack2/Rack-SDK`. If your SDK lives there, you can skip the export.
|
||
|
|
|
||
|
|
The `nisps-core` headers (the ML engine) are included in the parent repository and referenced automatically via the Makefile.
|
||
|
|
|
||
|
|
## Quick Start
|
||
|
|
|
||
|
|
1. Add **MEMLNaut** from the module browser (under Controller / Utility).
|
||
|
|
2. Patch two LFOs (or any CV source) into the **X** and **Y** inputs.
|
||
|
|
3. Connect several of the 12 outputs to parameters on your synth voice -- filter cutoff, oscillator pitch, waveshape, VCA level, etc.
|
||
|
|
4. Press **RAND** to randomize the network. You should hear your synth respond as the LFOs sweep.
|
||
|
|
5. Flip the **LEARN** switch on.
|
||
|
|
6. When you hear something you like, press **+** (thumbs up). When you hear something you dislike, press **-** (thumbs down).
|
||
|
|
7. Keep exploring. The mapping will converge toward sounds you prefer.
|
||
|
|
|
||
|
|
## Panel Controls
|
||
|
|
|
||
|
|
### Knobs
|
||
|
|
|
||
|
|
| Control | Description |
|
||
|
|
|---------|-------------|
|
||
|
|
| **SPREAD** | Controls weight initialization scale, RL noise amplitude, and weight decay. Low values produce extreme, polarized mappings. High values produce balanced, subtle mappings. Default: 60%. |
|
||
|
|
| **RATE** | Inference rate. Full CCW = ~170 Hz (block rate, cheapest). Noon = ~2 kHz (good for CV). Full CW = 44.1 kHz (audio rate, most expensive). |
|
||
|
|
|
||
|
|
### Buttons
|
||
|
|
|
||
|
|
| Control | Description |
|
||
|
|
|---------|-------------|
|
||
|
|
| **+** | Thumbs up. Captures the current input/output pair as a training example and trains the network. Requires LEARN to be enabled. |
|
||
|
|
| **-** | Thumbs down. Increases exploration noise and perturbs the network weights to try something different. Requires LEARN to be enabled. |
|
||
|
|
| **LEARN** | Toggle switch. Enables/disables RL feedback. When off, the module still runs inference -- it just ignores +/- presses. |
|
||
|
|
| **RAND** | Randomize all network weights (using current SPREAD setting). |
|
||
|
|
| **CLEAR** | Long-press (~1 second) to clear all training examples and reset the network. |
|
||
|
|
|
||
|
|
### Inputs
|
||
|
|
|
||
|
|
| Port | Description |
|
||
|
|
|------|-------------|
|
||
|
|
| **X** | Primary CV input. Default range: 0-10V (unipolar). |
|
||
|
|
| **Y** | Secondary CV input. Default range: 0-10V (unipolar). |
|
||
|
|
| **SPREAD CV** | CV modulation of the SPREAD knob (added to knob value, 0-10V). |
|
||
|
|
| **LEARN** | Gate input. High = enable learning. Works alongside the LEARN toggle (either enables it). |
|
||
|
|
| **+ TRIG** | Trigger input for thumbs-up. Alternative to pressing the + button. |
|
||
|
|
| **- TRIG** | Trigger input for thumbs-down. Alternative to pressing the - button. |
|
||
|
|
|
||
|
|
### Outputs
|
||
|
|
|
||
|
|
| Port | Description |
|
||
|
|
|------|-------------|
|
||
|
|
| **OUT 1-12** | Raw MLP outputs, each with its own attenuverter trimpot and LED. Default: 0-10V unipolar. |
|
||
|
|
| **MEAN** | Mean of the 12 raw outputs (0-10V). |
|
||
|
|
| **STD** | Standard deviation of the 12 raw outputs (0-10V). |
|
||
|
|
| **DELTA** | Rate of change across all outputs (L2 norm of frame-to-frame difference). |
|
||
|
|
| **NOVELTY** | How far the current input is from any training example. 10V when untrained, drops as you add examples near the current position. |
|
||
|
|
| **CONFIDENCE** | Inverse of novelty. 0V when untrained, rises as examples accumulate near the current input. |
|
||
|
|
|
||
|
|
Each of the 12 raw outputs has a **trimpot attenuverter** (-100% to +100%) for scaling and inverting individual outputs without external modules.
|
||
|
|
|
||
|
|
### LEDs
|
||
|
|
|
||
|
|
| LED | Meaning |
|
||
|
|
|-----|---------|
|
||
|
|
| **LEARN** (green) | Lit when learning is enabled. |
|
||
|
|
| **TRAIN** (yellow) | Flashes during background training. |
|
||
|
|
| **Output LEDs** (white) | Brightness tracks each output's current level. |
|
||
|
|
|
||
|
|
### Display
|
||
|
|
|
||
|
|
The built-in bar graph shows all 12 output levels in real time, color-coded by output index. The top-left corner shows the current noise level (N:). "TRAIN" appears in the top-right during active training.
|
||
|
|
|
||
|
|
## RL Workflow
|
||
|
|
|
||
|
|
The reinforcement learning loop works like this:
|
||
|
|
|
||
|
|
1. **Start exploring.** Patch LFOs or sequencers into X and Y. Connect outputs to interesting synth parameters. Press RAND a few times to hear different random mappings.
|
||
|
|
|
||
|
|
2. **Enable learning.** Flip the LEARN switch on (or send a gate to the LEARN input).
|
||
|
|
|
||
|
|
3. **Thumbs up (+)** when you like what you hear. This:
|
||
|
|
- Saves the current input position and output values as a training example
|
||
|
|
- Trains the network to reproduce this mapping
|
||
|
|
- Slightly reduces exploration noise (the network becomes more "settled")
|
||
|
|
|
||
|
|
4. **Thumbs down (-)** when you dislike what you hear. This:
|
||
|
|
- Increases exploration noise
|
||
|
|
- Perturbs the network weights to try a different mapping
|
||
|
|
- Does NOT save any training example
|
||
|
|
|
||
|
|
5. **Repeat.** Over time, the network learns to produce outputs you tend to like across the input space. Regions near your thumbs-up examples will be stable; distant regions remain exploratory.
|
||
|
|
|
||
|
|
6. **Disable learning** when you are happy with the mapping. The module continues running inference with the trained network. You now have a complex, personalized CV source.
|
||
|
|
|
||
|
|
### Tips
|
||
|
|
|
||
|
|
- Give thumbs-up at several different input positions to teach the network about different regions of the input space.
|
||
|
|
- The network holds up to 100 examples. Oldest examples are dropped when full (FIFO).
|
||
|
|
- Use the SPREAD knob to control how wild the exploration is. Low spread = dramatic changes. High spread = subtle refinements.
|
||
|
|
- The NOVELTY and CONFIDENCE outputs are useful for self-patching: route NOVELTY to control something that signals "unexplored territory."
|
||
|
|
|
||
|
|
## Context Menu
|
||
|
|
|
||
|
|
Right-click the module to access these settings:
|
||
|
|
|
||
|
|
### Output Ranges
|
||
|
|
|
||
|
|
Toggle each output between **unipolar (0-10V)** and **bipolar (+/-5V)**. Default is unipolar. Use bipolar for parameters that expect centered modulation (e.g., FM depth, panning).
|
||
|
|
|
||
|
|
### Input Ranges
|
||
|
|
|
||
|
|
Toggle each input between **unipolar (0-10V)** and **bipolar (+/-5V)**. Default is unipolar. Set to bipolar if your input source produces +/-5V signals (e.g., standard LFOs).
|
||
|
|
|
||
|
|
### Output Slew
|
||
|
|
|
||
|
|
Smoothing time applied when network weights change (after training or perturbation). Prevents audible clicks from sudden output jumps. Options: 0, 5, 10, 20, 50, 100 ms. Default: 10 ms.
|
||
|
|
|
||
|
|
### Presets (.nisps)
|
||
|
|
|
||
|
|
- **Save .nisps preset...** -- Export the full module state (weights, training examples, knob positions, ranges) to a `.nisps` JSON file.
|
||
|
|
- **Load .nisps preset...** -- Import a `.nisps` file, restoring the network and all settings.
|
||
|
|
|
||
|
|
### OSC Bridge
|
||
|
|
|
||
|
|
- **Enable OSC server** -- Start a UDP/WebSocket OSC server for live communication with the companion web app.
|
||
|
|
- **OSC listen port** -- Choose the port (default 9000). Change this if running multiple MEMLNaut instances.
|
||
|
|
|
||
|
|
## Presets
|
||
|
|
|
||
|
|
MEMLNaut uses `.nisps` files for saving and sharing trained networks.
|
||
|
|
|
||
|
|
### What gets saved
|
||
|
|
|
||
|
|
- All network weights (the learned mapping)
|
||
|
|
- All training examples (input/output pairs from thumbs-up)
|
||
|
|
- Knob positions (SPREAD, RATE, attenuverters)
|
||
|
|
- Input/output range settings
|
||
|
|
- Noise level and slew time
|
||
|
|
|
||
|
|
### Saving and loading
|
||
|
|
|
||
|
|
1. Right-click the module.
|
||
|
|
2. Under "Presets (.nisps)", choose **Save** or **Load**.
|
||
|
|
3. Pick a location and filename.
|
||
|
|
|
||
|
|
### Sharing between VCV and the web playground
|
||
|
|
|
||
|
|
The `.nisps` format is shared with the [NISPS web playground](https://musicallyembodiedml.github.io/memlnaut/). However, the VCV module and web app use different network architectures (VCV: 12 outputs, web: 126 outputs), so weights are not directly transferable between them. Training examples and configuration metadata are preserved for reference. The `mlpConfig.layers` field in the file lets each loader detect architecture mismatches.
|
||
|
|
|
||
|
|
### Patch save/load
|
||
|
|
|
||
|
|
Full module state is also saved automatically with your VCV Rack patch file. You do not need to manually export `.nisps` files to preserve your work between sessions.
|
||
|
|
|
||
|
|
## OSC Integration
|
||
|
|
|
||
|
|
MEMLNaut can communicate with the companion web app over OSC for live, bidirectional state sync.
|
||
|
|
|
||
|
|
### Setup
|
||
|
|
|
||
|
|
1. Right-click the module and enable **OSC server** (default port 9000).
|
||
|
|
2. In the web playground, open the OSC connection panel and connect to `localhost:9000`.
|
||
|
|
3. The web app connects via a WebSocket-to-OSC bridge.
|
||
|
|
|
||
|
|
### What syncs
|
||
|
|
|
||
|
|
| OSC Address | Direction | Content |
|
||
|
|
|-------------|-----------|---------|
|
||
|
|
| `/nisps/outputs` | VCV -> Web | Current output values (~10 times/sec) |
|
||
|
|
| `/nisps/inputs` | VCV -> Web | Current input values (~10 times/sec) |
|
||
|
|
| `/nisps/weights` | Both | Full weight transfer |
|
||
|
|
| `/nisps/state` | Both | Complete state sync (weights + examples + config) |
|
||
|
|
|
||
|
|
### Multiple instances
|
||
|
|
|
||
|
|
Each MEMLNaut instance needs its own OSC port. Use the port selector in the context menu (9000, 9001, 9002, 8000, 7000) to avoid conflicts.
|
||
|
|
|
||
|
|
## Technical Details
|
||
|
|
|
||
|
|
### Network architecture
|
||
|
|
|
||
|
|
The module uses a multi-layer perceptron (MLP) with the following default architecture:
|
||
|
|
|
||
|
|
```
|
||
|
|
Inputs: 2 (+ 1 bias = 3 input nodes)
|
||
|
|
Hidden: 16 -> 24 -> 16 (3 hidden layers, ReLU activation)
|
||
|
|
Output: 12 (sigmoid activation, producing values in [0, 1])
|
||
|
|
```
|
||
|
|
|
||
|
|
### Inference
|
||
|
|
|
||
|
|
The MLP runs in the VCV `process()` callback. The RATE knob controls how often inference runs, from once per audio block (~170 Hz) to every sample (44.1 kHz). Between inference steps, outputs are linearly interpolated to avoid staircase artifacts.
|
||
|
|
|
||
|
|
### Threading
|
||
|
|
|
||
|
|
- **Audio thread**: Reads CV inputs, runs MLP inference, writes CV outputs. Never blocks.
|
||
|
|
- **Background thread**: Handles training (thumbs-up) and weight perturbation (thumbs-down). When complete, signals the audio thread to crossfade to the new outputs.
|
||
|
|
- Each module instance has its own independent background thread and ML engine.
|
||
|
|
|
||
|
|
### Performance
|
||
|
|
|
||
|
|
The default network is small (~20 KB of weights). At block-rate inference, CPU usage is negligible. At audio-rate inference with all 12 outputs patched, expect moderate CPU usage comparable to a complex oscillator module. Multiple instances scale linearly.
|
||
|
|
|
||
|
|
## Building
|
||
|
|
|
||
|
|
### Full build commands
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cd vcv
|
||
|
|
export RACK_DIR=/path/to/Rack-SDK
|
||
|
|
make # build the plugin
|
||
|
|
make install # install to VCV plugins directory
|
||
|
|
make clean # remove build artifacts
|
||
|
|
```
|
||
|
|
|
||
|
|
### SDK setup
|
||
|
|
|
||
|
|
1. Download the VCV Rack SDK from https://vcvrack.com/manual/PluginDevelopmentTutorial
|
||
|
|
2. Extract it somewhere (e.g., `~/Rack-SDK`)
|
||
|
|
3. Set `RACK_DIR` to that path, or place it at `~/.local/share/Rack2/Rack-SDK`
|
||
|
|
|
||
|
|
### Project structure
|
||
|
|
|
||
|
|
```
|
||
|
|
vcv/
|
||
|
|
Makefile # Build configuration
|
||
|
|
plugin.json # Plugin manifest (name, version, tags)
|
||
|
|
src/
|
||
|
|
plugin.cpp # Plugin initialization
|
||
|
|
plugin.hpp # Plugin globals
|
||
|
|
MEMLNaut.cpp # Module logic, UI, serialization
|
||
|
|
osc_server.hpp # OSC bridge server
|
||
|
|
res/
|
||
|
|
MEMLNaut.svg # Panel artwork
|
||
|
|
dep/ # Build dependencies
|
||
|
|
```
|
||
|
|
|
||
|
|
### Dependencies
|
||
|
|
|
||
|
|
- **VCV Rack SDK** (v2.x) -- provides the module framework
|
||
|
|
- **nisps-core** -- header-only C++20 ML library (included in the parent repo at `../nisps-core/`)
|
||
|
|
|
||
|
|
No external package manager dependencies are required. The nisps-core headers are referenced directly from the Makefile.
|