Complete documentation system for LLM consumption: primary LLM_CONTEXT.md (27KB system overview), 4 module deep-dives (composer, reaper-builder, reaper-scripting, calibrator), 2 JSON Schema draft-07 contracts, CLI reference, and README correction (FL Studio -> REAPER identity).
6.4 KiB
Calibrator Module
Parent doc: LLM_CONTEXT.md
Purpose
Post-processing mix calibration. Applies role-based volume, EQ, pan, sends, and master chain configuration to a SongDefinition after track construction and before .rpp generation.
Location: src/calibrator/
Public API
Calibrator (__init__.py)
class Calibrator:
@staticmethod
def apply(song: SongDefinition) -> SongDefinition
apply(): Applies role-based volume, pan, sends, and master chain calibration. Mutatessongin-place and returns it. Skips tracks named"Reverb"or"Delay"(return tracks).- Pipeline:
_calibrate_volumes()→_calibrate_pans()→_calibrate_sends()→_swap_master_chain()
Role Resolution (__init__.py)
@staticmethod
def _resolve_role(track_name: str) -> str | None
Maps track name to role key using substring matching:
| Track Name Pattern | Role | Description |
|---|---|---|
"Drumloop", "drum*" |
"drumloop" |
Drum loop |
"808 Bass", "*bass*" |
"bass" |
Bass |
"Chords", "*chord*" |
"chords" |
Chords/harmony |
"Lead", "*lead*", "*synth*", "*melody*" |
"lead" |
Lead melody |
"Clap", "*snare*", "*clap*" |
"clap" |
Clap/snare |
"Pad", "*pad*", "*atmos*", "*ambient*" |
"pad" |
Pad/atmosphere |
"Perc", "*perc*" |
"perc" |
Percussion |
"Reverb", "Delay" |
None |
Return tracks (skipped) |
| Any other name | None |
Unknown role (skipped) |
Presets (presets.py)
VOLUME_PRESETS: dict[str, float]
PAN_PRESETS: dict[str, float]
EQ_PRESETS: dict[str, dict[int, float]]
SEND_PRESETS: dict[str, tuple[float, float]]
Volume Presets (0.0–1.0 REAPER volume)
| Role | Volume |
|---|---|
drumloop |
0.85 |
bass |
0.82 |
chords |
0.75 |
lead |
0.80 |
clap |
0.78 |
pad |
0.70 |
perc |
0.80 |
Pan Presets (-1.0 to 1.0)
| Role | Pan |
|---|---|
drumloop |
0.0 (center) |
bass |
0.0 (center) |
chords |
0.5 (right) |
lead |
0.3 (slightly right) |
clap |
-0.15 (slightly left) |
pad |
-0.5 (left) |
perc |
0.12 (slightly right) |
EQ Presets (ReaEQ VST2 param slots)
| Role | Band Enabled | Filter Type | Frequency |
|---|---|---|---|
drumloop |
1 (on) | 1 (HPF) | 60 Hz |
bass |
1 (on) | 0 (LPF) | 300 Hz |
chords |
1 (on) | 1 (HPF) | 200 Hz |
lead |
1 (on) | 1 (HPF) | 200 Hz |
clap |
1 (on) | 1 (HPF) | 200 Hz |
pad |
1 (on) | 1 (HPF) | 100 Hz |
perc |
1 (on) | 1 (HPF) | 200 Hz |
EQ presets use ReaEQ's VST2 parameter layout (slot 0 = enabled, slot 1 = filter type, slot 2 = frequency). These are passed to PluginDef.params and applied by the ReaScript's configure_fx_params action.
Send Presets (reverb, delay) — 0.0–1.0
| Role | Reverb Send | Delay Send |
|---|---|---|
drumloop |
0.10 | 0.00 |
bass |
0.05 | 0.00 |
chords |
0.40 | 0.10 |
lead |
0.30 | 0.15 |
clap |
0.10 | 0.00 |
pad |
0.50 | 0.20 |
perc |
0.10 | 0.00 |
Master Chain Upgrade (__init__.py)
@staticmethod
def _swap_master_chain(song: SongDefinition) -> None
Replaces master_plugins with a processing chain:
- Ozone 12 triplet:
Ozone_12_Equalizer→Ozone_12_Dynamics→Ozone_12_Maximizer - Fallback (if any Ozone plugin missing from
PLUGIN_REGISTRY):Pro-Q_3→Pro-C_2→Pro-L_2
Check is performed at calibration time. Missing plugins silently fall back.
Data Flow
SongDefinition (after track construction)
│
▼
Calibrator.apply(song)
│
├── _calibrate_volumes(song)
│ For each track:
│ role = _resolve_role(track.name)
│ if role in VOLUME_PRESETS: track.volume = VOLUME_PRESETS[role]
│
├── _calibrate_pans(song)
│ For each track:
│ role = _resolve_role(track.name)
│ if role in PAN_PRESETS: track.pan = PAN_PRESETS[role]
│
├── _calibrate_sends(song)
│ Count content tracks and return tracks
│ reverb_idx = num_content; delay_idx = num_content + 1
│ For each track:
│ role = _resolve_role(track.name)
│ if role in SEND_PRESETS:
│ track.send_level[reverb_idx] = SEND_PRESETS[role][0]
│ track.send_level[delay_idx] = SEND_PRESETS[role][1]
│
└── _swap_master_chain(song)
Check REGISTRY for Ozone triplet availability
song.master_plugins = ozone_triplet or fallback_triplet
│
▼
SongDefinition (calibrated, mutated in-place)
Dependencies
src/core/schema.py—SongDefinition,TrackDef,PluginDefsrc/calibrator/presets.py—VOLUME_PRESETS,PAN_PRESETS,EQ_PRESETS,SEND_PRESETSsrc/reaper_builder(lazy import in_swap_master_chain) —PLUGIN_REGISTRY
Known Gotchas
-
Calibration mutates in-place:
apply()modifies theSongDefinitiondirectly. If you need the uncalibrated version, keep a copy before calling. -
Role resolution is substring-based:
"drum"substring matches"drumloop"role."bass"substring matches"bass". Track names like"Bassline"or"Drum Kit"will resolve correctly. But"SubBass"also matches because"bass"is a substring. -
Send indices depend on track ordering:
_calibrate_sends()counts content tracks (non-return) to compute reverb/delay indices. If tracks are reordered after calibration, send indices will be wrong. -
EQ presets require ReaScript execution:
EQ_PRESETSvalues are not applied byCalibratordirectly. They must be embedded inPluginDef.paramsfor ReaEQ instances and applied by the ReaScript'sconfigure_fx_paramsaction. -
Master chain is replaced, not extended:
_swap_master_chain()replaces the entiremaster_pluginslist. Any pre-existing master plugins are lost. -
Ozone fallback is silent: If Ozone plugins aren't in the registry, the FabFilter fallback is used with no warning.
-
Return tracks identified by exact name: Only tracks named
"Reverb"or"Delay"(case-insensitive) are treated as return tracks."Reverb Return"or"Verb"would NOT be recognized. -
Calibration disabled via SongMeta.calibrate: If
song.meta.calibrateisFalse, the pipeline (inscripts/compose.py) skipsCalibrator.apply(). But a caller could still invokeapply()directly.