docs: LLM-ready documentation suite — LLM_CONTEXT.md, module deep-dives, JSON schemas, CLI reference
Complete documentation system for LLM consumption: primary LLM_CONTEXT.md (27KB system overview), 4 module deep-dives (composer, reaper-builder, reaper-scripting, calibrator), 2 JSON Schema draft-07 contracts, CLI reference, and README correction (FL Studio -> REAPER identity).
This commit is contained in:
68
.sdd/changes/archive/llm-docs/design.md
Normal file
68
.sdd/changes/archive/llm-docs/design.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Design: LLM-Ready Documentation Suite
|
||||
|
||||
## Technical Approach
|
||||
|
||||
Write 9 documentation files under `docs/` (currently empty) that ground any LLM in the real REAPER `.rpp` system. One primary file (`LLM_CONTEXT.md`, target ~35KB) serves as single-read grounding. Four module deep-dives, two JSON Schemas, and one CLI reference provide targeted detail. README.md gets a truth-preserving rewrite: FL Studio → REAPER, `.flp` → `.rpp`, no MCP. Zero code changes — all content derived from reading source files (`src/core/schema.py`, `src/reaper_builder/__init__.py`, `src/reaper_scripting/commands.py`, `src/calibrator/__init__.py`, `src/composer/*.py`, `scripts/*.py`).
|
||||
|
||||
LLM_CONTEXT.md sections follow a "what → how → where" flow: system identity first (REAPER target, `.rpp` format), then data model (all 11 dataclasses), then pipeline (compose → build → calibrate → validate → ReaScript), then module map, CLI reference, conventions, and extension guide. Each section self-contains the critical details so no multi-hop reading is required.
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Decision 1: LLM_CONTEXT.md — Flat Linear Sections
|
||||
|
||||
| Aspect | Flat Sections | Hierarchical (index + sub-pages) |
|
||||
|--------|--------------|----------------------------------|
|
||||
| LLM context window | One read → full knowledge | Multi-hop; LLM must follow links |
|
||||
| Maintenance | Single file, coherent | Multiple files, drift risk |
|
||||
| Size budget | ~35KB (within 40KB cap) | Index ~5KB, but requires cross-referencing |
|
||||
| **Chosen** | ✅ | |
|
||||
|
||||
**Rationale**: LLMs process flat context windows efficiently. A self-contained 35KB file means the LLM reads once and understands architecture, data model, pipeline, module layout, and CLI entry points. Module deep-dives (`docs/modules/*.md`) are supplementary for targeted modifications, not prerequisites. Avoids the anti-pattern of forcing an LLM to chase links for basic system comprehension.
|
||||
|
||||
### Decision 2: JSON Schema — Hand-Written from Dataclass Source
|
||||
|
||||
| Aspect | Hand-Written | Auto-Generated (dataclasses-jsonschema) |
|
||||
|--------|-------------|----------------------------------------|
|
||||
| Exact draft-07 | Full control | Libraries target draft-2020-12 |
|
||||
| Optional/Union types | Can match `str\|None` → `{"type": ["string","null"]}` exactly | May lose nullability nuance |
|
||||
| Dependency | None (docs-only) | Adds pip dependency |
|
||||
| Scale | 11 dataclasses — manageable | Overkill for this count |
|
||||
| **Chosen** | ✅ | |
|
||||
|
||||
**Rationale**: 11 dataclasses total (`SongDefinition`, `SongMeta`, `TrackDef`, `ClipDef`, `MidiNote`, `SectionDef`, `PluginDef`, `PatternDef`, `ArrangementItemDef`, `CCEvent`, `ArrangementTrack`) are small enough for manual authoring. Manual writing ensures exact draft-07 compliance and correct handling of `Optional` types (`str | None`) and `field(default_factory=list)`. Auto-generation would add a tooling dependency for a documentation-only change and risk schema draft mismatches (spec requires draft-07, proposal mentions draft-2020-12 — resolved below).
|
||||
|
||||
## File Changes
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `docs/LLM_CONTEXT.md` | Create | Primary entry point: architecture diagram, dataclasses, pipeline, module map, CLI, conventions, extension guide. Target 35KB. |
|
||||
| `docs/CLI.md` | Create | Complete CLI reference: `scripts/compose.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --no-calibrate), `scripts/generate.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --validate), `scripts/run_in_reaper.py` (<rpp_path>, --output, --timeout, --plugins-config, --action) |
|
||||
| `docs/modules/composer.md` | Create | Pattern generators (`patterns.py`, `rhythm.py`), chord engine (`chords.py`), melody engine (`melody_engine.py`), converters, templates, variation |
|
||||
| `docs/modules/reaper-builder.md` | Create | `RPPBuilder` class, `PLUGIN_REGISTRY` (~150 entries), `ALIAS_MAP`, `PLUGIN_PRESETS`, preset transformer, `render.py` headless render |
|
||||
| `docs/modules/reaper-scripting.md` | Create | `ReaScriptGenerator`, `ReaScriptCommand`, `ReaScriptResult`, command/result JSON contract, `ProtocolVersionError` |
|
||||
| `docs/modules/calibrator.md` | Create | `Calibrator.apply()` post-processing, mix calibration presets (`calibrator/presets.py`) |
|
||||
| `docs/schemas/song-definition.json` | Create | JSON Schema draft-07 for `SongDefinition` + all nested dataclasses |
|
||||
| `docs/schemas/reascript-protocol.json` | Create | JSON Schema draft-07 for `ReaScriptCommand` and `ReaScriptResult` |
|
||||
| `README.md` | Modify | Rewrite: REAPER `.rpp` target, real CLI scripts, link to `docs/LLM_CONTEXT.md`, remove FL Studio/MCP |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
| Layer | What to Test | Approach |
|
||||
|-------|-------------|----------|
|
||||
| Link integrity | All `[text](./path)` and `[text](#heading)` references | Shell script: extract all markdown links, verify each target exists on disk or as heading in destination file |
|
||||
| Schema validity | `song-definition.json`, `reascript-protocol.json` | Validate each `.json` against JSON Schema meta-schema (draft-07) using `ajv` or Python `jsonschema`; validate a sample `SongDefinition.to_json()` output against `song-definition.json` |
|
||||
| Field name accuracy | Dataclass field names in docs match `schema.py` | `grep` cross-check: for each field name in docs, verify exact match in source; run `grep -r "bpm\|velocity\|send_reverb" docs/` and diff against `schema.py` dataclass attributes |
|
||||
| README truthiness | No FL Studio/MCP mentions | `grep -i "fl studio\|\.flp\|mcp" README.md` must return empty |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] **Schema draft version**: Spec requires draft-07, proposal mentions draft-2020-12. Recommend draft-07 as it has wider LLM/tool support and matches spec requirement.
|
||||
- [ ] **Module doc depth**: Should module docs include internal helper signatures (e.g., `_section_active`, `_get_kick_cache`) or only the public API surface (`RPPBuilder.write()`, `ChordEngine.progression()`)? Recommend public API only to avoid maintenance burden.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None. All content derived from reading existing source files. No new packages, no code changes.
|
||||
|
||||
## Rollout
|
||||
|
||||
`git add docs/ README.md && git commit`. All changes additive to `docs/` + README.md rewrite. No code paths depend on docs. Zero-risk rollout.
|
||||
65
.sdd/changes/archive/llm-docs/proposal.md
Normal file
65
.sdd/changes/archive/llm-docs/proposal.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Proposal: LLM-Ready Documentation
|
||||
|
||||
## Intent
|
||||
|
||||
No LLM can read this codebase and understand it. README.md is wrong (claims FL Studio `.flp`, mentions non-existent MCP server). `.sdd/design.md` references dead `FLPBuilder`. `docs/` is empty. An LLM encountering this project today would be misled for 3+ rounds before discovering it targets REAPER `.rpp`, not FL Studio `.flp`. We need a single entry-point document that instantly grounds any LLM in the real architecture.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
- `docs/LLM_CONTEXT.md` — primary entry point: architecture, data model, pipeline, module map, CLI reference, naming conventions, how to extend
|
||||
- `docs/CLI.md` — complete CLI reference (compose, generate, run_in_reaper)
|
||||
- `docs/modules/composer.md` — composition engine: pattern generators, converters, melody engine
|
||||
- `docs/modules/reaper-builder.md` — RPP format, `RPPBuilder`, plugin registry (~150 plugins), presets
|
||||
- `docs/modules/reaper-scripting.md` — ReaScript generation protocol, command/result JSON bridge
|
||||
- `docs/modules/calibrator.md` — mix calibration presets and post-processing pipeline
|
||||
- `docs/schemas/song-definition.json` — JSON Schema for `SongDefinition` (SongMeta, TrackDef, ClipDef, MidiNote, SectionDef)
|
||||
- `docs/schemas/reascript-protocol.json` — JSON Schema for command.json / result.json contract
|
||||
- `README.md` — fix: FL Studio → REAPER, remove MCP references, correct project structure
|
||||
|
||||
### Out of Scope
|
||||
- `.sdd/design.md` update (separate change)
|
||||
- Sphinx/pdoc API docs generation
|
||||
- i18n docs
|
||||
- Human-focused tutorials
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
None — documentation only; no behavioral capability changes.
|
||||
|
||||
### Modified Capabilities
|
||||
None.
|
||||
|
||||
## Approach
|
||||
|
||||
Hybrid: one primary file (`LLM_CONTEXT.md`, ~30KB) as immediate grounding document for any LLM session, plus modular deep-dive docs and JSON Schemas for contract validation. All docs live under existing `docs/` directory. Schemas use standard JSON Schema draft-2020-12. README.md gets a truth-preserving rewrite. Zero code changes — docs are generated manually with LLM assistance, not via docstring extraction.
|
||||
|
||||
## Affected Areas
|
||||
|
||||
| Area | Impact | Description |
|
||||
|------|--------|-------------|
|
||||
| `docs/` | New | Fill with 9 files: LLM_CONTEXT.md, CLI.md, modules/*.md, schemas/*.json |
|
||||
| `README.md` | Modified | Fix FL Studio → REAPER, remove MCP, correct structure |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|------------|------------|
|
||||
| LLM_CONTEXT.md diverges from code | Low | Docs reference exact source files; verified during spec phase via grep cross-check |
|
||||
| Plugin registry changes break builder docs | Low | Registry path is stable; doc notes it reflects current `PLUGIN_REGISTRY` dict |
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
`git revert` the commit. All changes are additive to `docs/` + README.md rewrite; no code paths depend on docs. Revert is instant with zero side effects.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- None (read-only source inspection; no new packages)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] LLM given only `docs/LLM_CONTEXT.md` correctly identifies: REAPER target, RPP format, module structure, CLI entry points
|
||||
- [ ] JSON Schemas validate against actual `SongDefinition` and reascript command structures
|
||||
- [ ] README.md accurately reflects project name, target DAW, and real features
|
||||
- [ ] All module docs reference actual source files and class names
|
||||
59
.sdd/changes/archive/llm-docs/spec.md
Normal file
59
.sdd/changes/archive/llm-docs/spec.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# llm-docs Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Documentation-only capability. Defines 9 LLM-ready files that ground any LLM in the REAPER `.rpp` generation system. Zero code changes — all artifacts are additive under `docs/` plus a README rewrite.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: LLM_CONTEXT.md Aggregates System Knowledge
|
||||
|
||||
`docs/LLM_CONTEXT.md` MUST be a single entry-point file under 40KB that an LLM can read to understand the entire system. Content MUST include: system description, ASCII architecture diagram, all dataclass definitions from `src/core/schema.py`, module index mapping each `src/` directory to its role, pipeline steps (compose → build → calibrate → validate), plugin system (`PLUGIN_REGISTRY`, `ALIAS_MAP`, presets), ReaScript protocol, CLI reference for `scripts/compose.py`, `scripts/generate.py`, `scripts/run_in_reaper.py`, naming conventions, and extension guide. File MUST be valid markdown.
|
||||
|
||||
#### Scenario: LLM grounds from LLM_CONTEXT.md alone
|
||||
|
||||
- GIVEN an LLM with no prior knowledge of fl_control
|
||||
- WHEN it reads `docs/LLM_CONTEXT.md`
|
||||
- THEN it identifies REAPER `.rpp` as the target format, understands `SongDefinition` → `TrackDef` → `ClipDef` as the core data model, knows module layout under `src/`, and locates CLI entry points
|
||||
- AND the file is under 40KB valid markdown with all required sections present
|
||||
|
||||
### Requirement: Module Deep-Dives Enable Targeted Modification
|
||||
|
||||
Each module doc under `docs/modules/` MUST document: public API signatures with types, data flow in/out, dependencies, and known gotchas. Each MUST reference `LLM_CONTEXT.md` as parent.
|
||||
|
||||
#### Scenario: LLM modifies a module after reading its deep-dive
|
||||
|
||||
- GIVEN `docs/modules/reaper-builder.md` exists
|
||||
- WHEN an LLM reads it
|
||||
- THEN it identifies `RPPBuilder`, `PLUGIN_REGISTRY` format (key → display_name, filename, uid_guid), `write()` signature, and dependency on `src/core/schema.py`
|
||||
|
||||
### Requirement: JSON Schemas Match Dataclass Definitions
|
||||
|
||||
`docs/schemas/song-definition.json` MUST be JSON Schema draft-07 matching every field in `SongDefinition`, `SongMeta`, `TrackDef`, `ClipDef`, `MidiNote`, `SectionDef`, `PluginDef`, `PatternDef`, `ArrangementItemDef`, `CCEvent` as defined in `src/core/schema.py`. `docs/schemas/reascript-protocol.json` MUST match `ReaScriptCommand` and `ReaScriptResult` in `src/reaper_scripting/commands.py`. Field names, types, and required/optional flags MUST match the dataclass source exactly.
|
||||
|
||||
#### Scenario: Schema validates SongDefinition instance
|
||||
|
||||
- GIVEN a `SongDefinition` serialized via `to_json()`
|
||||
- WHEN validated against `song-definition.json`
|
||||
- THEN zero schema violations; every field name, type, and required constraint matches the dataclass
|
||||
|
||||
### Requirement: README.md Reflects True System Identity
|
||||
|
||||
`README.md` MUST NOT reference FL Studio, `.flp` files, or MCP server. MUST reference REAPER `.rpp` generation, list real CLI entry points (`compose.py`, `generate.py`, `run_in_reaper.py`), and point to `docs/LLM_CONTEXT.md`.
|
||||
|
||||
#### Scenario: New developer reads README
|
||||
|
||||
- GIVEN a developer reading `README.md`
|
||||
- THEN they see "REAPER", ".rpp", and `docs/LLM_CONTEXT.md`
|
||||
- AND they see NO mention of "FL Studio", ".flp", or "MCP"
|
||||
|
||||
### Requirement: Cross-Reference Integrity
|
||||
|
||||
All internal markdown links in docs MUST resolve to existing files or headings. Module names in docs MUST match actual Python module names under `src/`. Dataclass field names in docs MUST match `src/core/schema.py` exactly.
|
||||
|
||||
#### Scenario: Links resolve and names match code
|
||||
|
||||
- GIVEN all doc files under `docs/`
|
||||
- WHEN an LLM or human follows every `[link](./path)` reference
|
||||
- THEN every link resolves; no broken cross-references exist
|
||||
- AND all module and field names match their definitions in code
|
||||
33
.sdd/changes/archive/llm-docs/tasks.md
Normal file
33
.sdd/changes/archive/llm-docs/tasks.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Tasks: LLM-Ready Documentation
|
||||
|
||||
## Phase 1: Primary Entry Point
|
||||
|
||||
- [x] 1.1 `docs/LLM_CONTEXT.md` — Write full entry-point (~35KB). Sections: system identity (REAPER `.rpp`), ASCII architecture diagram, all 11 dataclasses from `src/core/schema.py`, pipeline (compose → build → calibrate → validate → ReaScript), module map (`src/` dirs → roles), plugin system (`PLUGIN_REGISTRY`, `ALIAS_MAP`, presets), ReaScript protocol, CLI reference (compose.py, generate.py, run_in_reaper.py), naming conventions, extension guide. Valid markdown, under 40KB.
|
||||
|
||||
## Phase 2: Module Deep-Dives
|
||||
|
||||
- [x] 2.1 `docs/modules/composer.md` — Document `src/composer/`: pattern generators (`patterns.py`, `rhythm.py`), chord engine (`chords.py`), melody engine (`melody_engine.py`), converters, templates, variation. Public API signatures, data flow, dependencies. Link to `LLM_CONTEXT.md`.
|
||||
|
||||
- [x] 2.2 `docs/modules/reaper-builder.md` — Document `RPPBuilder` class, `PLUGIN_REGISTRY` (~150 entries: key→display_name,filename,uid_guid), `ALIAS_MAP`, `PLUGIN_PRESETS`, preset transformer, `render.py` headless render. `write()` signature. Dependency on `src/core/schema.py`. Link to `LLM_CONTEXT.md`.
|
||||
|
||||
- [x] 2.3 `docs/modules/reaper-scripting.md` — Document `ReaScriptCommand`, `ReaScriptResult` (from `src/reaper_scripting/commands.py`), `ReaScriptGenerator`, command/result JSON contract, `ProtocolVersionError`. Link to `LLM_CONTEXT.md`.
|
||||
|
||||
- [x] 2.4 `docs/modules/calibrator.md` — Document `Calibrator.apply()` post-processing pipeline, mix calibration presets (`src/calibrator/presets.py`). Link to `LLM_CONTEXT.md`.
|
||||
|
||||
## Phase 3: Schemas and CLI Reference
|
||||
|
||||
- [x] 3.1 `docs/schemas/song-definition.json` — JSON Schema draft-07 for `SongDefinition` + all nested dataclasses (`SongMeta`, `TrackDef`, `ClipDef`, `MidiNote`, `SectionDef`, `PluginDef`, `PatternDef`, `ArrangementItemDef`, `CCEvent`, `ArrangementTrack`). Match field names and types exactly from `src/core/schema.py`.
|
||||
|
||||
- [x] 3.2 `docs/schemas/reascript-protocol.json` — JSON Schema draft-07 for `ReaScriptCommand` and `ReaScriptResult`. Match field names and types from `src/reaper_scripting/commands.py`.
|
||||
|
||||
- [x] 3.3 `docs/CLI.md` — Complete CLI reference: `compose.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --no-calibrate), `generate.py` (--bpm, --key, --output, --seed, --emotion, --inversion, --validate), `run_in_reaper.py` (<rpp_path>, --output, --timeout, --plugins-config, --action). Link to `LLM_CONTEXT.md`.
|
||||
|
||||
## Phase 4: README Correction and Verification
|
||||
|
||||
- [x] 4.1 `README.md` — Replace FL Studio/`.flp` with REAPER/`.rpp`. Remove all MCP server references. List real CLI scripts. Link to `docs/LLM_CONTEXT.md`.
|
||||
|
||||
- [x] 4.2 Verify all internal links — Extract every `[text](path)` and `[text](#heading)` from all doc files; confirm each target file/heading exists.
|
||||
|
||||
- [x] 4.3 Validate JSON schemas — Validate `song-definition.json` and `reascript-protocol.json` against JSON Schema draft-07 meta-schema. Validate sample `SongDefinition.to_json()` output against `song-definition.json`.
|
||||
|
||||
- [x] 4.4 Verify field names match code — `grep` each dataclass field name found in docs against `src/core/schema.py`. `grep -i "fl studio|\.flp|mcp" README.md` must return empty.
|
||||
86
README.md
86
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.
|
||||
|
||||
209
docs/CLI.md
Normal file
209
docs/CLI.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# CLI Reference
|
||||
|
||||
> Parent doc: [LLM_CONTEXT.md](LLM_CONTEXT.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Three CLI entry points, all in `scripts/`:
|
||||
|
||||
| Script | Role | Default output |
|
||||
|--------|------|---------------|
|
||||
| `compose.py` | Full composition pipeline | `.rpp` file |
|
||||
| `generate.py` | Thin wrapper around compose | `.rpp` file + optional validation |
|
||||
| `run_in_reaper.py` | ReaScript generation & execution | ReaScript `.py`, result JSON, LUFS JSON |
|
||||
|
||||
## compose.py
|
||||
|
||||
Full pipeline: builds all tracks, applies calibration, generates `.rpp`.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python scripts/compose.py [--bpm BPM] [--key KEY] [--output PATH] [--seed SEED]
|
||||
[--emotion EMOTION] [--inversion INVERSION]
|
||||
[--no-calibrate]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--bpm` | `float` | `99` | Tempo. Must be > 0. |
|
||||
| `--key` | `str` | `"Am"` | Musical key. Format: `[A-G][b#]?m?` (e.g. `"Am"`, `"Dm"`, `"G"`, `"F#m"`) |
|
||||
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` file path. Parent directory created if missing. |
|
||||
| `--seed` | `int` | `None` | Random seed. `None` = non-deterministic (system time). |
|
||||
| `--emotion` | `str` | `"romantic"` | Chord emotion: `romantic`, `dark`, `club`, `classic` |
|
||||
| `--inversion` | `str` | `"root"` | Preferred chord inversion: `root`, `first`, `second` |
|
||||
| `--no-calibrate` | `flag` | `False` | Skip post-processing mix calibration |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Default reggaetón at 99 BPM in Am
|
||||
python scripts/compose.py
|
||||
|
||||
# Specific BPM and key
|
||||
python scripts/compose.py --bpm 95 --key Dm
|
||||
|
||||
# Deterministic output with seed
|
||||
python scripts/compose.py --bpm 95 --key Am --seed 42
|
||||
|
||||
# Dark emotion with first inversion chords
|
||||
python scripts/compose.py --emotion dark --inversion first
|
||||
|
||||
# Skip calibration (raw volumes from VOLUME_LEVELS in compose.py)
|
||||
python scripts/compose.py --no-calibrate
|
||||
|
||||
# Custom output path
|
||||
python scripts/compose.py --output my_project/song_v2.rpp
|
||||
```
|
||||
|
||||
### Output Structure
|
||||
|
||||
```
|
||||
output/song.rpp # REAPER project file
|
||||
```
|
||||
|
||||
Console output includes:
|
||||
- BPM and key confirmation
|
||||
- Section count and total duration (bars + beats)
|
||||
- Kick detection cache summary (per drumloop file)
|
||||
- Final output path
|
||||
|
||||
## generate.py
|
||||
|
||||
Thin wrapper around `compose.py`. Delegates by setting `sys.argv` and calling `compose.main()`.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python scripts/generate.py [--bpm BPM] [--key KEY] [--output PATH] [--seed SEED]
|
||||
[--emotion EMOTION] [--inversion INVERSION]
|
||||
[--validate]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--bpm` | `float` | `95` | Tempo. Must be > 0. |
|
||||
| `--key` | `str` | `"Am"` | Musical key |
|
||||
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` path |
|
||||
| `--seed` | `int` | `42` | Random seed |
|
||||
| `--emotion` | `str` | `"romantic"` | Chord emotion |
|
||||
| `--inversion` | `str` | `"root"` | Chord inversion |
|
||||
| `--validate` | `flag` | `False` | Run validator after generation |
|
||||
|
||||
### Differences from compose.py
|
||||
|
||||
- Default BPM is 95 (vs 99 in compose.py)
|
||||
- Default seed is 42 (vs None in compose.py)
|
||||
- Has `--validate` flag (runs `validate_rpp_output()` after generation)
|
||||
- No `--no-calibrate` flag (calibration always applied)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Generate with default settings
|
||||
python scripts/generate.py
|
||||
|
||||
# Generate with validation
|
||||
python scripts/generate.py --bpm 95 --key Dm --seed 123 --validate
|
||||
|
||||
# Full custom generation
|
||||
python scripts/generate.py --bpm 100 --key Fm --output output/custom.rpp --seed 7 --emotion club
|
||||
```
|
||||
|
||||
### Validation Errors
|
||||
|
||||
When `--validate` is set, errors are printed to stderr:
|
||||
```
|
||||
Validation errors:
|
||||
- Expected 416 beats, found 400 beats
|
||||
- Missing drumloop in section: chorus
|
||||
```
|
||||
|
||||
Exit code 1 on validation failure.
|
||||
|
||||
## run_in_reaper.py
|
||||
|
||||
Generates a ReaScript for post-processing and polls for results.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python scripts/run_in_reaper.py <rpp_path> [--output WAV_PATH] [--timeout SECONDS]
|
||||
[--plugins-config JSON_PATH] [--action ACTIONS]
|
||||
```
|
||||
|
||||
### Arguments and Flags
|
||||
|
||||
| Arg/Flag | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `rpp_path` | positional | *(required)* | Path to `.rpp` file |
|
||||
| `--output`, `-o` | `str` | `<rpp_stem>_rendered.wav` | Rendered WAV output path |
|
||||
| `--timeout` | `int` | `120` | Seconds to poll for result JSON |
|
||||
| `--plugins-config` | `str` | `None` | Path to JSON-serialized `SongDefinition` (for deriving `plugins_to_add`) |
|
||||
| `--action` | `str` | `"calibrate"` | Space-separated action pipeline |
|
||||
|
||||
### Actions
|
||||
|
||||
Space-separated list of up to 5 actions, executed in order:
|
||||
|
||||
```
|
||||
add_plugins configure_fx_params verify_fx calibrate render
|
||||
```
|
||||
|
||||
Each action name maps to a function in the generated ReaScript. Unknown action names are ignored.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Basic calibration on an existing .rpp
|
||||
python scripts/run_in_reaper.py output/song.rpp
|
||||
|
||||
# Full pipeline: load plugins, calibrate, render
|
||||
python scripts/run_in_reaper.py output/song.rpp --action "add_plugins calibrate render"
|
||||
|
||||
# With plugin config from JSON SongDefinition
|
||||
python scripts/run_in_reaper.py output/song.rpp --plugins-config output/song.json --action "add_plugins configure_fx_params"
|
||||
|
||||
# Custom render output and extended timeout
|
||||
python scripts/run_in_reaper.py output/song.rpp -o output/final_mix.wav --timeout 300 --action "calibrate render"
|
||||
```
|
||||
|
||||
### Output Files
|
||||
|
||||
```
|
||||
scripts/fl_control_phase2.py # Generated ReaScript
|
||||
scripts/fl_control_command.json # Command JSON for ReaScript
|
||||
scripts/fl_control_result.json # Result JSON from ReaScript (polled)
|
||||
output/<song>_lufs.json # LUFS metrics
|
||||
output/<song>_fx_errors.json # FX errors (if any)
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Error: .rpp not found, plugin config not found, result read error, or REAPER error |
|
||||
| 2 | Timeout: result JSON not found within timeout |
|
||||
|
||||
## Key Validation
|
||||
|
||||
All scripts validate the key format: `^[A-G][b#]?m?$`
|
||||
|
||||
Valid examples: `"Am"`, `"C"`, `"D#m"`, `"Gb"`, `"F#"`.
|
||||
|
||||
Invalid: `"A minor"`, `"c"` (lowercase), `"H"`, `"Am7"`.
|
||||
|
||||
## BPM Validation
|
||||
|
||||
BPM must be > 0. Min/max enforced by `SongDefinition.validate()`: 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.
|
||||
544
docs/LLM_CONTEXT.md
Normal file
544
docs/LLM_CONTEXT.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# fl_control — LLM System Overview
|
||||
|
||||
> **About the name**: The directory is named `fl_control` for historical reasons, but the system generates **REAPER `.rpp` files**, not FL Studio `.flp` files. There is NO FL Studio support. The name has been retained for backward compatibility with repository URLs and CI/CD pipelines.
|
||||
|
||||
## What Is This?
|
||||
|
||||
`fl_control` is a Python system that generates complete **REAPER `.rpp` projects** from the command line. Given a key, BPM, and emotion, it composes a full reggaetón instrumental with drums, 808 bass, chord progressions (with voice leading), lead melodies (hook-based call-and-response), pads, percussion, and transition FX. The output is a valid `.rpp` file that REAPER opens directly, plus an auto-generated ReaScript (`.py`) for post-processing (plugin loading, mix calibration, rendering).
|
||||
|
||||
**Target**: REAPER (Windows, v7.x). **Format**: `.rpp` text files. **No DAW dependencies** beyond reading sample files from disk.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Generate a song (defaults: 95 BPM, Am key, romantic emotion)
|
||||
python scripts/generate.py --bpm 95 --key Am --output output/song.rpp --seed 42
|
||||
|
||||
# Generate with validation
|
||||
python scripts/generate.py --bpm 95 --key Dm --output output/song.rpp --seed 42 --validate
|
||||
|
||||
# Compose directly (more detailed output)
|
||||
python scripts/compose.py --bpm 99 --key Am --emotion romantic --output output/song.rpp
|
||||
```
|
||||
|
||||
Output: a ready-to-open `.rpp` file at the specified path.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────┐
|
||||
│ CLI Layer │
|
||||
│ scripts/generate.py (thin wrapper) │
|
||||
│ scripts/compose.py (full pipeline) │
|
||||
│ scripts/run_in_reaper.py (ReaScript) │
|
||||
└──────────────┬────────────────────────┘
|
||||
│
|
||||
┌──────────────────────┼──────────────────────┐
|
||||
│ ▼ │
|
||||
┌────────────────┴──────┐ ┌────────────────────┴──┐ ┌───────┴──────────┐
|
||||
│ src/composer/ │ │ src/calibrator/ │ │ src/selector/ │
|
||||
│ ┌──────────────────┐ │ │ ┌────────────────┐ │ │ SampleSelector │
|
||||
│ │ chords.py │ │ │ │ Calibrator │ │ └────────────────┘
|
||||
│ │ ChordEngine │ │ │ │ .apply() │ │
|
||||
│ │ melody_engine.py │ │ │ │ presets.py │ └───────────────────┘
|
||||
│ │ build_motif() │ │ │ │ VOLUME_PRESETS │
|
||||
│ │ patterns.py │ │ │ │ PAN_PRESETS │ ┌──────────────────┐
|
||||
│ │ rhythm.py │ │ │ │ SEND_PRESETS │ │ src/core/ │
|
||||
│ │ templates.py │ │ │ └────────────────┘ │ schema.py │
|
||||
│ └──────────────────┘ │ └─────────────────────────────┘ │ SongDefinition │
|
||||
└───────────┬────────────┘ │ TrackDef │
|
||||
│ │ ClipDef │
|
||||
▼ │ MidiNote │
|
||||
┌──────────────────────────┐ │ PluginDef │
|
||||
│ SongDefinition │◄──────────────────────────────│ SectionDef │
|
||||
│ ┌────────────────────┐ │ │ PatternDef │
|
||||
│ │ meta: SongMeta │ │ │ Arrangement... │
|
||||
│ │ tracks: [TrackDef] │ │ │ CCEvent │
|
||||
│ │ patterns: [...] │ │ └──────────────────┘
|
||||
│ │ sections: [...] │ │
|
||||
│ │ master_plugins │ │
|
||||
│ └────────────────────┘ │
|
||||
└───────────┬──────────────┘
|
||||
│
|
||||
┌─────────┼─────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌───────────────┐ ┌─────────────────┐
|
||||
│ RPPBuilder │ │ ReaScriptGen │ │ Validator │
|
||||
│ .write() → │ │ generate() → │ │ validate_rpp() │
|
||||
│ song.rpp │ │ fl_control_ │ │ │
|
||||
│ │ │ phase2.py │ │ │
|
||||
└──────────────┘ └───────┬───────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ REAPER │
|
||||
│ opens .rpp │
|
||||
│ runs script │
|
||||
│ renders WAV │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**Pipeline**: CLI → compose `SongDefinition` → Calibrator.apply() (post-processing) → RPPBuilder.write() → `.rpp` file → ReaScriptGenerator.generate() → ReaScript → REAPER execution.
|
||||
|
||||
## Data Model
|
||||
|
||||
All types live in `src/core/schema.py`. Every entity is a Python dataclass.
|
||||
|
||||
### SongMeta
|
||||
|
||||
Song-level metadata.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `bpm` | `float` | *(required)* | Tempo 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 `<GUID>`, VST3 uses `{GUID}`
|
||||
|
||||
### ALIAS_MAP
|
||||
|
||||
Backward compatibility mapping: old key names → new `PLUGIN_REGISTRY` keys. Example:
|
||||
|
||||
```python
|
||||
ALIAS_MAP = {
|
||||
"Serum2": "Serum_2",
|
||||
"FabFilter Pro-Q 3": "Pro-Q_3",
|
||||
"Valhalla Delay": "ValhallaDelay",
|
||||
"Pro-Q 3": "Pro-Q_3",
|
||||
}
|
||||
```
|
||||
|
||||
FabFilter plugins map space-separated names to short VST3 keys. SoundToys plugins keep their underscore names.
|
||||
|
||||
### PLUGIN_PRESETS
|
||||
|
||||
Role-aware preset data. Format:
|
||||
|
||||
```python
|
||||
PLUGIN_PRESETS: dict[tuple[str, str], list[str]] = {
|
||||
("Pro-Q_3", ""): [chunk1, chunk2, ...], # default
|
||||
("Serum_2", "bass"): [chunk1, chunk2, ...], # role-specific
|
||||
}
|
||||
```
|
||||
|
||||
Lookup chain: `(key, role)` → `(key, "")` (default) → `fallback` → `None`.
|
||||
|
||||
### REAPER_BUILTINS
|
||||
|
||||
Set of Cockos native plugin keys (ReaEQ, ReaComp, ReaVerb, etc.). Plugins matching this are deferred to ReaScript insertion (`TrackFX_AddByName`) rather than written to `.rpp` as VST elements.
|
||||
|
||||
### Plugin Lookup in RPPBuilder
|
||||
|
||||
```
|
||||
PluginDef.name → ALIAS_MAP.get(name, name) → PLUGIN_REGISTRY.get(resolved)
|
||||
│
|
||||
┌───────────────┤
|
||||
found│ │not found
|
||||
▼ ▼
|
||||
Build VST element Fallback: .dll
|
||||
with display_name, with 19 param
|
||||
filename, uid_guid slots
|
||||
```
|
||||
|
||||
## ReaScript Protocol
|
||||
|
||||
### Overview
|
||||
|
||||
ReaScript is a self-contained Python file generated by `ReaScriptGenerator` that REAPER executes internally. It communicates via JSON files on disk:
|
||||
|
||||
```
|
||||
fl_control_command.json ──read──► ReaScript ──write──► fl_control_result.json
|
||||
```
|
||||
|
||||
### ReaScriptCommand
|
||||
|
||||
Defined in `src/reaper_scripting/commands.py`:
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `version` | `int` | `1` | Protocol version |
|
||||
| `action` | `str \| list[str]` | `"calibrate"` | Single action or pipeline list |
|
||||
| `rpp_path` | `str` | `""` | Path to `.rpp` file to open |
|
||||
| `render_path` | `str` | `""` | Path for rendered WAV output |
|
||||
| `timeout` | `int` | `120` | Polling timeout in seconds |
|
||||
| `track_calibration` | `list[dict]` | `[]` | Volume/pan/send calibration per track |
|
||||
| `plugins_to_add` | `list[dict]` | `[]` | `{"track_name": str, "fx_name": str, "params": {...}}` |
|
||||
|
||||
### ReaScriptResult
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `version` | `int` | `1` | Protocol version |
|
||||
| `status` | `str` | `"ok"` | `"ok"`, `"error"`, or `"timeout"` |
|
||||
| `message` | `str` | `""` | Error or status message |
|
||||
| `lufs` | `float \| None` | `None` | Integrated LUFS |
|
||||
| `integrated_lufs` | `float \| None` | `None` | Integrated LUFS reading |
|
||||
| `short_term_lufs` | `float \| None` | `None` | Short-term LUFS |
|
||||
| `fx_errors` | `list[dict]` | `[]` | FX verification errors |
|
||||
| `tracks_verified` | `int` | `0` | Number of tracks verified |
|
||||
| `added_plugins` | `list[dict]` | `[]` | `{"fx_name", "instance_id", "track_name", "status"}` |
|
||||
|
||||
### ProtocolVersionError
|
||||
|
||||
Raised by `read_result()` when result version doesn't match expected version.
|
||||
|
||||
### Key Functions
|
||||
|
||||
- `write_command(path, cmd)` — Serialize `ReaScriptCommand` to JSON file
|
||||
- `read_result(path, expected_version=1)` — Deserialize `ReaScriptResult` from JSON file. Raises `ProtocolVersionError` on version mismatch.
|
||||
|
||||
### Known Actions
|
||||
|
||||
`add_plugins`, `configure_fx_params`, `verify_fx`, `calibrate`, `render`
|
||||
|
||||
### ReaScript JSON Parser
|
||||
|
||||
Since REAPER's internal Python has no `json` module, the ReaScript includes a hand-rolled JSON parser (~100 lines) that handles strings, integers, floats, booleans, nulls, nested objects, and arrays via string splitting.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
### scripts/compose.py
|
||||
|
||||
Main composition pipeline. Builds a full reggaetón track from scratch.
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--bpm` | `float` | `99` | Tempo |
|
||||
| `--key` | `str` | `"Am"` | Musical key |
|
||||
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` path |
|
||||
| `--seed` | `int` | `None` | Random seed for determinism |
|
||||
| `--emotion` | `str` | `"romantic"` | `romantic` / `dark` / `club` / `classic` |
|
||||
| `--inversion` | `str` | `"root"` | `root` / `first` / `second` |
|
||||
| `--no-calibrate` | `flag` | `False` | Skip post-processing mix calibration |
|
||||
|
||||
### scripts/generate.py
|
||||
|
||||
Thin wrapper around `compose.py`. Delegates via `sys.argv` manipulation.
|
||||
|
||||
| Flag | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `--bpm` | `float` | `95` | Tempo |
|
||||
| `--key` | `str` | `"Am"` | Musical key |
|
||||
| `--output` | `str` | `"output/song.rpp"` | Output `.rpp` path |
|
||||
| `--seed` | `int` | `42` | Random seed |
|
||||
| `--emotion` | `str` | `"romantic"` | Chord emotion |
|
||||
| `--inversion` | `str` | `"root"` | Chord inversion |
|
||||
| `--validate` | `flag` | `False` | Run `validate_rpp_output()` after generation |
|
||||
|
||||
### scripts/run_in_reaper.py
|
||||
|
||||
ReaScript execution CLI. Generates and runs a Phase 2 ReaScript.
|
||||
|
||||
| Arg/Flag | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `rpp_path` | positional | *(required)* | Path to `.rpp` file |
|
||||
| `--output`, `-o` | `str` | auto-derived | Rendered WAV output path |
|
||||
| `--timeout` | `int` | `120` | Seconds to poll for result JSON |
|
||||
| `--plugins-config` | `str` | `None` | JSON SongDefinition for deriving plugins_to_add |
|
||||
| `--action` | `str` | `"calibrate"` | Space-separated action pipeline |
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
python scripts/run_in_reaper.py output/song.rpp --action "add_plugins calibrate render" --timeout 300
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
From `AGENTS.md`:
|
||||
|
||||
- **Python**: Type hints on all function signatures
|
||||
- **Dataclasses**: Over dicts for structured data
|
||||
- **Deterministic output**: Seed-based RNG (`random.Random(seed)`), no global random state
|
||||
- **No bare except clauses**
|
||||
- **Testing**: `pytest` only. Unit tests for all new functions. Integration tests for end-to-end flows.
|
||||
- **Architecture**: Separate modules by concern (calibrator, composer, builder, selector, validator)
|
||||
- **Post-processing**: Calibrator.apply() pattern — modify in-place after construction, not inline
|
||||
- **Schema changes**: Must be backward-compatible (new fields get defaults)
|
||||
- **SDD**: All changes follow spec-driven development pipeline (propose → spec → design → tasks → apply → verify)
|
||||
|
||||
## How to Extend
|
||||
|
||||
### Adding a New Track Role
|
||||
|
||||
1. Add role to `TRACK_ACTIVITY` in `scripts/compose.py` — define which sections it plays in
|
||||
2. Add volume/presets to `src/calibrator/presets.py` (`VOLUME_PRESETS`, `PAN_PRESETS`, `SEND_PRESETS`)
|
||||
3. Add FX chain to `FX_CHAINS` in `scripts/compose.py`
|
||||
4. Write a `build_<role>_track()` function
|
||||
5. Add the track to the `tracks` list in `main()`
|
||||
6. Add calibration role mapping in `Calibrator._resolve_role()`
|
||||
|
||||
### Adding a New Plugin to the Registry
|
||||
|
||||
1. Scan the plugin in REAPER or extract from a ground-truth `.rpp`
|
||||
2. Add entry to `PLUGIN_REGISTRY`: `"key": ("display_name", "filename", "uid_guid")`
|
||||
3. If needed, add preset data to `_PRESETS_FLAT` with the key
|
||||
4. Add aliases to `ALIAS_MAP` if there are alternate names
|
||||
5. Use `make_plugin(registry_key, index, role)` to create `PluginDef` instances
|
||||
|
||||
### Adding a New CLI Flag
|
||||
|
||||
1. Add `parser.add_argument()` in `scripts/compose.py` (and `scripts/generate.py` if wrapping)
|
||||
2. Thread the flag value through the composition pipeline
|
||||
3. Pass to relevant builder/track functions
|
||||
|
||||
### Adding a New Emotion to ChordEngine
|
||||
|
||||
1. Add entry to `EMOTION_PROGRESSIONS` in `src/composer/chords.py`
|
||||
2. Format: `"name": [(semitone_offset, quality), ...]` for a 4-chord loop
|
||||
3. Add choice to `--emotion` argparse choices
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **Hardcoded paths**: Drumloop samples reference `C:\ProgramData\Ableton\...` paths. These must exist on the build machine or the pipeline will skip those clips.
|
||||
- **Environment-specific registry**: `PLUGIN_REGISTRY` was generated from a specific REAPER/plugin installation. Different machines may have different plugin paths or GUIDs.
|
||||
- **Windows only**: REAPER executable path in `render.py` defaults to `C:\Program Files\REAPER (x64)\reaper.exe`. The ReaScript REAPER API calls are Windows-specific.
|
||||
- **No CI**: No automated test/validation pipeline. Tests run manually with `pytest`.
|
||||
- **Single genre**: Only reggaetón is fully implemented. Other genres exist in `knowledge/` as JSON definitions but the main pipeline (`compose.py`) is hardcoded for reggaetón.
|
||||
- **Sample library**: Requires pre-existing sample files in expected directories. No bundled samples.
|
||||
- **ReaScript execution**: `run_in_reaper.py` generates scripts but does NOT automatically launch REAPER. REAPER must be running and monitoring the scripts directory, or the script must be executed manually from within REAPER's action list.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [CLI Reference](CLI.md) — Complete CLI documentation
|
||||
- [Module: Composer](modules/composer.md) — Composition engine deep-dive
|
||||
- [Module: Reaper Builder](modules/reaper-builder.md) — RPP format and plugin registry
|
||||
- [Module: Reaper Scripting](modules/reaper-scripting.md) — ReaScript protocol
|
||||
- [Module: Calibrator](modules/calibrator.md) — Mix calibration presets
|
||||
- [JSON Schema: Song Definition](schemas/song-definition.json) — Data model schema
|
||||
- [JSON Schema: ReaScript Protocol](schemas/reascript-protocol.json) — Command/result schema
|
||||
174
docs/modules/calibrator.md
Normal file
174
docs/modules/calibrator.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Calibrator Module
|
||||
|
||||
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
Post-processing mix calibration. Applies role-based volume, EQ, pan, sends, and master chain configuration to a `SongDefinition` after track construction and before `.rpp` generation.
|
||||
|
||||
**Location**: `src/calibrator/`
|
||||
|
||||
## Public API
|
||||
|
||||
### Calibrator (`__init__.py`)
|
||||
|
||||
```python
|
||||
class Calibrator:
|
||||
@staticmethod
|
||||
def apply(song: SongDefinition) -> SongDefinition
|
||||
```
|
||||
|
||||
- **`apply()`**: Applies role-based volume, pan, sends, and master chain calibration. Mutates `song` in-place and returns it. Skips tracks named `"Reverb"` or `"Delay"` (return tracks).
|
||||
- **Pipeline**: `_calibrate_volumes()` → `_calibrate_pans()` → `_calibrate_sends()` → `_swap_master_chain()`
|
||||
|
||||
### Role Resolution (`__init__.py`)
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def _resolve_role(track_name: str) -> str | None
|
||||
```
|
||||
|
||||
Maps track name to role key using substring matching:
|
||||
|
||||
| Track Name Pattern | Role | Description |
|
||||
|-------------------|------|-------------|
|
||||
| `"Drumloop"`, `"drum*"` | `"drumloop"` | Drum loop |
|
||||
| `"808 Bass"`, `"*bass*"` | `"bass"` | Bass |
|
||||
| `"Chords"`, `"*chord*"` | `"chords"` | Chords/harmony |
|
||||
| `"Lead"`, `"*lead*"`, `"*synth*"`, `"*melody*"` | `"lead"` | Lead melody |
|
||||
| `"Clap"`, `"*snare*"`, `"*clap*"` | `"clap"` | Clap/snare |
|
||||
| `"Pad"`, `"*pad*"`, `"*atmos*"`, `"*ambient*"` | `"pad"` | Pad/atmosphere |
|
||||
| `"Perc"`, `"*perc*"` | `"perc"` | Percussion |
|
||||
| `"Reverb"`, `"Delay"` | `None` | Return tracks (skipped) |
|
||||
| Any other name | `None` | Unknown role (skipped) |
|
||||
|
||||
### Presets (`presets.py`)
|
||||
|
||||
```python
|
||||
VOLUME_PRESETS: dict[str, float]
|
||||
PAN_PRESETS: dict[str, float]
|
||||
EQ_PRESETS: dict[str, dict[int, float]]
|
||||
SEND_PRESETS: dict[str, tuple[float, float]]
|
||||
```
|
||||
|
||||
#### Volume Presets (0.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.
|
||||
177
docs/modules/composer.md
Normal file
177
docs/modules/composer.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Composer Module
|
||||
|
||||
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
The composer module generates musical content for reggaetón tracks: chord progressions, melodies, rhythm patterns, and drum analysis. All generators are deterministic given a seed.
|
||||
|
||||
**Location**: `src/composer/`
|
||||
|
||||
## Public API
|
||||
|
||||
### ChordEngine (`chords.py`)
|
||||
|
||||
```python
|
||||
class ChordEngine:
|
||||
def __init__(self, key: str, seed: int = 42) -> None
|
||||
|
||||
def progression(
|
||||
self,
|
||||
bars: int,
|
||||
emotion: str = "classic",
|
||||
beats_per_chord: int = 4,
|
||||
inversion: str = "root",
|
||||
) -> list[list[int]]
|
||||
```
|
||||
|
||||
- **Input**: Key string (`"Am"`, `"Dm"`), number of bars, emotion name, beats per chord, inversion preference
|
||||
- **Output**: List of voicings — each voicing is `list[int]` of MIDI notes
|
||||
- **Emotions**: `"romantic"`, `"dark"`, `"club"`, `"classic"` (unknown falls back to classic)
|
||||
- **Deterministic**: Same `(key, seed)` → identical output. RNG re-seeded per call.
|
||||
- **Voice leading**: Greedy min-score path through root, first, and second inversion candidates. Uses `random.Random(seed)` as micro-tiebreaker.
|
||||
|
||||
**Emotion progressions** (all 4-chord loops, semitone offsets from tonic):
|
||||
|
||||
| Emotion | Offsets | Labels |
|
||||
|---------|---------|--------|
|
||||
| romantic | (0,m7), (8,7), (3,7), (10,7) | 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.
|
||||
230
docs/modules/reaper-builder.md
Normal file
230
docs/modules/reaper-builder.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Reaper Builder Module
|
||||
|
||||
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
Generates valid REAPER `.rpp` project files from a `SongDefinition`. Maintains the plugin registry (~97 VST2/VST3 plugins with GUIDs), builds the `.rpp` element tree, and supports headless rendering via REAPER CLI.
|
||||
|
||||
**Location**: `src/reaper_builder/`
|
||||
|
||||
## Public API
|
||||
|
||||
### RPPBuilder (`__init__.py`)
|
||||
|
||||
```python
|
||||
class RPPBuilder:
|
||||
def __init__(self, song: SongDefinition, seed: int | None = None) -> None
|
||||
|
||||
def write(self, path: str | Path) -> None
|
||||
```
|
||||
|
||||
- **`__init__()`**: Receives a `SongDefinition` and optional seed. Seeds `random` for deterministic GUID generation when seed is not None.
|
||||
- **`write()`**: Serializes the project to a `.rpp` text file. Raises `OSError` if file cannot be written. Uses the `rpp` library (`Element`, `dumps`) for XML-like structure.
|
||||
- **Internal**: `_build_element()` → `_build_track(track)` → `_build_plugin(plugin)` → `_build_clip(clip)` → `_build_midi_source(clip)`.
|
||||
|
||||
### Plugin Registry (`__init__.py`)
|
||||
|
||||
```python
|
||||
PLUGIN_REGISTRY: dict[str, tuple[str, str, str]]
|
||||
```
|
||||
|
||||
Format: `"key": ("display_name", "filename", "uid_guid")`
|
||||
|
||||
~97 entries covering:
|
||||
- **FabFilter**: Pro-Q 3, Pro-C 2, Pro-R 2, Pro-L 2, Saturn 2, Timeless 3, Pro-DS, Pro-G, Pro-MB, Micro, One, Simplon, Twin 3, Volcano 3 (both VST2 and VST3 versions)
|
||||
- **SoundToys**: Decapitator, EchoBoy, Crystallizer, Devil-Loc, EffectRack, FilterFreak, MicroShift, PanMan, PhaseMistress, PrimalTap, Radiator, Tremolator, Little AlterBoy, Little MicroShift, Little PrimalTap, Little Radiator
|
||||
- **iZotope Ozone 12**: Equalizer, Dynamics, Maximizer, plus 17 additional Ozone modules
|
||||
- **Spectrasonics**: Omnisphere, FX-Omnisphere
|
||||
- **Xfer**: Serum 2, Serum 2 FX
|
||||
- **u-he**: Diva
|
||||
- **Arturia**: Pigments
|
||||
- **Cableguys**: ShaperBox 3
|
||||
- **Native Instruments**: Kontakt 7, VC 160, VC 2A, VC 76
|
||||
- **Cockos (REAPER native)**: ReaEQ, ReaComp, ReaDelay, ReaVerb, ReaVerbate, ReaFIR, ReaGate, ReaLimit, ReaPitch, ReaXcomp, ReaTune, ReaSynth, ReaSamplOmatic5000, and more (20+ plugins)
|
||||
- **Other**: ValhallaDelay, Gullfoss (Standard/Master/Live), The Glue, Trackspacer 2.5, ravity, Tone2 Electra
|
||||
|
||||
```python
|
||||
ALIAS_MAP: dict[str, str]
|
||||
```
|
||||
|
||||
Maps alternate names to registry keys:
|
||||
- `"Serum2"` → `"Serum_2"`
|
||||
- `"FabFilter Pro-Q 3"` → `"Pro-Q_3"` (space-separated → underscore VST3 key)
|
||||
- `"Pro-Q 3"` → `"Pro-Q_3"` (short name → VST3 key)
|
||||
- `"Valhalla Delay"` → `"ValhallaDelay"`
|
||||
- VST2 SoundToys old names → new underscore names
|
||||
|
||||
```python
|
||||
REAPER_BUILTINS: frozenset[str]
|
||||
```
|
||||
|
||||
Set of Cockos plugin keys. Plugins in this set are deferred to ReaScript insertion.
|
||||
|
||||
```python
|
||||
def get_builtin_plugins(song: SongDefinition) -> list[dict[str, object]]
|
||||
```
|
||||
|
||||
Extracts plugins where `PluginDef.builtin == True` or name is in `REAPER_BUILTINS`. Returns list of `{"track_name", "fx_name", "params"}` dicts for `ReaScriptCommand.plugins_to_add`.
|
||||
|
||||
### Preset Transformer (`preset_transformer.py`)
|
||||
|
||||
```python
|
||||
# Transforms flat _PRESETS_FLAT dict → role-aware PLUGIN_PRESETS
|
||||
# Format: {(plugin_key, role): [chunk1, chunk2, ...]}
|
||||
PLUGIN_PRESETS: dict[tuple[str, str], list[str]]
|
||||
```
|
||||
|
||||
Lookup chain: `(key, role)` → `(key, "")` (default) → `fallback` → `None`.
|
||||
|
||||
Plugins with preset data: Arcade, Omnisphere, Pro-Q_3, Pro-C_2, Pro-R_2, Pro-L_2, Saturn 2, Timeless 3, The Glue, ValhallaDelay, Serum 2, Diva, Kontakt 7, VC 160, VC 76, Gullfoss, Gullfoss Master, Decapitator, EchoBoy, EffectRack, FilterFreak1, MicroShift, Little AlterBoy, PhaseMistress, Tremolator.
|
||||
|
||||
Plugins with empty presets (no saved state): Most Ozone 12 modules, Pigments, ShaperBox 3, Gullfoss Live, Trackspacer 2.5, Crystallizer, FilterFreak2, Little MicroShift, Little PrimalTap, Little Radiator, PanMan, VC 2A, Elektra.
|
||||
|
||||
### Headless Render (`render.py`)
|
||||
|
||||
```python
|
||||
def render_project(
|
||||
rpp_path: str | Path,
|
||||
output_wav: str | Path,
|
||||
reaper_exe: str | Path | None = None,
|
||||
timeout_seconds: int = 120,
|
||||
) -> None
|
||||
```
|
||||
|
||||
Renders a `.rpp` to WAV via `reaper.exe -nosplash -render`. Default REAPER path: `C:\Program Files\REAPER (x64)\reaper.exe`. Raises `FileNotFoundError` if REAPER not found, `RuntimeError` on render failure.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
SongDefinition
|
||||
│
|
||||
▼
|
||||
RPPBuilder.__init__(song, seed)
|
||||
│
|
||||
▼
|
||||
RPPBuilder.write(path)
|
||||
│ _build_element()
|
||||
│ ├── Project header (_PROJECT_HEADER static lines)
|
||||
│ ├── TEMPO line (dynamic from song.meta)
|
||||
│ ├── Master TRACK with FXCHAIN
|
||||
│ │ ├── master_plugins resolved via ALIAS_MAP → PLUGIN_REGISTRY
|
||||
│ │ └── VST elements with preset data
|
||||
│ ├── Per-track TRACK elements
|
||||
│ │ ├── NAME, VOLPAN (from TrackDef.volume/.pan), TRACKID
|
||||
│ │ ├── AUXRECV (from send_level dict)
|
||||
│ │ ├── FXCHAIN with VST elements (non-builtin plugins)
|
||||
│ │ └── ITEM elements (clips)
|
||||
│ │ ├── SOURCE WAVE (audio clips: FILE path)
|
||||
│ │ └── SOURCE MIDI (MIDI clips: E-lines at 960 PPQ)
|
||||
│ └── TEXT output → .rpp file
|
||||
▼
|
||||
output/song.rpp
|
||||
```
|
||||
|
||||
## `.rpp` File Structure
|
||||
|
||||
The generated `.rpp` follows REAPER's ground-truth format extracted from `output/test_vst3.rpp`:
|
||||
|
||||
```rpp
|
||||
<REAPER_PROJECT 0.1 "7.65/win64" ...>
|
||||
<NOTES ...>
|
||||
...static project metadata...
|
||||
TEMPO 95 4 4 0
|
||||
<TRACK {master-guid}>
|
||||
NAME master
|
||||
...
|
||||
<FXCHAIN>
|
||||
<VST "VST3: Pro-Q 3 (FabFilter)" FabFilter {GUID} ...>
|
||||
<VST "VST3: Pro-C 2 (FabFilter)" FabFilter {GUID} ...>
|
||||
<VST "VST3: Pro-L 2 (FabFilter)" FabFilter {GUID} ...>
|
||||
>
|
||||
>
|
||||
<TRACK {track-guid}>
|
||||
NAME "808 Bass"
|
||||
VOLPAN 0.820000 0.000000 -1 -1 1
|
||||
<FXCHAIN>
|
||||
<VST "VST3i: Serum 2 (Xfer Records)" Serum2.vst3 {GUID} ...>
|
||||
>
|
||||
AUXRECV 8 0.050000 -1 -1 0
|
||||
AUXRECV 9 0.000000 -1 -1 0
|
||||
<ITEM>
|
||||
POSITION 0.0
|
||||
LENGTH 32.0
|
||||
NAME "Verse 808"
|
||||
<SOURCE MIDI>
|
||||
HASDATA 1 960 QN
|
||||
E 0 90 21 50
|
||||
E 960 80 21 00
|
||||
...
|
||||
>
|
||||
>
|
||||
>
|
||||
<TRACK {track-guid}>
|
||||
NAME "Reverb"
|
||||
...
|
||||
>
|
||||
<TRACK {track-guid}>
|
||||
NAME "Delay"
|
||||
...
|
||||
>
|
||||
>
|
||||
```
|
||||
|
||||
### VST Element Format
|
||||
|
||||
**VST3** (`.vst3` files with `{GUID}`):
|
||||
```rpp
|
||||
<VST "VST3: Pro-Q 3 (FabFilter)" FabFilter {72C4DB71...} 0 "" 1158812272...>
|
||||
base64chunk1
|
||||
base64chunk2
|
||||
...
|
||||
>
|
||||
```
|
||||
|
||||
**VST2** (`.dll` files with `<GUID>`):
|
||||
```rpp
|
||||
<VST "VST: Decapitator (SoundToys)" Decapitator.dll 0 "" <56535453744463...> 0 "" ...>
|
||||
base64chunk1
|
||||
base64chunk2
|
||||
...
|
||||
>
|
||||
```
|
||||
|
||||
### MIDI Source Format
|
||||
|
||||
MIDI data is written as E-lines at 960 PPQ with 16th-note quantization (120-tick grid):
|
||||
```rpp
|
||||
<SOURCE MIDI>
|
||||
HASDATA 1 960 QN
|
||||
E 0 90 21 50 ; delta=0, note-on, pitch=0x21 (33=A1), velocity=0x50 (80)
|
||||
E 1440 80 21 00 ; delta=1440, note-off, pitch=0x21
|
||||
E 120 B0 0B 32 ; delta=120, CC 11 (Expression), value=0x32 (50)
|
||||
...
|
||||
>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `src/core/schema.py` — `SongDefinition`, `TrackDef`, `ClipDef`, `PluginDef`
|
||||
- `rpp` library — `Element`, `dumps` for `.rpp` XML-like serialization
|
||||
- `uuid`, `random` (stdlib) — GUID generation
|
||||
- `subprocess` (for `render.py`) — REAPER CLI execution
|
||||
|
||||
## Known Gotchas
|
||||
|
||||
1. **VST3 display names MUST match exactly**: The display_name in the registry must match what `TrackFX_AddByName()` expects in REAPER. A typo in the display_name field causes silent plugin load failure.
|
||||
|
||||
2. **Plugin builtin flag skips .rpp writing**: When `PluginDef.builtin == True`, the plugin is NOT written to the `.rpp` file. It's deferred to ReaScript. If the ReaScript doesn't run, the plugin won't be loaded.
|
||||
|
||||
3. **GUID format matters**: VST2 uses `<GUID>` with angle brackets in the `.rpp`. VST3 uses `{GUID}` with curly braces. Using the wrong delimiter format causes REAPER to reject the plugin entry.
|
||||
|
||||
4. **REAPER version string is hardcoded**: `_build_element()` hardcodes `"7.65/win64"` as the REAPER version. Different REAPER versions may behave differently.
|
||||
|
||||
5. **VOLPAN format**: REAPER expects volume as a linear value 0.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.
|
||||
205
docs/modules/reaper-scripting.md
Normal file
205
docs/modules/reaper-scripting.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Reaper Scripting Module
|
||||
|
||||
> Parent doc: [LLM_CONTEXT.md](../LLM_CONTEXT.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
Generates self-contained Python ReaScript files that REAPER executes for post-processing: plugin loading, FX parameter configuration, mix calibration, verification, and rendering. Communicates via JSON files on disk.
|
||||
|
||||
**Location**: `src/reaper_scripting/`
|
||||
|
||||
## Public API
|
||||
|
||||
### ReaScriptGenerator (`__init__.py`)
|
||||
|
||||
```python
|
||||
class ReaScriptGenerator:
|
||||
def generate(self, path: Path, command: ReaScriptCommand) -> None
|
||||
```
|
||||
|
||||
- **`generate()`**: Writes a self-contained Python ReaScript to `path`. The script opens the `.rpp` file, executes the action pipeline, and writes results to `fl_control_result.json`.
|
||||
- **Internal**: `_build_script(command)` assembles the full script source, conditionally including only the action functions needed by the command.
|
||||
|
||||
### ReaScriptCommand (`commands.py`)
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ReaScriptCommand:
|
||||
version: int = 1
|
||||
action: str | list[str] = "calibrate"
|
||||
rpp_path: str = ""
|
||||
render_path: str = ""
|
||||
timeout: int = 120
|
||||
track_calibration: list[dict] = field(default_factory=list)
|
||||
plugins_to_add: list[dict] = field(default_factory=list)
|
||||
```
|
||||
|
||||
### ReaScriptResult (`commands.py`)
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ReaScriptResult:
|
||||
version: int = 1
|
||||
status: str = "ok"
|
||||
message: str = ""
|
||||
lufs: float | None = None
|
||||
integrated_lufs: float | None = None
|
||||
short_term_lufs: float | None = None
|
||||
fx_errors: list[dict] = field(default_factory=list)
|
||||
tracks_verified: int = 0
|
||||
added_plugins: list[dict] = field(default_factory=list)
|
||||
```
|
||||
|
||||
### ProtocolVersionError (`commands.py`)
|
||||
|
||||
```python
|
||||
class ProtocolVersionError(Exception):
|
||||
"""Raised when command/result version doesn't match expected."""
|
||||
```
|
||||
|
||||
### Serialization Functions (`commands.py`)
|
||||
|
||||
```python
|
||||
def write_command(path: Path, cmd: ReaScriptCommand) -> None
|
||||
def read_result(path: Path, expected_version: int = 1) -> ReaScriptResult
|
||||
```
|
||||
|
||||
- **`write_command()`**: Serializes `ReaScriptCommand` to JSON file. Omits `plugins_to_add` key if empty.
|
||||
- **`read_result()`**: Deserializes `ReaScriptResult` from JSON file. Raises `ProtocolVersionError` if version != expected_version.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
ReaScriptCommand
|
||||
│
|
||||
▼
|
||||
ReaScriptGenerator.generate(script_path, command)
|
||||
│ _build_script(command)
|
||||
│ ├── check_api() — verifies REAPER API function availability
|
||||
│ ├── parse_json() / write_json() — hand-rolled JSON (no stdlib json)
|
||||
│ ├── find_track() — lookup by name via RPR_GetSetMediaTrackInfo_String
|
||||
│ ├── add_plugins() — RPR_TrackFX_AddByName for built-in plugins
|
||||
│ ├── configure_fx_params() — RPR_TrackFX_SetParam for param config
|
||||
│ ├── calibrate() — RPR_SetMediaTrackInfo_Value (volume/pan) + sends
|
||||
│ ├── verify_fx() — enumerates all tracks/FX, reports missing
|
||||
│ ├── render() — RPR_Main_RenderFile for WAV export
|
||||
│ ├── measure_loudness() — RPR_CalcMediaSrcLoudness for LUFS
|
||||
│ └── main() — dispatch loop: read command → open project → actions → write result
|
||||
▼
|
||||
fl_control_phase2.py (self-contained ReaScript)
|
||||
│ (REAPER executes this)
|
||||
▼
|
||||
fl_control_result.json
|
||||
│
|
||||
▼
|
||||
read_result() → ReaScriptResult
|
||||
```
|
||||
|
||||
## Command/Result Contract
|
||||
|
||||
### Command JSON (`fl_control_command.json`)
|
||||
|
||||
Written by `write_command()`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"action": ["add_plugins", "calibrate", "render"],
|
||||
"rpp_path": "C:/path/to/song.rpp",
|
||||
"render_path": "C:/path/to/song_rendered.wav",
|
||||
"timeout": 120,
|
||||
"track_calibration": [
|
||||
{"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []}
|
||||
],
|
||||
"plugins_to_add": [
|
||||
{"track_name": "808 Bass", "fx_name": "ReaEQ", "params": {"0": "1", "1": "0", "2": "300.0"}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Result JSON (`fl_control_result.json`)
|
||||
|
||||
Written by the ReaScript's `write_json()`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"status": "ok",
|
||||
"message": "",
|
||||
"lufs": -14.2,
|
||||
"integrated_lufs": -14.2,
|
||||
"short_term_lufs": -13.8,
|
||||
"fx_errors": [],
|
||||
"tracks_verified": 10,
|
||||
"added_plugins": [
|
||||
{"fx_name": "ReaEQ", "instance_id": 0, "track_name": "808 Bass", "status": "ok"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Known Actions
|
||||
|
||||
| Action | Function | Description |
|
||||
|--------|----------|-------------|
|
||||
| `add_plugins` | `add_plugins(cmd)` | Loads plugins via `TrackFX_AddByName`. Returns per-plugin status. |
|
||||
| `configure_fx_params` | `configure_fx_params(cmd)` | Sets FX parameters via `TrackFX_SetParam`. |
|
||||
| `verify_fx` | `verify_fx()` | Enumerates all tracks/FX, reports empty/missing FX names. |
|
||||
| `calibrate` | `calibrate(track_cal)` | Sets track volume, pan, and sends from calibration data. |
|
||||
| `render` | `do_render(render_path)` + `measure_loudness()` | Renders WAV and measures LUFS via `RPR_CalcMediaSrcLoudness`. |
|
||||
|
||||
Actions execute in the order specified by the `action` list. The script's `main()` opens the project once, then runs the action dispatch loop, then writes the result JSON.
|
||||
|
||||
## ReaScript Architecture
|
||||
|
||||
The generated script is a single self-contained `.py` file with:
|
||||
|
||||
1. **Zero external imports**: No `json`, no `os`, no third-party libraries. Only REAPER's built-in `RPR_*` functions.
|
||||
2. **Hand-rolled JSON parser**: ~100 lines of string-splitting logic (`parse_json()`). Handles strings, integers, floats, booleans, nulls, nested objects, arrays.
|
||||
3. **API availability check**: `check_api()` verifies all required REAPER API functions exist before any work begins. Missing APIs → immediate error result.
|
||||
4. **Conditional function inclusion**: Only the action functions needed by the specific command are emitted in the script. The generator checks `action` list and includes only relevant source blocks.
|
||||
5. **Adaptive API check**: `check_api()` includes `TrackFX_AddByName` and `TrackFX_SetParam` only when `add_plugins` or `configure_fx_params` actions are present.
|
||||
|
||||
### REAPER API Functions Used
|
||||
|
||||
| API Function | Purpose |
|
||||
|-------------|---------|
|
||||
| `RPR_CountTracks` | Enumerate tracks |
|
||||
| `RPR_GetTrack` | Get track by index |
|
||||
| `RPR_GetSetMediaTrackInfo_String` | Get/set track name |
|
||||
| `RPR_SetMediaTrackInfo_Value` | Set track volume (D_VOL), pan (D_PAN) |
|
||||
| `RPR_TrackFX_GetCount` | Count FX on a track |
|
||||
| `RPR_TrackFX_GetFXName` | Get FX name by index |
|
||||
| `RPR_TrackFX_AddByName` | Add FX by display name |
|
||||
| `RPR_TrackFX_SetParam` | Set FX parameter by index |
|
||||
| `RPR_CreateTrackSend` | Create send between tracks |
|
||||
| `RPR_Main_openProject` | Open .rpp project |
|
||||
| `RPR_Main_RenderFile` | Render to file |
|
||||
| `RPR_CalcMediaSrcLoudness` | Measure LUFS |
|
||||
| `RPR_ResourcePath` | Get REAPER resource directory path |
|
||||
| `RPR_GetFunctionMetadata` | Check API function availability |
|
||||
| `RPR_GetLastError` | Get last error |
|
||||
| `RPR_GetProjectName` | Get project name |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `src/reaper_scripting/commands.py` — `ReaScriptCommand`, `ReaScriptResult`, `write_command()`, `read_result()`
|
||||
- `src/core/schema.py` — `SongDefinition`, `PluginDef` (indirect, via `get_builtin_plugins()`)
|
||||
- `pathlib` (stdlib) — file path handling
|
||||
|
||||
## Known Gotchas
|
||||
|
||||
1. **REAPER Python has no `json` module**: The generated ReaScript cannot `import json`. All serialization uses the hand-rolled parser. This is a fundamental REAPER limitation — its embedded Python is a stripped-down distribution.
|
||||
|
||||
2. **Command JSON MUST be at the right path**: The ReaScript reads `fl_control_command.json` from `RPR_ResourcePath() + "scripts/"`. If the file isn't there when the script runs, it fails immediately.
|
||||
|
||||
3. **`write_command()` omits empty `plugins_to_add`**: If `plugins_to_add` is empty, the key is not written. The ReaScript's `cmd.get("plugins_to_add", [])` handles this with a default.
|
||||
|
||||
4. **Track name matching is case-insensitive**: `find_track()` normalizes both the search name and the REAPER track name to lowercase for matching. Track names that differ only by case will collide.
|
||||
|
||||
5. **`configure_fx_params` uses string keys for param indices**: The `params` dict in `plugins_to_add` uses string keys (`"0"`, `"1"`) because JSON keys are always strings. The ReaScript converts to `int` before calling `TrackFX_SetParam`.
|
||||
|
||||
6. **No error recovery per action**: If `add_plugins` fails for one track, it continues with the next. But if any action crashes (unhandled exception), the script terminates without writing the result JSON — leading to polling timeout in `run_in_reaper.py`.
|
||||
|
||||
7. **Calibrate sends use `CreateTrackSend`**: Every call to `calibrate()` with sends creates a new send connection. If calibrate is called multiple times, duplicate sends accumulate.
|
||||
|
||||
8. **LUFS is optional**: If `RPR_CalcMediaSrcLoudness` returns an unexpected format or throws, `measure_loudness()` returns `None` values silently.
|
||||
158
docs/schemas/reascript-protocol.json
Normal file
158
docs/schemas/reascript-protocol.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://fl-control/schemas/reascript-protocol.json",
|
||||
"title": "ReaScript Protocol",
|
||||
"description": "Command/result protocol for ReaScript two-way JSON communication",
|
||||
"definitions": {
|
||||
"ReaScriptCommand": {
|
||||
"title": "ReaScriptCommand",
|
||||
"description": "Command sent to the ReaScript via fl_control_command.json",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Protocol version",
|
||||
"default": 1
|
||||
},
|
||||
"action": {
|
||||
"description": "Single action name or pipeline list. Known values: add_plugins, configure_fx_params, verify_fx, calibrate, render",
|
||||
"default": "calibrate",
|
||||
"oneOf": [
|
||||
{ "type": "string" },
|
||||
{
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"rpp_path": {
|
||||
"type": "string",
|
||||
"description": "Path to .rpp file to open",
|
||||
"default": ""
|
||||
},
|
||||
"render_path": {
|
||||
"type": "string",
|
||||
"description": "Path for rendered WAV output",
|
||||
"default": ""
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Polling timeout in seconds",
|
||||
"default": 120
|
||||
},
|
||||
"track_calibration": {
|
||||
"type": "array",
|
||||
"description": "Per-track volume/pan/send calibration",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"track_index": { "type": "integer" },
|
||||
"volume": { "type": "number" },
|
||||
"pan": { "type": "number" },
|
||||
"sends": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dest_track_index": { "type": "integer" },
|
||||
"level": { "type": "number" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins_to_add": {
|
||||
"type": "array",
|
||||
"description": "Each dict: {\"track_name\": str, \"fx_name\": str, \"params\": {\"0\": float, ...}}",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"track_name": { "type": "string" },
|
||||
"fx_name": { "type": "string" },
|
||||
"params": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"required": ["track_name", "fx_name"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReaScriptResult": {
|
||||
"title": "ReaScriptResult",
|
||||
"description": "Result written by the ReaScript to fl_control_result.json",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Protocol version",
|
||||
"default": 1
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "ok | error | timeout",
|
||||
"default": "ok",
|
||||
"enum": ["ok", "error", "timeout"]
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Error or status message",
|
||||
"default": ""
|
||||
},
|
||||
"lufs": {
|
||||
"type": ["number", "null"],
|
||||
"description": "Integrated LUFS",
|
||||
"default": null
|
||||
},
|
||||
"integrated_lufs": {
|
||||
"type": ["number", "null"],
|
||||
"description": "Integrated LUFS reading",
|
||||
"default": null
|
||||
},
|
||||
"short_term_lufs": {
|
||||
"type": ["number", "null"],
|
||||
"description": "Short-term LUFS",
|
||||
"default": null
|
||||
},
|
||||
"fx_errors": {
|
||||
"type": "array",
|
||||
"description": "FX verification errors. Each dict: {\"track_index\": int, \"fx_index\": int, \"name\": str, \"expected\": str}",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"track_index": { "type": "integer" },
|
||||
"fx_index": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"expected": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracks_verified": {
|
||||
"type": "integer",
|
||||
"description": "Number of tracks verified",
|
||||
"default": 0
|
||||
},
|
||||
"added_plugins": {
|
||||
"type": "array",
|
||||
"description": "Each dict: {\"fx_name\": str, \"instance_id\": int, \"track_name\": str, \"status\": str}",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fx_name": { "type": "string" },
|
||||
"instance_id": { "type": "integer" },
|
||||
"track_name": { "type": "string" },
|
||||
"status": { "type": "string" }
|
||||
},
|
||||
"required": ["fx_name", "track_name", "status"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
423
docs/schemas/song-definition.json
Normal file
423
docs/schemas/song-definition.json
Normal file
@@ -0,0 +1,423 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://fl-control/schemas/song-definition.json",
|
||||
"title": "SongDefinition",
|
||||
"description": "Complete song definition — the source of truth for one .rpp file",
|
||||
"type": "object",
|
||||
"required": ["meta"],
|
||||
"properties": {
|
||||
"meta": { "$ref": "#/definitions/SongMeta" },
|
||||
"tracks": {
|
||||
"type": "array",
|
||||
"description": "List of REAPER tracks with clips and plugins",
|
||||
"default": [],
|
||||
"items": { "$ref": "#/definitions/TrackDef" }
|
||||
},
|
||||
"patterns": {
|
||||
"type": "array",
|
||||
"description": "Pattern definitions for arrangement",
|
||||
"default": [],
|
||||
"items": { "$ref": "#/definitions/PatternDef" }
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Arrangement items referencing patterns",
|
||||
"default": [],
|
||||
"items": { "$ref": "#/definitions/ArrangementItemDef" }
|
||||
},
|
||||
"progression_name": {
|
||||
"type": "string",
|
||||
"description": "Chord progression name (e.g. i-VII-VI-VII)",
|
||||
"default": "i-VII-VI-VII"
|
||||
},
|
||||
"section_template": {
|
||||
"type": "string",
|
||||
"description": "Section template name",
|
||||
"default": "standard"
|
||||
},
|
||||
"samples": {
|
||||
"type": "object",
|
||||
"description": "Sample file map (name → filename)",
|
||||
"default": {},
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"sections": {
|
||||
"type": "array",
|
||||
"description": "Section definitions in playback order",
|
||||
"default": [],
|
||||
"items": { "$ref": "#/definitions/SectionDef" }
|
||||
},
|
||||
"master_plugins": {
|
||||
"type": "array",
|
||||
"description": "List of plugin registry keys for master FX chain",
|
||||
"default": [],
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"SongMeta": {
|
||||
"title": "SongMeta",
|
||||
"description": "Song metadata — tempo, key, time signature",
|
||||
"type": "object",
|
||||
"required": ["bpm", "key"],
|
||||
"properties": {
|
||||
"bpm": {
|
||||
"type": "number",
|
||||
"description": "Tempo 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user