Files
reaper-control/.sdd/changes/sidechain/design.md
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

83 lines
4.0 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.
# 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
```python
# 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.