Stage 2: full-band compressor with constant-latency look-ahead

Implements the single full-band feed-forward compressor (the engine that will be
reused per band + for the 'All' channel). Design follows Giannoulis et al. 2012:
log-domain gain computer with a quadratic soft knee feeding a smooth decoupled
peak detector for attack/release ballistics. Stereo-linked peak detection.

Look-ahead uses a fixed audio delay with a constant reported latency (set once in
initialize); the knob only moves the detector tap within that delay. This avoids
renegotiating latency from process(), which crashed FL Studio when the look-ahead
was adjusted during playback.

- src/dsp/compressor.rs: Compressor + CompressorSettings, RT-safe (no alloc in
  process; buffers sized in prepare; envelope denormals flushed in-code)
- src/lib.rs: nested CompressorParams (threshold/ratio/knee/attack/release/makeup/
  bypass) + global look-ahead; egui ParamSlider grid; latency reported once
- 5 unit tests (static curve, knee continuity, steady-state convergence, constant
  latency); Cargo.toml lib crate-type added so tests link
- README: 'All' channel architecture already documented; look-ahead spec updated

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-06-15 19:33:31 +09:00
parent d9dc61cf90
commit 8d9eaeaffc
5 changed files with 503 additions and 35 deletions
+2 -1
View File
@@ -14,7 +14,8 @@ license = "GPL-3.0-or-later"
members = ["xtask"] members = ["xtask"]
[lib] [lib]
crate-type = ["cdylib"] # `cdylib` is the plugin binary; `lib` (rlib) lets `cargo test` link the unit tests.
crate-type = ["cdylib", "lib"]
[dependencies] [dependencies]
# Pinned to the exact commit of the local ./nih-plug reference clone so the build # Pinned to the exact commit of the local ./nih-plug reference clone so the build
+1 -1
View File
@@ -86,7 +86,7 @@ a first-class mode, not an afterthought.
### Global ### Global
- `input_gain` — pre-gain before filterbank (dB) - `input_gain` — pre-gain before filterbank (dB)
- `output_ceiling` — brickwall ceiling (dBFS, default 0.0) - `output_ceiling` — brickwall ceiling (dBFS, default 0.0)
- `look_ahead_ms` — look-ahead time (010 ms) - `look_ahead_ms` — look-ahead time (05 ms). Reported latency is **constant** (the max look-ahead); the knob only moves the detector tap within that fixed delay, so it is safe to adjust during playback (changing reported latency mid-stream crashes some hosts, FL included)
- `crossover_low_hz` — low/mid crossover frequency - `crossover_low_hz` — low/mid crossover frequency
- `crossover_high_hz` — mid/high crossover frequency - `crossover_high_hz` — mid/high crossover frequency
### Per-Channel Compressor (× 4: low, mid, high, **all** — one `#[nested]` params struct reused) ### Per-Channel Compressor (× 4: low, mid, high, **all** — one `#[nested]` params struct reused)
+301
View File
@@ -0,0 +1,301 @@
//! Single full-band feed-forward compressor with look-ahead.
//!
//! Design follows Giannoulis, Massberg & Reiss, "Digital Dynamic Range Compressor
//! Design — A Tutorial and Analysis" (JAES 2012):
//!
//! * a **log-domain gain computer** with a quadratic **soft knee**, and
//! * a **smooth, decoupled peak detector** for the attack/release ballistics
//! (their preferred topology — avoids the artefacts of naive branching smoothers).
//!
//! Detection is **stereo-linked** (the control signal is `max(|ch|)` across channels)
//! so a single gain is applied to every channel and the stereo image is preserved.
//!
//! Look-ahead is a per-channel delay line on the audio path: the output sample is the
//! input from `L` samples ago, while the gain is computed from the *current* input —
//! so the gain reduction leads the audio by `L` samples. `L` is the plugin's latency.
/// Maximum look-ahead. This is also the **fixed** latency the plugin reports: the audio is
/// always delayed by this much and the latency is reported once, so the look-ahead knob can be
/// adjusted during playback without ever renegotiating latency with the host (which crashes
/// some DAWs, FL included). The knob only moves where the detector taps within this window.
pub const MAX_LOOKAHEAD_MS: f32 = 5.0;
/// Most channels we ever process in one frame (our audio layouts are mono/stereo).
const MAX_CHANNELS: usize = 2;
/// ~ -240 dBFS; keeps `log10` away from zero without affecting audible levels.
const LEVEL_EPS: f32 = 1e-12;
/// Flush a decaying envelope value to zero once it is far below audibility, so the
/// exponential tail can't drift into denormal range (which causes CPU spikes).
#[inline]
fn flush_denormal(x: f32) -> f32 {
if x.abs() < 1e-30 {
0.0
} else {
x
}
}
/// Per-block compressor settings. Cheap to copy; rebuilt each process block from params.
#[derive(Clone, Copy)]
pub struct CompressorSettings {
pub threshold_db: f32,
pub ratio: f32,
pub knee_db: f32,
/// One-pole coefficient for the attack ramp (see [`Compressor::time_to_coef`]).
pub attack_coef: f32,
/// One-pole coefficient for the release ramp.
pub release_coef: f32,
pub makeup_db: f32,
/// How far (in samples) the detector reads *ahead* of the output, 0..=`fixed_delay`.
/// This does NOT change the reported latency — the audio delay is always `fixed_delay`.
pub lookahead_samples: usize,
pub bypass: bool,
}
pub struct Compressor {
sample_rate: f32,
/// Per-channel circular delay line, each `capacity` samples long.
delay: Vec<Vec<f32>>,
capacity: usize,
write_pos: usize,
/// Constant audio delay applied to every sample == the reported plugin latency.
fixed_delay: usize,
/// Smooth decoupled peak-detector state, expressed as dB of **attenuation** (>= 0).
y1: f32, // release branch (peak-with-decay)
yl: f32, // attack-smoothed output
}
impl Default for Compressor {
fn default() -> Self {
Self {
sample_rate: 48_000.0,
delay: Vec::new(),
capacity: 0,
write_pos: 0,
fixed_delay: 0,
y1: 0.0,
yl: 0.0,
}
}
}
impl Compressor {
pub fn new() -> Self {
Self::default()
}
/// Allocate delay buffers for the worst-case look-ahead. Call from `initialize()`,
/// where allocation is allowed — never from `process()`.
pub fn prepare(&mut self, sample_rate: f32, num_channels: usize, max_lookahead_ms: f32) {
self.sample_rate = sample_rate;
self.fixed_delay = (max_lookahead_ms * 0.001 * sample_rate).ceil() as usize;
// +1 so the oldest (output) sample and the newest (write) sample never alias.
self.capacity = self.fixed_delay + 1;
let channels = num_channels.clamp(1, MAX_CHANNELS);
self.delay = vec![vec![0.0; self.capacity]; channels];
self.reset();
}
/// Clear all state. Real-time safe (no allocation).
pub fn reset(&mut self) {
for ch in self.delay.iter_mut() {
ch.iter_mut().for_each(|s| *s = 0.0);
}
self.write_pos = 0;
self.y1 = 0.0;
self.yl = 0.0;
}
/// Convert an attack/release time in milliseconds to a one-pole smoothing coefficient.
///
/// Convention: after `time_ms`, a step response reaches ~63% (1 1/e) of its target.
/// 0 ms (or less) yields coefficient 0 = instantaneous.
pub fn time_to_coef(time_ms: f32, sample_rate: f32) -> f32 {
if time_ms <= 0.0 {
0.0
} else {
(-1.0 / (time_ms * 0.001 * sample_rate)).exp()
}
}
/// Static compressor curve. Returns gain reduction in dB (<= 0) for an input `level_db`.
/// Quadratic soft knee of width `knee_db`, centred on `threshold_db`.
fn gain_computer(level_db: f32, threshold_db: f32, ratio: f32, knee_db: f32) -> f32 {
let slope = 1.0 / ratio - 1.0; // <= 0 for ratio >= 1
let over = level_db - threshold_db;
if knee_db > 0.0 && 2.0 * over.abs() <= knee_db {
// Inside the knee: a parabola joining the two regions with a continuous slope.
let x = over + knee_db * 0.5; // 0..knee
slope * x * x / (2.0 * knee_db)
} else if over > 0.0 {
// Above the knee (also covers the hard-knee case): linear region.
slope * over
} else {
0.0
}
}
/// The plugin's fixed reported latency in samples (the constant audio delay).
pub fn latency(&self) -> u32 {
self.fixed_delay as u32
}
/// Process one sample frame in place: `input[ch]` -> `output[ch]`.
///
/// `input` and `output` are short stack slices (one value per channel), so this
/// performs no allocation. Detection is linked across the provided channels.
///
/// The audio is always delayed by `fixed_delay`; `set.lookahead_samples` (0..=fixed_delay)
/// only chooses how far *ahead* of the output the detector taps, so changing it never
/// alters latency.
pub fn process(&mut self, input: &[f32], output: &mut [f32], set: &CompressorSettings) {
debug_assert_eq!(input.len(), output.len());
let n = input.len().min(self.delay.len());
let l = set.lookahead_samples.min(self.fixed_delay);
// 1) Write the current input into the delay lines.
for ch in 0..n {
self.delay[ch][self.write_pos] = input[ch];
}
// 2) Tap positions (circular). Output is always `fixed_delay` old; the detector reads
// `l` samples newer than the output, i.e. `l` samples into the output's future.
let out_pos = (self.write_pos + self.capacity - self.fixed_delay) % self.capacity;
let det_pos = (self.write_pos + self.capacity - (self.fixed_delay - l)) % self.capacity;
// 3) Linked peak detector at the look-ahead tap.
let mut peak = 0.0f32;
for ch in 0..n {
peak = peak.max(self.delay[ch][det_pos].abs());
}
// 4) Gain computer + ballistics. On bypass we keep the delay aligned (so toggling
// bypass doesn't shift timing) but apply unity gain and no makeup.
let gain_lin = if set.bypass {
1.0
} else {
let level_db = 20.0 * (peak + LEVEL_EPS).log10();
// Desired attenuation in dB, as a positive quantity.
let target = -Self::gain_computer(level_db, set.threshold_db, set.ratio, set.knee_db);
// Smooth, decoupled peak detector (Giannoulis eq. 1718) on the attenuation:
// y1 = max(target, release-smoothed y1) (fast up / slow down "peak hold")
// yl = attack-smoothed y1
self.y1 = flush_denormal(
target.max(set.release_coef * self.y1 + (1.0 - set.release_coef) * target),
);
self.yl = flush_denormal(set.attack_coef * self.yl + (1.0 - set.attack_coef) * self.y1);
let total_db = set.makeup_db - self.yl;
10.0f32.powf(total_db / 20.0)
};
// 5) Output = delayed input (always `fixed_delay` old) * gain.
for ch in 0..n {
output[ch] = self.delay[ch][out_pos] * gain_lin;
}
// 6) Advance the write head.
self.write_pos = (self.write_pos + 1) % self.capacity;
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f32 = 48_000.0;
fn assert_close(a: f32, b: f32, tol: f32) {
assert!((a - b).abs() <= tol, "expected {a} ≈ {b} (tol {tol})");
}
fn settings(threshold_db: f32, ratio: f32, knee_db: f32) -> CompressorSettings {
CompressorSettings {
threshold_db,
ratio,
knee_db,
attack_coef: Compressor::time_to_coef(1.0, SR),
release_coef: Compressor::time_to_coef(1.0, SR),
makeup_db: 0.0,
lookahead_samples: 0,
bypass: false,
}
}
#[test]
fn below_threshold_is_untouched() {
// -30 dB input, -20 dB threshold -> no reduction.
assert_eq!(Compressor::gain_computer(-30.0, -20.0, 4.0, 6.0), 0.0);
}
#[test]
fn above_knee_follows_ratio() {
// 10 dB over threshold at 4:1 -> output only 2.5 dB over -> 7.5 dB reduction.
let r = Compressor::gain_computer(-10.0, -20.0, 4.0, 0.0);
assert_close(r, -7.5, 1e-4);
}
#[test]
fn knee_is_continuous_with_linear_region() {
// At the upper knee edge the soft-knee and linear formulas must agree.
let (t, ratio, knee) = (0.0, 4.0, 6.0);
let edge = t + knee / 2.0;
let knee_val = Compressor::gain_computer(edge, t, ratio, knee);
let linear_val = (1.0 / ratio - 1.0) * (edge - t);
assert_close(knee_val, linear_val, 1e-4);
// At the lower edge there is still no reduction.
assert_close(Compressor::gain_computer(t - knee / 2.0, t, ratio, knee), 0.0, 1e-6);
}
#[test]
fn steady_state_matches_static_curve() {
// Constant 0.5 (-6.02 dBFS) into a -18 dB / 2:1 / hard-knee comp:
// reduction = -0.5 * (6.02 18) = 5.99 dB, so output ≈ 0.5 * 10^(5.99/20).
let mut comp = Compressor::new();
comp.prepare(SR, 1, MAX_LOOKAHEAD_MS);
let set = settings(-18.0, 2.0, 0.0);
let mut out = [0.0f32];
for _ in 0..SR as usize {
comp.process(&[0.5], &mut out, &set);
}
let level_db = 20.0 * 0.5f32.log10();
let reduction = (1.0 / 2.0 - 1.0) * (level_db - (-18.0));
let expected = 0.5 * 10.0f32.powf(reduction / 20.0);
assert_close(out[0], expected, 1e-3);
}
#[test]
fn latency_is_constant_regardless_of_lookahead() {
// Audio is always delayed by `fixed_delay` (== reported latency); the look-ahead
// knob must NOT change that (this is what keeps live adjustment from renegotiating
// latency and crashing the host).
let mut comp = Compressor::new();
comp.prepare(SR, 1, 0.5); // small max so the fixed delay is quick to test
let d = comp.latency() as usize;
assert!(d > 0);
for &l in &[0usize, d / 2, d] {
comp.reset();
let mut set = settings(0.0, 1.0, 0.0);
set.bypass = true; // unity gain -> isolate the delay behaviour
set.lookahead_samples = l;
let mut out = [0.0f32];
for n in 0..(d + 5) {
let x = if n == 0 { 1.0 } else { 0.0 };
comp.process(&[x], &mut out, &set);
if n == d {
assert_close(out[0], 1.0, 1e-6); // impulse always emerges after `d`, any L
} else {
assert_close(out[0], 0.0, 1e-6);
}
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
//! DSP building blocks for Codename 206.
//!
//! Stage 2 introduces the full-band compressor (also the engine that will be reused
//! per band and for the 'All' aggregate channel — see README.md). Later stages add the
//! crossover filterbank, output limiter, and oversampler alongside it.
pub mod compressor;
+192 -33
View File
@@ -2,31 +2,59 @@ use nih_plug::prelude::*;
use nih_plug_egui::{create_egui_editor, egui, widgets, EguiState}; use nih_plug_egui::{create_egui_editor, egui, widgets, EguiState};
use std::sync::Arc; use std::sync::Arc;
/// Codename 206 — landmark build. mod dsp;
use dsp::compressor::{Compressor, CompressorSettings, MAX_LOOKAHEAD_MS};
/// Codename 206 — Stage 2: a single full-band compressor with look-ahead.
/// ///
/// This is intentionally tiny: a single full-band gain parameter exposed through one /// The `CompressorParams` struct is `#[nested]` so the exact same controls + DSP can be
/// egui slider. It is the foundation the multiband compressor/limiter (see README.md) /// reused for the three bands and the 'All' aggregate channel in later stages.
/// will be built on top of. The signal path is currently just `sample *= gain`.
struct Codename206 { struct Codename206 {
params: Arc<Codename206Params>, params: Arc<Codename206Params>,
sample_rate: f32,
comp: Compressor,
} }
#[derive(Params)] #[derive(Params)]
struct Codename206Params { struct Codename206Params {
/// The egui editor's window state (size). Persisted with the parameter state so a
/// resized window is restored on reload.
#[persist = "editor-state"] #[persist = "editor-state"]
editor_state: Arc<EguiState>, editor_state: Arc<EguiState>,
/// Full-band output gain. Stored as a linear gain multiplier but displayed in dB. /// Look-ahead time: how far ahead the detector reads so gain reduction can lead
#[id = "gain"] /// transients. The reported latency is constant (the max look-ahead) regardless of this
pub gain: FloatParam, /// 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 = "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 { impl Default for Codename206 {
fn default() -> Self { fn default() -> Self {
Self { Self {
params: Arc::new(Codename206Params::default()), params: Arc::new(Codename206Params::default()),
sample_rate: 48_000.0,
comp: Compressor::new(),
} }
} }
} }
@@ -34,28 +62,88 @@ impl Default for Codename206 {
impl Default for Codename206Params { impl Default for Codename206Params {
fn default() -> Self { fn default() -> Self {
Self { Self {
editor_state: EguiState::from_size(300, 140), editor_state: EguiState::from_size(360, 360),
// Stored as linear gain, displayed/edited in dB. Skewed so the slider feels look_ahead_ms: FloatParam::new(
// linear in decibels across the -30..+30 dB range. "Look-ahead",
gain: FloatParam::new( 2.0,
"Gain", FloatRange::Linear { min: 0.0, max: MAX_LOOKAHEAD_MS },
util::db_to_gain(0.0),
FloatRange::Skewed {
min: util::db_to_gain(-30.0),
max: util::db_to_gain(30.0),
factor: FloatRange::gain_skew_factor(-30.0, 30.0),
},
) )
// Linear-gain storage needs logarithmic smoothing to avoid zipper noise. .with_unit(" ms")
.with_smoother(SmoothingStyle::Logarithmic(50.0)) .with_value_to_string(formatters::v2s_f32_rounded(2)),
.with_unit(" dB")
.with_value_to_string(formatters::v2s_f32_gain_to_db(2)) comp: CompressorParams::default(),
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
} }
} }
} }
impl Default for CompressorParams {
fn default() -> Self {
Self {
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 { impl Plugin for Codename206 {
const NAME: &'static str = "206 prototype"; const NAME: &'static str = "206 prototype";
const VENDOR: &'static str = "Novoyuuparosk"; const VENDOR: &'static str = "Novoyuuparosk";
@@ -97,25 +185,96 @@ impl Plugin for Codename206 {
|_, _| {}, |_, _| {},
move |egui_ctx, setter, _state| { move |egui_ctx, setter, _state| {
egui::CentralPanel::default().show(egui_ctx, |ui| { egui::CentralPanel::default().show(egui_ctx, |ui| {
ui.heading("Codename 206"); ui.heading(Self::NAME);
ui.separator(); ui.separator();
ui.label("Gain"); egui::Grid::new("params").num_columns(2).show(ui, |ui| {
ui.add(widgets::ParamSlider::for_param(&params.gain, setter)); 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( fn process(
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext<Self>, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
for channel_samples in buffer.iter_samples() { // Look-ahead is a detector-tap offset within a fixed delay; it never changes latency.
let gain = self.params.gain.smoothed.next(); let lookahead = self.lookahead_samples();
for sample in channel_samples {
*sample *= gain; // 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,
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];
} }
} }
@@ -126,7 +285,7 @@ impl Plugin for Codename206 {
impl ClapPlugin for Codename206 { impl ClapPlugin for Codename206 {
const CLAP_ID: &'static str = "com.mikkeli.codename-206"; const CLAP_ID: &'static str = "com.mikkeli.codename-206";
const CLAP_DESCRIPTION: Option<&'static str> = const CLAP_DESCRIPTION: Option<&'static str> =
Some("Multiband compressor/limiter (landmark: full-band gain)"); Some("Multiband compressor/limiter (stage 2: full-band compressor)");
const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
const CLAP_SUPPORT_URL: Option<&'static str> = None; const CLAP_SUPPORT_URL: Option<&'static str> = None;
const CLAP_FEATURES: &'static [ClapFeature] = &[ const CLAP_FEATURES: &'static [ClapFeature] = &[