Files
codename-206/src/params.rs
T
Mikkeli Matlock 9a54413bfa feat: allow makeup down to -24 dB on all channels
Extend the per-channel makeup range floor from -12 to -24 dB so makeup can
attenuate as well as boost, giving more post-compressor gain-staging headroom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:14:15 +09:00

214 lines
7.2 KiB
Rust

//! Plugin parameters and their layout.
//!
//! Holds the global controls plus four `CompressorParams` blocks (low/mid/high + the 'All'
//! aggregate channel). `build_settings` translates a channel's params into the per-block
//! [`CompressorSettings`] the DSP consumes.
use nih_plug::prelude::*;
use nih_plug_egui::EguiState;
use std::sync::Arc;
use crate::dsp::compressor::{Compressor, CompressorSettings, MAX_LOOKAHEAD_MS};
/// Level-detection mode for a compressor's detector.
#[derive(Enum, PartialEq, Clone, Copy)]
pub enum DetectionMode {
#[id = "peak"]
#[name = "Peak"]
Peak,
#[id = "rms"]
#[name = "RMS"]
Rms,
}
#[derive(Params)]
pub struct Codename206Params {
#[persist = "editor-state"]
pub editor_state: Arc<EguiState>,
/// Low/Mid crossover frequency.
#[id = "xover_lo"]
pub crossover_low_hz: FloatParam,
/// Mid/High crossover frequency.
#[id = "xover_hi"]
pub crossover_high_hz: FloatParam,
/// Global look-ahead time (constant reported latency — safe to adjust during playback).
#[id = "lookahead"]
pub look_ahead_ms: FloatParam,
/// Output brickwall ceiling (the limiter never lets output exceed this).
#[id = "ceiling"]
pub output_ceiling_db: FloatParam,
/// Output limiter release time.
#[id = "lim_rel"]
pub limiter_release_ms: FloatParam,
#[nested(id_prefix = "low", group = "Low")]
pub low: CompressorParams,
#[nested(id_prefix = "mid", group = "Mid")]
pub mid: CompressorParams,
#[nested(id_prefix = "high", group = "High")]
pub high: CompressorParams,
#[nested(id_prefix = "all", group = "All")]
pub all: CompressorParams,
}
#[derive(Params)]
pub struct CompressorParams {
/// Drive into the compressor: scales the signal **before** detection, so it both pushes the
/// channel further into compression and feeds the downstream sum/limiter harder. Combined with
/// makeup (post-comp), this gives full per-channel input/output gain-staging.
#[id = "pregain"]
pub pre_gain_db: FloatParam,
#[id = "detect"]
pub detection: EnumParam<DetectionMode>,
#[id = "thresh"]
pub threshold_db: FloatParam,
#[id = "ratio"]
pub ratio: FloatParam,
#[id = "knee"]
pub knee_db: FloatParam,
#[id = "attack"]
pub attack_ms: FloatParam,
#[id = "release"]
pub release_ms: FloatParam,
#[id = "makeup"]
pub makeup_db: FloatParam,
#[id = "bypass"]
pub bypass: BoolParam,
}
impl Default for Codename206Params {
fn default() -> Self {
Self {
editor_state: EguiState::from_size(760, 520),
crossover_low_hz: FloatParam::new(
"Crossover Lo/Mid",
200.0,
FloatRange::Skewed { min: 30.0, max: 1_000.0, factor: FloatRange::skew_factor(-1.0) },
)
.with_value_to_string(formatters::v2s_f32_hz_then_khz(0))
.with_string_to_value(formatters::s2v_f32_hz_then_khz()),
crossover_high_hz: FloatParam::new(
"Crossover Mid/Hi",
2_500.0,
FloatRange::Skewed { min: 500.0, max: 18_000.0, factor: FloatRange::skew_factor(-1.0) },
)
.with_value_to_string(formatters::v2s_f32_hz_then_khz(0))
.with_string_to_value(formatters::s2v_f32_hz_then_khz()),
look_ahead_ms: FloatParam::new(
"Look-ahead",
2.0,
FloatRange::Linear { min: 0.0, max: MAX_LOOKAHEAD_MS },
)
.with_unit(" ms")
.with_value_to_string(formatters::v2s_f32_rounded(2)),
output_ceiling_db: FloatParam::new(
"Ceiling",
0.0,
FloatRange::Linear { min: -24.0, max: 0.0 },
)
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
limiter_release_ms: FloatParam::new(
"Limiter Release",
100.0,
FloatRange::Skewed { min: 1.0, max: 1_000.0, factor: FloatRange::skew_factor(-2.0) },
)
.with_unit(" ms")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
low: CompressorParams::default(),
mid: CompressorParams::default(),
high: CompressorParams::default(),
all: CompressorParams::default(),
}
}
}
impl Default for CompressorParams {
fn default() -> Self {
Self {
pre_gain_db: FloatParam::new(
"Pre-gain",
0.0,
FloatRange::Linear { min: -24.0, max: 36.0 },
)
.with_smoother(SmoothingStyle::Linear(20.0))
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
detection: EnumParam::new("Detection", DetectionMode::Peak),
threshold_db: FloatParam::new(
"Threshold",
-18.0,
FloatRange::Linear { min: -60.0, max: 0.0 },
)
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
ratio: FloatParam::new(
"Ratio",
2.0,
FloatRange::Skewed { min: 1.0, max: 20.0, factor: FloatRange::skew_factor(-1.0) },
)
.with_value_to_string(Arc::new(|v| format!("{v:.2} : 1")))
.with_string_to_value(Arc::new(|s| {
s.split(':').next().and_then(|x| x.trim().parse::<f32>().ok())
})),
knee_db: FloatParam::new("Knee", 6.0, FloatRange::Linear { min: 0.0, max: 24.0 })
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
attack_ms: FloatParam::new(
"Attack",
10.0,
FloatRange::Skewed { min: 0.0, max: 100.0, factor: FloatRange::skew_factor(-2.0) },
)
.with_unit(" ms")
.with_value_to_string(formatters::v2s_f32_rounded(2)),
release_ms: FloatParam::new(
"Release",
100.0,
FloatRange::Skewed { min: 1.0, max: 1_000.0, factor: FloatRange::skew_factor(-2.0) },
)
.with_unit(" ms")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
makeup_db: FloatParam::new("Makeup", 0.0, FloatRange::Linear { min: -24.0, max: 24.0 })
.with_smoother(SmoothingStyle::Linear(20.0))
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
bypass: BoolParam::new("Bypass", false),
}
}
}
/// Build the per-block compressor settings for one channel's params (makeup filled per sample).
pub fn build_settings(
p: &CompressorParams,
lookahead: usize,
sample_rate: f32,
) -> CompressorSettings {
CompressorSettings {
threshold_db: p.threshold_db.value(),
ratio: p.ratio.value(),
knee_db: p.knee_db.value(),
attack_coef: Compressor::time_to_coef(p.attack_ms.value(), sample_rate),
release_coef: Compressor::time_to_coef(p.release_ms.value(), sample_rate),
makeup_db: 0.0,
lookahead_samples: lookahead,
use_rms: p.detection.value() == DetectionMode::Rms,
bypass: p.bypass.value(),
}
}