feat: per-channel meters, ceiling lamp, and pre-gain drive
Stage 6 begins. Add lock-free Meters (atomics shared audio->GUI) with a peak-with-decay ballistic, published once per block and gated on the editor being open. Editor draws a per-channel level + gain-reduction meter panel and a ceiling lamp fed by the output limiter. Compressor/Limiter expose gain_reduction_db() for this. Also add a smoothed per-channel pre-gain applied before each compressor (and before the All compressor), driving the signal into compression and on into the limiter for a compressed semi-distortion. Pairs with makeup for full per-channel input/output gain-staging. Compressor DSP untouched; 16 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+64
-3
@@ -3,13 +3,19 @@ use std::sync::Arc;
|
||||
|
||||
mod dsp;
|
||||
mod editor;
|
||||
mod meters;
|
||||
mod params;
|
||||
|
||||
use dsp::compressor::{Compressor, MAX_LOOKAHEAD_MS};
|
||||
use dsp::crossover::Crossover;
|
||||
use dsp::limiter::Limiter;
|
||||
use meters::Meters;
|
||||
use params::{build_settings, Codename206Params};
|
||||
|
||||
/// Peak-meter fall: after this long of silence the bars decay by 12 dB. (Matches nih-plug's
|
||||
/// gain-gui example feel.)
|
||||
const METER_DECAY_MS: f64 = 150.0;
|
||||
|
||||
/// Band indices into the compressor array: low, mid, high, then the 'All' aggregate channel.
|
||||
const LOW: usize = 0;
|
||||
const MID: usize = 1;
|
||||
@@ -29,6 +35,11 @@ struct Codename206 {
|
||||
comps: [Compressor; 4],
|
||||
/// Output brickwall limiter (final stage).
|
||||
limiter: Limiter,
|
||||
/// Lock-free meter state shared with the editor.
|
||||
meters: Arc<Meters>,
|
||||
/// Per-sample decay factor for the meter peak-hold (computed from the sample rate; raised to
|
||||
/// the block length when applied once per block in `process`).
|
||||
meter_decay_weight: f32,
|
||||
}
|
||||
|
||||
impl Default for Codename206 {
|
||||
@@ -39,6 +50,8 @@ impl Default for Codename206 {
|
||||
crossover: Crossover::new(),
|
||||
comps: [Compressor::new(), Compressor::new(), Compressor::new(), Compressor::new()],
|
||||
limiter: Limiter::new(),
|
||||
meters: Arc::new(Meters::default()),
|
||||
meter_decay_weight: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +96,7 @@ impl Plugin for Codename206 {
|
||||
}
|
||||
|
||||
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||
editor::create(self.params.clone())
|
||||
editor::create(self.params.clone(), self.meters.clone())
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
@@ -98,6 +111,10 @@ impl Plugin for Codename206 {
|
||||
.map(NonZeroU32::get)
|
||||
.unwrap_or(2) as usize;
|
||||
|
||||
// Per-block decay so the meters fall ~12 dB over METER_DECAY_MS of silence.
|
||||
self.meter_decay_weight =
|
||||
0.25f64.powf((self.sample_rate as f64 * METER_DECAY_MS / 1000.0).recip()) as f32;
|
||||
|
||||
for comp in &mut self.comps {
|
||||
comp.prepare(self.sample_rate, channels, MAX_LOOKAHEAD_MS);
|
||||
}
|
||||
@@ -154,6 +171,14 @@ impl Plugin for Codename206 {
|
||||
let limiter_release =
|
||||
Compressor::time_to_coef(self.params.limiter_release_ms.value(), self.sample_rate);
|
||||
|
||||
// Only do the (cheap) metering work when the editor is actually open.
|
||||
let metering = self.params.editor_state.is_open();
|
||||
let num_samples = buffer.samples();
|
||||
let mut lvl_l = [0.0f32; meters::NUM_CHANNELS];
|
||||
let mut lvl_r = [0.0f32; meters::NUM_CHANNELS];
|
||||
let mut gr = [0.0f32; meters::NUM_CHANNELS];
|
||||
let mut lim_gr = 0.0f32;
|
||||
|
||||
let mut in_frame = [0.0f32; 2];
|
||||
let mut band_in = [[0.0f32; 2]; 3];
|
||||
let mut band_out = [[0.0f32; 2]; 3];
|
||||
@@ -163,6 +188,7 @@ impl Plugin for Codename206 {
|
||||
|
||||
for mut frame in buffer.iter_samples() {
|
||||
let n = frame.len().min(2);
|
||||
let r = (n - 1).min(1); // right-channel index (== left when mono)
|
||||
for ch in 0..n {
|
||||
in_frame[ch] = *frame.get_mut(ch).unwrap();
|
||||
}
|
||||
@@ -175,28 +201,63 @@ impl Plugin for Codename206 {
|
||||
band_in[HIGH][ch] = hi;
|
||||
}
|
||||
|
||||
// Compress each band (per-sample smoothed makeup), then sum.
|
||||
// Drive + compress each band (per-sample smoothed pre-gain & makeup), then sum.
|
||||
summed[..n].fill(0.0);
|
||||
for b in 0..3 {
|
||||
let pre = util::db_to_gain(band_params[b].pre_gain_db.smoothed.next());
|
||||
for ch in 0..n {
|
||||
band_in[b][ch] *= pre;
|
||||
}
|
||||
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];
|
||||
}
|
||||
if metering {
|
||||
lvl_l[b] = lvl_l[b].max(band_out[b][0].abs());
|
||||
lvl_r[b] = lvl_r[b].max(band_out[b][r].abs());
|
||||
gr[b] = gr[b]
|
||||
.max(if band_set[b].bypass { 0.0 } else { self.comps[b].gain_reduction_db() });
|
||||
}
|
||||
}
|
||||
|
||||
// 'All' aggregate channel over the summed bands.
|
||||
// 'All' aggregate channel over the summed bands (driven before its compressor).
|
||||
let all_pre = util::db_to_gain(self.params.all.pre_gain_db.smoothed.next());
|
||||
for ch in 0..n {
|
||||
summed[ch] *= all_pre;
|
||||
}
|
||||
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);
|
||||
|
||||
if metering {
|
||||
lvl_l[ALL] = lvl_l[ALL].max(out_frame[0].abs());
|
||||
lvl_r[ALL] = lvl_r[ALL].max(out_frame[r].abs());
|
||||
gr[ALL] = gr[ALL]
|
||||
.max(if all_set.bypass { 0.0 } else { self.comps[ALL].gain_reduction_db() });
|
||||
lim_gr = lim_gr.max(self.limiter.gain_reduction_db());
|
||||
}
|
||||
|
||||
for ch in 0..n {
|
||||
*frame.get_mut(ch).unwrap() = lim_frame[ch];
|
||||
}
|
||||
}
|
||||
|
||||
// Publish one decimated value per meter for this block. The decay weight is per-sample,
|
||||
// so raise it to the block length to keep the fall time constant independent of buffer size
|
||||
// (we apply it once per block, not once per sample).
|
||||
if metering {
|
||||
let w = self.meter_decay_weight.powi(num_samples as i32);
|
||||
for i in 0..meters::NUM_CHANNELS {
|
||||
meters::decay_store(&self.meters.level_l[i], lvl_l[i], w);
|
||||
meters::decay_store(&self.meters.level_r[i], lvl_r[i], w);
|
||||
meters::decay_store(&self.meters.gain_reduction_db[i], gr[i], w);
|
||||
}
|
||||
meters::decay_store(&self.meters.limiter_gr_db, lim_gr, w);
|
||||
}
|
||||
|
||||
ProcessStatus::Normal
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user