Files
reaper-control/.sdd/changes/presets-pack/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

81 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: presets-pack
## Technical Approach
Restructure `PLUGIN_PRESETS` to `{(plugin, role): chunks}`, add `PresetTransformer` class with per-plugin decoders (Serum=JSON, SoundToys=key=value text, Omnisphere=SynthMaster text), and thread `role` parameter through `make_plugin()``_build_plugin()`. No new dependencies — pure Python `base64`, `json`, `re`.
## Architecture Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Data structure | `dict[tuple[str,str], list[str]]``{(plugin, role): chunks}` | Avoids dict-of-dicts nesting; simpler iteration in tests |
| Transformation | Separate `PresetTransformer` class per plugin format | Serum/JSON, SoundToys/text, Omnisphere/text are different parsers; isolation = testability |
| Role threading | Optional `role` param on `make_plugin()` and `_build_plugin()` | Zero breaking changes; None = current behavior |
| Fallback chain | role → default → None | Backward compatible; existing tests don't break |
## Data Flow
```
compose.py: make_plugin("Serum_2", 0, role="bass")
→ _resolve_preset("Serum_2", "bass")
→ PLUGIN_PRESETS[("Serum_2", "bass")] ← PresetTransformer output
→ PluginDef(preset_data=bass_chunks)
reaper_builder: _build_plugin(plugin)
→ entry = PLUGIN_REGISTRY.get(resolved_name)
→ preset_data = PLUGIN_PRESETS.get((resolved_name, plugin.role))
or PLUGIN_PRESETS.get((resolved_name, "default"))
→ _build_plugin_element(display, file, uid, preset_data)
```
## File Changes
| File | Action | Description |
|------|--------|-------------|
| `src/reaper_builder/__init__.py` | Modify | Restructure `PLUGIN_PRESETS` to `{(k,role): chunks}`; `_build_plugin()` reads `plugin.role` for lookup |
| `src/reaper_builder/preset_transformer.py` | Create | `PresetTransformer` class with `decode()`, `transform(role)`, `encode()`; per-plugin transformers |
| `src/composer/templates.py` | Modify | `_parse_vst_block()` and `_make_plugin_template()` handle new `PLUGIN_PRESETS` structure |
| `scripts/compose.py` | Modify | `make_plugin()` accepts `role` param, threads from `FX_CHAINS` key |
| `src/core/schema.py` | Modify | `PluginDef` gets optional `role: str \| None = None` field |
| `tests/test_preset_transform.py` | Create | Round-trip tests for 3 plugins × N roles |
## PresetTransformer Design
```python
class PresetTransformer:
TRANSFORMERS: dict[str, Callable] = {
"Serum_2": _transform_serum,
"Decapitator": _transform_decapitator,
"Omnisphere": _transform_omnisphere,
}
@staticmethod
def derive(plugin: str, default_chunks: list[str], role: str) -> list[str]:
transformer = PresetTransformer.TRANSFORMERS.get(plugin)
if not transformer:
return default_chunks # no transform = use default unchanged
return transformer(default_chunks, role)
```
Per-transformer functions:
- `_transform_serum(chunks, role)` — decode JSON body, modify `processor.osc.type`, `processor.filter.cutoff`, `processor.fx`
- `_transform_decapitator(chunks, role)` — decode text body, modify `Drive`, `Tone`, `Style` lines
- `_transform_omnisphere(chunks, role)` — decode SynthMaster body, modify `Atk`, `Dec`, `Filter Freq` lines
## Testing Strategy
| Layer | What | Approach |
|-------|------|----------|
| Unit | PresetTransformer per plugin | decode → modify → encode for each (plugin, role); verify JSON/keys changed |
| Integration | make_plugin + _build_plugin with role | Build PluginDef, verify preset_data differs per role |
| Regression | `test_make_plugin_known_key` | Existing tests pass unchanged (role=None fallback) |
| Round-trip | encode(decode(chunks)) == chunks | Each plugin × role; verify chunk count, base64 charset, structure integrity |
## Migration / Rollout
No migration required. `PluginDef.role` defaults to None. Existing callers that don't pass role continue working with `"default"` preset. Revert: flatten `PLUGIN_PRESETS` back to single-level dict, remove `role` param.
## Open Questions
None.