Files
codename-206/src/lib.rs
T

327 lines
11 KiB
Rust
Raw Normal View History

use nih_plug::prelude::*;
use nih_plug_egui::{create_egui_editor, egui, widgets, EguiState};
use std::sync::Arc;
mod dsp;
use dsp::compressor::{Compressor, CompressorSettings, MAX_LOOKAHEAD_MS};
/// Level-detection mode for the compressor's detector.
#[derive(Enum, PartialEq, Clone, Copy)]
enum DetectionMode {
#[id = "peak"]
#[name = "Peak"]
Peak,
#[id = "rms"]
#[name = "RMS"]
Rms,
}
/// Codename 206 — Stage 2: a single full-band compressor with look-ahead.
///
/// The `CompressorParams` struct is `#[nested]` so the exact same controls + DSP can be
/// reused for the three bands and the 'All' aggregate channel in later stages.
struct Codename206 {
params: Arc<Codename206Params>,
sample_rate: f32,
comp: Compressor,
}
#[derive(Params)]
struct Codename206Params {
#[persist = "editor-state"]
editor_state: Arc<EguiState>,
/// Look-ahead time: how far ahead the detector reads so gain reduction can lead
/// transients. The reported latency is constant (the max look-ahead) regardless of this
/// value, so it is safe to adjust during playback.
#[id = "lookahead"]
pub look_ahead_ms: FloatParam,
/// The full-band compressor controls (reused per band + 'All' channel later).
#[nested(group = "Compressor")]
pub comp: CompressorParams,
}
#[derive(Params)]
struct CompressorParams {
#[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 Codename206 {
fn default() -> Self {
Self {
params: Arc::new(Codename206Params::default()),
sample_rate: 48_000.0,
comp: Compressor::new(),
}
}
}
impl Default for Codename206Params {
fn default() -> Self {
Self {
editor_state: EguiState::from_size(360, 360),
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)),
comp: CompressorParams::default(),
}
}
}
impl Default for CompressorParams {
fn default() -> Self {
Self {
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: 1000.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: -12.0, max: 24.0 },
)
// Applied per sample, so smooth it to avoid zipper noise.
.with_smoother(SmoothingStyle::Linear(20.0))
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_rounded(1)),
bypass: BoolParam::new("Bypass", false),
}
}
}
impl Codename206 {
/// Look-ahead in samples for the current parameter value and sample rate.
fn lookahead_samples(&self) -> usize {
(self.params.look_ahead_ms.value() * 0.001 * self.sample_rate).round() as usize
}
}
impl Plugin for Codename206 {
2026-06-14 21:34:06 +09:00
const NAME: &'static str = "206 prototype";
const VENDOR: &'static str = "Novoyuuparosk";
const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
2026-06-14 21:34:06 +09:00
const EMAIL: &'static str = "mikkeli@novoyuuparosk.org";
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
AudioIOLayout {
main_input_channels: NonZeroU32::new(2),
main_output_channels: NonZeroU32::new(2),
..AudioIOLayout::const_default()
},
AudioIOLayout {
main_input_channels: NonZeroU32::new(1),
main_output_channels: NonZeroU32::new(1),
..AudioIOLayout::const_default()
},
];
const MIDI_INPUT: MidiConfig = MidiConfig::None;
const MIDI_OUTPUT: MidiConfig = MidiConfig::None;
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
type SysExMessage = ();
type BackgroundTask = ();
fn params(&self) -> Arc<dyn Params> {
self.params.clone()
}
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
let params = self.params.clone();
create_egui_editor(
self.params.editor_state.clone(),
(),
|_, _| {},
move |egui_ctx, setter, _state| {
egui::CentralPanel::default().show(egui_ctx, |ui| {
ui.heading(Self::NAME);
ui.separator();
egui::Grid::new("params").num_columns(2).show(ui, |ui| {
ui.label("Detection");
ui.add(widgets::ParamSlider::for_param(&params.comp.detection, setter));
ui.end_row();
ui.label("Threshold");
ui.add(widgets::ParamSlider::for_param(&params.comp.threshold_db, setter));
ui.end_row();
ui.label("Ratio");
ui.add(widgets::ParamSlider::for_param(&params.comp.ratio, setter));
ui.end_row();
ui.label("Knee");
ui.add(widgets::ParamSlider::for_param(&params.comp.knee_db, setter));
ui.end_row();
ui.label("Attack");
ui.add(widgets::ParamSlider::for_param(&params.comp.attack_ms, setter));
ui.end_row();
ui.label("Release");
ui.add(widgets::ParamSlider::for_param(&params.comp.release_ms, setter));
ui.end_row();
ui.label("Makeup");
ui.add(widgets::ParamSlider::for_param(&params.comp.makeup_db, setter));
ui.end_row();
ui.label("Look-ahead");
ui.add(widgets::ParamSlider::for_param(&params.look_ahead_ms, setter));
ui.end_row();
ui.label("Bypass");
ui.add(widgets::ParamSlider::for_param(&params.comp.bypass, setter));
ui.end_row();
});
});
},
)
}
fn initialize(
&mut self,
audio_io_layout: &AudioIOLayout,
buffer_config: &BufferConfig,
context: &mut impl InitContext<Self>,
) -> bool {
self.sample_rate = buffer_config.sample_rate;
let channels = audio_io_layout
.main_output_channels
.map(NonZeroU32::get)
.unwrap_or(2) as usize;
self.comp.prepare(self.sample_rate, channels, MAX_LOOKAHEAD_MS);
// Latency is constant (the fixed audio delay) and reported exactly once, so changing
// the look-ahead knob during playback never renegotiates latency with the host.
context.set_latency_samples(self.comp.latency());
true
}
fn reset(&mut self) {
self.comp.reset();
}
fn process(
&mut self,
buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext<Self>,
) -> ProcessStatus {
// Look-ahead is a detector-tap offset within a fixed delay; it never changes latency.
let lookahead = self.lookahead_samples();
// Block-rate compressor settings (these change slowly; makeup is smoothed per sample).
let c = &self.params.comp;
let mut set = CompressorSettings {
threshold_db: c.threshold_db.value(),
ratio: c.ratio.value(),
knee_db: c.knee_db.value(),
attack_coef: Compressor::time_to_coef(c.attack_ms.value(), self.sample_rate),
release_coef: Compressor::time_to_coef(c.release_ms.value(), self.sample_rate),
makeup_db: 0.0,
lookahead_samples: lookahead,
use_rms: c.detection.value() == DetectionMode::Rms,
bypass: c.bypass.value(),
};
let mut in_frame = [0.0f32; 2];
let mut out_frame = [0.0f32; 2];
for mut frame in buffer.iter_samples() {
set.makeup_db = c.makeup_db.smoothed.next();
let n = frame.len().min(2);
for ch in 0..n {
in_frame[ch] = *frame.get_mut(ch).unwrap();
}
self.comp.process(&in_frame[..n], &mut out_frame[..n], &set);
for ch in 0..n {
*frame.get_mut(ch).unwrap() = out_frame[ch];
}
}
ProcessStatus::Normal
}
}
impl ClapPlugin for Codename206 {
const CLAP_ID: &'static str = "com.mikkeli.codename-206";
const CLAP_DESCRIPTION: Option<&'static str> =
Some("Multiband compressor/limiter (stage 2: full-band compressor)");
const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
const CLAP_SUPPORT_URL: Option<&'static str> = None;
const CLAP_FEATURES: &'static [ClapFeature] = &[
ClapFeature::AudioEffect,
ClapFeature::Stereo,
ClapFeature::Mono,
ClapFeature::Compressor,
ClapFeature::Limiter,
];
}
impl Vst3Plugin for Codename206 {
const VST3_CLASS_ID: [u8; 16] = *b"Codename206Maxi!";
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
&[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics];
}
nih_export_clap!(Codename206);
nih_export_vst3!(Codename206);