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

7.1 KiB
Raw Permalink Blame History

Composer Module

Parent doc: 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)

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)

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:

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)

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)

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)

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.pyMidiNote
  • 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.