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 <noreply@anthropic.com>
This commit is contained in:
@@ -256,9 +256,15 @@ NIH-plug provides `Smoother` — use it for all gain/threshold params to avoid z
|
|||||||
|
|
||||||
### Thread safety
|
### Thread safety
|
||||||
Params are atomics. The editor and audio thread communicate only through params and a shared
|
Params are atomics. The editor and audio thread communicate only through params and a shared
|
||||||
`Arc<Meters>` of **lock-free atomics** (`meters.rs`) — never a mutex on the audio path. `process()`
|
`Arc<Meters>` (`meters.rs`) — never a mutex on the audio path. Two lock-free feeds, both gated on
|
||||||
publishes one value per meter per block (gated on the editor being open); the editor reads them
|
the editor being open:
|
||||||
each frame. Never pass DSP state to the UI directly.
|
- **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
|
### VST3 licensing
|
||||||
You must accept Steinberg's VST3 SDK licence before distributing VST3 binaries.
|
You must accept Steinberg's VST3 SDK licence before distributing VST3 binaries.
|
||||||
|
|||||||
+25
-15
@@ -171,7 +171,7 @@ impl Plugin for Codename206 {
|
|||||||
&mut self,
|
&mut self,
|
||||||
buffer: &mut Buffer,
|
buffer: &mut Buffer,
|
||||||
_aux: &mut AuxiliaryBuffers,
|
_aux: &mut AuxiliaryBuffers,
|
||||||
_context: &mut impl ProcessContext<Self>,
|
context: &mut impl ProcessContext<Self>,
|
||||||
) -> ProcessStatus {
|
) -> ProcessStatus {
|
||||||
let lookahead = self.lookahead_samples();
|
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.
|
// Only do the (cheap) metering work when the editor is actually open.
|
||||||
let metering = self.params.editor_state.is_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 num_samples = buffer.samples();
|
||||||
let mut lvl_l = [0.0f32; meters::NUM_CHANNELS];
|
let mut lvl_l = [0.0f32; meters::NUM_CHANNELS];
|
||||||
let mut lvl_r = [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_l[b] = lvl_l[b].max(out_l);
|
||||||
lvl_r[b] = lvl_r[b].max(out_r);
|
lvl_r[b] = lvl_r[b].max(out_r);
|
||||||
gr[b] = gr[b].max(g);
|
gr[b] = gr[b].max(g);
|
||||||
self.scope_in[b] = self.scope_in[b].max(in_mono);
|
if playing {
|
||||||
self.scope_out[b] = self.scope_out[b].max(out_l.max(out_r));
|
self.scope_in[b] = self.scope_in[b].max(in_mono);
|
||||||
self.scope_gr[b] = self.scope_gr[b].max(g);
|
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);
|
lvl_r[ALL] = lvl_r[ALL].max(out_r);
|
||||||
gr[ALL] = gr[ALL].max(g);
|
gr[ALL] = gr[ALL].max(g);
|
||||||
lim_gr = lim_gr.max(self.limiter.gain_reduction_db());
|
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).
|
// Only advance the scope while the transport is playing, so it freezes (rather than
|
||||||
self.scope_samples += 1;
|
// scrolling silence) when the host is paused/stopped but still calling process().
|
||||||
if self.scope_samples >= self.scope_bucket_len {
|
if playing {
|
||||||
self.meters.scope.push(&self.scope_in, &self.scope_out, &self.scope_gr);
|
self.scope_in[ALL] = self.scope_in[ALL].max(in_mono);
|
||||||
self.scope_in = [0.0; meters::NUM_CHANNELS];
|
self.scope_out[ALL] = self.scope_out[ALL].max(out_l.max(out_r));
|
||||||
self.scope_out = [0.0; meters::NUM_CHANNELS];
|
self.scope_gr[ALL] = self.scope_gr[ALL].max(g);
|
||||||
self.scope_gr = [0.0; meters::NUM_CHANNELS];
|
|
||||||
self.scope_samples = 0;
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user