From 514fd964f67d2fca28fdb190ebd42bd959d0fbec Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Wed, 24 Jun 2026 16:25:08 +0900 Subject: [PATCH] fix: freeze the scope when the transport is stopped FL keeps calling process() with silence while stopped/paused, so the scope scrolled silence instead of holding. Gate the plot's bucket advance on context.transport().playing, so it freezes on stop/pause and resumes on play. Bar meters still fall to silence as before. Update README thread-safety notes for the ScopeRing feed and transport gating. Co-Authored-By: Claude Opus 4.8 --- README.md | 12 +++++++++--- src/lib.rs | 40 +++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 02e84f0..c17fe9c 100644 --- a/README.md +++ b/README.md @@ -256,9 +256,15 @@ NIH-plug provides `Smoother` — use it for all gain/threshold params to avoid z ### Thread safety Params are atomics. The editor and audio thread communicate only through params and a shared -`Arc` of **lock-free atomics** (`meters.rs`) — never a mutex on the audio path. `process()` -publishes one value per meter per block (gated on the editor being open); the editor reads them -each frame. Never pass DSP state to the UI directly. +`Arc` (`meters.rs`) — never a mutex on the audio path. Two lock-free feeds, both gated on +the editor being open: +- **Bar meters** — decayed atomic scalars, one store per block; the editor reads them each frame. +- **Scrolling plot** — a single-producer/single-consumer `ScopeRing` of buckets clocked at + ~200 Hz, so the plot's horizontal resolution is decoupled from the ~60 fps repaint. The editor + drains all new buckets each frame. The scope is **transport-gated** (advances only while playing) + so it freezes rather than scrolling silence when the host is stopped/paused. + +Never pass DSP state to the UI directly. ### VST3 licensing You must accept Steinberg's VST3 SDK licence before distributing VST3 binaries. diff --git a/src/lib.rs b/src/lib.rs index 92e3fe1..ea9d151 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,7 +171,7 @@ impl Plugin for Codename206 { &mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, - _context: &mut impl ProcessContext, + context: &mut impl ProcessContext, ) -> ProcessStatus { let lookahead = self.lookahead_samples(); @@ -198,6 +198,9 @@ impl Plugin for Codename206 { // Only do the (cheap) metering work when the editor is actually open. let metering = self.params.editor_state.is_open(); + // The host keeps calling process() with silence while stopped/paused (FL does), so the + // scope is gated on the transport actually playing — otherwise it would scroll silence. + let playing = context.transport().playing; let num_samples = buffer.samples(); let mut lvl_l = [0.0f32; meters::NUM_CHANNELS]; let mut lvl_r = [0.0f32; meters::NUM_CHANNELS]; @@ -246,9 +249,11 @@ impl Plugin for Codename206 { 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); + if playing { + 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); + } } } @@ -272,18 +277,23 @@ impl Plugin for Codename206 { 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; + // Only advance the scope while the transport is playing, so it freezes (rather than + // scrolling silence) when the host is paused/stopped but still calling process(). + if playing { + 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; + } } }