Files
reaper-control/docs/modules/composer.md
renato97 7bcd8052a9 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).
2026-05-04 10:30:24 -03:00

178 lines
7.1 KiB
Markdown
Raw Blame History

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