Files
reaper-control/docs/modules/calibrator.md
renato97 7bcd8052a9 docs: LLM-ready documentation suite — LLM_CONTEXT.md, module deep-dives, JSON schemas, CLI reference
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).
2026-05-04 10:30:24 -03:00

6.4 KiB
Raw Permalink Blame History

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. Mutates song in-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.01.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.01.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:

  1. Ozone 12 triplet: Ozone_12_EqualizerOzone_12_DynamicsOzone_12_Maximizer
  2. Fallback (if any Ozone plugin missing from PLUGIN_REGISTRY): Pro-Q_3Pro-C_2Pro-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.pySongDefinition, TrackDef, PluginDef
  • src/calibrator/presets.pyVOLUME_PRESETS, PAN_PRESETS, EQ_PRESETS, SEND_PRESETS
  • src/reaper_builder (lazy import in _swap_master_chain) — PLUGIN_REGISTRY

Known Gotchas

  1. Calibration mutates in-place: apply() modifies the SongDefinition directly. If you need the uncalibrated version, keep a copy before calling.

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

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

  4. EQ presets require ReaScript execution: EQ_PRESETS values are not applied by Calibrator directly. They must be embedded in PluginDef.params for ReaEQ instances and applied by the ReaScript's configure_fx_params action.

  5. Master chain is replaced, not extended: _swap_master_chain() replaces the entire master_plugins list. Any pre-existing master plugins are lost.

  6. Ozone fallback is silent: If Ozone plugins aren't in the registry, the FabFilter fallback is used with no warning.

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

  8. Calibration disabled via SongMeta.calibrate: If song.meta.calibrate is False, the pipeline (in scripts/compose.py) skips Calibrator.apply(). But a caller could still invoke apply() directly.