Files
renato97 014e636889 feat: professional reggaeton production engine — 7 SDD changes, 302 tests
- section-energy: track activity matrix + volume/velocity multipliers per section
- smart-chords: ChordEngine with voice leading, inversions, 4 emotion modes
- hook-melody: melody engine with hook/stabs/smooth styles, call-and-response
- mix-calibration: Calibrator module (LUFS volumes, HPF/LPF, stereo, sends, master)
- transitions-fx: FX track with risers/impacts/sweeps at section boundaries
- sidechain: MIDI CC11 bass ducking on kick hits via DrumLoopAnalyzer
- presets-pack: role-aware plugin presets (Serum/Decapitator/Omnisphere per role)

Full SDD pipeline (propose→spec→design→tasks→apply→verify) for all 7 changes.
302/302 tests passing.
2026-05-03 23:54:29 -03:00

4.0 KiB
Raw Permalink Blame History

Design: 808 Bass Sidechain Ducking

Technical Approach

Extend ClipDef with midi_cc: list[CCEvent], inject kick positions from DrumLoopAnalyzer into build_bass_track(), and modify _build_midi_source() to emit CC E-lines interleaved with notes. Pure MIDI — zero plugin or REAPER-specific features required.

Architecture Decisions

Decision Choice Rejected Rationale
CC representation dataclass CCEvent(controller, time, value) Dict/reuse MidiNote Controller field orthogonal to pitch; typed dataclass catches errors at import time
CC in _build_midi_source Sort notes+cc by time, single pass Separate CC loop after notes Single sorted pass guarantees correct delta-encoding; avoids cursor reset bugs
Kick cache lifetime Module-level dict[str, list[float]] in compose.py per-function lru_cache Drumloop reused across sections; WAV path is natural stable key
Duck shape constants _CC11_DIP=50, _CC11_HOLD=0.02, _CC11_RELEASE=0.18 Configuration file 3 constants — config file is overkill; easy to change in-code
DrumLoopAnalyzer integration Call analyze() once per unique WAV path Per-section analysis ~1s per analysis; caching avoids N×1s for N sections

Data Flow

drumloop WAV
  → DrumLoopAnalyzer.analyze() → transient_positions("kick")
  → filter confidence ≥ 0.6 → convert seconds→beats via bpm
  → _get_kick_cache() returns list[float]
  → build_bass_track(sections, offsets, key_root, key_minor, kick_cache)
  → per section: filter kicks within [clip_start, clip_end] beats
  → per kick in range: CCEvent(11, kick_t, 50), CCEvent(11, kick_t+0.02, 50), CCEvent(11, kick_t+0.18, 127)
  → ClipDef(..., midi_cc=[...])
  → RPPBuilder._build_midi_source()
    → merge notes+cc, sort by time → emit E-lines delta-encoded

File Changes

File Action Description
src/core/schema.py Modify Add CCEvent dataclass; add midi_cc: list[CCEvent] = field(default_factory=list) to ClipDef
scripts/compose.py Modify Add _KICK_CONFIDENCE_THRESHOLD, _CC11_* constants; add _get_kick_cache() function; modify build_bass_track() signature and CC generation; update main() to build kick cache
src/reaper_builder/__init__.py Modify Merge clip.midi_notes + clip.midi_cc sorted by time in _build_midi_source(); emit E delta B0 0B {value:02x} for CC events

Interfaces / Contracts

# src/core/schema.py — new dataclass
@dataclass
class CCEvent:
    controller: int   # 11 = Expression (CC11)
    time: float       # beats from clip start
    value: int        # 0127

# ClipDef — new field
midi_cc: list[CCEvent] = field(default_factory=list)

# compose.py — new function
def _get_kick_cache(drumloop_paths: list[str], bpm: float) -> dict[str, list[float]]:
    """Analyze drumloops, return {path: [kick_time_beats]}."""

E-line Encoding Detail

Current: E {delta_ticks} {status} {data1} {data2} Note on: E 480 90 3c 50 (note 60, vel 80, delta=480 ticks) Note off: E 960 80 3c 00 CC11: E 0 B0 0B 32 (controller 11=B, CC message 0xB0, val 50=0x32)

Merging: sort [(n.start, "n", note), (c.time, "c", cc), ...] by time. CC events contribute zero to cursor (no duration — delta-only).

Testing Strategy

Layer What Approach
Unit CCEvent dataclass Round-trip serialization, default values
Unit _build_midi_source CC emission Feed ClipDef with CC events, parse output for B0 0B lines
Integration build_bass_track with kick cache Mock DrumLoopAnalyzer, verify midi_cc populated
E2E Full pipeline with real drumloop Generate .rpp, grep for B0 0B in output, verify in REAPER

Migration / Rollout

No migration required. midi_cc defaults to empty list — all existing code paths unchanged. One-commit revert: remove midi_cc field, revert builder merge, delete _get_kick_cache().

Open Questions

None.