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

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

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

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