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

View File

@@ -0,0 +1,68 @@
# Design: LLM-Ready Documentation Suite
## Technical Approach
Write 9 documentation files under `docs/` (currently empty) that ground any LLM in the real REAPER `.rpp` system. One primary file (`LLM_CONTEXT.md`, target ~35KB) serves as single-read grounding. Four module deep-dives, two JSON Schemas, and one CLI reference provide targeted detail. README.md gets a truth-preserving rewrite: FL Studio → REAPER, `.flp``.rpp`, no MCP. Zero code changes — all content derived from reading source files (`src/core/schema.py`, `src/reaper_builder/__init__.py`, `src/reaper_scripting/commands.py`, `src/calibrator/__init__.py`, `src/composer/*.py`, `scripts/*.py`).
LLM_CONTEXT.md sections follow a "what → how → where" flow: system identity first (REAPER target, `.rpp` format), then data model (all 11 dataclasses), then pipeline (compose → build → calibrate → validate → ReaScript), then module map, CLI reference, conventions, and extension guide. Each section self-contains the critical details so no multi-hop reading is required.
## Architecture Decisions
### Decision 1: LLM_CONTEXT.md — Flat Linear Sections
| Aspect | Flat Sections | Hierarchical (index + sub-pages) |
|--------|--------------|----------------------------------|
| LLM context window | One read → full knowledge | Multi-hop; LLM must follow links |
| Maintenance | Single file, coherent | Multiple files, drift risk |
| Size budget | ~35KB (within 40KB cap) | Index ~5KB, but requires cross-referencing |
| **Chosen** | ✅ | |
**Rationale**: LLMs process flat context windows efficiently. A self-contained 35KB file means the LLM reads once and understands architecture, data model, pipeline, module layout, and CLI entry points. Module deep-dives (`docs/modules/*.md`) are supplementary for targeted modifications, not prerequisites. Avoids the anti-pattern of forcing an LLM to chase links for basic system comprehension.
### Decision 2: JSON Schema — Hand-Written from Dataclass Source
| Aspect | Hand-Written | Auto-Generated (dataclasses-jsonschema) |
|--------|-------------|----------------------------------------|
| Exact draft-07 | Full control | Libraries target draft-2020-12 |
| Optional/Union types | Can match `str\|None``{"type": ["string","null"]}` exactly | May lose nullability nuance |
| Dependency | None (docs-only) | Adds pip dependency |
| Scale | 11 dataclasses — manageable | Overkill for this count |
| **Chosen** | ✅ | |
**Rationale**: 11 dataclasses total (`SongDefinition`, `SongMeta`, `TrackDef`, `ClipDef`, `MidiNote`, `SectionDef`, `PluginDef`, `PatternDef`, `ArrangementItemDef`, `CCEvent`, `ArrangementTrack`) are small enough for manual authoring. Manual writing ensures exact draft-07 compliance and correct handling of `Optional` types (`str | None`) and `field(default_factory=list)`. Auto-generation would add a tooling dependency for a documentation-only change and risk schema draft mismatches (spec requires draft-07, proposal mentions draft-2020-12 — resolved below).
## File Changes
| File | Action | Description |
|------|--------|-------------|
| `docs/LLM_CONTEXT.md` | Create | Primary entry point: architecture diagram, dataclasses, pipeline, module map, CLI, conventions, extension guide. Target 35KB. |
| `docs/CLI.md` | Create | Complete CLI reference: `scripts/compose.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --no-calibrate), `scripts/generate.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --validate), `scripts/run_in_reaper.py` (<rpp_path>, --output, --timeout, --plugins-config, --action) |
| `docs/modules/composer.md` | Create | Pattern generators (`patterns.py`, `rhythm.py`), chord engine (`chords.py`), melody engine (`melody_engine.py`), converters, templates, variation |
| `docs/modules/reaper-builder.md` | Create | `RPPBuilder` class, `PLUGIN_REGISTRY` (~150 entries), `ALIAS_MAP`, `PLUGIN_PRESETS`, preset transformer, `render.py` headless render |
| `docs/modules/reaper-scripting.md` | Create | `ReaScriptGenerator`, `ReaScriptCommand`, `ReaScriptResult`, command/result JSON contract, `ProtocolVersionError` |
| `docs/modules/calibrator.md` | Create | `Calibrator.apply()` post-processing, mix calibration presets (`calibrator/presets.py`) |
| `docs/schemas/song-definition.json` | Create | JSON Schema draft-07 for `SongDefinition` + all nested dataclasses |
| `docs/schemas/reascript-protocol.json` | Create | JSON Schema draft-07 for `ReaScriptCommand` and `ReaScriptResult` |
| `README.md` | Modify | Rewrite: REAPER `.rpp` target, real CLI scripts, link to `docs/LLM_CONTEXT.md`, remove FL Studio/MCP |
## Testing Strategy
| Layer | What to Test | Approach |
|-------|-------------|----------|
| Link integrity | All `[text](./path)` and `[text](#heading)` references | Shell script: extract all markdown links, verify each target exists on disk or as heading in destination file |
| Schema validity | `song-definition.json`, `reascript-protocol.json` | Validate each `.json` against JSON Schema meta-schema (draft-07) using `ajv` or Python `jsonschema`; validate a sample `SongDefinition.to_json()` output against `song-definition.json` |
| Field name accuracy | Dataclass field names in docs match `schema.py` | `grep` cross-check: for each field name in docs, verify exact match in source; run `grep -r "bpm\|velocity\|send_reverb" docs/` and diff against `schema.py` dataclass attributes |
| README truthiness | No FL Studio/MCP mentions | `grep -i "fl studio\|\.flp\|mcp" README.md` must return empty |
## Open Questions
- [ ] **Schema draft version**: Spec requires draft-07, proposal mentions draft-2020-12. Recommend draft-07 as it has wider LLM/tool support and matches spec requirement.
- [ ] **Module doc depth**: Should module docs include internal helper signatures (e.g., `_section_active`, `_get_kick_cache`) or only the public API surface (`RPPBuilder.write()`, `ChordEngine.progression()`)? Recommend public API only to avoid maintenance burden.
## Dependencies
None. All content derived from reading existing source files. No new packages, no code changes.
## Rollout
`git add docs/ README.md && git commit`. All changes additive to `docs/` + README.md rewrite. No code paths depend on docs. Zero-risk rollout.

View File

@@ -0,0 +1,65 @@
# Proposal: LLM-Ready Documentation
## Intent
No LLM can read this codebase and understand it. README.md is wrong (claims FL Studio `.flp`, mentions non-existent MCP server). `.sdd/design.md` references dead `FLPBuilder`. `docs/` is empty. An LLM encountering this project today would be misled for 3+ rounds before discovering it targets REAPER `.rpp`, not FL Studio `.flp`. We need a single entry-point document that instantly grounds any LLM in the real architecture.
## Scope
### In Scope
- `docs/LLM_CONTEXT.md` — primary entry point: architecture, data model, pipeline, module map, CLI reference, naming conventions, how to extend
- `docs/CLI.md` — complete CLI reference (compose, generate, run_in_reaper)
- `docs/modules/composer.md` — composition engine: pattern generators, converters, melody engine
- `docs/modules/reaper-builder.md` — RPP format, `RPPBuilder`, plugin registry (~150 plugins), presets
- `docs/modules/reaper-scripting.md` — ReaScript generation protocol, command/result JSON bridge
- `docs/modules/calibrator.md` — mix calibration presets and post-processing pipeline
- `docs/schemas/song-definition.json` — JSON Schema for `SongDefinition` (SongMeta, TrackDef, ClipDef, MidiNote, SectionDef)
- `docs/schemas/reascript-protocol.json` — JSON Schema for command.json / result.json contract
- `README.md` — fix: FL Studio → REAPER, remove MCP references, correct project structure
### Out of Scope
- `.sdd/design.md` update (separate change)
- Sphinx/pdoc API docs generation
- i18n docs
- Human-focused tutorials
## Capabilities
### New Capabilities
None — documentation only; no behavioral capability changes.
### Modified Capabilities
None.
## Approach
Hybrid: one primary file (`LLM_CONTEXT.md`, ~30KB) as immediate grounding document for any LLM session, plus modular deep-dive docs and JSON Schemas for contract validation. All docs live under existing `docs/` directory. Schemas use standard JSON Schema draft-2020-12. README.md gets a truth-preserving rewrite. Zero code changes — docs are generated manually with LLM assistance, not via docstring extraction.
## Affected Areas
| Area | Impact | Description |
|------|--------|-------------|
| `docs/` | New | Fill with 9 files: LLM_CONTEXT.md, CLI.md, modules/*.md, schemas/*.json |
| `README.md` | Modified | Fix FL Studio → REAPER, remove MCP, correct structure |
## Risks
| Risk | Likelihood | Mitigation |
|------|------------|------------|
| LLM_CONTEXT.md diverges from code | Low | Docs reference exact source files; verified during spec phase via grep cross-check |
| Plugin registry changes break builder docs | Low | Registry path is stable; doc notes it reflects current `PLUGIN_REGISTRY` dict |
## Rollback Plan
`git revert` the commit. All changes are additive to `docs/` + README.md rewrite; no code paths depend on docs. Revert is instant with zero side effects.
## Dependencies
- None (read-only source inspection; no new packages)
## Success Criteria
- [ ] LLM given only `docs/LLM_CONTEXT.md` correctly identifies: REAPER target, RPP format, module structure, CLI entry points
- [ ] JSON Schemas validate against actual `SongDefinition` and reascript command structures
- [ ] README.md accurately reflects project name, target DAW, and real features
- [ ] All module docs reference actual source files and class names

View File

@@ -0,0 +1,59 @@
# llm-docs Specification
## Purpose
Documentation-only capability. Defines 9 LLM-ready files that ground any LLM in the REAPER `.rpp` generation system. Zero code changes — all artifacts are additive under `docs/` plus a README rewrite.
## Requirements
### Requirement: LLM_CONTEXT.md Aggregates System Knowledge
`docs/LLM_CONTEXT.md` MUST be a single entry-point file under 40KB that an LLM can read to understand the entire system. Content MUST include: system description, ASCII architecture diagram, all dataclass definitions from `src/core/schema.py`, module index mapping each `src/` directory to its role, pipeline steps (compose → build → calibrate → validate), plugin system (`PLUGIN_REGISTRY`, `ALIAS_MAP`, presets), ReaScript protocol, CLI reference for `scripts/compose.py`, `scripts/generate.py`, `scripts/run_in_reaper.py`, naming conventions, and extension guide. File MUST be valid markdown.
#### Scenario: LLM grounds from LLM_CONTEXT.md alone
- GIVEN an LLM with no prior knowledge of fl_control
- WHEN it reads `docs/LLM_CONTEXT.md`
- THEN it identifies REAPER `.rpp` as the target format, understands `SongDefinition``TrackDef``ClipDef` as the core data model, knows module layout under `src/`, and locates CLI entry points
- AND the file is under 40KB valid markdown with all required sections present
### Requirement: Module Deep-Dives Enable Targeted Modification
Each module doc under `docs/modules/` MUST document: public API signatures with types, data flow in/out, dependencies, and known gotchas. Each MUST reference `LLM_CONTEXT.md` as parent.
#### Scenario: LLM modifies a module after reading its deep-dive
- GIVEN `docs/modules/reaper-builder.md` exists
- WHEN an LLM reads it
- THEN it identifies `RPPBuilder`, `PLUGIN_REGISTRY` format (key → display_name, filename, uid_guid), `write()` signature, and dependency on `src/core/schema.py`
### Requirement: JSON Schemas Match Dataclass Definitions
`docs/schemas/song-definition.json` MUST be JSON Schema draft-07 matching every field in `SongDefinition`, `SongMeta`, `TrackDef`, `ClipDef`, `MidiNote`, `SectionDef`, `PluginDef`, `PatternDef`, `ArrangementItemDef`, `CCEvent` as defined in `src/core/schema.py`. `docs/schemas/reascript-protocol.json` MUST match `ReaScriptCommand` and `ReaScriptResult` in `src/reaper_scripting/commands.py`. Field names, types, and required/optional flags MUST match the dataclass source exactly.
#### Scenario: Schema validates SongDefinition instance
- GIVEN a `SongDefinition` serialized via `to_json()`
- WHEN validated against `song-definition.json`
- THEN zero schema violations; every field name, type, and required constraint matches the dataclass
### Requirement: README.md Reflects True System Identity
`README.md` MUST NOT reference FL Studio, `.flp` files, or MCP server. MUST reference REAPER `.rpp` generation, list real CLI entry points (`compose.py`, `generate.py`, `run_in_reaper.py`), and point to `docs/LLM_CONTEXT.md`.
#### Scenario: New developer reads README
- GIVEN a developer reading `README.md`
- THEN they see "REAPER", ".rpp", and `docs/LLM_CONTEXT.md`
- AND they see NO mention of "FL Studio", ".flp", or "MCP"
### Requirement: Cross-Reference Integrity
All internal markdown links in docs MUST resolve to existing files or headings. Module names in docs MUST match actual Python module names under `src/`. Dataclass field names in docs MUST match `src/core/schema.py` exactly.
#### Scenario: Links resolve and names match code
- GIVEN all doc files under `docs/`
- WHEN an LLM or human follows every `[link](./path)` reference
- THEN every link resolves; no broken cross-references exist
- AND all module and field names match their definitions in code

View File

@@ -0,0 +1,33 @@
# Tasks: LLM-Ready Documentation
## Phase 1: Primary Entry Point
- [x] 1.1 `docs/LLM_CONTEXT.md` — Write full entry-point (~35KB). Sections: system identity (REAPER `.rpp`), ASCII architecture diagram, all 11 dataclasses from `src/core/schema.py`, pipeline (compose → build → calibrate → validate → ReaScript), module map (`src/` dirs → roles), plugin system (`PLUGIN_REGISTRY`, `ALIAS_MAP`, presets), ReaScript protocol, CLI reference (compose.py, generate.py, run_in_reaper.py), naming conventions, extension guide. Valid markdown, under 40KB.
## Phase 2: Module Deep-Dives
- [x] 2.1 `docs/modules/composer.md` — Document `src/composer/`: pattern generators (`patterns.py`, `rhythm.py`), chord engine (`chords.py`), melody engine (`melody_engine.py`), converters, templates, variation. Public API signatures, data flow, dependencies. Link to `LLM_CONTEXT.md`.
- [x] 2.2 `docs/modules/reaper-builder.md` — Document `RPPBuilder` class, `PLUGIN_REGISTRY` (~150 entries: key→display_name,filename,uid_guid), `ALIAS_MAP`, `PLUGIN_PRESETS`, preset transformer, `render.py` headless render. `write()` signature. Dependency on `src/core/schema.py`. Link to `LLM_CONTEXT.md`.
- [x] 2.3 `docs/modules/reaper-scripting.md` — Document `ReaScriptCommand`, `ReaScriptResult` (from `src/reaper_scripting/commands.py`), `ReaScriptGenerator`, command/result JSON contract, `ProtocolVersionError`. Link to `LLM_CONTEXT.md`.
- [x] 2.4 `docs/modules/calibrator.md` — Document `Calibrator.apply()` post-processing pipeline, mix calibration presets (`src/calibrator/presets.py`). Link to `LLM_CONTEXT.md`.
## Phase 3: Schemas and CLI Reference
- [x] 3.1 `docs/schemas/song-definition.json` — JSON Schema draft-07 for `SongDefinition` + all nested dataclasses (`SongMeta`, `TrackDef`, `ClipDef`, `MidiNote`, `SectionDef`, `PluginDef`, `PatternDef`, `ArrangementItemDef`, `CCEvent`, `ArrangementTrack`). Match field names and types exactly from `src/core/schema.py`.
- [x] 3.2 `docs/schemas/reascript-protocol.json` — JSON Schema draft-07 for `ReaScriptCommand` and `ReaScriptResult`. Match field names and types from `src/reaper_scripting/commands.py`.
- [x] 3.3 `docs/CLI.md` — Complete CLI reference: `compose.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --no-calibrate), `generate.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --validate), `run_in_reaper.py` (<rpp_path>, --output, --timeout, --plugins-config, --action). Link to `LLM_CONTEXT.md`.
## Phase 4: README Correction and Verification
- [x] 4.1 `README.md` — Replace FL Studio/`.flp` with REAPER/`.rpp`. Remove all MCP server references. List real CLI scripts. Link to `docs/LLM_CONTEXT.md`.
- [x] 4.2 Verify all internal links — Extract every `[text](path)` and `[text](#heading)` from all doc files; confirm each target file/heading exists.
- [x] 4.3 Validate JSON schemas — Validate `song-definition.json` and `reascript-protocol.json` against JSON Schema draft-07 meta-schema. Validate sample `SongDefinition.to_json()` output against `song-definition.json`.
- [x] 4.4 Verify field names match code — `grep` each dataclass field name found in docs against `src/core/schema.py`. `grep -i "fl studio|\.flp|mcp" README.md` must return empty.

View File

@@ -1,27 +1,36 @@
# FL Control — Reggaeton Production System
# fl_control — REAPER .rpp Reggaetón Generator
Python system for generating complete reggaeton `.flp` projects for FL Studio from the command line, using intelligent sample selection and algorithmic composition.
> **About the name**: Despite the directory name `fl_control`, this system generates **REAPER `.rpp` project files**. The name is retained for backward compatibility with repository URLs and CI/CD pipelines. This tool targets REAPER exclusively.
Python system for generating complete reggaetón `.rpp` projects for REAPER from the command line, using algorithmic composition, deterministic melody engines, and calibrated mixing.
## Features
- **Forensic Sample Analyzer** — 4-layer audio analysis (signal, perceptual, musical, timbre) using aubio
- **Intelligent Sample Selector** — scores samples by key compatibility (circle of fifths), BPM proximity, and character
- **Melodic Generators** — reggaeton-idiomatic patterns: bass tresillo, melodic hooks, chord blocks, sustained pads
- **FLP Builder** — assembles valid FL Studio project files from a JSON song definition
- **MCP Server** — 28-tool Model Context Protocol server for AI-assisted production
- **Chord Engine** — Emotion-aware progressions with voice leading (romantic, dark, club, classic)
- **Melody Engine** — Hook-based call-and-response motifs (hook, stabs, smooth styles)
- **808 Bass** — Proven reggaetón harmonic pattern with CC11 sidechain ducking from drum analysis
- **RPP Builder** — Generates valid REAPER `.rpp` files from a `SongDefinition` dataclass
- **ReaScript Generator** — Self-contained Python scripts for REAPER post-processing (plugin loading, mix calibration, rendering, LUFS measurement)
- **Calibrator** — Role-based volume, pan, EQ, and send presets plus master chain configuration
- **Sample Selector** — Scores samples by key compatibility (circle of fifths), BPM proximity, and character
- **Plugin Registry** — ~97 VST2/VST3 plugins with verified GUIDs and preset data
## Quick Start
```bash
pip install -r requirements.txt
# Analyze your sample library
1_ANALIZAR.bat # or: python src/analyzer/__init__.py
# Generate a reggaetón track
python scripts/generate.py --bpm 95 --key Am --output output/song.rpp --seed 42
# Compose a track
python scripts/compose_track.py --key Am --bpm 95 --bars 8 --output output/track.flp
# or double-click:
COMPONER.bat
# Or use the full pipeline directly
python scripts/compose.py --bpm 99 --key Am --emotion romantic --output output/song.rpp
# Validate output
python scripts/generate.py --bpm 95 --key Dm --seed 123 --validate
# Open the result in REAPER
start output/song.rpp
```
## Project Structure
@@ -29,30 +38,49 @@ COMPONER.bat
```
fl_control/
├── src/
│ ├── analyzer/ # Forensic audio feature extraction
│ ├── composer/ # Pattern generators (rhythm + melodic)
│ ├── flp_builder/ # FL Studio .flp binary assembly
── selector/ # Intelligent sample scoring & selection
├── mcp/ # MCP server (28 tools for AI integration)
├── scripts/ # CLI entry points
├── knowledge/ # Musical domain knowledge (progressions, templates)
├── data/ # Generated indexes (gitignored)
├── .sdd/ # Spec-Driven Development artifacts
└── COMPONER.bat # Quick-compose launcher
│ ├── core/ # Data model (SongDefinition, TrackDef, ClipDef, etc.)
│ ├── composer/ # Chord engine, melody engine, rhythm patterns
│ ├── reaper_builder/ # RPPBuilder, PLUGIN_REGISTRY (~97 plugins), render
── reaper_scripting/ # ReaScript generator, command/result protocol
│ ├── calibrator/ # Mix calibration presets (volume, pan, sends, EQ)
├── selector/ # Intelligent sample scoring & selection
│ └── validator/ # .rpp output validation
├── scripts/ # CLI entry points
│ ├── compose.py # Full composition pipeline
│ ├── generate.py # Thin wrapper around compose
│ └── run_in_reaper.py # ReaScript generation & execution
├── knowledge/ # Musical domain knowledge (progressions, templates)
├── data/ # Generated indexes (gitignored)
├── .sdd/ # Spec-Driven Development artifacts
└── docs/ # LLM-ready documentation
├── LLM_CONTEXT.md # Complete system overview for LLMs
├── CLI.md # Full CLI reference
├── modules/ # Module deep-dives
└── schemas/ # JSON Schemas (draft-07)
```
## System Requirements
- Python 3.10+
- FL Studio (for opening generated `.flp` files)
- ~4GB disk space for sample library (not included)
- REAPER (Windows, v7.x) — for opening generated `.rpp` files and running ReaScripts
- Sample library with drumloops and FX samples (not included)
## Workflow
1. Drop your sample library into `librerias/`
2. Run `1_ANALIZAR.bat` to build the sample index
3. Run `COMPONER.bat` to generate a track
1. Ensure sample files exist at configured paths (see `scripts/compose.py` for drumloop paths)
2. Run `scripts/generate.py --bpm 95 --key Am --output output/song.rpp`
3. Open `output/song.rpp` in REAPER
4. (Optional) Run ReaScript post-processing: `python scripts/run_in_reaper.py output/song.rpp --action "add_plugins calibrate render"`
## Architecture
The system uses a JSON `SongDefinition` as the single source of truth decoupling composition logic from FLP binary rendering. See `.sdd/` for full technical specs and design documents.
The system uses a `SongDefinition` dataclass as the single source of truth, decoupling composition logic from `.rpp` rendering. The pipeline is:
```
CLI → compose SongDefinition → Calibrator.apply() → RPPBuilder.write() → .rpp file
ReaScriptGenerator.generate() → ReaScript → REAPER
```
See [docs/LLM_CONTEXT.md](docs/LLM_CONTEXT.md) for the complete system overview, data model reference, and extension guide.

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"
}
}
}
}
}