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: 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

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.