docs: LLM-ready documentation suite — LLM_CONTEXT.md, module deep-dives, JSON schemas, CLI reference

Complete documentation system for LLM consumption: primary LLM_CONTEXT.md
(27KB system overview), 4 module deep-dives (composer, reaper-builder,
reaper-scripting, calibrator), 2 JSON Schema draft-07 contracts, CLI
reference, and README correction (FL Studio -> REAPER identity).
This commit is contained in:
renato97
2026-05-04 10:30:24 -03:00
parent b08dcccca2
commit 7bcd8052a9
13 changed files with 2402 additions and 29 deletions

209
docs/CLI.md Normal file
View File

@@ -0,0 +1,209 @@
# CLI Reference
> Parent doc: [LLM_CONTEXT.md](LLM_CONTEXT.md)
## Overview
Three CLI entry points, all in `scripts/`:
| Script | Role | Default output |
|--------|------|---------------|
| `compose.py` | Full composition pipeline | `.rpp` file |
| `generate.py` | Thin wrapper around compose | `.rpp` file + optional validation |
| `run_in_reaper.py` | ReaScript generation & execution | ReaScript `.py`, result JSON, LUFS JSON |
## compose.py
Full pipeline: builds all tracks, applies calibration, generates `.rpp`.
### Usage
```bash
python scripts/compose.py [--bpm BPM] [--key KEY] [--output PATH] [--seed SEED]
[--emotion EMOTION] [--inversion INVERSION]
[--no-calibrate]
```
### Flags
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--bpm` | `float` | `99` | Tempo. Must be > 0. |
| `--key` | `str` | `"Am"` | Musical key. Format: `[A-G][b#]?m?` (e.g. `"Am"`, `"Dm"`, `"G"`, `"F#m"`) |
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` file path. Parent directory created if missing. |
| `--seed` | `int` | `None` | Random seed. `None` = non-deterministic (system time). |
| `--emotion` | `str` | `"romantic"` | Chord emotion: `romantic`, `dark`, `club`, `classic` |
| `--inversion` | `str` | `"root"` | Preferred chord inversion: `root`, `first`, `second` |
| `--no-calibrate` | `flag` | `False` | Skip post-processing mix calibration |
### Examples
```bash
# Default reggaetón at 99 BPM in Am
python scripts/compose.py
# Specific BPM and key
python scripts/compose.py --bpm 95 --key Dm
# Deterministic output with seed
python scripts/compose.py --bpm 95 --key Am --seed 42
# Dark emotion with first inversion chords
python scripts/compose.py --emotion dark --inversion first
# Skip calibration (raw volumes from VOLUME_LEVELS in compose.py)
python scripts/compose.py --no-calibrate
# Custom output path
python scripts/compose.py --output my_project/song_v2.rpp
```
### Output Structure
```
output/song.rpp # REAPER project file
```
Console output includes:
- BPM and key confirmation
- Section count and total duration (bars + beats)
- Kick detection cache summary (per drumloop file)
- Final output path
## generate.py
Thin wrapper around `compose.py`. Delegates by setting `sys.argv` and calling `compose.main()`.
### Usage
```bash
python scripts/generate.py [--bpm BPM] [--key KEY] [--output PATH] [--seed SEED]
[--emotion EMOTION] [--inversion INVERSION]
[--validate]
```
### Flags
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--bpm` | `float` | `95` | Tempo. Must be > 0. |
| `--key` | `str` | `"Am"` | Musical key |
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` path |
| `--seed` | `int` | `42` | Random seed |
| `--emotion` | `str` | `"romantic"` | Chord emotion |
| `--inversion` | `str` | `"root"` | Chord inversion |
| `--validate` | `flag` | `False` | Run validator after generation |
### Differences from compose.py
- Default BPM is 95 (vs 99 in compose.py)
- Default seed is 42 (vs None in compose.py)
- Has `--validate` flag (runs `validate_rpp_output()` after generation)
- No `--no-calibrate` flag (calibration always applied)
### Examples
```bash
# Generate with default settings
python scripts/generate.py
# Generate with validation
python scripts/generate.py --bpm 95 --key Dm --seed 123 --validate
# Full custom generation
python scripts/generate.py --bpm 100 --key Fm --output output/custom.rpp --seed 7 --emotion club
```
### Validation Errors
When `--validate` is set, errors are printed to stderr:
```
Validation errors:
- Expected 416 beats, found 400 beats
- Missing drumloop in section: chorus
```
Exit code 1 on validation failure.
## run_in_reaper.py
Generates a ReaScript for post-processing and polls for results.
### Usage
```bash
python scripts/run_in_reaper.py <rpp_path> [--output WAV_PATH] [--timeout SECONDS]
[--plugins-config JSON_PATH] [--action ACTIONS]
```
### Arguments and Flags
| Arg/Flag | Type | Default | Description |
|----------|------|---------|-------------|
| `rpp_path` | positional | *(required)* | Path to `.rpp` file |
| `--output`, `-o` | `str` | `<rpp_stem>_rendered.wav` | Rendered WAV output path |
| `--timeout` | `int` | `120` | Seconds to poll for result JSON |
| `--plugins-config` | `str` | `None` | Path to JSON-serialized `SongDefinition` (for deriving `plugins_to_add`) |
| `--action` | `str` | `"calibrate"` | Space-separated action pipeline |
### Actions
Space-separated list of up to 5 actions, executed in order:
```
add_plugins configure_fx_params verify_fx calibrate render
```
Each action name maps to a function in the generated ReaScript. Unknown action names are ignored.
### Examples
```bash
# Basic calibration on an existing .rpp
python scripts/run_in_reaper.py output/song.rpp
# Full pipeline: load plugins, calibrate, render
python scripts/run_in_reaper.py output/song.rpp --action "add_plugins calibrate render"
# With plugin config from JSON SongDefinition
python scripts/run_in_reaper.py output/song.rpp --plugins-config output/song.json --action "add_plugins configure_fx_params"
# Custom render output and extended timeout
python scripts/run_in_reaper.py output/song.rpp -o output/final_mix.wav --timeout 300 --action "calibrate render"
```
### Output Files
```
scripts/fl_control_phase2.py # Generated ReaScript
scripts/fl_control_command.json # Command JSON for ReaScript
scripts/fl_control_result.json # Result JSON from ReaScript (polled)
output/<song>_lufs.json # LUFS metrics
output/<song>_fx_errors.json # FX errors (if any)
```
### Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Error: .rpp not found, plugin config not found, result read error, or REAPER error |
| 2 | Timeout: result JSON not found within timeout |
## Key Validation
All scripts validate the key format: `^[A-G][b#]?m?$`
Valid examples: `"Am"`, `"C"`, `"D#m"`, `"Gb"`, `"F#"`.
Invalid: `"A minor"`, `"c"` (lowercase), `"H"`, `"Am7"`.
## BPM Validation
BPM must be > 0. Min/max enforced by `SongDefinition.validate()`: 20999.
## Determinism
- When `--seed` is provided, all random operations use `random.Random(seed)` for reproducible output.
- Same `(--bpm, --key, --seed, --emotion, --inversion)` produces identical `.rpp` files.
- ReaScript seed is derived from the CLI seed. Same command → same script.

544
docs/LLM_CONTEXT.md Normal file
View File

@@ -0,0 +1,544 @@
# fl_control — LLM System Overview
> **About the name**: The directory is named `fl_control` for historical reasons, but the system generates **REAPER `.rpp` files**, not FL Studio `.flp` files. There is NO FL Studio support. The name has been retained for backward compatibility with repository URLs and CI/CD pipelines.
## What Is This?
`fl_control` is a Python system that generates complete **REAPER `.rpp` projects** from the command line. Given a key, BPM, and emotion, it composes a full reggaetón instrumental with drums, 808 bass, chord progressions (with voice leading), lead melodies (hook-based call-and-response), pads, percussion, and transition FX. The output is a valid `.rpp` file that REAPER opens directly, plus an auto-generated ReaScript (`.py`) for post-processing (plugin loading, mix calibration, rendering).
**Target**: REAPER (Windows, v7.x). **Format**: `.rpp` text files. **No DAW dependencies** beyond reading sample files from disk.
## Quick Start
```bash
# Install dependencies
pip install -r requirements.txt
# Generate a song (defaults: 95 BPM, Am key, romantic emotion)
python scripts/generate.py --bpm 95 --key Am --output output/song.rpp --seed 42
# Generate with validation
python scripts/generate.py --bpm 95 --key Dm --output output/song.rpp --seed 42 --validate
# Compose directly (more detailed output)
python scripts/compose.py --bpm 99 --key Am --emotion romantic --output output/song.rpp
```
Output: a ready-to-open `.rpp` file at the specified path.
## Architecture
```
┌───────────────────────────────────────┐
│ CLI Layer │
│ scripts/generate.py (thin wrapper) │
│ scripts/compose.py (full pipeline) │
│ scripts/run_in_reaper.py (ReaScript) │
└──────────────┬────────────────────────┘
┌──────────────────────┼──────────────────────┐
│ ▼ │
┌────────────────┴──────┐ ┌────────────────────┴──┐ ┌───────┴──────────┐
│ src/composer/ │ │ src/calibrator/ │ │ src/selector/ │
│ ┌──────────────────┐ │ │ ┌────────────────┐ │ │ SampleSelector │
│ │ chords.py │ │ │ │ Calibrator │ │ └────────────────┘
│ │ ChordEngine │ │ │ │ .apply() │ │
│ │ melody_engine.py │ │ │ │ presets.py │ └───────────────────┘
│ │ build_motif() │ │ │ │ VOLUME_PRESETS │
│ │ patterns.py │ │ │ │ PAN_PRESETS │ ┌──────────────────┐
│ │ rhythm.py │ │ │ │ SEND_PRESETS │ │ src/core/ │
│ │ templates.py │ │ │ └────────────────┘ │ schema.py │
│ └──────────────────┘ │ └─────────────────────────────┘ │ SongDefinition │
└───────────┬────────────┘ │ TrackDef │
│ │ ClipDef │
▼ │ MidiNote │
┌──────────────────────────┐ │ PluginDef │
│ SongDefinition │◄──────────────────────────────│ SectionDef │
│ ┌────────────────────┐ │ │ PatternDef │
│ │ meta: SongMeta │ │ │ Arrangement... │
│ │ tracks: [TrackDef] │ │ │ CCEvent │
│ │ patterns: [...] │ │ └──────────────────┘
│ │ sections: [...] │ │
│ │ master_plugins │ │
│ └────────────────────┘ │
└───────────┬──────────────┘
┌─────────┼─────────┐
▼ ▼ ▼
┌──────────────┐ ┌───────────────┐ ┌─────────────────┐
│ RPPBuilder │ │ ReaScriptGen │ │ Validator │
│ .write() → │ │ generate() → │ │ validate_rpp() │
│ song.rpp │ │ fl_control_ │ │ │
│ │ │ phase2.py │ │ │
└──────────────┘ └───────┬───────┘ └─────────────────┘
┌──────────────┐
│ REAPER │
│ opens .rpp │
│ runs script │
│ renders WAV │
└──────────────┘
```
**Pipeline**: CLI → compose `SongDefinition` → Calibrator.apply() (post-processing) → RPPBuilder.write() → `.rpp` file → ReaScriptGenerator.generate() → ReaScript → REAPER execution.
## Data Model
All types live in `src/core/schema.py`. Every entity is a Python dataclass.
### SongMeta
Song-level metadata.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `bpm` | `float` | *(required)* | Tempo 20999 |
| `key` | `str` | *(required)* | Key string, e.g. `"Am"`, `"Dm"`, `"G"` |
| `title` | `str` | `""` | Song title |
| `ppq` | `int` | `960` | Ticks per quarter note (REAPER default) |
| `time_sig_num` | `int` | `4` | Time signature numerator |
| `time_sig_den` | `int` | `4` | Time signature denominator |
| `calibrate` | `bool` | `True` | Enable post-processing mix calibration |
### MidiNote
A single MIDI note event within a clip.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `pitch` | `int` | *(required)* | MIDI note number 0127 (60 = middle C) |
| `start` | `float` | *(required)* | Start time in beats from item start |
| `duration` | `float` | *(required)* | Duration in beats |
| `velocity` | `int` | `64` | Velocity 0127 |
### CCEvent
A MIDI CC event within a clip.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `controller` | `int` | *(required)* | CC number (e.g. 11 = Expression) |
| `time` | `float` | *(required)* | Position in beats from clip start |
| `value` | `int` | *(required)* | CC value 0127 |
### ClipDef
A clip placed on a track — either audio or MIDI.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `position` | `float` | *(required)* | Start position in beats |
| `length` | `float` | *(required)* | Duration in beats |
| `name` | `str` | `""` | Display name |
| `audio_path` | `str \| None` | `None` | Absolute path to audio file (audio clips) |
| `midi_notes` | `list[MidiNote]` | `[]` | MIDI notes (MIDI clips) |
| `midi_cc` | `list[CCEvent]` | `[]` | MIDI CC events |
| `loop` | `bool` | `False` | Whether the audio clip loops |
| `fade_in` | `float` | `0.0` | Fade-in duration in seconds |
| `fade_out` | `float` | `0.0` | Fade-out duration in seconds |
| `vol_mult` | `float` | `1.0` | Volume multiplier applied at clip level |
Properties: `is_midi` → True when `midi_notes` is non-empty; `is_audio` → True when `audio_path` is not None.
### PluginDef
A VST plugin instance on a track.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | `str` | *(required)* | Display name (e.g. `"Serum 2"`) |
| `path` | `str` | *(required)* | Plugin path/identifier |
| `index` | `int` | `0` | Chain position (0 = first) |
| `params` | `dict[int, float]` | `{}` | Parameter index → value |
| `preset_data` | `list[str] \| None` | `None` | Base64 preset chunks |
| `role` | `str` | `""` | Track role for role-aware preset lookup |
| `builtin` | `bool` | `False` | True → deferred to ReaScript, not written to .rpp |
### TrackDef
A track in the REAPER project.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | `str` | *(required)* | Track display name |
| `volume` | `float` | `0.85` | 0.01.0 (maps to REAPER volume fader) |
| `pan` | `float` | `0.0` | -1.0 (left) to 1.0 (right) |
| `color` | `int` | `0` | REAPER color index 067 |
| `clips` | `list[ClipDef]` | `[]` | Audio/MIDI clips |
| `plugins` | `list[PluginDef]` | `[]` | VST plugins on this track |
| `send_reverb` | `float` | `0.0` | Reverb send level 0.01.0 |
| `send_delay` | `float` | `0.0` | Delay send level 0.01.0 |
| `send_level` | `dict[int, float]` | `{}` | Return track index → send level |
### SectionDef
A section in the song arrangement.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | `str` | *(required)* | Display name (e.g. `"intro"`, `"chorus"`) |
| `bars` | `int` | *(required)* | Length in bars |
| `energy` | `float` | `0.5` | Energy level 0.01.0 |
| `velocity_mult` | `float` | `1.0` | Velocity multiplier for all notes in section |
| `vol_mult` | `float` | `1.0` | Volume multiplier for tracks in section |
### PatternDef
A pattern definition with generator and variation axes.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `id` | `int` | *(required)* | Unique pattern ID |
| `name` | `str` | *(required)* | Display name |
| `instrument` | `str` | *(required)* | Sample/instrument key |
| `channel` | `int` | *(required)* | MIDI channel |
| `bars` | `int` | *(required)* | Length in bars |
| `generator` | `str` | *(required)* | Generator function name |
| `velocity_mult` | `float` | `1.0` | Velocity multiplier 0.851.1 |
| `density` | `float` | `1.0` | Note density 0.01.0 |
### ArrangementItemDef
An item placed in the arrangement referencing a pattern on a track.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `pattern` | `int` | *(required)* | Pattern ID |
| `bar` | `float` | *(required)* | Start position in bars |
| `bars` | `float` | *(required)* | Length in bars |
| `track` | `int` | *(required)* | Track index |
### ArrangementTrack
A track in the REAPER arrangement with index and display name.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `index` | `int` | *(required)* | Track index |
| `name` | `str` | *(required)* | Display name |
### SongDefinition
Complete song definition — the source of truth for one `.rpp` file.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `meta` | `SongMeta` | *(required)* | Song metadata |
| `tracks` | `list[TrackDef]` | `[]` | REAPER tracks with clips and plugins |
| `patterns` | `list[PatternDef]` | `[]` | Pattern definitions for arrangement |
| `items` | `list[ArrangementItemDef]` | `[]` | Arrangement items referencing patterns |
| `progression_name` | `str` | `"i-VII-VI-VII"` | Chord progression label |
| `section_template` | `str` | `"standard"` | Section template name |
| `samples` | `dict[str, str]` | `{}` | Sample file map (name → filename) |
| `sections` | `list[SectionDef]` | `[]` | Section definitions in playback order |
| `master_plugins` | `list[str]` | `[]` | Plugin registry keys for master FX chain |
Key methods:
- `validate()``list[str]` — returns list of validation errors (empty = valid)
- `length_beats` (property) → `float` — total length computed from all clips
- `to_json(indent=2)``str` — serialize to JSON via `dataclasses.asdict`
## Module Index
| Module | Location | Role |
|--------|----------|------|
| **schema** | `src/core/schema.py` | All dataclass definitions. Single source of truth for data model. |
| **composer** | `src/composer/` | Composition engine: chord progression, melody generation, rhythm patterns, drum analysis |
| **reaper_builder** | `src/reaper_builder/` | RPPBuilder: `.rpp` file generation from `SongDefinition`. Plugin registry (~97 entries). VST2/VST3 element builders. Headless render. |
| **reaper_scripting** | `src/reaper_scripting/` | ReaScript generation: self-contained Python scripts for REAPER post-processing |
| **calibrator** | `src/calibrator/` | Post-processing mix calibration: volume, pan, sends, master chain by track role |
| **selector** | `src/selector/` | Sample selection: scores samples by key compatibility, BPM proximity, character |
| **validator** | `src/validator/` | `.rpp` output validation |
| **CLI scripts** | `scripts/` | `compose.py` (full pipeline), `generate.py` (thin wrapper), `run_in_reaper.py` (ReaScript execution) |
### composer/ sub-modules
| File | Role |
|------|------|
| `chords.py` | `ChordEngine` — emotion-aware chord progressions with voice leading |
| `melody_engine.py` | `build_motif()`, `build_call_response()` — hook-based melody generation |
| `patterns.py` | Pattern weight tables extracted from real reggaetón tracks |
| `rhythm.py` | Dembow rhythm generators (classic, perreo, trápico) |
| `templates.py` | RPP template extraction and generation |
| `converters.py` | Conversion utilities |
| `variation.py` | Pattern variation logic |
| `melodic.py` | Melodic pattern generators |
| `drum_analyzer.py` | `DrumLoopAnalyzer` — transient detection for sidechain CC generation |
| `__init__.py` | Scale intervals, chord type tables, legacy pattern generators |
## Pipeline
The complete composition and build pipeline, step by step:
### Phase 1: Composition (`scripts/compose.py`)
1. **Parse CLI args**: `--bpm`, `--key`, `--emotion`, `--inversion`, `--seed`, `--no-calibrate`
2. **Load `SampleSelector`**: Reads `data/sample_index.json` for clap/snare/FX sample selection
3. **Build sections**: 9-section structure (intro → verse → pre-chorus → chorus → verse2 → chorus2 → bridge → final → outro), each with energy level and velocity/volume multipliers
4. **Build tracks** (in order):
- `Drumloop` — audio clips cycling between seco/filtrado variants per section
- `Perc` — percussion audio loops per section
- `808 Bass` — MIDI notes using proven i-iv-i-V harmonic pattern with CC11 sidechain ducking
- `Chords``ChordEngine.progression()` with voice leading; i-VII-VI-VII progression
- `Lead``build_motif("hook")` + `build_call_response()` via melody engine
- `Clap` — Short audio samples on dembow grid (beats 2.0, 3.5)
- `Transition FX` — Audio clips at section boundaries (risers, impacts, sweeps)
- `Pad` — Arpeggiated chord tones on eighth notes
- `Reverb` / `Delay` — Return tracks with Pro-R 2 and ValhallaDelay
5. **Wire sends**: Return track sends by role from `SEND_LEVELS`
6. **Assemble `SongDefinition`**: Meta + tracks + sections + master_plugins
7. **Calibrate** (unless `--no-calibrate`): `Calibrator.apply(song)` — role-based volume, pan, sends, master chain
### Phase 2: Build (`src/reaper_builder/RPPBuilder`)
1. `RPPBuilder(song, seed).write(path)` serializes to `.rpp`:
- Project header (static metadata from ground-truth `test_vst3.rpp`)
- Dynamic TEMPO line
- Master track with FX chain (Ozone 12 triplet or FabFilter fallback)
- Per-track TRACK elements with VOLPAN, AUXRECV (sends)
- Per-track FXCHAIN with VST elements (resolved from `PLUGIN_REGISTRY`)
- Per-clip ITEM elements with SOURCE (WAVE or MIDI with E-lines)
- Built-in plugins (Cockos) deferred to ReaScript insertion
### Phase 3: ReaScript (Phase 2B)
`scripts/run_in_reaper.py` generates a self-contained Python ReaScript that REAPER executes:
1. Reads `fl_control_command.json` (command file)
2. Opens the `.rpp` project
3. Executes action pipeline: `add_plugins``configure_fx_params``verify_fx``calibrate``render`
4. Writes `fl_control_result.json` (LUFS metrics, FX errors, plugin results)
### Validation
`SongDefinition.validate()` checks: BPM range (20999), key format (regex), unique track names, clips with neither audio nor MIDI.
## Plugin System
### PLUGIN_REGISTRY
Located in `src/reaper_builder/__init__.py` (~97 entries). Format:
```python
PLUGIN_REGISTRY: dict[str, tuple[str, str, str]] = {
"key": ("display_name", "filename", "uid_guid"),
}
```
- **key**: Short registry key (e.g. `"Serum_2"`, `"Pro-Q_3"`, `"Decapitator"`)
- **display_name**: Full REAPER display name (e.g. `"VST3i: Serum 2 (Xfer Records)"`)
- **filename**: File on disk (e.g. `"Serum2.vst3"`, `"Decapitator.dll"`)
- **uid_guid**: Unique identifier — VST2 uses `<GUID>`, VST3 uses `{GUID}`
### ALIAS_MAP
Backward compatibility mapping: old key names → new `PLUGIN_REGISTRY` keys. Example:
```python
ALIAS_MAP = {
"Serum2": "Serum_2",
"FabFilter Pro-Q 3": "Pro-Q_3",
"Valhalla Delay": "ValhallaDelay",
"Pro-Q 3": "Pro-Q_3",
}
```
FabFilter plugins map space-separated names to short VST3 keys. SoundToys plugins keep their underscore names.
### PLUGIN_PRESETS
Role-aware preset data. Format:
```python
PLUGIN_PRESETS: dict[tuple[str, str], list[str]] = {
("Pro-Q_3", ""): [chunk1, chunk2, ...], # default
("Serum_2", "bass"): [chunk1, chunk2, ...], # role-specific
}
```
Lookup chain: `(key, role)``(key, "")` (default) → `fallback``None`.
### REAPER_BUILTINS
Set of Cockos native plugin keys (ReaEQ, ReaComp, ReaVerb, etc.). Plugins matching this are deferred to ReaScript insertion (`TrackFX_AddByName`) rather than written to `.rpp` as VST elements.
### Plugin Lookup in RPPBuilder
```
PluginDef.name → ALIAS_MAP.get(name, name) → PLUGIN_REGISTRY.get(resolved)
┌───────────────┤
found│ │not found
▼ ▼
Build VST element Fallback: .dll
with display_name, with 19 param
filename, uid_guid slots
```
## ReaScript Protocol
### Overview
ReaScript is a self-contained Python file generated by `ReaScriptGenerator` that REAPER executes internally. It communicates via JSON files on disk:
```
fl_control_command.json ──read──► ReaScript ──write──► fl_control_result.json
```
### ReaScriptCommand
Defined in `src/reaper_scripting/commands.py`:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `version` | `int` | `1` | Protocol version |
| `action` | `str \| list[str]` | `"calibrate"` | Single action or pipeline list |
| `rpp_path` | `str` | `""` | Path to `.rpp` file to open |
| `render_path` | `str` | `""` | Path for rendered WAV output |
| `timeout` | `int` | `120` | Polling timeout in seconds |
| `track_calibration` | `list[dict]` | `[]` | Volume/pan/send calibration per track |
| `plugins_to_add` | `list[dict]` | `[]` | `{"track_name": str, "fx_name": str, "params": {...}}` |
### ReaScriptResult
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `version` | `int` | `1` | Protocol version |
| `status` | `str` | `"ok"` | `"ok"`, `"error"`, or `"timeout"` |
| `message` | `str` | `""` | Error or status message |
| `lufs` | `float \| None` | `None` | Integrated LUFS |
| `integrated_lufs` | `float \| None` | `None` | Integrated LUFS reading |
| `short_term_lufs` | `float \| None` | `None` | Short-term LUFS |
| `fx_errors` | `list[dict]` | `[]` | FX verification errors |
| `tracks_verified` | `int` | `0` | Number of tracks verified |
| `added_plugins` | `list[dict]` | `[]` | `{"fx_name", "instance_id", "track_name", "status"}` |
### ProtocolVersionError
Raised by `read_result()` when result version doesn't match expected version.
### Key Functions
- `write_command(path, cmd)` — Serialize `ReaScriptCommand` to JSON file
- `read_result(path, expected_version=1)` — Deserialize `ReaScriptResult` from JSON file. Raises `ProtocolVersionError` on version mismatch.
### Known Actions
`add_plugins`, `configure_fx_params`, `verify_fx`, `calibrate`, `render`
### ReaScript JSON Parser
Since REAPER's internal Python has no `json` module, the ReaScript includes a hand-rolled JSON parser (~100 lines) that handles strings, integers, floats, booleans, nulls, nested objects, and arrays via string splitting.
## CLI Reference
### scripts/compose.py
Main composition pipeline. Builds a full reggaetón track from scratch.
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--bpm` | `float` | `99` | Tempo |
| `--key` | `str` | `"Am"` | Musical key |
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` path |
| `--seed` | `int` | `None` | Random seed for determinism |
| `--emotion` | `str` | `"romantic"` | `romantic` / `dark` / `club` / `classic` |
| `--inversion` | `str` | `"root"` | `root` / `first` / `second` |
| `--no-calibrate` | `flag` | `False` | Skip post-processing mix calibration |
### scripts/generate.py
Thin wrapper around `compose.py`. Delegates via `sys.argv` manipulation.
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--bpm` | `float` | `95` | Tempo |
| `--key` | `str` | `"Am"` | Musical key |
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` path |
| `--seed` | `int` | `42` | Random seed |
| `--emotion` | `str` | `"romantic"` | Chord emotion |
| `--inversion` | `str` | `"root"` | Chord inversion |
| `--validate` | `flag` | `False` | Run `validate_rpp_output()` after generation |
### scripts/run_in_reaper.py
ReaScript execution CLI. Generates and runs a Phase 2 ReaScript.
| Arg/Flag | Type | Default | Description |
|----------|------|---------|-------------|
| `rpp_path` | positional | *(required)* | Path to `.rpp` file |
| `--output`, `-o` | `str` | auto-derived | Rendered WAV output path |
| `--timeout` | `int` | `120` | Seconds to poll for result JSON |
| `--plugins-config` | `str` | `None` | JSON SongDefinition for deriving plugins_to_add |
| `--action` | `str` | `"calibrate"` | Space-separated action pipeline |
Example:
```bash
python scripts/run_in_reaper.py output/song.rpp --action "add_plugins calibrate render" --timeout 300
```
## Conventions
From `AGENTS.md`:
- **Python**: Type hints on all function signatures
- **Dataclasses**: Over dicts for structured data
- **Deterministic output**: Seed-based RNG (`random.Random(seed)`), no global random state
- **No bare except clauses**
- **Testing**: `pytest` only. Unit tests for all new functions. Integration tests for end-to-end flows.
- **Architecture**: Separate modules by concern (calibrator, composer, builder, selector, validator)
- **Post-processing**: Calibrator.apply() pattern — modify in-place after construction, not inline
- **Schema changes**: Must be backward-compatible (new fields get defaults)
- **SDD**: All changes follow spec-driven development pipeline (propose → spec → design → tasks → apply → verify)
## How to Extend
### Adding a New Track Role
1. Add role to `TRACK_ACTIVITY` in `scripts/compose.py` — define which sections it plays in
2. Add volume/presets to `src/calibrator/presets.py` (`VOLUME_PRESETS`, `PAN_PRESETS`, `SEND_PRESETS`)
3. Add FX chain to `FX_CHAINS` in `scripts/compose.py`
4. Write a `build_<role>_track()` function
5. Add the track to the `tracks` list in `main()`
6. Add calibration role mapping in `Calibrator._resolve_role()`
### Adding a New Plugin to the Registry
1. Scan the plugin in REAPER or extract from a ground-truth `.rpp`
2. Add entry to `PLUGIN_REGISTRY`: `"key": ("display_name", "filename", "uid_guid")`
3. If needed, add preset data to `_PRESETS_FLAT` with the key
4. Add aliases to `ALIAS_MAP` if there are alternate names
5. Use `make_plugin(registry_key, index, role)` to create `PluginDef` instances
### Adding a New CLI Flag
1. Add `parser.add_argument()` in `scripts/compose.py` (and `scripts/generate.py` if wrapping)
2. Thread the flag value through the composition pipeline
3. Pass to relevant builder/track functions
### Adding a New Emotion to ChordEngine
1. Add entry to `EMOTION_PROGRESSIONS` in `src/composer/chords.py`
2. Format: `"name": [(semitone_offset, quality), ...]` for a 4-chord loop
3. Add choice to `--emotion` argparse choices
## Known Limitations
- **Hardcoded paths**: Drumloop samples reference `C:\ProgramData\Ableton\...` paths. These must exist on the build machine or the pipeline will skip those clips.
- **Environment-specific registry**: `PLUGIN_REGISTRY` was generated from a specific REAPER/plugin installation. Different machines may have different plugin paths or GUIDs.
- **Windows only**: REAPER executable path in `render.py` defaults to `C:\Program Files\REAPER (x64)\reaper.exe`. The ReaScript REAPER API calls are Windows-specific.
- **No CI**: No automated test/validation pipeline. Tests run manually with `pytest`.
- **Single genre**: Only reggaetón is fully implemented. Other genres exist in `knowledge/` as JSON definitions but the main pipeline (`compose.py`) is hardcoded for reggaetón.
- **Sample library**: Requires pre-existing sample files in expected directories. No bundled samples.
- **ReaScript execution**: `run_in_reaper.py` generates scripts but does NOT automatically launch REAPER. REAPER must be running and monitoring the scripts directory, or the script must be executed manually from within REAPER's action list.
## Further Reading
- [CLI Reference](CLI.md) — Complete CLI documentation
- [Module: Composer](modules/composer.md) — Composition engine deep-dive
- [Module: Reaper Builder](modules/reaper-builder.md) — RPP format and plugin registry
- [Module: Reaper Scripting](modules/reaper-scripting.md) — ReaScript protocol
- [Module: Calibrator](modules/calibrator.md) — Mix calibration presets
- [JSON Schema: Song Definition](schemas/song-definition.json) — Data model schema
- [JSON Schema: ReaScript Protocol](schemas/reascript-protocol.json) — Command/result schema

174
docs/modules/calibrator.md Normal file
View File

@@ -0,0 +1,174 @@
# Calibrator Module
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
## Purpose
Post-processing mix calibration. Applies role-based volume, EQ, pan, sends, and master chain configuration to a `SongDefinition` after track construction and before `.rpp` generation.
**Location**: `src/calibrator/`
## Public API
### Calibrator (`__init__.py`)
```python
class Calibrator:
@staticmethod
def apply(song: SongDefinition) -> SongDefinition
```
- **`apply()`**: Applies role-based volume, pan, sends, and master chain calibration. Mutates `song` in-place and returns it. Skips tracks named `"Reverb"` or `"Delay"` (return tracks).
- **Pipeline**: `_calibrate_volumes()``_calibrate_pans()``_calibrate_sends()``_swap_master_chain()`
### Role Resolution (`__init__.py`)
```python
@staticmethod
def _resolve_role(track_name: str) -> str | None
```
Maps track name to role key using substring matching:
| Track Name Pattern | Role | Description |
|-------------------|------|-------------|
| `"Drumloop"`, `"drum*"` | `"drumloop"` | Drum loop |
| `"808 Bass"`, `"*bass*"` | `"bass"` | Bass |
| `"Chords"`, `"*chord*"` | `"chords"` | Chords/harmony |
| `"Lead"`, `"*lead*"`, `"*synth*"`, `"*melody*"` | `"lead"` | Lead melody |
| `"Clap"`, `"*snare*"`, `"*clap*"` | `"clap"` | Clap/snare |
| `"Pad"`, `"*pad*"`, `"*atmos*"`, `"*ambient*"` | `"pad"` | Pad/atmosphere |
| `"Perc"`, `"*perc*"` | `"perc"` | Percussion |
| `"Reverb"`, `"Delay"` | `None` | Return tracks (skipped) |
| Any other name | `None` | Unknown role (skipped) |
### Presets (`presets.py`)
```python
VOLUME_PRESETS: dict[str, float]
PAN_PRESETS: dict[str, float]
EQ_PRESETS: dict[str, dict[int, float]]
SEND_PRESETS: dict[str, tuple[float, float]]
```
#### Volume Presets (0.01.0 REAPER volume)
| Role | Volume |
|------|--------|
| `drumloop` | 0.85 |
| `bass` | 0.82 |
| `chords` | 0.75 |
| `lead` | 0.80 |
| `clap` | 0.78 |
| `pad` | 0.70 |
| `perc` | 0.80 |
#### Pan Presets (-1.0 to 1.0)
| Role | Pan |
|------|-----|
| `drumloop` | 0.0 (center) |
| `bass` | 0.0 (center) |
| `chords` | 0.5 (right) |
| `lead` | 0.3 (slightly right) |
| `clap` | -0.15 (slightly left) |
| `pad` | -0.5 (left) |
| `perc` | 0.12 (slightly right) |
#### EQ Presets (ReaEQ VST2 param slots)
| Role | Band Enabled | Filter Type | Frequency |
|------|-------------|-------------|-----------|
| `drumloop` | 1 (on) | 1 (HPF) | 60 Hz |
| `bass` | 1 (on) | 0 (LPF) | 300 Hz |
| `chords` | 1 (on) | 1 (HPF) | 200 Hz |
| `lead` | 1 (on) | 1 (HPF) | 200 Hz |
| `clap` | 1 (on) | 1 (HPF) | 200 Hz |
| `pad` | 1 (on) | 1 (HPF) | 100 Hz |
| `perc` | 1 (on) | 1 (HPF) | 200 Hz |
EQ presets use ReaEQ's VST2 parameter layout (slot 0 = enabled, slot 1 = filter type, slot 2 = frequency). These are passed to `PluginDef.params` and applied by the ReaScript's `configure_fx_params` action.
#### Send Presets (reverb, delay) — 0.01.0
| Role | Reverb Send | Delay Send |
|------|------------|------------|
| `drumloop` | 0.10 | 0.00 |
| `bass` | 0.05 | 0.00 |
| `chords` | 0.40 | 0.10 |
| `lead` | 0.30 | 0.15 |
| `clap` | 0.10 | 0.00 |
| `pad` | 0.50 | 0.20 |
| `perc` | 0.10 | 0.00 |
### Master Chain Upgrade (`__init__.py`)
```python
@staticmethod
def _swap_master_chain(song: SongDefinition) -> None
```
Replaces `master_plugins` with a processing chain:
1. **Ozone 12 triplet**: `Ozone_12_Equalizer``Ozone_12_Dynamics``Ozone_12_Maximizer`
2. **Fallback** (if any Ozone plugin missing from `PLUGIN_REGISTRY`): `Pro-Q_3``Pro-C_2``Pro-L_2`
Check is performed at calibration time. Missing plugins silently fall back.
## Data Flow
```
SongDefinition (after track construction)
Calibrator.apply(song)
├── _calibrate_volumes(song)
│ For each track:
│ role = _resolve_role(track.name)
│ if role in VOLUME_PRESETS: track.volume = VOLUME_PRESETS[role]
├── _calibrate_pans(song)
│ For each track:
│ role = _resolve_role(track.name)
│ if role in PAN_PRESETS: track.pan = PAN_PRESETS[role]
├── _calibrate_sends(song)
│ Count content tracks and return tracks
│ reverb_idx = num_content; delay_idx = num_content + 1
│ For each track:
│ role = _resolve_role(track.name)
│ if role in SEND_PRESETS:
│ track.send_level[reverb_idx] = SEND_PRESETS[role][0]
│ track.send_level[delay_idx] = SEND_PRESETS[role][1]
└── _swap_master_chain(song)
Check REGISTRY for Ozone triplet availability
song.master_plugins = ozone_triplet or fallback_triplet
SongDefinition (calibrated, mutated in-place)
```
## Dependencies
- `src/core/schema.py``SongDefinition`, `TrackDef`, `PluginDef`
- `src/calibrator/presets.py``VOLUME_PRESETS`, `PAN_PRESETS`, `EQ_PRESETS`, `SEND_PRESETS`
- `src/reaper_builder` (lazy import in `_swap_master_chain`) — `PLUGIN_REGISTRY`
## Known Gotchas
1. **Calibration mutates in-place**: `apply()` modifies the `SongDefinition` directly. If you need the uncalibrated version, keep a copy before calling.
2. **Role resolution is substring-based**: `"drum"` substring matches `"drumloop"` role. `"bass"` substring matches `"bass"`. Track names like `"Bassline"` or `"Drum Kit"` will resolve correctly. But `"SubBass"` also matches because `"bass"` is a substring.
3. **Send indices depend on track ordering**: `_calibrate_sends()` counts content tracks (non-return) to compute reverb/delay indices. If tracks are reordered after calibration, send indices will be wrong.
4. **EQ presets require ReaScript execution**: `EQ_PRESETS` values are not applied by `Calibrator` directly. They must be embedded in `PluginDef.params` for ReaEQ instances and applied by the ReaScript's `configure_fx_params` action.
5. **Master chain is replaced, not extended**: `_swap_master_chain()` replaces the entire `master_plugins` list. Any pre-existing master plugins are lost.
6. **Ozone fallback is silent**: If Ozone plugins aren't in the registry, the FabFilter fallback is used with no warning.
7. **Return tracks identified by exact name**: Only tracks named `"Reverb"` or `"Delay"` (case-insensitive) are treated as return tracks. `"Reverb Return"` or `"Verb"` would NOT be recognized.
8. **Calibration disabled via SongMeta.calibrate**: If `song.meta.calibrate` is `False`, the pipeline (in `scripts/compose.py`) skips `Calibrator.apply()`. But a caller could still invoke `apply()` directly.

177
docs/modules/composer.md Normal file
View File

@@ -0,0 +1,177 @@
# Composer Module
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
## Purpose
The composer module generates musical content for reggaetón tracks: chord progressions, melodies, rhythm patterns, and drum analysis. All generators are deterministic given a seed.
**Location**: `src/composer/`
## Public API
### ChordEngine (`chords.py`)
```python
class ChordEngine:
def __init__(self, key: str, seed: int = 42) -> None
def progression(
self,
bars: int,
emotion: str = "classic",
beats_per_chord: int = 4,
inversion: str = "root",
) -> list[list[int]]
```
- **Input**: Key string (`"Am"`, `"Dm"`), number of bars, emotion name, beats per chord, inversion preference
- **Output**: List of voicings — each voicing is `list[int]` of MIDI notes
- **Emotions**: `"romantic"`, `"dark"`, `"club"`, `"classic"` (unknown falls back to classic)
- **Deterministic**: Same `(key, seed)` → identical output. RNG re-seeded per call.
- **Voice leading**: Greedy min-score path through root, first, and second inversion candidates. Uses `random.Random(seed)` as micro-tiebreaker.
**Emotion progressions** (all 4-chord loops, semitone offsets from tonic):
| Emotion | Offsets | Labels |
|---------|---------|--------|
| romantic | (0,m7), (8,7), (3,7), (10,7) | i7VI7III7VII7 |
| dark | (0,m7), (5,m7), (10,7), (3,7) | i7iv7VII7III7 |
| club | (0,m7), (8,7), (10,7), (7,7) | i7VI7VII7V7 |
| classic | (0,m7), (10,7), (8,7), (7,7) | i7VII7VI7V7 |
### Melody Engine (`melody_engine.py`)
```python
def build_motif(
key_root: str,
key_minor: bool,
style: str,
bars: int = 4,
seed: int = 42,
) -> list[MidiNote]
def apply_variation(
motif: list[MidiNote],
shift_beats: float = 0.0,
transpose_semitones: int = 0,
) -> list[MidiNote]
def build_call_response(
motif: list[MidiNote],
bars: int = 8,
key_root: str = "A",
key_minor: bool = True,
seed: int = 42,
) -> list[MidiNote]
```
- **`build_motif()`**: Generate a repeating motif. Styles: `"hook"` (arch contour, chord-tone emphasis), `"stabs"` (short hits on dembow grid), `"smooth"` (stepwise scalar motion). Raises `ValueError` for invalid style. Bars clamped to 28.
- **`apply_variation()`**: Apply beat shift and/or semitone transpose to a motif. Returns new list; original unchanged.
- **`build_call_response()`**: Build call-and-response structure. First half: motif repeats, last note forced to V or VII (tension). Second half: motif repeats, last note forced to i (resolution).
### Pattern Tables (`patterns.py`)
Weight tables extracted from real reggaetón track analysis (99.4 BPM and 132.5 BPM tracks). Contains 16th-note position frequency weights for kick, snare, and hihat. Used by rhythm generators to produce idiomatically accurate patterns.
### Rhythm Generators (`rhythm.py`)
Pure functions returning note dicts per MIDI channel:
```python
def generate_kick(bars: int, style: str, ...) -> list[dict]
def generate_snare(bars: int, ...) -> list[dict]
def generate_hihat(bars: int, ...) -> list[dict]
```
Dembow styles: `"classico"` (kicks on 1, 3, 4&), `"perreo"` (adds kick on 2&), `"trapico"` (half-time kicks on 1, 3). MIDI channels: CH_K=11, CH_S=12, CH_R=13, CH_H=15, CH_CL=16.
### RPP Templates (`templates.py`)
```python
def extract_template(rpp_path: str) -> TemplateProject
def generate_rpp(template: TemplateProject, output_path: str) -> None
```
Extracts track/FX chain structures from professionally-built `.rpp` files, fixes GUIDs using `reaper-vstplugins64.ini`, and regenerates working `.rpp` files with correct plugin references.
### Legacy Generators (`__init__.py`)
```python
def generate_dembow(bars: int = 8, ppq: int = 96) -> list[dict]
def generate_bass_808(chord_progression: list[str], ...) -> list[dict]
def generate_piano_stabs(chord_progression: list[str], ...) -> list[dict]
def generate_lead_hook(chord_progression: list[str], ...) -> list[dict]
def generate_pad(chord_progression: list[str], ...) -> list[dict]
def generate_latin_perc(bars: int = 8) -> list[dict]
def compose_from_genre(genre_path: str | Path, ...) -> dict
```
These are the legacy pattern generators used by `compose_from_genre()`. They produce note dicts with keys `{"position", "length", "key", "velocity"}`. The modern pipeline (`scripts/compose.py`) uses `ChordEngine` and `melody_engine` instead.
Constants:
- `SCALE_INTERVALS`: major, minor, harmonic_minor, melodic_minor, dorian, phrygian
- `CHORD_TYPES`: maj [0,4,7], min [0,3,7], dim [0,3,6], aug [0,4,8], 7 [0,4,7,10], m7 [0,3,7,10], sus2 [0,2,7], sus4 [0,5,7]
### Drum Analyzer (`drum_analyzer.py`)
```python
class DrumLoopAnalyzer:
def __init__(self, audio_path: str) -> None
def analyze(self) -> Analysis
```
Analyzes audio files for transient detection. Used by `scripts/compose.py` to detect kick drum positions for CC11 sidechain ducking on the 808 bass track.
## Data Flow
```
CLI args (--key, --emotion, --inversion, --seed)
ChordEngine(key, seed)
│ .progression(bars, emotion, beats_per_chord, inversion)
list[list[int]] ← voicings (each is a list of MIDI notes)
compose.py builds Chord clips: MidiNote objects per bar
CLI args (--key, --seed)
build_motif(key_root, key_minor, "hook", bars, seed)
list[MidiNote]
build_call_response(motif, bars, key_root, key_minor, seed+1)
list[MidiNote] ← call-response melody for lead track
```
## Dependencies
- `src/core/schema.py``MidiNote`
- `src/composer/` internal:
- `chords.py` depends on `__init__.py` (`CHORD_TYPES`)
- `melody_engine.py` depends on `schema.py`
- `templates.py` depends on `src/reaper_builder` (`PLUGIN_REGISTRY`, `ALIAS_MAP`, `vst2_element`, `vst3_element`, `PLUGIN_PRESETS`)
- `random` (stdlib) — seeded RNG for determinism
## Known Gotchas
1. **ChordEngine._voice_leading is greedy, not optimal**: Picks the lowest-scoring candidate per step. There's no backtracking. Two seeds can produce noticeably different voicings because the greedy path depends on the RNG shuffle of candidates.
2. **Emotion fallback is silent**: Unknown emotion strings silently fall back to `"classic"`. No warning or error.
3. **Melody bars clamping**: `build_motif()` clamps bars to 28 silently. Requesting 16 bars returns exactly what 8 bars would return.
4. **Call-response hardcodes `_CHORD_PROGRESSION`**: `build_call_response()` uses its own i-VI-III-VII progression from `melody_engine.py` (duplicated from `compose.py`), NOT what was used by `ChordEngine`. If the chords and lead use different progressions, they will clash.
5. **Legacy generators use dict format, not MidiNote**: `generate_dembow()`, `generate_bass_808()`, etc. return `list[dict]` with string keys (`"position"`, `"length"`, `"key"`, `"velocity"`). The modern pipeline uses `list[MidiNote]` dataclass instances. These are NOT interchangeable.
6. **Drum analyzer timing**: `DrumLoopAnalyzer` converts seconds to beats using BPM, but the BPM is the project BPM, not necessarily the sample's BPM. If a drumloop is recorded at 90 BPM but the project is 99 BPM, kick positions will be misaligned.

View File

@@ -0,0 +1,230 @@
# Reaper Builder Module
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
## Purpose
Generates valid REAPER `.rpp` project files from a `SongDefinition`. Maintains the plugin registry (~97 VST2/VST3 plugins with GUIDs), builds the `.rpp` element tree, and supports headless rendering via REAPER CLI.
**Location**: `src/reaper_builder/`
## Public API
### RPPBuilder (`__init__.py`)
```python
class RPPBuilder:
def __init__(self, song: SongDefinition, seed: int | None = None) -> None
def write(self, path: str | Path) -> None
```
- **`__init__()`**: Receives a `SongDefinition` and optional seed. Seeds `random` for deterministic GUID generation when seed is not None.
- **`write()`**: Serializes the project to a `.rpp` text file. Raises `OSError` if file cannot be written. Uses the `rpp` library (`Element`, `dumps`) for XML-like structure.
- **Internal**: `_build_element()``_build_track(track)``_build_plugin(plugin)``_build_clip(clip)``_build_midi_source(clip)`.
### Plugin Registry (`__init__.py`)
```python
PLUGIN_REGISTRY: dict[str, tuple[str, str, str]]
```
Format: `"key": ("display_name", "filename", "uid_guid")`
~97 entries covering:
- **FabFilter**: Pro-Q 3, Pro-C 2, Pro-R 2, Pro-L 2, Saturn 2, Timeless 3, Pro-DS, Pro-G, Pro-MB, Micro, One, Simplon, Twin 3, Volcano 3 (both VST2 and VST3 versions)
- **SoundToys**: Decapitator, EchoBoy, Crystallizer, Devil-Loc, EffectRack, FilterFreak, MicroShift, PanMan, PhaseMistress, PrimalTap, Radiator, Tremolator, Little AlterBoy, Little MicroShift, Little PrimalTap, Little Radiator
- **iZotope Ozone 12**: Equalizer, Dynamics, Maximizer, plus 17 additional Ozone modules
- **Spectrasonics**: Omnisphere, FX-Omnisphere
- **Xfer**: Serum 2, Serum 2 FX
- **u-he**: Diva
- **Arturia**: Pigments
- **Cableguys**: ShaperBox 3
- **Native Instruments**: Kontakt 7, VC 160, VC 2A, VC 76
- **Cockos (REAPER native)**: ReaEQ, ReaComp, ReaDelay, ReaVerb, ReaVerbate, ReaFIR, ReaGate, ReaLimit, ReaPitch, ReaXcomp, ReaTune, ReaSynth, ReaSamplOmatic5000, and more (20+ plugins)
- **Other**: ValhallaDelay, Gullfoss (Standard/Master/Live), The Glue, Trackspacer 2.5, ravity, Tone2 Electra
```python
ALIAS_MAP: dict[str, str]
```
Maps alternate names to registry keys:
- `"Serum2"``"Serum_2"`
- `"FabFilter Pro-Q 3"``"Pro-Q_3"` (space-separated → underscore VST3 key)
- `"Pro-Q 3"``"Pro-Q_3"` (short name → VST3 key)
- `"Valhalla Delay"``"ValhallaDelay"`
- VST2 SoundToys old names → new underscore names
```python
REAPER_BUILTINS: frozenset[str]
```
Set of Cockos plugin keys. Plugins in this set are deferred to ReaScript insertion.
```python
def get_builtin_plugins(song: SongDefinition) -> list[dict[str, object]]
```
Extracts plugins where `PluginDef.builtin == True` or name is in `REAPER_BUILTINS`. Returns list of `{"track_name", "fx_name", "params"}` dicts for `ReaScriptCommand.plugins_to_add`.
### Preset Transformer (`preset_transformer.py`)
```python
# Transforms flat _PRESETS_FLAT dict → role-aware PLUGIN_PRESETS
# Format: {(plugin_key, role): [chunk1, chunk2, ...]}
PLUGIN_PRESETS: dict[tuple[str, str], list[str]]
```
Lookup chain: `(key, role)``(key, "")` (default) → `fallback``None`.
Plugins with preset data: Arcade, Omnisphere, Pro-Q_3, Pro-C_2, Pro-R_2, Pro-L_2, Saturn 2, Timeless 3, The Glue, ValhallaDelay, Serum 2, Diva, Kontakt 7, VC 160, VC 76, Gullfoss, Gullfoss Master, Decapitator, EchoBoy, EffectRack, FilterFreak1, MicroShift, Little AlterBoy, PhaseMistress, Tremolator.
Plugins with empty presets (no saved state): Most Ozone 12 modules, Pigments, ShaperBox 3, Gullfoss Live, Trackspacer 2.5, Crystallizer, FilterFreak2, Little MicroShift, Little PrimalTap, Little Radiator, PanMan, VC 2A, Elektra.
### Headless Render (`render.py`)
```python
def render_project(
rpp_path: str | Path,
output_wav: str | Path,
reaper_exe: str | Path | None = None,
timeout_seconds: int = 120,
) -> None
```
Renders a `.rpp` to WAV via `reaper.exe -nosplash -render`. Default REAPER path: `C:\Program Files\REAPER (x64)\reaper.exe`. Raises `FileNotFoundError` if REAPER not found, `RuntimeError` on render failure.
## Data Flow
```
SongDefinition
RPPBuilder.__init__(song, seed)
RPPBuilder.write(path)
│ _build_element()
│ ├── Project header (_PROJECT_HEADER static lines)
│ ├── TEMPO line (dynamic from song.meta)
│ ├── Master TRACK with FXCHAIN
│ │ ├── master_plugins resolved via ALIAS_MAP → PLUGIN_REGISTRY
│ │ └── VST elements with preset data
│ ├── Per-track TRACK elements
│ │ ├── NAME, VOLPAN (from TrackDef.volume/.pan), TRACKID
│ │ ├── AUXRECV (from send_level dict)
│ │ ├── FXCHAIN with VST elements (non-builtin plugins)
│ │ └── ITEM elements (clips)
│ │ ├── SOURCE WAVE (audio clips: FILE path)
│ │ └── SOURCE MIDI (MIDI clips: E-lines at 960 PPQ)
│ └── TEXT output → .rpp file
output/song.rpp
```
## `.rpp` File Structure
The generated `.rpp` follows REAPER's ground-truth format extracted from `output/test_vst3.rpp`:
```rpp
<REAPER_PROJECT 0.1 "7.65/win64" ...>
<NOTES ...>
...static project metadata...
TEMPO 95 4 4 0
<TRACK {master-guid}>
NAME master
...
<FXCHAIN>
<VST "VST3: Pro-Q 3 (FabFilter)" FabFilter {GUID} ...>
<VST "VST3: Pro-C 2 (FabFilter)" FabFilter {GUID} ...>
<VST "VST3: Pro-L 2 (FabFilter)" FabFilter {GUID} ...>
>
>
<TRACK {track-guid}>
NAME "808 Bass"
VOLPAN 0.820000 0.000000 -1 -1 1
<FXCHAIN>
<VST "VST3i: Serum 2 (Xfer Records)" Serum2.vst3 {GUID} ...>
>
AUXRECV 8 0.050000 -1 -1 0
AUXRECV 9 0.000000 -1 -1 0
<ITEM>
POSITION 0.0
LENGTH 32.0
NAME "Verse 808"
<SOURCE MIDI>
HASDATA 1 960 QN
E 0 90 21 50
E 960 80 21 00
...
>
>
>
<TRACK {track-guid}>
NAME "Reverb"
...
>
<TRACK {track-guid}>
NAME "Delay"
...
>
>
```
### VST Element Format
**VST3** (`.vst3` files with `{GUID}`):
```rpp
<VST "VST3: Pro-Q 3 (FabFilter)" FabFilter {72C4DB71...} 0 "" 1158812272...>
base64chunk1
base64chunk2
...
>
```
**VST2** (`.dll` files with `<GUID>`):
```rpp
<VST "VST: Decapitator (SoundToys)" Decapitator.dll 0 "" <56535453744463...> 0 "" ...>
base64chunk1
base64chunk2
...
>
```
### MIDI Source Format
MIDI data is written as E-lines at 960 PPQ with 16th-note quantization (120-tick grid):
```rpp
<SOURCE MIDI>
HASDATA 1 960 QN
E 0 90 21 50 ; delta=0, note-on, pitch=0x21 (33=A1), velocity=0x50 (80)
E 1440 80 21 00 ; delta=1440, note-off, pitch=0x21
E 120 B0 0B 32 ; delta=120, CC 11 (Expression), value=0x32 (50)
...
>
```
## Dependencies
- `src/core/schema.py``SongDefinition`, `TrackDef`, `ClipDef`, `PluginDef`
- `rpp` library — `Element`, `dumps` for `.rpp` XML-like serialization
- `uuid`, `random` (stdlib) — GUID generation
- `subprocess` (for `render.py`) — REAPER CLI execution
## Known Gotchas
1. **VST3 display names MUST match exactly**: The display_name in the registry must match what `TrackFX_AddByName()` expects in REAPER. A typo in the display_name field causes silent plugin load failure.
2. **Plugin builtin flag skips .rpp writing**: When `PluginDef.builtin == True`, the plugin is NOT written to the `.rpp` file. It's deferred to ReaScript. If the ReaScript doesn't run, the plugin won't be loaded.
3. **GUID format matters**: VST2 uses `<GUID>` with angle brackets in the `.rpp`. VST3 uses `{GUID}` with curly braces. Using the wrong delimiter format causes REAPER to reject the plugin entry.
4. **REAPER version string is hardcoded**: `_build_element()` hardcodes `"7.65/win64"` as the REAPER version. Different REAPER versions may behave differently.
5. **VOLPAN format**: REAPER expects volume as a linear value 0.01.0, not dB. Pan is -1.0 to 1.0. Values outside these ranges produce unexpected behavior.
6. **AUXRECV indices**: Send indices reference return track positions. Reverb is at index `len(content_tracks)`, Delay at `len(content_tracks) + 1`. Adding content tracks before wiring sends shifts these indices.
7. **Preset data is raw base64**: Plugin preset data is stored as base64-encoded binary chunks extracted from ground-truth `.rpp` files. They are NOT parameter lists — they're full plugin state dumps. Modifying presets requires re-extracting from REAPER.
8. **Headless render requires REAPER installed**: `render_project()` calls `reaper.exe` as a subprocess. REAPER must be installed at the expected path for rendering to work.

View File

@@ -0,0 +1,205 @@
# Reaper Scripting Module
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
## Purpose
Generates self-contained Python ReaScript files that REAPER executes for post-processing: plugin loading, FX parameter configuration, mix calibration, verification, and rendering. Communicates via JSON files on disk.
**Location**: `src/reaper_scripting/`
## Public API
### ReaScriptGenerator (`__init__.py`)
```python
class ReaScriptGenerator:
def generate(self, path: Path, command: ReaScriptCommand) -> None
```
- **`generate()`**: Writes a self-contained Python ReaScript to `path`. The script opens the `.rpp` file, executes the action pipeline, and writes results to `fl_control_result.json`.
- **Internal**: `_build_script(command)` assembles the full script source, conditionally including only the action functions needed by the command.
### ReaScriptCommand (`commands.py`)
```python
@dataclass
class ReaScriptCommand:
version: int = 1
action: str | list[str] = "calibrate"
rpp_path: str = ""
render_path: str = ""
timeout: int = 120
track_calibration: list[dict] = field(default_factory=list)
plugins_to_add: list[dict] = field(default_factory=list)
```
### ReaScriptResult (`commands.py`)
```python
@dataclass
class ReaScriptResult:
version: int = 1
status: str = "ok"
message: str = ""
lufs: float | None = None
integrated_lufs: float | None = None
short_term_lufs: float | None = None
fx_errors: list[dict] = field(default_factory=list)
tracks_verified: int = 0
added_plugins: list[dict] = field(default_factory=list)
```
### ProtocolVersionError (`commands.py`)
```python
class ProtocolVersionError(Exception):
"""Raised when command/result version doesn't match expected."""
```
### Serialization Functions (`commands.py`)
```python
def write_command(path: Path, cmd: ReaScriptCommand) -> None
def read_result(path: Path, expected_version: int = 1) -> ReaScriptResult
```
- **`write_command()`**: Serializes `ReaScriptCommand` to JSON file. Omits `plugins_to_add` key if empty.
- **`read_result()`**: Deserializes `ReaScriptResult` from JSON file. Raises `ProtocolVersionError` if version != expected_version.
## Data Flow
```
ReaScriptCommand
ReaScriptGenerator.generate(script_path, command)
│ _build_script(command)
│ ├── check_api() — verifies REAPER API function availability
│ ├── parse_json() / write_json() — hand-rolled JSON (no stdlib json)
│ ├── find_track() — lookup by name via RPR_GetSetMediaTrackInfo_String
│ ├── add_plugins() — RPR_TrackFX_AddByName for built-in plugins
│ ├── configure_fx_params() — RPR_TrackFX_SetParam for param config
│ ├── calibrate() — RPR_SetMediaTrackInfo_Value (volume/pan) + sends
│ ├── verify_fx() — enumerates all tracks/FX, reports missing
│ ├── render() — RPR_Main_RenderFile for WAV export
│ ├── measure_loudness() — RPR_CalcMediaSrcLoudness for LUFS
│ └── main() — dispatch loop: read command → open project → actions → write result
fl_control_phase2.py (self-contained ReaScript)
│ (REAPER executes this)
fl_control_result.json
read_result() → ReaScriptResult
```
## Command/Result Contract
### Command JSON (`fl_control_command.json`)
Written by `write_command()`:
```json
{
"version": 1,
"action": ["add_plugins", "calibrate", "render"],
"rpp_path": "C:/path/to/song.rpp",
"render_path": "C:/path/to/song_rendered.wav",
"timeout": 120,
"track_calibration": [
{"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []}
],
"plugins_to_add": [
{"track_name": "808 Bass", "fx_name": "ReaEQ", "params": {"0": "1", "1": "0", "2": "300.0"}}
]
}
```
### Result JSON (`fl_control_result.json`)
Written by the ReaScript's `write_json()`:
```json
{
"version": 1,
"status": "ok",
"message": "",
"lufs": -14.2,
"integrated_lufs": -14.2,
"short_term_lufs": -13.8,
"fx_errors": [],
"tracks_verified": 10,
"added_plugins": [
{"fx_name": "ReaEQ", "instance_id": 0, "track_name": "808 Bass", "status": "ok"}
]
}
```
## Known Actions
| Action | Function | Description |
|--------|----------|-------------|
| `add_plugins` | `add_plugins(cmd)` | Loads plugins via `TrackFX_AddByName`. Returns per-plugin status. |
| `configure_fx_params` | `configure_fx_params(cmd)` | Sets FX parameters via `TrackFX_SetParam`. |
| `verify_fx` | `verify_fx()` | Enumerates all tracks/FX, reports empty/missing FX names. |
| `calibrate` | `calibrate(track_cal)` | Sets track volume, pan, and sends from calibration data. |
| `render` | `do_render(render_path)` + `measure_loudness()` | Renders WAV and measures LUFS via `RPR_CalcMediaSrcLoudness`. |
Actions execute in the order specified by the `action` list. The script's `main()` opens the project once, then runs the action dispatch loop, then writes the result JSON.
## ReaScript Architecture
The generated script is a single self-contained `.py` file with:
1. **Zero external imports**: No `json`, no `os`, no third-party libraries. Only REAPER's built-in `RPR_*` functions.
2. **Hand-rolled JSON parser**: ~100 lines of string-splitting logic (`parse_json()`). Handles strings, integers, floats, booleans, nulls, nested objects, arrays.
3. **API availability check**: `check_api()` verifies all required REAPER API functions exist before any work begins. Missing APIs → immediate error result.
4. **Conditional function inclusion**: Only the action functions needed by the specific command are emitted in the script. The generator checks `action` list and includes only relevant source blocks.
5. **Adaptive API check**: `check_api()` includes `TrackFX_AddByName` and `TrackFX_SetParam` only when `add_plugins` or `configure_fx_params` actions are present.
### REAPER API Functions Used
| API Function | Purpose |
|-------------|---------|
| `RPR_CountTracks` | Enumerate tracks |
| `RPR_GetTrack` | Get track by index |
| `RPR_GetSetMediaTrackInfo_String` | Get/set track name |
| `RPR_SetMediaTrackInfo_Value` | Set track volume (D_VOL), pan (D_PAN) |
| `RPR_TrackFX_GetCount` | Count FX on a track |
| `RPR_TrackFX_GetFXName` | Get FX name by index |
| `RPR_TrackFX_AddByName` | Add FX by display name |
| `RPR_TrackFX_SetParam` | Set FX parameter by index |
| `RPR_CreateTrackSend` | Create send between tracks |
| `RPR_Main_openProject` | Open .rpp project |
| `RPR_Main_RenderFile` | Render to file |
| `RPR_CalcMediaSrcLoudness` | Measure LUFS |
| `RPR_ResourcePath` | Get REAPER resource directory path |
| `RPR_GetFunctionMetadata` | Check API function availability |
| `RPR_GetLastError` | Get last error |
| `RPR_GetProjectName` | Get project name |
## Dependencies
- `src/reaper_scripting/commands.py``ReaScriptCommand`, `ReaScriptResult`, `write_command()`, `read_result()`
- `src/core/schema.py``SongDefinition`, `PluginDef` (indirect, via `get_builtin_plugins()`)
- `pathlib` (stdlib) — file path handling
## Known Gotchas
1. **REAPER Python has no `json` module**: The generated ReaScript cannot `import json`. All serialization uses the hand-rolled parser. This is a fundamental REAPER limitation — its embedded Python is a stripped-down distribution.
2. **Command JSON MUST be at the right path**: The ReaScript reads `fl_control_command.json` from `RPR_ResourcePath() + "scripts/"`. If the file isn't there when the script runs, it fails immediately.
3. **`write_command()` omits empty `plugins_to_add`**: If `plugins_to_add` is empty, the key is not written. The ReaScript's `cmd.get("plugins_to_add", [])` handles this with a default.
4. **Track name matching is case-insensitive**: `find_track()` normalizes both the search name and the REAPER track name to lowercase for matching. Track names that differ only by case will collide.
5. **`configure_fx_params` uses string keys for param indices**: The `params` dict in `plugins_to_add` uses string keys (`"0"`, `"1"`) because JSON keys are always strings. The ReaScript converts to `int` before calling `TrackFX_SetParam`.
6. **No error recovery per action**: If `add_plugins` fails for one track, it continues with the next. But if any action crashes (unhandled exception), the script terminates without writing the result JSON — leading to polling timeout in `run_in_reaper.py`.
7. **Calibrate sends use `CreateTrackSend`**: Every call to `calibrate()` with sends creates a new send connection. If calibrate is called multiple times, duplicate sends accumulate.
8. **LUFS is optional**: If `RPR_CalcMediaSrcLoudness` returns an unexpected format or throws, `measure_loudness()` returns `None` values silently.

View File

@@ -0,0 +1,158 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://fl-control/schemas/reascript-protocol.json",
"title": "ReaScript Protocol",
"description": "Command/result protocol for ReaScript two-way JSON communication",
"definitions": {
"ReaScriptCommand": {
"title": "ReaScriptCommand",
"description": "Command sent to the ReaScript via fl_control_command.json",
"type": "object",
"properties": {
"version": {
"type": "integer",
"description": "Protocol version",
"default": 1
},
"action": {
"description": "Single action name or pipeline list. Known values: add_plugins, configure_fx_params, verify_fx, calibrate, render",
"default": "calibrate",
"oneOf": [
{ "type": "string" },
{
"type": "array",
"items": { "type": "string" }
}
]
},
"rpp_path": {
"type": "string",
"description": "Path to .rpp file to open",
"default": ""
},
"render_path": {
"type": "string",
"description": "Path for rendered WAV output",
"default": ""
},
"timeout": {
"type": "integer",
"description": "Polling timeout in seconds",
"default": 120
},
"track_calibration": {
"type": "array",
"description": "Per-track volume/pan/send calibration",
"default": [],
"items": {
"type": "object",
"properties": {
"track_index": { "type": "integer" },
"volume": { "type": "number" },
"pan": { "type": "number" },
"sends": {
"type": "array",
"items": {
"type": "object",
"properties": {
"dest_track_index": { "type": "integer" },
"level": { "type": "number" }
}
}
}
}
}
},
"plugins_to_add": {
"type": "array",
"description": "Each dict: {\"track_name\": str, \"fx_name\": str, \"params\": {\"0\": float, ...}}",
"default": [],
"items": {
"type": "object",
"properties": {
"track_name": { "type": "string" },
"fx_name": { "type": "string" },
"params": {
"type": "object",
"additionalProperties": { "type": "number" }
}
},
"required": ["track_name", "fx_name"]
}
}
}
},
"ReaScriptResult": {
"title": "ReaScriptResult",
"description": "Result written by the ReaScript to fl_control_result.json",
"type": "object",
"properties": {
"version": {
"type": "integer",
"description": "Protocol version",
"default": 1
},
"status": {
"type": "string",
"description": "ok | error | timeout",
"default": "ok",
"enum": ["ok", "error", "timeout"]
},
"message": {
"type": "string",
"description": "Error or status message",
"default": ""
},
"lufs": {
"type": ["number", "null"],
"description": "Integrated LUFS",
"default": null
},
"integrated_lufs": {
"type": ["number", "null"],
"description": "Integrated LUFS reading",
"default": null
},
"short_term_lufs": {
"type": ["number", "null"],
"description": "Short-term LUFS",
"default": null
},
"fx_errors": {
"type": "array",
"description": "FX verification errors. Each dict: {\"track_index\": int, \"fx_index\": int, \"name\": str, \"expected\": str}",
"default": [],
"items": {
"type": "object",
"properties": {
"track_index": { "type": "integer" },
"fx_index": { "type": "integer" },
"name": { "type": "string" },
"expected": { "type": "string" }
}
}
},
"tracks_verified": {
"type": "integer",
"description": "Number of tracks verified",
"default": 0
},
"added_plugins": {
"type": "array",
"description": "Each dict: {\"fx_name\": str, \"instance_id\": int, \"track_name\": str, \"status\": str}",
"default": [],
"items": {
"type": "object",
"properties": {
"fx_name": { "type": "string" },
"instance_id": { "type": "integer" },
"track_name": { "type": "string" },
"status": { "type": "string" }
},
"required": ["fx_name", "track_name", "status"]
}
}
}
}
}
}

View File

@@ -0,0 +1,423 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://fl-control/schemas/song-definition.json",
"title": "SongDefinition",
"description": "Complete song definition — the source of truth for one .rpp file",
"type": "object",
"required": ["meta"],
"properties": {
"meta": { "$ref": "#/definitions/SongMeta" },
"tracks": {
"type": "array",
"description": "List of REAPER tracks with clips and plugins",
"default": [],
"items": { "$ref": "#/definitions/TrackDef" }
},
"patterns": {
"type": "array",
"description": "Pattern definitions for arrangement",
"default": [],
"items": { "$ref": "#/definitions/PatternDef" }
},
"items": {
"type": "array",
"description": "Arrangement items referencing patterns",
"default": [],
"items": { "$ref": "#/definitions/ArrangementItemDef" }
},
"progression_name": {
"type": "string",
"description": "Chord progression name (e.g. i-VII-VI-VII)",
"default": "i-VII-VI-VII"
},
"section_template": {
"type": "string",
"description": "Section template name",
"default": "standard"
},
"samples": {
"type": "object",
"description": "Sample file map (name → filename)",
"default": {},
"additionalProperties": { "type": "string" }
},
"sections": {
"type": "array",
"description": "Section definitions in playback order",
"default": [],
"items": { "$ref": "#/definitions/SectionDef" }
},
"master_plugins": {
"type": "array",
"description": "List of plugin registry keys for master FX chain",
"default": [],
"items": { "type": "string" }
}
},
"definitions": {
"SongMeta": {
"title": "SongMeta",
"description": "Song metadata — tempo, key, time signature",
"type": "object",
"required": ["bpm", "key"],
"properties": {
"bpm": {
"type": "number",
"description": "Tempo 20999",
"minimum": 20,
"maximum": 999
},
"key": {
"type": "string",
"description": "Key string e.g. Am, Dm, Gm",
"pattern": "^[A-G][b#]?m?$"
},
"title": {
"type": "string",
"description": "Song title",
"default": ""
},
"ppq": {
"type": "integer",
"description": "Ticks per quarter note (REAPER default)",
"default": 960
},
"time_sig_num": {
"type": "integer",
"description": "Time signature numerator",
"default": 4
},
"time_sig_den": {
"type": "integer",
"description": "Time signature denominator",
"default": 4
},
"calibrate": {
"type": "boolean",
"description": "Enable post-processing mix calibration",
"default": true
}
}
},
"MidiNote": {
"title": "MidiNote",
"description": "A single MIDI note event",
"type": "object",
"required": ["pitch", "start", "duration"],
"properties": {
"pitch": {
"type": "integer",
"description": "MIDI note number 0127 (60 = middle C)",
"minimum": 0,
"maximum": 127
},
"start": {
"type": "number",
"description": "Start time in beats from item start"
},
"duration": {
"type": "number",
"description": "Duration in beats"
},
"velocity": {
"type": "integer",
"description": "Velocity 0127",
"minimum": 0,
"maximum": 127,
"default": 64
}
}
},
"CCEvent": {
"title": "CCEvent",
"description": "A MIDI CC event within a clip",
"type": "object",
"required": ["controller", "time", "value"],
"properties": {
"controller": {
"type": "integer",
"description": "CC number (e.g. 11 = Expression)"
},
"time": {
"type": "number",
"description": "Position in beats from clip start"
},
"value": {
"type": "integer",
"description": "CC value 0127",
"minimum": 0,
"maximum": 127
}
}
},
"ClipDef": {
"title": "ClipDef",
"description": "A clip placed on a track — either audio or MIDI",
"type": "object",
"required": ["position", "length"],
"properties": {
"position": {
"type": "number",
"description": "Start position in beats"
},
"length": {
"type": "number",
"description": "Duration in beats"
},
"name": {
"type": "string",
"description": "Display name",
"default": ""
},
"audio_path": {
"type": ["string", "null"],
"description": "Absolute path to audio file (for audio clips)",
"default": null
},
"midi_notes": {
"type": "array",
"description": "List of MIDI notes (for MIDI clips)",
"default": [],
"items": { "$ref": "#/definitions/MidiNote" }
},
"midi_cc": {
"type": "array",
"description": "MIDI CC events",
"default": [],
"items": { "$ref": "#/definitions/CCEvent" }
},
"loop": {
"type": "boolean",
"description": "Whether the audio clip loops",
"default": false
},
"fade_in": {
"type": "number",
"description": "Fade-in duration in seconds",
"default": 0.0
},
"fade_out": {
"type": "number",
"description": "Fade-out duration in seconds",
"default": 0.0
},
"vol_mult": {
"type": "number",
"description": "Volume multiplier applied at clip level",
"default": 1.0
}
}
},
"PluginDef": {
"title": "PluginDef",
"description": "A VST plugin instance on a track",
"type": "object",
"required": ["name", "path"],
"properties": {
"name": {
"type": "string",
"description": "Display name (e.g. Serum 2)"
},
"path": {
"type": "string",
"description": "Plugin path/identifier (e.g. VST3: Serum 2 (Xfer Records))"
},
"index": {
"type": "integer",
"description": "Chain position (0 = first)",
"default": 0
},
"params": {
"type": "object",
"description": "Optional dict of parameter index → value",
"default": {},
"additionalProperties": { "type": "number" }
},
"preset_data": {
"type": ["array", "null"],
"description": "Base64 preset chunks",
"default": null,
"items": { "type": "string" }
},
"role": {
"type": "string",
"description": "Track role for role-aware preset lookup (e.g. bass, lead, pad)",
"default": ""
},
"builtin": {
"type": "boolean",
"description": "True → deferred to ReaScript, not written to .rpp",
"default": false
}
}
},
"TrackDef": {
"title": "TrackDef",
"description": "A track in the REAPER project",
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "Track display name"
},
"volume": {
"type": "number",
"description": "0.01.0 (maps to REAPER volume fader)",
"default": 0.85
},
"pan": {
"type": "number",
"description": "-1.0 to 1.0",
"default": 0.0
},
"color": {
"type": "integer",
"description": "REAPER color index (067), 0 = default",
"default": 0
},
"clips": {
"type": "array",
"description": "Audio/MIDI clips placed on this track",
"default": [],
"items": { "$ref": "#/definitions/ClipDef" }
},
"plugins": {
"type": "array",
"description": "VST plugins on this track",
"default": [],
"items": { "$ref": "#/definitions/PluginDef" }
},
"send_reverb": {
"type": "number",
"description": "Reverb send level 0.01.0",
"default": 0.0
},
"send_delay": {
"type": "number",
"description": "Delay send level 0.01.0",
"default": 0.0
},
"send_level": {
"type": "object",
"description": "Dict mapping return track index → send level 0.01.0",
"default": {},
"additionalProperties": { "type": "number" }
}
}
},
"SectionDef": {
"title": "SectionDef",
"description": "A section in the song arrangement with energy and dynamics",
"type": "object",
"required": ["name", "bars"],
"properties": {
"name": {
"type": "string",
"description": "Display name (e.g. intro, chorus, verse)"
},
"bars": {
"type": "integer",
"description": "Length in bars"
},
"energy": {
"type": "number",
"description": "Energy level 0.01.0 (controls velocity multiplier)",
"default": 0.5
},
"velocity_mult": {
"type": "number",
"description": "Velocity multiplier applied to all notes in section",
"default": 1.0
},
"vol_mult": {
"type": "number",
"description": "Volume multiplier applied to track in section",
"default": 1.0
}
}
},
"PatternDef": {
"title": "PatternDef",
"description": "A pattern definition with generator and variation axes",
"type": "object",
"required": ["id", "name", "instrument", "channel", "bars", "generator"],
"properties": {
"id": {
"type": "integer",
"description": "Unique pattern ID"
},
"name": {
"type": "string",
"description": "Display name (e.g. Kick Main)"
},
"instrument": {
"type": "string",
"description": "Sample/instrument key (e.g. kick, snare)"
},
"channel": {
"type": "integer",
"description": "MIDI channel (11 = kick, 12 = snare, etc.)"
},
"bars": {
"type": "integer",
"description": "Length in bars"
},
"generator": {
"type": "string",
"description": "Generator function name"
},
"velocity_mult": {
"type": "number",
"description": "Velocity multiplier (0.851.1)",
"default": 1.0
},
"density": {
"type": "number",
"description": "Note density 0.01.0",
"default": 1.0
}
}
},
"ArrangementItemDef": {
"title": "ArrangementItemDef",
"description": "An item placed in the arrangement referencing a pattern on a track",
"type": "object",
"required": ["pattern", "bar", "bars", "track"],
"properties": {
"pattern": {
"type": "integer",
"description": "Pattern ID"
},
"bar": {
"type": "number",
"description": "Start position in bars"
},
"bars": {
"type": "number",
"description": "Length in bars"
},
"track": {
"type": "integer",
"description": "Track index"
}
}
},
"ArrangementTrack": {
"title": "ArrangementTrack",
"description": "A track in the REAPER arrangement with index and display name",
"type": "object",
"required": ["index", "name"],
"properties": {
"index": {
"type": "integer",
"description": "Track index"
},
"name": {
"type": "string",
"description": "Display name"
}
}
}
}
}