Stage 4a: base-rate look-ahead brickwall limiter
Adds the output limiter stage after the 'All' channel. Guarantees the output never exceeds the ceiling: fixed 1.5 ms look-ahead, stereo-linked sliding-max peak detection over the look-ahead window -> gain = ceiling/window_max, decoupled smoothing (fast attack / user release), and a final clamp as the hard guarantee. - src/dsp/limiter.rs: Limiter (sample-peak; true-peak via oversampling is 4b) - src/lib.rs: wired as final stage; new globals output_ceiling_db (-24..0) and limiter_release_ms; latency now the constant three-stage total (bands+All+limiter); two UI sliders added to the global row - 14 unit tests (4 new: ceiling guarantee on spikes, loud-sine limiting, transparency below ceiling, latency) - README/docs updated (Stage 4 split into 4a done / 4b oversampling) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -73,9 +73,9 @@ a first-class mode, not an afterthought.
|
|||||||
- Bands are individually bypassable; with all three bypassed the (phase-coherent) crossover sum equals the dry input, so the 'All' channel alone acts as a full-band comp/lim
|
- Bands are individually bypassable; with all three bypassed the (phase-coherent) crossover sum equals the dry input, so the 'All' channel alone acts as a full-band comp/lim
|
||||||
- Has its own look-ahead; the plugin reports a single **constant** total latency (the fixed band + 'All' look-ahead), set once — see Latency below
|
- Has its own look-ahead; the plugin reports a single **constant** total latency (the fixed band + 'All' look-ahead), set once — see Latency below
|
||||||
### Output Limiter
|
### Output Limiter
|
||||||
- True-peak brickwall (ceiling = 0 dBFS or user-defined)
|
- Brickwall, ceiling = 0 dBFS or user-defined (`output_ceiling`). Look-ahead + sliding-max peak detection + a ceiling clamp guarantee the output never exceeds the ceiling
|
||||||
- 4x oversampling for inter-sample peak detection
|
- Short attack (≤ 0.1 ms), auto-release (release time user-set)
|
||||||
- Short attack (≤ 0.1 ms), auto-release
|
- **Sample-peak today**; 4x-oversampled true-peak (inter-sample) detection is the remaining Stage-4 work
|
||||||
### Latency
|
### Latency
|
||||||
- Reported via `context.set_latency_samples()` in `initialize()` — **never** from `process()`; renegotiating latency mid-stream crashes some hosts (FL included)
|
- Reported via `context.set_latency_samples()` in `initialize()` — **never** from `process()`; renegotiating latency mid-stream crashes some hosts (FL included)
|
||||||
- Reported latency is a **constant** (the max look-ahead); the look-ahead control only moves the detector tap within that fixed delay
|
- Reported latency is a **constant** (the max look-ahead); the look-ahead control only moves the detector tap within that fixed delay
|
||||||
@@ -87,6 +87,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)
|
||||||
|
- `limiter_release_ms` — output limiter release time
|
||||||
- `look_ahead_ms` — look-ahead time (0–5 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)
|
- `look_ahead_ms` — look-ahead time (0–5 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
|
||||||
@@ -116,8 +117,8 @@ src/
|
|||||||
compressor.rs # ✅ full-band comp: peak/RMS detector, gain computer, ballistics, look-ahead delay
|
compressor.rs # ✅ full-band comp: peak/RMS detector, gain computer, ballistics, look-ahead delay
|
||||||
crossover.rs # ✅ LR4 3-band filterbank with all-pass phase compensation
|
crossover.rs # ✅ LR4 3-band filterbank with all-pass phase compensation
|
||||||
biquad.rs # ✅ generic biquad (Transposed Direct Form II)
|
biquad.rs # ✅ generic biquad (Transposed Direct Form II)
|
||||||
limiter.rs # (planned) output true-peak brickwall limiter
|
limiter.rs # ✅ look-ahead brickwall limiter (sample-peak; true-peak pending)
|
||||||
delay.rs # (planned) look-ahead delay (currently lives inside compressor.rs)
|
delay.rs # (planned) look-ahead delay (currently inside compressor.rs / limiter.rs)
|
||||||
oversampler.rs # (planned) 4x oversampler for true-peak detection
|
oversampler.rs # (planned) 4x oversampler for true-peak detection
|
||||||
editor/
|
editor/
|
||||||
mod.rs # (planned) egui editor split out of lib.rs
|
mod.rs # (planned) egui editor split out of lib.rs
|
||||||
@@ -175,12 +176,11 @@ is essential — without it FL silently skips a plugin it has seen before.)
|
|||||||
|
|
||||||
Work through these stages in order — each stage produces a loadable, audible plugin.
|
Work through these stages in order — each stage produces a loadable, audible plugin.
|
||||||
|
|
||||||
**Status (2026-06-17):** Stages 1–3 complete — full-band compressor, peak/RMS detection, and now
|
**Status (2026-06-19):** Stages 1–3 plus the **base-rate brickwall limiter** (Stage 4a) are done:
|
||||||
the 3-band LR4 crossover feeding per-band compressors summed into the 'All' channel (4 reusable
|
3-band LR4 crossover → per-band compressors (peak/RMS) → 'All' channel → look-ahead brickwall
|
||||||
`Compressor` instances). Look-ahead + latency (Stage 4) and a basic 4-column UI (Stage 5) are in.
|
limiter, with a basic 4-column UI. **Next: Stage 4b — 4× oversampling for true-peak (inter-sample)
|
||||||
**Next: Stage 4 — output brickwall limiter + oversampler.** DSP is in `src/dsp/`
|
limiting** (deferred as the CPU-heavy part). DSP is in `src/dsp/` (`biquad.rs`, `crossover.rs`,
|
||||||
(`biquad.rs`, `crossover.rs`, `compressor.rs`); params and the egui editor are still inline in
|
`compressor.rs`, `limiter.rs`); params and the egui editor are still inline in `src/lib.rs`.
|
||||||
`src/lib.rs` (not yet split into `params.rs` / `editor/`).
|
|
||||||
|
|
||||||
### Stage 1 — Skeleton plugin ✅
|
### Stage 1 — Skeleton plugin ✅
|
||||||
- [x] NIH-plug "passthrough" compiling and loading in DAW
|
- [x] NIH-plug "passthrough" compiling and loading in DAW
|
||||||
@@ -200,12 +200,13 @@ the 3-band LR4 crossover feeding per-band compressors summed into the 'All' chan
|
|||||||
- [x] Apply per-band compressor to each band
|
- [x] Apply per-band compressor to each band
|
||||||
- [x] Sum bands back together
|
- [x] Sum bands back together
|
||||||
- [x] Run the summed signal through the 'All' channel compressor before output
|
- [x] Run the summed signal through the 'All' channel compressor before output
|
||||||
### Stage 4 — Output brickwall limiter + oversampler ⬅ next *(look-ahead + latency already done)*
|
### Stage 4 — Output brickwall limiter + oversampler *(4a done; 4b = oversampling)*
|
||||||
- [x] Look-ahead delay (circular buffer) — inside `compressor.rs`, no separate `delay.rs`
|
- [x] Look-ahead delay (circular buffer) — inside `compressor.rs` and `limiter.rs`, no separate `delay.rs`
|
||||||
- [x] Wire look-ahead: detector reads N samples ahead of the VCA
|
- [x] Wire look-ahead: detector reads N samples ahead of the VCA
|
||||||
- [x] Report latency — `context.set_latency_samples()` once; now the constant two-stage total (bands + 'All')
|
- [x] Report latency — `context.set_latency_samples()` once; now the constant three-stage total (bands + 'All' + limiter)
|
||||||
- [ ] Implement `oversampler.rs` (4x, use a polyphase FIR or windowed sinc)
|
- [x] Brickwall output limiter (`limiter.rs`): look-ahead + sliding-max peak detect + ceiling clamp guarantee; limits **sample** peaks
|
||||||
- [ ] Implement brickwall output limiter with true-peak detection
|
- [ ] `oversampler.rs` (4x, polyphase FIR / windowed sinc) ⬅ next
|
||||||
|
- [ ] True-peak (inter-sample) limiting on top of the brickwall, via the oversampler
|
||||||
### Stage 5 — Basic egui UI *(basic version done early)*
|
### Stage 5 — Basic egui UI *(basic version done early)*
|
||||||
- [x] Add `nih_plug_egui` editor
|
- [x] Add `nih_plug_egui` editor
|
||||||
- [x] Sliders for all current parameters (`ParamSlider` grid)
|
- [x] Sliders for all current parameters (`ParamSlider` grid)
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
//! Look-ahead brickwall peak limiter (base-rate).
|
||||||
|
//!
|
||||||
|
//! Guarantees the output never exceeds the ceiling. A short fixed look-ahead lets the gain ramp
|
||||||
|
//! down *before* a peak reaches the output (click-free), driven by a **sliding maximum** over the
|
||||||
|
//! look-ahead window so the reduction is fully in place in time. A final clamp at the ceiling is
|
||||||
|
//! the hard guarantee against any residual from smoothing lag or float error.
|
||||||
|
//!
|
||||||
|
//! Detection is stereo-linked (one gain for all channels). This stage limits **sample** peaks at
|
||||||
|
//! the base rate; true-peak (inter-sample) limiting via oversampling is a later addition.
|
||||||
|
|
||||||
|
const MAX_CHANNELS: usize = 2;
|
||||||
|
/// Fixed look-ahead — also this stage's constant latency contribution.
|
||||||
|
const LOOKAHEAD_MS: f32 = 1.5;
|
||||||
|
/// Near-instant attack; the look-ahead gives it time to act before the peak arrives.
|
||||||
|
const ATTACK_MS: f32 = 0.05;
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Limiter {
|
||||||
|
/// Per-channel audio delay ring.
|
||||||
|
delay: Vec<Vec<f32>>,
|
||||||
|
/// Linked `|x|` history, same length as the delay ring (for the sliding maximum).
|
||||||
|
peaks: Vec<f32>,
|
||||||
|
capacity: usize,
|
||||||
|
write_pos: usize,
|
||||||
|
fixed_delay: usize,
|
||||||
|
/// Current smoothed gain (<= 1).
|
||||||
|
gain: f32,
|
||||||
|
attack_coef: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Limiter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
delay: Vec::new(),
|
||||||
|
peaks: Vec::new(),
|
||||||
|
capacity: 0,
|
||||||
|
write_pos: 0,
|
||||||
|
fixed_delay: 0,
|
||||||
|
gain: 1.0,
|
||||||
|
attack_coef: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Limiter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate buffers. Call from `initialize()` (allocation allowed).
|
||||||
|
pub fn prepare(&mut self, sample_rate: f32, num_channels: usize) {
|
||||||
|
self.fixed_delay = (LOOKAHEAD_MS * 0.001 * sample_rate).ceil() as usize;
|
||||||
|
self.capacity = self.fixed_delay + 1;
|
||||||
|
self.attack_coef = time_to_coef(ATTACK_MS, sample_rate);
|
||||||
|
let channels = num_channels.clamp(1, MAX_CHANNELS);
|
||||||
|
self.delay = vec![vec![0.0; self.capacity]; channels];
|
||||||
|
self.peaks = vec![0.0; self.capacity];
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
for ch in &mut self.delay {
|
||||||
|
ch.iter_mut().for_each(|s| *s = 0.0);
|
||||||
|
}
|
||||||
|
self.peaks.iter_mut().for_each(|p| *p = 0.0);
|
||||||
|
self.write_pos = 0;
|
||||||
|
self.gain = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constant reported latency (the fixed look-ahead delay).
|
||||||
|
pub fn latency(&self) -> u32 {
|
||||||
|
self.fixed_delay as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limit one frame in place: `input[ch]` -> `output[ch]`.
|
||||||
|
///
|
||||||
|
/// `ceiling` is linear gain (e.g. `util::db_to_gain(ceiling_db)`); `release_coef` comes from a
|
||||||
|
/// release time. Output is guaranteed `|y| <= ceiling`.
|
||||||
|
pub fn process(&mut self, input: &[f32], output: &mut [f32], ceiling: f32, release_coef: f32) {
|
||||||
|
let n = input.len().min(self.delay.len());
|
||||||
|
|
||||||
|
// Linked peak of the current input.
|
||||||
|
let mut peak = 0.0f32;
|
||||||
|
for &x in &input[..n] {
|
||||||
|
peak = peak.max(x.abs());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write into the ring.
|
||||||
|
for ch in 0..n {
|
||||||
|
self.delay[ch][self.write_pos] = input[ch];
|
||||||
|
}
|
||||||
|
self.peaks[self.write_pos] = peak;
|
||||||
|
|
||||||
|
// Sliding maximum over the look-ahead window (= the whole ring). Because the oldest sample
|
||||||
|
// (the one we output now) is in this window, `ceiling / window_max` applied to it can never
|
||||||
|
// exceed the ceiling, and the gain has pre-dropped for any louder sample still to come.
|
||||||
|
let mut window_max = 0.0f32;
|
||||||
|
for &p in &self.peaks {
|
||||||
|
window_max = window_max.max(p);
|
||||||
|
}
|
||||||
|
let target = if window_max > ceiling { ceiling / window_max } else { 1.0 };
|
||||||
|
|
||||||
|
// Decoupled smoothing: fast attack down, slow release up.
|
||||||
|
self.gain = if target < self.gain {
|
||||||
|
self.attack_coef * self.gain + (1.0 - self.attack_coef) * target
|
||||||
|
} else {
|
||||||
|
release_coef * self.gain + (1.0 - release_coef) * target
|
||||||
|
};
|
||||||
|
|
||||||
|
// Output the delayed sample, clamped to the ceiling as the hard guarantee.
|
||||||
|
let out_pos = (self.write_pos + 1) % self.capacity; // oldest sample = fixed_delay ago
|
||||||
|
for ch in 0..n {
|
||||||
|
output[ch] = (self.delay[ch][out_pos] * self.gain).clamp(-ceiling, ceiling);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write_pos = (self.write_pos + 1) % self.capacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::f32::consts::TAU;
|
||||||
|
|
||||||
|
const SR: f32 = 48_000.0;
|
||||||
|
|
||||||
|
fn release() -> f32 {
|
||||||
|
time_to_coef(50.0, SR)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn never_exceeds_ceiling_on_spikes() {
|
||||||
|
// Mostly silence with occasional large spikes — output must never exceed the ceiling.
|
||||||
|
let mut lim = Limiter::new();
|
||||||
|
lim.prepare(SR, 1);
|
||||||
|
let ceiling = 1.0;
|
||||||
|
let mut out = [0.0f32];
|
||||||
|
for i in 0..10_000 {
|
||||||
|
let x = if i % 500 == 0 { 5.0 } else { 0.01 };
|
||||||
|
lim.process(&[x], &mut out, ceiling, release());
|
||||||
|
assert!(out[0].abs() <= ceiling + 1e-6, "overshoot at {i}: {}", out[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn limits_loud_sine_to_ceiling() {
|
||||||
|
// A sine well above the ceiling settles to ~ceiling, not silenced.
|
||||||
|
let mut lim = Limiter::new();
|
||||||
|
lim.prepare(SR, 1);
|
||||||
|
let ceiling = 1.0;
|
||||||
|
let (amp, freq) = (2.0f32, 1_000.0);
|
||||||
|
let mut out = [0.0f32];
|
||||||
|
let mut max_tail = 0.0f32;
|
||||||
|
let total = SR as usize;
|
||||||
|
for i in 0..total {
|
||||||
|
let x = amp * (TAU * freq * i as f32 / SR).sin();
|
||||||
|
lim.process(&[x], &mut out, ceiling, release());
|
||||||
|
if i >= total - 4_800 {
|
||||||
|
max_tail = max_tail.max(out[0].abs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(max_tail <= ceiling + 1e-6, "exceeded ceiling: {max_tail}");
|
||||||
|
assert!(max_tail > 0.9, "over-attenuated: {max_tail}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transparent_below_ceiling() {
|
||||||
|
// A signal under the ceiling passes through unattenuated (just delayed).
|
||||||
|
let mut lim = Limiter::new();
|
||||||
|
lim.prepare(SR, 1);
|
||||||
|
let ceiling = 1.0;
|
||||||
|
let (amp, freq) = (0.5f32, 1_000.0);
|
||||||
|
let mut out = [0.0f32];
|
||||||
|
let mut max_tail = 0.0f32;
|
||||||
|
let total = SR as usize / 2;
|
||||||
|
for i in 0..total {
|
||||||
|
let x = amp * (TAU * freq * i as f32 / SR).sin();
|
||||||
|
lim.process(&[x], &mut out, ceiling, release());
|
||||||
|
if i >= total - 4_800 {
|
||||||
|
max_tail = max_tail.max(out[0].abs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!((max_tail - amp).abs() < 1e-3, "not transparent: {max_tail}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latency_is_the_lookahead() {
|
||||||
|
let mut lim = Limiter::new();
|
||||||
|
lim.prepare(SR, 1);
|
||||||
|
let expected = (LOOKAHEAD_MS * 0.001 * SR).ceil() as u32;
|
||||||
|
assert_eq!(lim.latency(), expected);
|
||||||
|
assert!(expected > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,3 +7,4 @@
|
|||||||
pub mod biquad;
|
pub mod biquad;
|
||||||
pub mod compressor;
|
pub mod compressor;
|
||||||
pub mod crossover;
|
pub mod crossover;
|
||||||
|
pub mod limiter;
|
||||||
|
|||||||
+47
-4
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
|||||||
mod dsp;
|
mod dsp;
|
||||||
use dsp::compressor::{Compressor, CompressorSettings, MAX_LOOKAHEAD_MS};
|
use dsp::compressor::{Compressor, CompressorSettings, MAX_LOOKAHEAD_MS};
|
||||||
use dsp::crossover::Crossover;
|
use dsp::crossover::Crossover;
|
||||||
|
use dsp::limiter::Limiter;
|
||||||
|
|
||||||
/// Band indices into the compressor array: low, mid, high, then the 'All' aggregate channel.
|
/// Band indices into the compressor array: low, mid, high, then the 'All' aggregate channel.
|
||||||
const LOW: usize = 0;
|
const LOW: usize = 0;
|
||||||
@@ -34,6 +35,8 @@ struct Codename206 {
|
|||||||
crossover: Crossover,
|
crossover: Crossover,
|
||||||
/// Compressors indexed by [`LOW`], [`MID`], [`HIGH`], [`ALL`].
|
/// Compressors indexed by [`LOW`], [`MID`], [`HIGH`], [`ALL`].
|
||||||
comps: [Compressor; 4],
|
comps: [Compressor; 4],
|
||||||
|
/// Output brickwall limiter (final stage).
|
||||||
|
limiter: Limiter,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Params)]
|
#[derive(Params)]
|
||||||
@@ -51,6 +54,13 @@ struct Codename206Params {
|
|||||||
#[id = "lookahead"]
|
#[id = "lookahead"]
|
||||||
pub look_ahead_ms: FloatParam,
|
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")]
|
#[nested(id_prefix = "low", group = "Low")]
|
||||||
pub low: CompressorParams,
|
pub low: CompressorParams,
|
||||||
#[nested(id_prefix = "mid", group = "Mid")]
|
#[nested(id_prefix = "mid", group = "Mid")]
|
||||||
@@ -88,6 +98,7 @@ impl Default for Codename206 {
|
|||||||
sample_rate: 48_000.0,
|
sample_rate: 48_000.0,
|
||||||
crossover: Crossover::new(),
|
crossover: Crossover::new(),
|
||||||
comps: [Compressor::new(), Compressor::new(), Compressor::new(), Compressor::new()],
|
comps: [Compressor::new(), Compressor::new(), Compressor::new(), Compressor::new()],
|
||||||
|
limiter: Limiter::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,6 +132,22 @@ impl Default for Codename206Params {
|
|||||||
.with_unit(" ms")
|
.with_unit(" ms")
|
||||||
.with_value_to_string(formatters::v2s_f32_rounded(2)),
|
.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(),
|
low: CompressorParams::default(),
|
||||||
mid: CompressorParams::default(),
|
mid: CompressorParams::default(),
|
||||||
high: CompressorParams::default(),
|
high: CompressorParams::default(),
|
||||||
@@ -271,6 +298,10 @@ impl Plugin for Codename206 {
|
|||||||
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_high_hz, setter));
|
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_high_hz, setter));
|
||||||
ui.label("Look-ahead");
|
ui.label("Look-ahead");
|
||||||
ui.add(widgets::ParamSlider::for_param(¶ms.look_ahead_ms, setter));
|
ui.add(widgets::ParamSlider::for_param(¶ms.look_ahead_ms, setter));
|
||||||
|
ui.label("Ceiling");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(¶ms.output_ceiling_db, setter));
|
||||||
|
ui.label("Lim Release");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(¶ms.limiter_release_ms, setter));
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.columns(4, |cols| {
|
ui.columns(4, |cols| {
|
||||||
@@ -305,10 +336,12 @@ impl Plugin for Codename206 {
|
|||||||
self.params.crossover_low_hz.value(),
|
self.params.crossover_low_hz.value(),
|
||||||
self.params.crossover_high_hz.value(),
|
self.params.crossover_high_hz.value(),
|
||||||
);
|
);
|
||||||
|
self.limiter.prepare(self.sample_rate, channels);
|
||||||
|
|
||||||
// Two compressor stages in series (bands → 'All'), each with the same fixed look-ahead
|
// Three series stages each with a fixed look-ahead delay: the bands, the 'All' channel,
|
||||||
// delay. Reported once as a constant; see the look-ahead note in the compressor module.
|
// 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();
|
let total_latency =
|
||||||
|
self.comps[LOW].latency() + self.comps[ALL].latency() + self.limiter.latency();
|
||||||
context.set_latency_samples(total_latency);
|
context.set_latency_samples(total_latency);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -318,6 +351,7 @@ impl Plugin for Codename206 {
|
|||||||
for comp in &mut self.comps {
|
for comp in &mut self.comps {
|
||||||
comp.reset();
|
comp.reset();
|
||||||
}
|
}
|
||||||
|
self.limiter.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(
|
fn process(
|
||||||
@@ -344,11 +378,17 @@ impl Plugin for Codename206 {
|
|||||||
];
|
];
|
||||||
let mut all_set = build_settings(&self.params.all, lookahead, self.sample_rate);
|
let mut all_set = build_settings(&self.params.all, lookahead, self.sample_rate);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
let mut in_frame = [0.0f32; 2];
|
let mut in_frame = [0.0f32; 2];
|
||||||
let mut band_in = [[0.0f32; 2]; 3];
|
let mut band_in = [[0.0f32; 2]; 3];
|
||||||
let mut band_out = [[0.0f32; 2]; 3];
|
let mut band_out = [[0.0f32; 2]; 3];
|
||||||
let mut summed = [0.0f32; 2];
|
let mut summed = [0.0f32; 2];
|
||||||
let mut out_frame = [0.0f32; 2];
|
let mut out_frame = [0.0f32; 2];
|
||||||
|
let mut lim_frame = [0.0f32; 2];
|
||||||
|
|
||||||
for mut frame in buffer.iter_samples() {
|
for mut frame in buffer.iter_samples() {
|
||||||
let n = frame.len().min(2);
|
let n = frame.len().min(2);
|
||||||
@@ -378,8 +418,11 @@ impl Plugin for Codename206 {
|
|||||||
all_set.makeup_db = self.params.all.makeup_db.smoothed.next();
|
all_set.makeup_db = self.params.all.makeup_db.smoothed.next();
|
||||||
self.comps[ALL].process(&summed[..n], &mut out_frame[..n], &all_set);
|
self.comps[ALL].process(&summed[..n], &mut out_frame[..n], &all_set);
|
||||||
|
|
||||||
|
// Output brickwall limiter.
|
||||||
|
self.limiter.process(&out_frame[..n], &mut lim_frame[..n], ceiling, limiter_release);
|
||||||
|
|
||||||
for ch in 0..n {
|
for ch in 0..n {
|
||||||
*frame.get_mut(ch).unwrap() = out_frame[ch];
|
*frame.get_mut(ch).unwrap() = lim_frame[ch];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user