diff --git a/.sdd/changes/archive/llm-docs/design.md b/.sdd/changes/archive/llm-docs/design.md new file mode 100644 index 0000000..89fccce --- /dev/null +++ b/.sdd/changes/archive/llm-docs/design.md @@ -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` (, --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. diff --git a/.sdd/changes/archive/llm-docs/proposal.md b/.sdd/changes/archive/llm-docs/proposal.md new file mode 100644 index 0000000..be0e6f7 --- /dev/null +++ b/.sdd/changes/archive/llm-docs/proposal.md @@ -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 diff --git a/.sdd/changes/archive/llm-docs/spec.md b/.sdd/changes/archive/llm-docs/spec.md new file mode 100644 index 0000000..2d9e73d --- /dev/null +++ b/.sdd/changes/archive/llm-docs/spec.md @@ -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 diff --git a/.sdd/changes/archive/llm-docs/tasks.md b/.sdd/changes/archive/llm-docs/tasks.md new file mode 100644 index 0000000..7eea31b --- /dev/null +++ b/.sdd/changes/archive/llm-docs/tasks.md @@ -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` (, --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. diff --git a/README.md b/README.md index 64221d7..ff43b35 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/CLI.md b/docs/CLI.md new file mode 100644 index 0000000..f4f6740 --- /dev/null +++ b/docs/CLI.md @@ -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 [--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` | `_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/_lufs.json # LUFS metrics +output/_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()`: 20–999. + +## 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. diff --git a/docs/LLM_CONTEXT.md b/docs/LLM_CONTEXT.md new file mode 100644 index 0000000..453c255 --- /dev/null +++ b/docs/LLM_CONTEXT.md @@ -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 20–999 | +| `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 0–127 (60 = middle C) | +| `start` | `float` | *(required)* | Start time in beats from item start | +| `duration` | `float` | *(required)* | Duration in beats | +| `velocity` | `int` | `64` | Velocity 0–127 | + +### 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 0–127 | + +### 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.0–1.0 (maps to REAPER volume fader) | +| `pan` | `float` | `0.0` | -1.0 (left) to 1.0 (right) | +| `color` | `int` | `0` | REAPER color index 0–67 | +| `clips` | `list[ClipDef]` | `[]` | Audio/MIDI clips | +| `plugins` | `list[PluginDef]` | `[]` | VST plugins on this track | +| `send_reverb` | `float` | `0.0` | Reverb send level 0.0–1.0 | +| `send_delay` | `float` | `0.0` | Delay send level 0.0–1.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.0–1.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.85–1.1 | +| `density` | `float` | `1.0` | Note density 0.0–1.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 (20–999), 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 ``, 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__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 diff --git a/docs/modules/calibrator.md b/docs/modules/calibrator.md new file mode 100644 index 0000000..c7584a7 --- /dev/null +++ b/docs/modules/calibrator.md @@ -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.0–1.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.0–1.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. diff --git a/docs/modules/composer.md b/docs/modules/composer.md new file mode 100644 index 0000000..fc5bc44 --- /dev/null +++ b/docs/modules/composer.md @@ -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) | i7–VI7–III7–VII7 | +| dark | (0,m7), (5,m7), (10,7), (3,7) | i7–iv7–VII7–III7 | +| club | (0,m7), (8,7), (10,7), (7,7) | i7–VI7–VII7–V7 | +| classic | (0,m7), (10,7), (8,7), (7,7) | i7–VII7–VI7–V7 | + +### 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 2–8. +- **`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 2–8 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. diff --git a/docs/modules/reaper-builder.md b/docs/modules/reaper-builder.md new file mode 100644 index 0000000..26bb6ff --- /dev/null +++ b/docs/modules/reaper-builder.md @@ -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 + + + ...static project metadata... + TEMPO 95 4 4 0 + + NAME master + ... + + + + + > + > + + NAME "808 Bass" + VOLPAN 0.820000 0.000000 -1 -1 1 + + + > + AUXRECV 8 0.050000 -1 -1 0 + AUXRECV 9 0.000000 -1 -1 0 + + POSITION 0.0 + LENGTH 32.0 + NAME "Verse 808" + + HASDATA 1 960 QN + E 0 90 21 50 + E 960 80 21 00 + ... + > + > + > + + NAME "Reverb" + ... + > + + NAME "Delay" + ... + > +> +``` + +### VST Element Format + +**VST3** (`.vst3` files with `{GUID}`): +```rpp + + base64chunk1 + base64chunk2 + ... +> +``` + +**VST2** (`.dll` files with ``): +```rpp + 0 "" ...> + base64chunk1 + base64chunk2 + ... +> +``` + +### MIDI Source Format + +MIDI data is written as E-lines at 960 PPQ with 16th-note quantization (120-tick grid): +```rpp + + 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 `` 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.0–1.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. diff --git a/docs/modules/reaper-scripting.md b/docs/modules/reaper-scripting.md new file mode 100644 index 0000000..c8f7b3b --- /dev/null +++ b/docs/modules/reaper-scripting.md @@ -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. diff --git a/docs/schemas/reascript-protocol.json b/docs/schemas/reascript-protocol.json new file mode 100644 index 0000000..a183bd0 --- /dev/null +++ b/docs/schemas/reascript-protocol.json @@ -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"] + } + } + } + } + } +} diff --git a/docs/schemas/song-definition.json b/docs/schemas/song-definition.json new file mode 100644 index 0000000..a6c1d02 --- /dev/null +++ b/docs/schemas/song-definition.json @@ -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 20–999", + "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 0–127 (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 0–127", + "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 0–127", + "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.0–1.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 (0–67), 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.0–1.0", + "default": 0.0 + }, + "send_delay": { + "type": "number", + "description": "Delay send level 0.0–1.0", + "default": 0.0 + }, + "send_level": { + "type": "object", + "description": "Dict mapping return track index → send level 0.0–1.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.0–1.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.85–1.1)", + "default": 1.0 + }, + "density": { + "type": "number", + "description": "Note density 0.0–1.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" + } + } + } + } +}