# RUB Song JSON — everything needed to generate a valid document

You are generating a **song document** for RUB, a 303-style acid synthesizer
(2 oscillators, resonant filter, 16-step monophonic sequencer, per-step
parameter lanes). Output one JSON object — no prose, no code fences unless
asked — matching the fixed schema below. The parser is strict about a few
things and tolerant about the rest; both are called out.

## Top-level shape

```json
{
  "format": "rub",
  "version": 4,
  "name": "My song",
  "bpm": 128,
  "mode": 0,
  "lane_blend": 1,
  "chain": [0],
  "patch": {},
  "patterns": [],
  "lanes": [],
  "osc1_custom": [],
  "osc2_custom": []
}
```

| Key | Type | Notes |
| --- | --- | --- |
| `format` | string | **Required.** Must be `"rub"` (`"rdsong"` is legacy, still loads, rewritten to `"rub"`). |
| `version` | number | **Required.** Must be `<= 4` (current writer emits `4`). Higher = rejected. |
| `name` | string | Song/preset name shown in the UI. |
| `bpm` | number | Tempo. Default `120` when omitted. Not a patch key. |
| `mode` | number | Chain playback mode: `0` = Chain (default), `1` = Global loop, `2` = Single loop. |
| `lane_blend` | number | `0.0`–`1.0`, default `1`. `0` = panel/base patch only, `1` = full param-lane override. |
| `chain` | int[] | Pattern indices `0`–`15`, max 99 entries. An out-of-range entry **fails the whole import**. Empty/`[]` becomes `[0]`. |
| `patch` | object | `id_str → number` pairs in real units — see the catalog below. Omitted keys keep factory defaults; **unknown keys are ignored** (forward compatibility). `bpm` and `lane_blend` are NOT patch keys. |
| `patterns` | array | Up to 16 patterns; each an array of up to 16 step objects (below). Missing indices = empty steps. |
| `lanes` | array | Param Seq lanes (below). Malformed entries are skipped, never fatal. |
| `osc1_custom` / `osc2_custom` | float[] | Optional. **Exactly 256 values** in `−1.0…+1.0`, one cycle. Only meaningful when the matching `osc1_type`/`osc2_type` is `8` (Custom). Omitted = default sine table. |
| `selected_pattern` | int | Optional UI hint, `0`–`15`. |
| `app`, `app_version`, `modified_at` | string | Optional metadata the app writes on export (`"RUB"`, build version, ISO-8601 UTC). |

## Step objects

Each of the 16 steps per pattern:

```json
{"note": 72, "accent": 0, "slide": 0}
```

| Field | Type | Notes |
| --- | --- | --- |
| `note` | int | MIDI note number `0`–`127`. `254` = empty step (rest), `255` = release (gate off). Omitted = empty. RUB's display labels MIDI 72 as C3. |
| `accent` | int | `0` = off, non-zero = accented (writer emits `0`/`1`). |
| `slide` | int | `0` = off, non-zero = slide into the next step. |

Notes:

- The sequencer is **monophonic** — one note per step.
- Slide behavior depends on the `slide_gate` patch param (`0` = classic destination slide, `1` = sustain through slides).
- A non-array pattern or non-object step **fails the whole import**.

## Param Seq lanes

Per-step automation of mappable parameters — one object per occupied lane:

```json
{"pattern": 0, "slot": 0, "param": "filter_cutoff",
 "values": [128, 128, 200, 200, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
 "active": [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 "enabled": 1}
```

| Field | Type | Notes |
| --- | --- | --- |
| `pattern` | int | `0`–`15` — which pattern this lane belongs to. |
| `slot` | int | `0`–`31` — lane slot index within that pattern. |
| `param` | string | A **lane-capable** `id_str` from the catalog (Lane = yes). Unknown or non-lane params are skipped. |
| `values` | float[] | Up to 16 values in the param's **real units** (same range as `patch` values). Missing steps get the param default. |
| `active` | int[] | Up to 16 `0`/`1`. `0` = the lane writes nothing that step; the last written value holds. |
| `enabled` | int | Optional, default `1`. `0` = muted (data kept, not applied). |

## What fails vs. what is tolerated

**Fatal:** invalid JSON, non-object root, missing/wrong `format`,
`version > 4`, an out-of-range `chain` entry, a non-array pattern, a
non-object step.

**Tolerated:** missing keys (defaults apply), extra/unknown top-level and
`patch` keys (ignored), malformed lane entries (skipped), `osc*_custom`
arrays of the wrong length (ignored), `bpm`/`mode`/`lane_blend` outside
their ranges (clamped/defaulted).

## Minimal working example

```json
{
  "format": "rub",
  "version": 4,
  "name": "Example",
  "bpm": 130,
  "mode": 0,
  "lane_blend": 1,
  "chain": [0, 0],
  "patch": {
    "filter_cutoff": 160,
    "filter_resonance": 190,
    "decay": 90,
    "osc1_type": 2,
    "osc2_type": 2,
    "osc_mix": 128
  },
  "patterns": [
    [
      {"note": 48, "accent": 1, "slide": 0},
      {"note": 254, "accent": 0, "slide": 0},
      {"note": 48, "accent": 0, "slide": 1},
      {"note": 60, "accent": 0, "slide": 0},
      {"note": 254, "accent": 0, "slide": 0},
      {"note": 48, "accent": 1, "slide": 0},
      {"note": 255, "accent": 0, "slide": 0},
      {"note": 254, "accent": 0, "slide": 0},
      {"note": 48, "accent": 0, "slide": 0},
      {"note": 254, "accent": 0, "slide": 0},
      {"note": 51, "accent": 0, "slide": 1},
      {"note": 48, "accent": 0, "slide": 0},
      {"note": 60, "accent": 1, "slide": 0},
      {"note": 254, "accent": 0, "slide": 0},
      {"note": 48, "accent": 0, "slide": 0},
      {"note": 63, "accent": 0, "slide": 0}
    ]
  ]
}
```

Omitted `patterns` entries (patterns 1–15 here) are empty; `chain: [0, 0]`
loops pattern 0 twice per pass.

## Parameter catalog

All knobs, LEDs, and BPM listed here can be MIDI-learned (right-click → MIDI Learn) unless noted in the UI.
There is **no factory CC map** — your mappings are saved with plugin/host state.

| Group | Name | `id_str` | Range | Unit | Default | Lane | Choices |
| --- | --- | --- | --- | --- | ---: | --- | --- |
| Filter | Cut Off | `filter_cutoff` | 0…255 | % | 128 | yes | — |
| Filter | Resonance | `filter_resonance` | 0…255 | % | 128 | yes | — |
| Filter | Env Mod | `filter_env_mod` | 0…255 | % | 0 | yes | — |
| Filter | Decay | `decay` | 0…255 | % | 128 | yes | — |
| Filter | Attack | `attack` | 0…255 | % | 128 | yes | — |
| Filter | Accent | `accent_amount` | 0…255 | % | 128 | yes | — |
| Oscillators | Tune A | `tune_a` | -1…1 | — | 0 | yes | — |
| Rub | Rub Enable | `rub_on` | 0 / 1 | — | 0 | yes | — |
| Rub | Screech | `screech` | 0…255 | % | 128 | yes | — |
| Rub | Oomph | `oomph` | 0…255 | % | 128 | yes | — |
| Rub | RA | `ra` | 0 / 1 | — | 0 | yes | — |
| Rub | RB | `rb` | 0 / 1 | — | 0 | yes | — |
| Oscillators | Osc 1 Wave | `osc1_type` | 0…8 | — | 2 | yes | Sine, Triangle, Saw, Double Saw, Square, Pulse 25%, Saw+Square, Noise, Custom |
| Oscillators | Osc 2 Wave | `osc2_type` | 0…8 | — | 0 | yes | Sine, Triangle, Saw, Double Saw, Square, Pulse 25%, Saw+Square, Noise, Custom |
| Oscillators | Osc B x2 | `osc2_x2` | 0 / 1 | — | 0 | yes | — |
| Oscillators | Mix | `osc_mix` | 0…255 | % | 128 | yes | — |
| Delay | Time | `delay_time` | 0…255 | ms | 0 | yes | — |
| Delay | Wet | `delay_wet` | 0…255 | % | 64 | yes | — |
| Delay | Feedback | `delay_feedback` | 0…255 | % | 128 | yes | — |
| Distortion | Drive | `distort_drive` | 0…255 | % | 128 | yes | — |
| Distortion | Gain | `distort_gain` | 0…255 | % | 0 | yes | — |
| Output | Master | `master` | 0…255 | % | 100 | yes | — |
| Transport | Tempo | `bpm` | 0.01…999 | BPM | 120 | no | — |
| Oscillators | Osc A x2 | `osc1_x2` | 0 / 1 | — | 0 | yes | — |
| Output | Comp Enable | `comp_on` | 0 / 1 | — | 1 | yes | — |
| Delay | Sync | `delay_sync` | 0 / 1 | — | 1 | no | — |
| Delay | Division | `delay_div` | 0…7 | — | 2 | yes | 1/32, 1/16, 1/8, 1/8D, 1/4, 1/4D, 1/2, 1/1 |
| Delay | Delay Enable | `delay_on` | 0 / 1 | — | 0 | yes | — |
| Distortion | Dist Enable | `distort_on` | 0 / 1 | — | 0 | yes | — |
| Sequencer | Lane Blend | `lane_blend` | 0…1 | % | 1 | no | — |
| Sequencer | Slide Gate | `slide_gate` | 0 / 1 | — | 0 | yes | — |
| Oscillators | Tune B | `tune_b` | -1…1 | — | 0 | yes | — |
| Oscillators | Tune Range | `tune_range` | 1…24 | st | 1 | yes | — |
| Oscillators | Sync A→B | `sync_a_to_b` | 0 / 1 | — | 0 | yes | — |
| Oscillators | Sync B→A | `sync_b_to_a` | 0 / 1 | — | 0 | yes | — |

## Song JSON in the app

RUB’s native song format is a fixed-schema JSON document (`format: "rub"`). The UI exposes the whole document for copy/paste workflows with AI tools.


## Open the dialog

Click the **braces** icon left of the shortcuts help (footer). The overlay shows live JSON while you edit the synth; editing the text marks it dirty until you **Apply**.

## Clipboard shortcuts (no dialog)

| Shortcut | Action |
| --- | --- |
| **Cmd/Ctrl+Shift+C** | Copy current song JSON to the system clipboard |
| **Cmd/Ctrl+Shift+V** | Validate clipboard JSON and apply it |

Invalid paste shows an error dialog. Pattern copy/paste remains **Cmd/Ctrl+C/X/V** (no Shift).

## Document shape

Top-level keys include `format`, `version`, `name`, `bpm`, `mode`, `chain`, `patch`, `patterns`, optional custom waves, and `lanes` (Param Seq).

| Key | Meaning |
| --- | --- |
| `osc1_custom` / `osc2_custom` | Optional 256-float arrays (−1…+1), one cycle per osc. Omitted when the table is the default sine. See [Oscillators](/docs/oscillators#custom-waveforms). |

Optional metadata (written on export / snapshot):

| Key | Meaning |
| --- | --- |
| `app` | `"RUB"` |
| `app_version` | Build that wrote the document |
| `modified_at` | ISO-8601 UTC timestamp |
| `selected_pattern` | UI pattern index hint (0…15) |

Older files without metadata still load; unknown patch keys are ignored for forward compatibility.

## AI workflow

1. Copy JSON (shortcut or dialog **Copy**).
2. Paste into your AI tool; ask for edits (notes, patch, lanes).
3. Put the revised document on the clipboard and **Cmd/Ctrl+Shift+V**, or paste into the dialog and **Apply**.

Keep `format: "rub"` and a supported `version`. Older files with `format: "rdsong"` still load and are rewritten to `"rub"` when scanned in User/Imported. Prefer changing `id_str` values from the [Parameters](/docs/parameters) table rather than inventing keys.
