# 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)