feat: decouple plot resolution from frame rate via a 200 Hz ring buffer
Feed the scrolling plot from a lock-free SPSC ScopeRing instead of sampling one atomic per egui frame, so horizontal resolution is set by the audio-clocked bucket rate (~200 Hz) rather than the ~60 fps repaint. process() accumulates a bucket every sample_rate/BUCKET_HZ samples (peak-preserving, spanning blocks) and pushes it; the editor drains all new buckets each frame and folds them into PLOT_N columns. Fast transients between frames are no longer dropped, and the plot is now audio-clocked (freezes on pause, falls to silence on stop/reset). Drop the per-frame plot_* atomics (the per-channel lamp now reads the decayed bar level). Also fix the area fill: render it as a strip of per-segment convex quads instead of one concave polygon, which egui fan-filled from a corner and left stray triangles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+54
-16
@@ -40,6 +40,15 @@ struct Codename206 {
|
||||
/// 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,
|
||||
|
||||
/// Per-channel max accumulators for the plot bucket currently being built (in/out linear, GR
|
||||
/// dB). Persist across blocks since a bucket spans many samples.
|
||||
scope_in: [f32; 4],
|
||||
scope_out: [f32; 4],
|
||||
scope_gr: [f32; 4],
|
||||
/// Samples accumulated into the current bucket, and the bucket length (= sample_rate / BUCKET_HZ).
|
||||
scope_samples: usize,
|
||||
scope_bucket_len: usize,
|
||||
}
|
||||
|
||||
impl Default for Codename206 {
|
||||
@@ -52,6 +61,11 @@ impl Default for Codename206 {
|
||||
limiter: Limiter::new(),
|
||||
meters: Arc::new(Meters::default()),
|
||||
meter_decay_weight: 1.0,
|
||||
scope_in: [0.0; 4],
|
||||
scope_out: [0.0; 4],
|
||||
scope_gr: [0.0; 4],
|
||||
scope_samples: 0,
|
||||
scope_bucket_len: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +129,10 @@ impl Plugin for Codename206 {
|
||||
self.meter_decay_weight =
|
||||
0.25f64.powf((self.sample_rate as f64 * METER_DECAY_MS / 1000.0).recip()) as f32;
|
||||
|
||||
// Plot bucket length: emit a scope bucket every ~1/BUCKET_HZ seconds.
|
||||
self.scope_bucket_len =
|
||||
((self.sample_rate / meters::BUCKET_HZ as f32).round() as usize).max(1);
|
||||
|
||||
for comp in &mut self.comps {
|
||||
comp.prepare(self.sample_rate, channels, MAX_LOOKAHEAD_MS);
|
||||
}
|
||||
@@ -140,8 +158,13 @@ impl Plugin for Codename206 {
|
||||
comp.reset();
|
||||
}
|
||||
self.limiter.reset();
|
||||
// Transport restart / sample-rate change: drop stale meter values to silence.
|
||||
// Transport restart / sample-rate change: drop stale meter values to silence and discard
|
||||
// the in-flight plot bucket.
|
||||
self.meters.clear();
|
||||
self.scope_in = [0.0; 4];
|
||||
self.scope_out = [0.0; 4];
|
||||
self.scope_gr = [0.0; 4];
|
||||
self.scope_samples = 0;
|
||||
}
|
||||
|
||||
fn process(
|
||||
@@ -178,7 +201,6 @@ impl Plugin for Codename206 {
|
||||
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 inp = [0.0f32; meters::NUM_CHANNELS]; // mono input peak (for the scrolling plot)
|
||||
let mut gr = [0.0f32; meters::NUM_CHANNELS];
|
||||
let mut lim_gr = 0.0f32;
|
||||
|
||||
@@ -217,11 +239,16 @@ impl Plugin for Codename206 {
|
||||
summed[ch] += band_out[b][ch];
|
||||
}
|
||||
if metering {
|
||||
inp[b] = inp[b].max(band_in[b][0].abs().max(band_in[b][r].abs()));
|
||||
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() });
|
||||
let in_mono = band_in[b][0].abs().max(band_in[b][r].abs());
|
||||
let out_l = band_out[b][0].abs();
|
||||
let out_r = band_out[b][r].abs();
|
||||
let g = if band_set[b].bypass { 0.0 } else { self.comps[b].gain_reduction_db() };
|
||||
lvl_l[b] = lvl_l[b].max(out_l);
|
||||
lvl_r[b] = lvl_r[b].max(out_r);
|
||||
gr[b] = gr[b].max(g);
|
||||
self.scope_in[b] = self.scope_in[b].max(in_mono);
|
||||
self.scope_out[b] = self.scope_out[b].max(out_l.max(out_r));
|
||||
self.scope_gr[b] = self.scope_gr[b].max(g);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,12 +264,27 @@ impl Plugin for Codename206 {
|
||||
self.limiter.process(&out_frame[..n], &mut lim_frame[..n], ceiling, limiter_release);
|
||||
|
||||
if metering {
|
||||
inp[ALL] = inp[ALL].max(summed[0].abs().max(summed[r].abs()));
|
||||
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() });
|
||||
let in_mono = summed[0].abs().max(summed[r].abs());
|
||||
let out_l = out_frame[0].abs();
|
||||
let out_r = out_frame[r].abs();
|
||||
let g = if all_set.bypass { 0.0 } else { self.comps[ALL].gain_reduction_db() };
|
||||
lvl_l[ALL] = lvl_l[ALL].max(out_l);
|
||||
lvl_r[ALL] = lvl_r[ALL].max(out_r);
|
||||
gr[ALL] = gr[ALL].max(g);
|
||||
lim_gr = lim_gr.max(self.limiter.gain_reduction_db());
|
||||
self.scope_in[ALL] = self.scope_in[ALL].max(in_mono);
|
||||
self.scope_out[ALL] = self.scope_out[ALL].max(out_l.max(out_r));
|
||||
self.scope_gr[ALL] = self.scope_gr[ALL].max(g);
|
||||
|
||||
// Emit a plot bucket every scope_bucket_len samples (~BUCKET_HZ).
|
||||
self.scope_samples += 1;
|
||||
if self.scope_samples >= self.scope_bucket_len {
|
||||
self.meters.scope.push(&self.scope_in, &self.scope_out, &self.scope_gr);
|
||||
self.scope_in = [0.0; meters::NUM_CHANNELS];
|
||||
self.scope_out = [0.0; meters::NUM_CHANNELS];
|
||||
self.scope_gr = [0.0; meters::NUM_CHANNELS];
|
||||
self.scope_samples = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for ch in 0..n {
|
||||
@@ -259,10 +301,6 @@ impl Plugin for Codename206 {
|
||||
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);
|
||||
// Raw block peaks for the scrolling plot (editor keeps its own history).
|
||||
meters::store_instant(&self.meters.plot_in[i], inp[i]);
|
||||
meters::store_instant(&self.meters.plot_out[i], lvl_l[i].max(lvl_r[i]));
|
||||
meters::store_instant(&self.meters.plot_gr[i], gr[i]);
|
||||
}
|
||||
meters::decay_store(&self.meters.limiter_gr_db, lim_gr, w);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user