2026-06-14 21:22:34 +09:00
|
|
|
use nih_plug::prelude::*;
|
2026-06-19 15:49:46 +09:00
|
|
|
use nih_plug_egui::{
|
|
|
|
|
create_egui_editor,
|
|
|
|
|
egui::{self, Vec2},
|
|
|
|
|
resizable_window::ResizableWindow,
|
|
|
|
|
widgets, EguiState,
|
|
|
|
|
};
|
2026-06-14 21:22:34 +09:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-06-15 19:33:31 +09:00
|
|
|
mod dsp;
|
|
|
|
|
use dsp::compressor::{Compressor, CompressorSettings, MAX_LOOKAHEAD_MS};
|
2026-06-17 20:04:24 +09:00
|
|
|
use dsp::crossover::Crossover;
|
2026-06-19 14:45:26 +09:00
|
|
|
use dsp::limiter::Limiter;
|
2026-06-15 19:33:31 +09:00
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
/// Band indices into the compressor array: low, mid, high, then the 'All' aggregate channel.
|
|
|
|
|
const LOW: usize = 0;
|
|
|
|
|
const MID: usize = 1;
|
|
|
|
|
const HIGH: usize = 2;
|
|
|
|
|
const ALL: usize = 3;
|
|
|
|
|
|
|
|
|
|
/// Level-detection mode for a compressor's detector.
|
2026-06-15 20:02:23 +09:00
|
|
|
#[derive(Enum, PartialEq, Clone, Copy)]
|
|
|
|
|
enum DetectionMode {
|
|
|
|
|
#[id = "peak"]
|
|
|
|
|
#[name = "Peak"]
|
|
|
|
|
Peak,
|
|
|
|
|
#[id = "rms"]
|
|
|
|
|
#[name = "RMS"]
|
|
|
|
|
Rms,
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
/// Codename 206 — Stage 3: 3-band crossover + per-band compressors summed into an 'All' channel.
|
2026-06-14 21:22:34 +09:00
|
|
|
///
|
2026-06-17 20:04:24 +09:00
|
|
|
/// Signal: input → LR4 crossover → {low, mid, high} each through their own compressor → sum →
|
|
|
|
|
/// 'All' compressor → output. Bypassing low+mid+high collapses it to a plain full-band comp
|
|
|
|
|
/// driven by the 'All' channel (the crossover sums flat).
|
2026-06-14 21:22:34 +09:00
|
|
|
struct Codename206 {
|
|
|
|
|
params: Arc<Codename206Params>,
|
2026-06-15 19:33:31 +09:00
|
|
|
sample_rate: f32,
|
2026-06-17 20:04:24 +09:00
|
|
|
crossover: Crossover,
|
|
|
|
|
/// Compressors indexed by [`LOW`], [`MID`], [`HIGH`], [`ALL`].
|
|
|
|
|
comps: [Compressor; 4],
|
2026-06-19 14:45:26 +09:00
|
|
|
/// Output brickwall limiter (final stage).
|
|
|
|
|
limiter: Limiter,
|
2026-06-14 21:22:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Params)]
|
|
|
|
|
struct Codename206Params {
|
|
|
|
|
#[persist = "editor-state"]
|
|
|
|
|
editor_state: Arc<EguiState>,
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
/// 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).
|
2026-06-15 19:33:31 +09:00
|
|
|
#[id = "lookahead"]
|
|
|
|
|
pub look_ahead_ms: FloatParam,
|
|
|
|
|
|
2026-06-19 14:45:26 +09:00
|
|
|
/// 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,
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
#[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,
|
2026-06-15 19:33:31 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Params)]
|
|
|
|
|
struct CompressorParams {
|
2026-06-15 20:02:23 +09:00
|
|
|
#[id = "detect"]
|
|
|
|
|
pub detection: EnumParam<DetectionMode>,
|
2026-06-15 19:33:31 +09:00
|
|
|
#[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,
|
2026-06-14 21:22:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Codename206 {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
params: Arc::new(Codename206Params::default()),
|
2026-06-15 19:33:31 +09:00
|
|
|
sample_rate: 48_000.0,
|
2026-06-17 20:04:24 +09:00
|
|
|
crossover: Crossover::new(),
|
|
|
|
|
comps: [Compressor::new(), Compressor::new(), Compressor::new(), Compressor::new()],
|
2026-06-19 14:45:26 +09:00
|
|
|
limiter: Limiter::new(),
|
2026-06-14 21:22:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Codename206Params {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
2026-06-17 20:04:24 +09:00
|
|
|
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()),
|
2026-06-15 19:33:31 +09:00
|
|
|
|
|
|
|
|
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)),
|
|
|
|
|
|
2026-06-19 14:45:26 +09:00
|
|
|
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)),
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
low: CompressorParams::default(),
|
|
|
|
|
mid: CompressorParams::default(),
|
|
|
|
|
high: CompressorParams::default(),
|
|
|
|
|
all: CompressorParams::default(),
|
2026-06-15 19:33:31 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for CompressorParams {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
2026-06-15 20:02:23 +09:00
|
|
|
detection: EnumParam::new("Detection", DetectionMode::Peak),
|
|
|
|
|
|
2026-06-15 19:33:31 +09:00
|
|
|
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())
|
|
|
|
|
})),
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
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)),
|
2026-06-15 19:33:31 +09:00
|
|
|
|
|
|
|
|
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,
|
2026-06-17 20:04:24 +09:00
|
|
|
FloatRange::Skewed { min: 1.0, max: 1_000.0, factor: FloatRange::skew_factor(-2.0) },
|
2026-06-15 19:33:31 +09:00
|
|
|
)
|
|
|
|
|
.with_unit(" ms")
|
|
|
|
|
.with_value_to_string(formatters::v2s_f32_rounded(1)),
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
makeup_db: FloatParam::new("Makeup", 0.0, FloatRange::Linear { min: -12.0, max: 24.0 })
|
|
|
|
|
.with_smoother(SmoothingStyle::Linear(20.0))
|
|
|
|
|
.with_unit(" dB")
|
|
|
|
|
.with_value_to_string(formatters::v2s_f32_rounded(1)),
|
2026-06-15 19:33:31 +09:00
|
|
|
|
|
|
|
|
bypass: BoolParam::new("Bypass", false),
|
2026-06-14 21:22:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
/// Build the per-block compressor settings for one channel's params (makeup filled per sample).
|
|
|
|
|
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(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 19:33:31 +09:00
|
|
|
impl Codename206 {
|
|
|
|
|
fn lookahead_samples(&self) -> usize {
|
|
|
|
|
(self.params.look_ahead_ms.value() * 0.001 * self.sample_rate).round() as usize
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 21:22:34 +09:00
|
|
|
impl Plugin for Codename206 {
|
2026-06-14 21:34:06 +09:00
|
|
|
const NAME: &'static str = "206 prototype";
|
|
|
|
|
const VENDOR: &'static str = "Novoyuuparosk";
|
2026-06-14 21:22:34 +09:00
|
|
|
const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
|
2026-06-14 21:34:06 +09:00
|
|
|
const EMAIL: &'static str = "mikkeli@novoyuuparosk.org";
|
2026-06-14 21:22:34 +09:00
|
|
|
|
|
|
|
|
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();
|
2026-06-19 15:49:46 +09:00
|
|
|
let egui_state = self.params.editor_state.clone();
|
2026-06-14 21:22:34 +09:00
|
|
|
create_egui_editor(
|
|
|
|
|
self.params.editor_state.clone(),
|
|
|
|
|
(),
|
|
|
|
|
|_, _| {},
|
|
|
|
|
move |egui_ctx, setter, _state| {
|
2026-06-17 20:04:24 +09:00
|
|
|
// One column of controls for a single compressor channel.
|
|
|
|
|
let band_col = |ui: &mut egui::Ui, title: &str, p: &CompressorParams| {
|
|
|
|
|
ui.strong(title);
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.detection, setter));
|
|
|
|
|
ui.label("Threshold");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.threshold_db, setter));
|
|
|
|
|
ui.label("Ratio");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.ratio, setter));
|
|
|
|
|
ui.label("Knee");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.knee_db, setter));
|
|
|
|
|
ui.label("Attack");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.attack_ms, setter));
|
|
|
|
|
ui.label("Release");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.release_ms, setter));
|
|
|
|
|
ui.label("Makeup");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.makeup_db, setter));
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(&p.bypass, setter));
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-19 15:49:46 +09:00
|
|
|
// Resizable window; vertical scroll so every control stays reachable even when the
|
|
|
|
|
// window is small. (Placeholder layout — Stage 6 will replace it.)
|
|
|
|
|
ResizableWindow::new("editor")
|
|
|
|
|
.min_size(Vec2::new(480.0, 320.0))
|
|
|
|
|
.show(egui_ctx, egui_state.as_ref(), |ui| {
|
|
|
|
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
|
|
|
|
ui.heading(Self::NAME);
|
|
|
|
|
// Global controls stacked vertically so they never overflow sideways.
|
|
|
|
|
egui::Grid::new("globals").num_columns(2).show(ui, |ui| {
|
|
|
|
|
ui.label("Xover Lo/Mid");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_low_hz, setter));
|
|
|
|
|
ui.end_row();
|
|
|
|
|
ui.label("Xover Mid/Hi");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_high_hz, setter));
|
|
|
|
|
ui.end_row();
|
|
|
|
|
ui.label("Look-ahead");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(¶ms.look_ahead_ms, setter));
|
|
|
|
|
ui.end_row();
|
|
|
|
|
ui.label("Ceiling");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(¶ms.output_ceiling_db, setter));
|
|
|
|
|
ui.end_row();
|
|
|
|
|
ui.label("Lim Release");
|
|
|
|
|
ui.add(widgets::ParamSlider::for_param(¶ms.limiter_release_ms, setter));
|
|
|
|
|
ui.end_row();
|
|
|
|
|
});
|
|
|
|
|
ui.separator();
|
|
|
|
|
ui.columns(4, |cols| {
|
|
|
|
|
band_col(&mut cols[0], "LOW", ¶ms.low);
|
|
|
|
|
band_col(&mut cols[1], "MID", ¶ms.mid);
|
|
|
|
|
band_col(&mut cols[2], "HIGH", ¶ms.high);
|
|
|
|
|
band_col(&mut cols[3], "ALL", ¶ms.all);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-17 20:04:24 +09:00
|
|
|
});
|
2026-06-14 21:22:34 +09:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 19:33:31 +09:00
|
|
|
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;
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
for comp in &mut self.comps {
|
|
|
|
|
comp.prepare(self.sample_rate, channels, MAX_LOOKAHEAD_MS);
|
|
|
|
|
}
|
|
|
|
|
self.crossover.prepare(channels);
|
|
|
|
|
self.crossover.update(
|
|
|
|
|
self.sample_rate,
|
|
|
|
|
self.params.crossover_low_hz.value(),
|
|
|
|
|
self.params.crossover_high_hz.value(),
|
|
|
|
|
);
|
2026-06-19 14:45:26 +09:00
|
|
|
self.limiter.prepare(self.sample_rate, channels);
|
2026-06-17 20:04:24 +09:00
|
|
|
|
2026-06-19 14:45:26 +09:00
|
|
|
// Three series stages each with a fixed look-ahead delay: the bands, the 'All' channel,
|
|
|
|
|
// and the output limiter. Reported once as a constant; see the compressor look-ahead note.
|
|
|
|
|
let total_latency =
|
|
|
|
|
self.comps[LOW].latency() + self.comps[ALL].latency() + self.limiter.latency();
|
2026-06-17 20:04:24 +09:00
|
|
|
context.set_latency_samples(total_latency);
|
2026-06-15 19:33:31 +09:00
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reset(&mut self) {
|
2026-06-17 20:04:24 +09:00
|
|
|
self.crossover.reset();
|
|
|
|
|
for comp in &mut self.comps {
|
|
|
|
|
comp.reset();
|
|
|
|
|
}
|
2026-06-19 14:45:26 +09:00
|
|
|
self.limiter.reset();
|
2026-06-15 19:33:31 +09:00
|
|
|
}
|
|
|
|
|
|
2026-06-14 21:22:34 +09:00
|
|
|
fn process(
|
|
|
|
|
&mut self,
|
|
|
|
|
buffer: &mut Buffer,
|
|
|
|
|
_aux: &mut AuxiliaryBuffers,
|
|
|
|
|
_context: &mut impl ProcessContext<Self>,
|
|
|
|
|
) -> ProcessStatus {
|
2026-06-15 19:33:31 +09:00
|
|
|
let lookahead = self.lookahead_samples();
|
|
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
// Crossover coefficients track the frequency params (recomputed per block — cheap).
|
|
|
|
|
self.crossover.update(
|
|
|
|
|
self.sample_rate,
|
|
|
|
|
self.params.crossover_low_hz.value(),
|
|
|
|
|
self.params.crossover_high_hz.value(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Block-rate settings for the three bands + the 'All' channel.
|
|
|
|
|
let band_params = [&self.params.low, &self.params.mid, &self.params.high];
|
|
|
|
|
let mut band_set = [
|
|
|
|
|
build_settings(&self.params.low, lookahead, self.sample_rate),
|
|
|
|
|
build_settings(&self.params.mid, lookahead, self.sample_rate),
|
|
|
|
|
build_settings(&self.params.high, lookahead, self.sample_rate),
|
|
|
|
|
];
|
|
|
|
|
let mut all_set = build_settings(&self.params.all, lookahead, self.sample_rate);
|
2026-06-15 19:33:31 +09:00
|
|
|
|
2026-06-19 14:45:26 +09:00
|
|
|
// Output limiter settings (block-rate).
|
|
|
|
|
let ceiling = util::db_to_gain(self.params.output_ceiling_db.value());
|
|
|
|
|
let limiter_release =
|
|
|
|
|
Compressor::time_to_coef(self.params.limiter_release_ms.value(), self.sample_rate);
|
|
|
|
|
|
2026-06-15 19:33:31 +09:00
|
|
|
let mut in_frame = [0.0f32; 2];
|
2026-06-17 20:04:24 +09:00
|
|
|
let mut band_in = [[0.0f32; 2]; 3];
|
|
|
|
|
let mut band_out = [[0.0f32; 2]; 3];
|
|
|
|
|
let mut summed = [0.0f32; 2];
|
2026-06-15 19:33:31 +09:00
|
|
|
let mut out_frame = [0.0f32; 2];
|
2026-06-19 14:45:26 +09:00
|
|
|
let mut lim_frame = [0.0f32; 2];
|
2026-06-15 19:33:31 +09:00
|
|
|
|
2026-06-17 20:04:24 +09:00
|
|
|
for mut frame in buffer.iter_samples() {
|
2026-06-15 19:33:31 +09:00
|
|
|
let n = frame.len().min(2);
|
|
|
|
|
for ch in 0..n {
|
|
|
|
|
in_frame[ch] = *frame.get_mut(ch).unwrap();
|
|
|
|
|
}
|
2026-06-17 20:04:24 +09:00
|
|
|
|
|
|
|
|
// Split each channel into low/mid/high.
|
|
|
|
|
for ch in 0..n {
|
|
|
|
|
let [lo, mid, hi] = self.crossover.split(ch, in_frame[ch]);
|
|
|
|
|
band_in[LOW][ch] = lo;
|
|
|
|
|
band_in[MID][ch] = mid;
|
|
|
|
|
band_in[HIGH][ch] = hi;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compress each band (per-sample smoothed makeup), then sum.
|
|
|
|
|
summed[..n].fill(0.0);
|
|
|
|
|
for b in 0..3 {
|
|
|
|
|
band_set[b].makeup_db = band_params[b].makeup_db.smoothed.next();
|
|
|
|
|
self.comps[b].process(&band_in[b][..n], &mut band_out[b][..n], &band_set[b]);
|
|
|
|
|
for ch in 0..n {
|
|
|
|
|
summed[ch] += band_out[b][ch];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 'All' aggregate channel over the summed bands.
|
|
|
|
|
all_set.makeup_db = self.params.all.makeup_db.smoothed.next();
|
|
|
|
|
self.comps[ALL].process(&summed[..n], &mut out_frame[..n], &all_set);
|
|
|
|
|
|
2026-06-19 14:45:26 +09:00
|
|
|
// Output brickwall limiter.
|
|
|
|
|
self.limiter.process(&out_frame[..n], &mut lim_frame[..n], ceiling, limiter_release);
|
|
|
|
|
|
2026-06-15 19:33:31 +09:00
|
|
|
for ch in 0..n {
|
2026-06-19 14:45:26 +09:00
|
|
|
*frame.get_mut(ch).unwrap() = lim_frame[ch];
|
2026-06-14 21:22:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ProcessStatus::Normal
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ClapPlugin for Codename206 {
|
|
|
|
|
const CLAP_ID: &'static str = "com.mikkeli.codename-206";
|
|
|
|
|
const CLAP_DESCRIPTION: Option<&'static str> =
|
2026-06-17 20:04:24 +09:00
|
|
|
Some("Multiband compressor/limiter (stage 3: 3-band + 'All' channel)");
|
2026-06-14 21:22:34 +09:00
|
|
|
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);
|