45735f71f7
Rework Low Curve from a one-sided saturating bend into a bipolar mid-bulge: the low region is anchored at BOTH the silence floor and the knee, Low Slope tilts the straight line between them, and Low Curve (-1..1) bows that line in the middle (4*t*(1-t), peak +/-12 dB) without moving either endpoint. Positive bulges up (boost the quiet middle), negative down (suppress). Still serial. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
240 lines
8.6 KiB
Rust
240 lines
8.6 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,
|
||
/// Low shaper slope at the silence floor (1 = unity; >1 fans up/boost, <1 fans down/cut).
|
||
#[id = "lowslope"]
|
||
pub low_slope: FloatParam,
|
||
/// Low shaper curvature (−1..1): bipolar mid-bulge, 0 = straight. +bulges up (boost quiet
|
||
/// middle), − bulges down (suppress). Endpoints (silence + knee) stay fixed.
|
||
#[id = "lowcurve"]
|
||
pub low_curve: FloatParam,
|
||
#[id = "attack"]
|
||
pub attack_ms: FloatParam,
|
||
#[id = "release"]
|
||
pub release_ms: FloatParam,
|
||
#[id = "makeup"]
|
||
pub makeup_db: FloatParam,
|
||
/// Dry/wet mix (parallel compression). 100% = fully processed, 0% = dry (a clean bypass).
|
||
#[id = "mix"]
|
||
pub mix: FloatParam,
|
||
}
|
||
|
||
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: 30.0 })
|
||
.with_unit(" dB")
|
||
.with_value_to_string(formatters::v2s_f32_rounded(1)),
|
||
|
||
low_slope: FloatParam::new(
|
||
"Low Slope",
|
||
1.0,
|
||
FloatRange::Skewed { min: 0.5, max: 3.0, factor: FloatRange::skew_factor(-1.0) },
|
||
)
|
||
.with_smoother(SmoothingStyle::Linear(20.0))
|
||
.with_value_to_string(formatters::v2s_f32_rounded(2)),
|
||
|
||
low_curve: FloatParam::new("Low Curve", 0.0, FloatRange::Linear { min: -1.0, max: 1.0 })
|
||
.with_smoother(SmoothingStyle::Linear(20.0))
|
||
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
||
.with_string_to_value(formatters::s2v_f32_percentage()),
|
||
|
||
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)),
|
||
|
||
mix: FloatParam::new("Mix", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 })
|
||
.with_smoother(SmoothingStyle::Linear(20.0))
|
||
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
||
.with_string_to_value(formatters::s2v_f32_percentage()),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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(),
|
||
low_slope: p.low_slope.value(),
|
||
low_curve: p.low_curve.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,
|
||
mix: p.mix.value(),
|
||
}
|
||
}
|