//! 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, /// 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, #[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, /// 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::().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)), 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(), 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(), } }