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