feat: ceiling-hit markers as top-edge ticks on the rolling plot
Flag each plot bucket with whether the output limiter hit the ceiling (limiter GR > 0.1 dB), carried through ScopeRing as a per-bucket hit field and folded into history columns. The editor draws a short red tick at the top of any hit column (one column wide, so runs merge into segments and a lone hit is a dot); nothing otherwise. Global marker, shown on every channel tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-5
@@ -85,6 +85,9 @@ pub struct ScopeRing {
|
||||
in_lin: Vec<AtomicF32>,
|
||||
out_lin: Vec<AtomicF32>,
|
||||
gr_db: Vec<AtomicF32>,
|
||||
/// Per-bucket (not per-channel) flag: `1.0` if the output limiter hit the ceiling in this
|
||||
/// bucket, else `0.0`. Indexed by `bucket_index % RING_N`.
|
||||
hit: Vec<AtomicF32>,
|
||||
/// Monotonic count of buckets ever written.
|
||||
write: AtomicU64,
|
||||
}
|
||||
@@ -92,7 +95,13 @@ pub struct ScopeRing {
|
||||
impl Default for ScopeRing {
|
||||
fn default() -> Self {
|
||||
let make = || (0..RING_N * NUM_CHANNELS).map(|_| AtomicF32::new(0.0)).collect();
|
||||
Self { in_lin: make(), out_lin: make(), gr_db: make(), write: AtomicU64::new(0) }
|
||||
Self {
|
||||
in_lin: make(),
|
||||
out_lin: make(),
|
||||
gr_db: make(),
|
||||
hit: (0..RING_N).map(|_| AtomicF32::new(0.0)).collect(),
|
||||
write: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,14 +112,17 @@ impl ScopeRing {
|
||||
in_lin: &[f32; NUM_CHANNELS],
|
||||
out_lin: &[f32; NUM_CHANNELS],
|
||||
gr_db: &[f32; NUM_CHANNELS],
|
||||
hit: f32,
|
||||
) {
|
||||
let w = self.write.load(Ordering::Relaxed); // producer is the sole writer of `write`
|
||||
let base = (w as usize % RING_N) * NUM_CHANNELS;
|
||||
let slot = w as usize % RING_N;
|
||||
let base = slot * NUM_CHANNELS;
|
||||
for ch in 0..NUM_CHANNELS {
|
||||
self.in_lin[base + ch].store(in_lin[ch], Ordering::Relaxed);
|
||||
self.out_lin[base + ch].store(out_lin[ch], Ordering::Relaxed);
|
||||
self.gr_db[base + ch].store(gr_db[ch], Ordering::Relaxed);
|
||||
}
|
||||
self.hit[slot].store(hit, Ordering::Relaxed);
|
||||
// Publish the bucket: the Release pairs with the consumer's Acquire so the stores above are
|
||||
// visible before the new count.
|
||||
self.write.store(w + 1, Ordering::Release);
|
||||
@@ -121,7 +133,7 @@ impl ScopeRing {
|
||||
pub fn drain(
|
||||
&self,
|
||||
cursor: &mut u64,
|
||||
mut on_bucket: impl FnMut(&[f32; NUM_CHANNELS], &[f32; NUM_CHANNELS], &[f32; NUM_CHANNELS]),
|
||||
mut on_bucket: impl FnMut(&[f32; NUM_CHANNELS], &[f32; NUM_CHANNELS], &[f32; NUM_CHANNELS], f32),
|
||||
) {
|
||||
let w = self.write.load(Ordering::Acquire);
|
||||
if *cursor > w {
|
||||
@@ -136,13 +148,15 @@ impl ScopeRing {
|
||||
let mut out_buf = [0.0f32; NUM_CHANNELS];
|
||||
let mut gr_buf = [0.0f32; NUM_CHANNELS];
|
||||
while *cursor < w {
|
||||
let base = (*cursor as usize % RING_N) * NUM_CHANNELS;
|
||||
let slot = *cursor as usize % RING_N;
|
||||
let base = slot * NUM_CHANNELS;
|
||||
for ch in 0..NUM_CHANNELS {
|
||||
in_buf[ch] = self.in_lin[base + ch].load(Ordering::Relaxed);
|
||||
out_buf[ch] = self.out_lin[base + ch].load(Ordering::Relaxed);
|
||||
gr_buf[ch] = self.gr_db[base + ch].load(Ordering::Relaxed);
|
||||
}
|
||||
on_bucket(&in_buf, &out_buf, &gr_buf);
|
||||
let hit = self.hit[slot].load(Ordering::Relaxed);
|
||||
on_bucket(&in_buf, &out_buf, &gr_buf, hit);
|
||||
*cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user