42c5f7dcd2
Replace the per-channel bypass toggle with a smoothed dry/wet `mix` (0..100%, default 100%). The blend is applied at the compressor output: out = delayed_input * ((1 - mix) + mix * wet_gain) Dry and wet share the same delayed input, so it's phase-aligned (parallel compression, no comb filtering). mix=0 is bit-identical to the old bypass. The detector now runs even at mix 0, so the GR meter shows the wet gain reduction regardless of mix, while the level/plot out trace reads the mixed output. "Bands at 0% = simple full-band comp via All" still holds. Update README + parameter docs (bypass -> mix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
218 lines
7.6 KiB
Rust
218 lines
7.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,
|
|
#[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: 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(),
|
|
}
|
|
}
|