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:
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.
|
||||
Reference in New Issue
Block a user