- Set
RACK_DIRto the SDK path in the Makefile (defaults to../Rack-SDK) - List all
.cppsources inSOURCES, includingdep/s7/s7.cfor the Scheme interpreter - Add
FLAGS += -Idep/s7to include the s7 headers - Add resource files with
DISTRIBUTABLES += $(wildcard res/*) make— buildsplugin.somake install— packages into.vcvpluginand copies to~/.local/share/Rack2/plugins-lin-x64/- Version in
plugin.jsonmust start with2for Rack v2 ABI compatibility
slug(plugin) — unique identifier, used as brand in module browserslug(module) — unique per-module, used in patch files, never change after releaseversion— must be2.x.xfor Rack v2tags— controls which category the module appears in- Plugin slug is
"ReplicatAudio"; contains 16 modules total
VCV Rack v2 uses an internal rack‑unit coordinate system.
| Constant | Value | Meaning |
|---|---|---|
RACK_GRID_WIDTH |
15 | width of 1 hp in rack‑units |
RACK_GRID_HEIGHT |
380 | standard panel height |
Conversions:
1 hp = 5.08 mm = 15 rack‑units
rack‑units → mm = value × 5.08 ÷ 15
mm → rack‑units = value × 15 ÷ 5.08
SvgPanel loads a panel SVG (authored in mm) and sets the widget's box.size
in rack‑units, scaling the SVG automatically.
For a 4 hp panel: viewBox="0 0 20.32 128.5" → box.size = (60, 380)
Every module has three parts:
All source files include "ra-components.hpp" (which itself includes "rack.hpp")
for widget type aliases instead of including "rack.hpp" directly.
Model *modelRaVca = createModel<RaVcaModule, RaVcaWidget>("ra-vca");The string "ra-vca" must match the module slug in plugin.json.
struct RaVcaModule : Module {
enum ParamIds { GAIN_PARAM, NUM_PARAMS };
enum InputIds { AUDIO_INPUT, CV_INPUT, NUM_INPUTS };
enum OutputIds { AUDIO_OUTPUT, NUM_OUTPUTS };
enum LightIds { NUM_LIGHTS };
RaVcaModule() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
configParam(GAIN_PARAM, 0.f, 2.f, 1.f, "Gain", " dB", 0.f, 20.f, 0.f);
configInput(AUDIO_INPUT, "Audio");
configInput(CV_INPUT, "CV");
configOutput(AUDIO_OUTPUT, "Audio");
}
void process(const ProcessArgs &args) override {
float gain = params[GAIN_PARAM].getValue() + inputs[CV_INPUT].getVoltage() / 10.f;
gain = clamp(gain, 0.f, 2.f);
outputs[AUDIO_OUTPUT].setVoltage(inputs[AUDIO_INPUT].getVoltage() * gain);
}
};struct RaVcaWidget : ModuleWidget {
RaVcaWidget(RaVcaModule *module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/ra-vca.svg")));
// Use addChild for screws, createWidget for non-interactive elements
// Use addParam/addInput/addOutput for params/ports
addChild(createWidget<RaScrew>(Vec(0, 0)));
addChild(createWidget<RaScrew>(Vec(box.size.x - RACK_GRID_WIDTH, 0)));
addChild(createWidget<RaScrew>(Vec(0, box.size.y - RACK_GRID_WIDTH)));
addChild(createWidget<RaScrew>(Vec(box.size.x - RACK_GRID_WIDTH, box.size.y - RACK_GRID_WIDTH)));
addParam(createParamCentered<RaKnob>(Vec(x, y), module, RaVcaModule::GAIN_PARAM));
addInput(createInputCentered<RaPort>(Vec(x, y), module, RaVcaModule::AUDIO_INPUT));
addOutput(createOutputCentered<RaPort>(Vec(x, y), module, RaVcaModule::AUDIO_OUTPUT));
}
};Left-side screws must be at x = 0 (flush with panel edge). Right-side screws must be at x = box.size.x - RACK_GRID_WIDTH (flush with panel edge — the screw SVG is exactly RACK_GRID_WIDTH wide).
| Position | Code |
|---|---|
| Top‑left | Vec(0, 0) |
| Top‑right | Vec(box.size.x - RACK_GRID_WIDTH, 0) |
| Bottom‑left | Vec(0, box.size.y - RACK_GRID_WIDTH) |
| Bottom‑right | Vec(box.size.x - RACK_GRID_WIDTH, box.size.y - RACK_GRID_WIDTH) |
Do NOT use RACK_GRID_WIDTH for left-side x positions — that puts the screw
7.5 units in from the left edge, which is incorrect for this project.
#include "rack.hpp"
using namespace rack;
Plugin *pluginInstance;
extern Model *modelRaVca;
extern Model *modelRaGnawbz4x;
extern Model *modelRaGnawbz1x4;
extern Model *modelRaUlfo;
extern Model *modelRaScaler;
extern Model *modelRaEndless;
extern Model *modelRaYscope;
extern Model *modelRaShapes;
extern Model *modelRaChance;
extern Model *modelRaAdd;
extern Model *modelRaSeer;
extern Model *modelRaBlank;
extern Model *modelRaAdsr;
extern Model *modelRaKlock;
extern Model *modelRaButtons;
extern Model *modelRaScheme;
void init(Plugin *p) {
pluginInstance = p;
p->addModel(modelRaVca);
p->addModel(modelRaGnawbz4x);
p->addModel(modelRaGnawbz1x4);
p->addModel(modelRaUlfo);
p->addModel(modelRaScaler);
p->addModel(modelRaEndless);
p->addModel(modelRaYscope);
p->addModel(modelRaShapes);
p->addModel(modelRaChance);
p->addModel(modelRaAdd);
p->addModel(modelRaSeer);
p->addModel(modelRaBlank);
p->addModel(modelRaAdsr);
p->addModel(modelRaKlock);
p->addModel(modelRaButtons);
p->addModel(modelRaScheme);
}pluginInstance— global pointer set ininit(), used byasset::plugin()to resolve resource pathsextern Model *modelXxx— forward-declare models defined in other.cppfilesinit()must have C linkage (declaredextern "C"inplugin/callbacks.hpp), so define it as a plain function
| Creator function | Widget | Usage |
|---|---|---|
createParamCentered |
RaKnob |
Standard macro knob |
createParamCentered |
RaKnobSmall |
Small trim knob |
createParamCentered |
RaSwitch2 |
2‑position toggle switch |
createParamCentered |
RaSwitch3 |
3‑position toggle switch |
configParam(paramId, minValue, maxValue, defaultValue, name, unit, displayBase, displayMultiplier, displayOffset);name— label shown in context menu and tooltipunit— appended after the value (e.g." dB"," %"," V")displayMultiplier/displayOffset— transform the raw value for display:displayed = raw × displayMultiplier + displayOffsetdisplayBase— if non‑zero, display usesdisplayMultiplier × base^raw + displayOffset
Example — gain shown in dB with 0 dB at unity:
configParam(GAIN_PARAM, 0.f, 2.f, 1.f, "Gain", " dB", 10.f, 20.f, 0.f);
// raw 0→20*10^0=20 dB, raw 0.5→20*sqrt(10)≈63 dB, raw 1→20*10=200 dBExample — scale knob shown as percentage:
configParam(SCALE_PARAM, 0.f, 1.f, 1.f, "Scale", "%", 0.f, 100.f);configSwitch(paramId, minValue, maxValue, defaultValue, name, labels);labels is an std::vector<std::string> where each element is the label for
that position. The raw value snaps to integer positions.
configSwitch(RANGE_PARAM, 0.f, 1.f, 0.f, "Range", {"\u00B15V", "0\u201310V"});Use \u00B5 for µ, \u00B1 for ±, \u2013 for en‑dash, \u2014 for em‑dash.
configInput(PORT_ID, "Name");
configOutput(PORT_ID, "Name");
// In process:
float v = inputs[PORT_ID].getVoltage();
outputs[PORT_ID].setVoltage(v);Inputs can be left unpatched — getVoltage() returns 0.f.
| Widget class | Size | Colour channels |
|---|---|---|
TinyLight<RedGreenBlueLight> |
3 mm | 3 (R, G, B via base colors) |
MediumLight<RedGreenBlueLight> |
5 mm | 3 |
LargeLight<RedGreenBlueLight> |
5 mm | 3 |
TinyLight<GreenRedBlueLight> |
3 mm | 3 (G, R, B order) |
TinyLight<WhiteLight> |
3 mm | 1 |
Multi‑colour lights consume multiple consecutive light indices.
RedGreenBlueLight adds 3 base colors → uses 3 indices per widget.
enum LightIds {
LIGHT1_R, // index 0 — red
LIGHT1_G, // index 1 — green
LIGHT1_B, // index 2 — blue
LIGHT2_R, // index 3 — red
// ...
NUM_LIGHTS // = 12 for 4 RGB widgets
};In the constructor:
for (int c = 0; c < 3; c++)
configLight(LIGHT1_R + i * 3 + c, "Light");addChild(createLightCentered<MediumLight<RedGreenBlueLight>>(
Vec(x, y), module, LIGHT1_R + i * 3));The lightId argument points to the first of the 3 consecutive indices.
void process(const ProcessArgs &args) override {
// ... compute output voltage v ...
float hue = clamp((v + 5.f) / 10.f, 0.f, 1.f); // bipolar normalisation
// HSV → RGB (manual, avoids trig)
int hi = (int)(hue * 6.f);
float f = hue * 6.f - hi;
float p = 1.f - f;
float q = 1.f - (1.f - f);
float r, g, b;
switch (hi % 6) {
case 0: r = 1.f; g = q; b = p; break;
case 1: r = p; g = 1.f; b = p; break;
// ... etc
}
lights[LIGHTn_R].setBrightness(r);
lights[LIGHTn_G].setBrightness(g);
lights[LIGHTn_B].setBrightness(b);
}lights[id].setBrightness(float) sets a single light channel. The 3 channels
must be set individually for an RGB widget.
- Units are millimeters. 1hp = 5.08mm.
viewBoxdimensions determine module size:viewBox="0 0 20.32 128.5"= 4hp × standard height- Must be included in the package via
DISTRIBUTABLESin Makefile - Labels, artwork, and LED‑hole indicators are drawn as regular SVG elements
To swap a panel, change the path in setPanel(createPanel(asset::plugin(...))).
| Widget | Usage |
|---|---|
RaKnob |
Standard knob (Davies1900hBlackKnob) |
RaKnobLarge |
Large knob (Davies1900hLargeBlackKnob) |
RaKnobSmall |
Small knob for tight spots (RoundSmallBlackKnob) |
RaKnobTrim |
Trim pot (Trimpot) |
RaPort |
Audio/CV jack — auto light/dark (ThemedPJ301MPort) |
RaSwitch2 |
2‑position toggle switch (CKSS) |
RaSwitch3 |
3‑position toggle switch (NKK) |
RaButton |
Momentary push button (VCVButton) |
RaLightButton |
Light-up push button (VCVLightButton) |
RaLightBezel |
Light-up bezel (VCVLightBezel) |
RaScrew |
Panel screw — auto light/dark (ThemedScrew) |
RaRGBLight |
5 mm RGB LED (MediumLight) |
All type aliases are defined in src/ra-components.hpp. Include it instead of
rack.hpp in module source files for convenience.
Rack renders module params/inputs/outputs in enum order, not addition order.
Keep NUM_PARAMS / NUM_INPUTS / NUM_OUTPUTS / NUM_LIGHTS last in
each enum.
util/make.sh— cleans, builds, and installs in one step- Override
RACK_DIRat invocation:RACK_DIR=../Rack-SDK ./util/make.sh - After install, restart Rack to pick up the updated
.vcvplugin
| Symptom | Cause |
|---|---|
"Failed to load plugin" |
version in plugin.json doesn't start with 2 |
Undefined symbol init |
Missing extern "C" linkage on init() |
Undefined symbol pluginInstance |
Missing Plugin *pluginInstance; in plugin.cpp |
| SVGs missing from package | Missing DISTRIBUTABLES += $(wildcard res/*) |
| Screws half off panel | Left screw x position should be 0, not RACK_GRID_WIDTH |
| Lights not showing | NUM_LIGHTS too small — count each RGB channel separately |
| Module not in browser | Missing p->addModel(modelXxx) in init() |
| Module doesn't save state | plugin.json slug mismatch with createModel string |
- 1 HP = 5.08 mm = 15 Rack units (also called px)
- 3U height = 380 Rack units (= 380 px at 100% zoom)
- 1 Rack unit ≈ 5.08/15 = 0.339 mm ≈ 1 px at screen DPI
- All widget positions (
Vec(x, y)) inModuleWidgetare in Rack units - The module
box.sizeis set bysetPanel()— width is rounded to nearest HP, height stays at 380
RACK_GRID_WIDTH = 15 // 1 HP
RACK_GRID_HEIGHT = 380 // 3U
RACK_GRID_SIZE = Vec(15, 380)
RACK_OFFSET = Vec(15 * 2000, 380 * 100) // rack grid origin offsetRack uses 72 DPI for SVG rendering (not Inkscape's 96 DPI).
The module panel SVG viewBox should be in mm:
- Width = nHP × 5.08mm
- Height = 128.5mm (covers 380 Rack units)
SVG mm → Rack units: multiply by 15 / 5.08 (≈ 2.953)
Rack units → SVG mm: multiply by 5.08 / 15 (≈ 0.339)
All sizes in Rack units (px) at 100% zoom, determined from SVG width/height attributes.
| Component | Width | Height | Radius |
|---|---|---|---|
| PJ301MPort | 23.7 | 23.7 | 11.85 |
| PJ301MPort-dark | 23.7 | 23.7 | 11.85 |
| PJ3410 | 29.4 | 29.4 | 14.7 |
| ADAT | 29.5 | 30.2 | 14.75 |
| MIDI_DIN | 48.2 | 48.2 | 24.1 |
| USB_B | 30.9 | 34.6 | — |
| Component | Width | Height | Radius |
|---|---|---|---|
| TL1105 | 15.36 | 15.36 | 7.68 |
| VCVButton | 18 | 18 | 9 |
| BefacoPush | 28 | 28 | 14 |
| PB61303 | 28.3 | 28.3 | 14.15 |
| Component | Width | Height | Radius |
|---|---|---|---|
| RoundHugeBlackKnob | 53.9 | 53.9 | 26.95 |
| RoundBigBlackKnob | 45 | 45 | 22.5 |
| RoundLargeBlackKnob | 36 | 36 | 18 |
| RoundBlackKnob | 28.35 | 28.35 | 14.17 |
| RoundSmallBlackKnob | 22.68 | 22.68 | 11.34 |
| Trimpot | 17.86 | 17.86 | 8.93 |
| BefacoTinyKnobWhite | 25.5 | 25.5 | 12.75 |
| BefacoBigKnob | 73.7 | 73.7 | 36.85 |
| Component | Width | Height | Radius |
|---|---|---|---|
| CKSS (2 position) | 14 | 20.64 | 7×10.3 |
| CKSSThree (3 pos) | 13.46 | 28.35 | 6.73×14.17 |
| CKSSThreeHorizontal (3 pos) | 28.35 | 13.46 | 14.17×6.73 |
| NKK (3 pos) | 32 | 43.9 | 16×22 |
| Component | Width | Height | Radius |
|---|---|---|---|
| ScrewSilver | 15 | 15 | 7.5 |
| ScrewBlack | 15 | 15 | 7.5 |
| VCVBezel | 21.3 | 21.3 | 10.65 |
| CL1362 | 35 | 35 | 17.5 |
Two items should be spaced so the gap between their visible edges is ≥ 2-4 Rack units for tight layouts, ≥ 6-8 for comfortable layouts.
min_center_distance = r1 + r2 + desired_gap
| Pair | Radii sum | Tight (gap=2) | Comfortable (gap=6) |
|---|---|---|---|
| Jack–Jack | 11.85+11.85=23.7 | 26 | 30 |
| Jack–TL1105 | 11.85+7.68=19.53 | 22 | 26 |
| TL1105–TL1105 | 7.68+7.68=15.36 | 18 | 22 |
| Jack–SmallKnob | 11.85+11.34=23.19 | 26 | 30 |
| Jack–RoundBlackKnob | 11.85+14.17=26.02 | 28 | 32 |
Minimum distance from component edge to panel edge:
- Jack: 11.85 (touching edge), recommended ≥ 13 (small gap)
- TL1105: 7.68, recommended ≥ 10
- Knobs: depends on knob size
When stacking rows of controls:
min_row_spacing = max(r1_bottom, r2_top_in_next_row) + gap
Common ULFO reference:
- Sine output at y=106, Cosine output at y=106 (same row)
- Inv sine at y=128, Inv cosine at y=128 (next row)
- Row spacing = 128 - 106 = 22 units
- Jack radius = 11.85, so row 1 bottom = 106+11.85=117.85, row 2 top = 128-11.85=116.15
- Vertical overlap: 117.85 - 116.15 = 1.7 units (slight overlap accepted)
- With 24 unit row spacing: 106+11.85=117.85 vs 130-11.85=118.15 → gap of 0.3
For jacks: 24 units between rows gives slight overlap (~2 units). For buttons: 22 units is fine. For clean separation with jacks: use 26-28 units between row centers.
Left-side screws go at x=0 to sit flush with the panel edge. Right-side screws go at box.size.x - RACK_GRID_WIDTH to sit flush with the right edge.
addChild(createWidget<RaScrew>(Vec(0, 0)));
addChild(createWidget<RaScrew>(Vec(box.size.x - RACK_GRID_WIDTH, 0)));
addChild(createWidget<RaScrew>(Vec(0, box.size.y - RACK_GRID_WIDTH)));
addChild(createWidget<RaScrew>(Vec(box.size.x - RACK_GRID_WIDTH, box.size.y - RACK_GRID_WIDTH)));x=30 (center) — RoundBlackKnob (r=14.17) at y=24 → edges: 15.8–44.2
x=52 (right side) — CKSS (r=7 wide) at y=46 → edges: 45–59
x=14 (left) — PJ301M (r=11.85) at y=52 → edges: 2.15–25.85
x=30 (center) — RoundSmallBlackKnob (r=11.34) at y=72 → edges: 18.66–41.34
x=16, 44 (L/R pair) — PJ301M at y=106 → edge margin ~4, gap between=4.3
x=16, 44 (L/R pair) — PJ301M at y=128
x=30 (center) — RoundSmallBlackKnob (r=11.34) at y=158
x=16, 44 (L/R pair) — RoundSmallBlackKnob + PJ301M at y≈194/190
Key takeaways from ULFO:
- Left/right output jack pair (16 and 44) = 28 units between centers
- Jack to panel edge margin: ~4 units (very tight; 16 - 11.85 = 4.15)
- Output jack rows: 22 units vertical spacing (slight overlap)
The agent has a documented history of failing at basic instructions. You must follow these rules:
- Do what the user says, exactly as they say it. Take their words literally. Do not reinterpret, improve upon, or get creative with their instructions.
- If you don't understand or are unsure, stop and ask. Do not guess. Do not try to solve ambiguous instructions with your own ideas.
- Your own ideas about layout, design, or architecture are not wanted. The user is the architect. Execute precisely.
- If you find yourself thinking "but maybe they meant..." — stop. You are about to make a mistake. Ask.
Violating these rules wastes the user's time and frustrates them. Don't do it.
- "vertical row of LEDs" = one column, stacked top-to-bottom (same x, different y)
- "vertical space" = room along the Y axis (380 units tall)
- "stack vertically" = put things at different y positions
- NEVER interpret "vertical" as side-by-side columns
- "horizontal row of LEDs" = one row, left-to-right (same y, different x)
- "horizontal space" = room along the X axis (60 units for 4HP)
| User says | Means | Do this |
|---|---|---|
| "stack vertically" | chains at different y | same x, different y |
| "vertical row of LEDs" | LEDs in a column | same x, different y |
| "horizontal row" | LEDs in a row | same y, different x |
| "to the left/right" | move in x | change x, keep y |
| "move IO left" | shift ports to smaller x | change x, keep y |
- Use
TinyLight<RedGreenBlueLight>unless told otherwise - Green (bottom/left) → Red (top/right) gradient for VU bars
- "8 in a row" = 8 LEDs in one horizontal row. "8 in a vertical row" = 8 LEDs stacked in one column.
- "per chain" = each signal path gets its own set
- "16 total" = 2 chains × 8 LEDs = 16
- Place screws first:
Vec(0, 0)(top-left),Vec(box.size.x - RACK_GRID_WIDTH, 0)(top-right) - Every
create*Centered()positions the component's center at the given Vec. - All SVGs in
res/are loaded withcreatePanel()and their viewBox width/height in mm set to nHP×5.08 × 128.5. - Label text in SVGs is placed at approximate positions (purely decorative — components are positioned by C++ code).
- SVG coordinate ≈ Rack coordinate × 5.08/15.
- Test visually: launch Rack, add module, check overlaps at 100% zoom.
The background SVG should just be an empty black image unless the user updates it.
You never need to update SVGs. The user is an artist and they can handle that.