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:
+47
-4
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
mod dsp;
|
||||
use dsp::compressor::{Compressor, CompressorSettings, MAX_LOOKAHEAD_MS};
|
||||
use dsp::crossover::Crossover;
|
||||
use dsp::limiter::Limiter;
|
||||
|
||||
/// Band indices into the compressor array: low, mid, high, then the 'All' aggregate channel.
|
||||
const LOW: usize = 0;
|
||||
@@ -34,6 +35,8 @@ struct Codename206 {
|
||||
crossover: Crossover,
|
||||
/// Compressors indexed by [`LOW`], [`MID`], [`HIGH`], [`ALL`].
|
||||
comps: [Compressor; 4],
|
||||
/// Output brickwall limiter (final stage).
|
||||
limiter: Limiter,
|
||||
}
|
||||
|
||||
#[derive(Params)]
|
||||
@@ -51,6 +54,13 @@ struct Codename206Params {
|
||||
#[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")]
|
||||
@@ -88,6 +98,7 @@ impl Default for Codename206 {
|
||||
sample_rate: 48_000.0,
|
||||
crossover: Crossover::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_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(),
|
||||
@@ -271,6 +298,10 @@ impl Plugin for Codename206 {
|
||||
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_high_hz, setter));
|
||||
ui.label("Look-ahead");
|
||||
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.columns(4, |cols| {
|
||||
@@ -305,10 +336,12 @@ impl Plugin for Codename206 {
|
||||
self.params.crossover_low_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
|
||||
// delay. Reported once as a constant; see the look-ahead note in the compressor module.
|
||||
let total_latency = self.comps[LOW].latency() + self.comps[ALL].latency();
|
||||
// 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();
|
||||
context.set_latency_samples(total_latency);
|
||||
true
|
||||
}
|
||||
@@ -318,6 +351,7 @@ impl Plugin for Codename206 {
|
||||
for comp in &mut self.comps {
|
||||
comp.reset();
|
||||
}
|
||||
self.limiter.reset();
|
||||
}
|
||||
|
||||
fn process(
|
||||
@@ -344,11 +378,17 @@ impl Plugin for Codename206 {
|
||||
];
|
||||
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 band_in = [[0.0f32; 2]; 3];
|
||||
let mut band_out = [[0.0f32; 2]; 3];
|
||||
let mut summed = [0.0f32; 2];
|
||||
let mut out_frame = [0.0f32; 2];
|
||||
let mut lim_frame = [0.0f32; 2];
|
||||
|
||||
for mut frame in buffer.iter_samples() {
|
||||
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();
|
||||
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 {
|
||||
*frame.get_mut(ch).unwrap() = out_frame[ch];
|
||||
*frame.get_mut(ch).unwrap() = lim_frame[ch];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user