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

3.4 KiB

presets-pack Specification

Purpose

Role-aware plugin preset system. Different track roles (bass/lead/chords/pad) get distinct preset data for the same plugin, replacing the current flat {plugin: [chunks]} lookup.

Requirements

Requirement: Role-Aware Preset Structure

PLUGIN_PRESETS MUST be restructured from dict[str, list[str]] (plugin → chunks) to dict[str, dict[str, list[str]]] (plugin → {role → chunks}). The "default" role key SHALL contain the original unmodified preset. Lookup SHALL fall back to "default" when a role has no specific variant.

Scenario: Role-specific preset found

  • GIVEN PLUGIN_PRESETS["Serum_2"]["bass"] and ["lead"] exist
  • WHEN resolving serum preset with role="bass"
  • THEN bass-specific chunks are returned
  • WHEN resolving with role="lead"
  • THEN lead-specific chunks are returned

Scenario: Fallback to default

  • GIVEN PLUGIN_PRESETS["Decapitator"]["default"] exists but ["pad"] does not
  • WHEN resolving Decapitator preset with role="pad"
  • THEN the "default" preset data is returned

Requirement: Preset Transformation Pipeline

The system SHALL provide a PresetTransformer that base64-decodes preset data, modifies role-specific parameters, and re-encodes. Each supported plugin MUST have its own decoder function keyed by plugin name.

Plugin Format Modifications per role
Serum_2 base64 → JSON Osc type (sine=0→bass, saw=1→lead), filter cutoff, FX bypass
Decapitator base64 → key=value Drive high→drums, Drive low→bass, Tone bright→drums, Tone dark→bass
Omnisphere base64 → SynthMaster Attack slow→pad, filter mod→pad, LFO rate up→pad

Scenario: Serum bass variant

  • GIVEN Serum_2 default preset decoded as JSON
  • WHEN transformed for role="bass"
  • THEN oscillator type set to sine (0), filter cutoff ≤ 200Hz

Scenario: Decapitator drums variant

  • GIVEN Decapitator default preset decoded as key=value text
  • WHEN transformed for role="drums"
  • THEN Drive=0.8, Tone=0.7, Style=A

Requirement: Round-Trip Integrity

Each preset transform MUST produce valid base64 output that decodes back to equivalent content. A round-trip test per (plugin, role) combination SHALL verify: encode(decode(chunks)) == original_chunks.

Scenario: Serum round-trip

  • GIVEN Serum_2 preset chunks [header, json_body, ...]
  • WHEN decoded, modified, re-encoded
  • THEN all chunks maintain original length and base64 character set
  • AND JSON body is valid JSON

Scenario: Decapitator round-trip

  • GIVEN Decapitator preset chunks [header, body, ...]
  • WHEN decoded, modified, re-encoded
  • THEN chunk count matches, first chunk (header) unchanged

Requirement: Role Propagation Through Pipeline

make_plugin() in compose.py and _build_plugin() in __init__.py MUST accept an optional role: str | None parameter. When role is provided, preset lookup SHALL use role-aware structure. FX_CHAINS layout is unchanged — role is the FX_CHAINS key (e.g., "bass", "lead").

Scenario: Bass track gets bass preset

  • GIVEN FX_CHAINS["bass"] = ["Serum_2", "Decapitator", ...]
  • WHEN make_plugin("Serum_2", 0, role="bass") is called
  • THEN preset_data resolved from PLUGIN_PRESETS["Serum_2"]["bass"]

Scenario: Unknown plugin with role

  • GIVEN plugin not in PLUGIN_PRESETS
  • WHEN called with any role
  • THEN returns PluginDef with preset_data=None (no crash)