mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-31 14:06:56 -06:00
Take a plugin that puts out NaN out of the mix instead of silencing it
One insert or instrument putting out NaN or infinity silenced the whole mix: the value spread through the busses and the main mix (NaN plus anything is NaN), and the output guard turned all of it into silence. It can also poison the state of filters, reverbs and compressors downstream, which then stay silent after the plugin has calmed down. After each insert (track or master) and each instrument processes, the engine checks its block. If the block holds NaN or infinity, the engine silences that block of the strip and skips the plugin until the next MixEngine::reset (playback starts or stops), which resets it and tries it again. Meanwhile the strip plays on without it, as if bypassed. The check costs about 60 ns per insert for a stereo 512-sample block. StripMeter::plugin_failures counts how often a strip lost a plugin and names the slot of the latest, so the UI names that plugin in the status line each time, also when it fails again after a restart. The AU host no longer zeroes non-finite output itself, so Audio Units are taken out and named like CLAP and VST3 plugins.
This commit is contained in:
@@ -170,13 +170,12 @@ impl Plugin for AuPlugin {
|
||||
if ch == 1 && au_ch >= 2 {
|
||||
if let (Some(l), Some(r)) = (self.inst.output(0, n), self.inst.output(1, n)) {
|
||||
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
|
||||
let v = 0.5 * (a + b);
|
||||
*d = if v.is_finite() { v } else { 0.0 };
|
||||
*d = 0.5 * (a + b);
|
||||
}
|
||||
}
|
||||
} else if let Some(src) = self.inst.output(c.min(au_ch.saturating_sub(1)), n) {
|
||||
for (d, &s) in dst.iter_mut().zip(src) {
|
||||
*d = if s.is_finite() { s } else { 0.0 };
|
||||
*d = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+131
-6
@@ -155,6 +155,24 @@ pub struct StripMeter {
|
||||
pub peaks: Vec<f32>,
|
||||
/// Gain reduction of the first dynamics insert, dB.
|
||||
pub gain_reduction_db: f32,
|
||||
pub plugin_failures: PluginFailures,
|
||||
}
|
||||
|
||||
/// How often a strip's plugins were taken out of the signal path for putting out NaN or infinity.
|
||||
/// Each stays out until the engine is next reset (playback starts or stops).
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct PluginFailures {
|
||||
/// It only grows.
|
||||
pub count: u32,
|
||||
/// The slot of the latest: an insert index or [`INSTRUMENT_SLOT`].
|
||||
pub last: usize,
|
||||
}
|
||||
|
||||
impl PluginFailures {
|
||||
fn record(&mut self, slot: usize) {
|
||||
self.count = self.count.wrapping_add(1);
|
||||
self.last = slot;
|
||||
}
|
||||
}
|
||||
|
||||
impl StripMeter {
|
||||
@@ -163,6 +181,7 @@ impl StripMeter {
|
||||
self.peak = o.peak;
|
||||
self.peaks.clone_from(&o.peaks);
|
||||
self.gain_reduction_db = o.gain_reduction_db;
|
||||
self.plugin_failures = o.plugin_failures;
|
||||
}
|
||||
|
||||
fn measure(&mut self, bufs: &[Vec<f32>], frames: usize, gr: f32) {
|
||||
@@ -191,6 +210,7 @@ struct PluginSlot {
|
||||
plugin: Box<dyn Plugin>,
|
||||
/// Last values pushed to the plugin, in the insert's parameter-map order.
|
||||
applied: Vec<f32>,
|
||||
failed: bool,
|
||||
}
|
||||
|
||||
struct Strip {
|
||||
@@ -217,6 +237,7 @@ struct Strip {
|
||||
align_pre: DelayLine,
|
||||
out_extra: DelayLine,
|
||||
outbuf: Vec<Vec<f32>>,
|
||||
failures: PluginFailures,
|
||||
}
|
||||
|
||||
/// A multichannel delay line (samples), resized only when the delay changes.
|
||||
@@ -287,6 +308,7 @@ impl Strip {
|
||||
align_pre: DelayLine::default(),
|
||||
out_extra: DelayLine::default(),
|
||||
outbuf: Vec::new(),
|
||||
failures: PluginFailures::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,7 +387,7 @@ impl SlotCtx<'_> {
|
||||
let state = if third { ins.state_bytes() } else { None };
|
||||
create_instance(&ins.plugin, state.as_deref(), self.sr, self.mb, ch).map(|n| n.plugin)
|
||||
}?;
|
||||
Some(PluginSlot { id: ins.plugin.clone(), ch, plugin, applied: Vec::new() })
|
||||
Some(PluginSlot { id: ins.plugin.clone(), ch, plugin, applied: Vec::new(), failed: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,10 +477,12 @@ impl MixEngine {
|
||||
for s in self.strips.values_mut() {
|
||||
for p in s.plugins.iter_mut().flatten() {
|
||||
p.plugin.reset();
|
||||
p.failed = false;
|
||||
}
|
||||
if let Some(i) = &mut s.instrument {
|
||||
i.plugin.all_notes_off();
|
||||
i.plugin.reset();
|
||||
i.failed = false;
|
||||
}
|
||||
s.held.clear();
|
||||
}
|
||||
@@ -668,9 +692,13 @@ impl MixEngine {
|
||||
if let (Some(ins), Some(Some(slot))) = (ins, strip.plugins.get_mut(i))
|
||||
&& ins.active
|
||||
&& !ins.bypass
|
||||
&& !slot.failed
|
||||
{
|
||||
apply_params(slot, ins, t, i, pos);
|
||||
slot.plugin.process(&mut strip.buf, frames);
|
||||
if contain_failure(slot, &mut strip.buf, frames) {
|
||||
strip.failures.record(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
let v0 = volume_db_at(t, pos);
|
||||
@@ -685,7 +713,9 @@ impl MixEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.meters.entry(t.id).or_default().measure(&self.main, frames, 0.0);
|
||||
let m = self.meters.entry(t.id).or_default();
|
||||
m.measure(&self.main, frames, 0.0);
|
||||
m.plugin_failures = strip.failures;
|
||||
}
|
||||
}
|
||||
// Metronome: summed after the master fader, so it reaches the speakers without passing
|
||||
@@ -728,7 +758,9 @@ impl MixEngine {
|
||||
|
||||
/// Send, meter and pan a processed strip into the busses / main mix.
|
||||
fn route_strip(&mut self, t: &Track, strip: &mut Strip, pos: Samples, frames: usize) {
|
||||
self.meters.entry(t.id).or_default().measure(&strip.buf, frames, strip.gr);
|
||||
let m = self.meters.entry(t.id).or_default();
|
||||
m.measure(&strip.buf, frames, strip.gr);
|
||||
m.plugin_failures = strip.failures;
|
||||
let src_fmt = strip_format(t, strip.buf.len());
|
||||
let law = self.pan_law;
|
||||
for (i, snd) in t.mixer.sends.iter().enumerate() {
|
||||
@@ -872,7 +904,7 @@ fn process_strip(
|
||||
}
|
||||
}
|
||||
TrackKind::Instrument | TrackKind::Midi => {
|
||||
if let Some(inst) = &mut strip.instrument {
|
||||
if let Some(inst) = strip.instrument.as_mut().filter(|i| !i.failed) {
|
||||
// #152: push the instrument's stored parameter values into the live plugin
|
||||
// before processing, the same way inserts do below. Without this, the values
|
||||
// loaded from a session (or set via `mix.insert_state`) never reach the plugin
|
||||
@@ -895,6 +927,9 @@ fn process_strip(
|
||||
*x = 0.5 * (*x + *y);
|
||||
}
|
||||
}
|
||||
if contain_failure(inst, &mut strip.buf, frames) {
|
||||
strip.failures.record(INSTRUMENT_SLOT);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -909,11 +944,15 @@ fn process_strip(
|
||||
let mut latency = 0usize;
|
||||
for (i, ins) in t.mixer.inserts.iter().enumerate() {
|
||||
let (Some(ins), Some(Some(slot))) = (ins, strip.plugins.get_mut(i)) else { continue };
|
||||
if !ins.active || ins.bypass {
|
||||
if !ins.active || ins.bypass || slot.failed {
|
||||
continue;
|
||||
}
|
||||
apply_params(slot, ins, t, i, pos);
|
||||
slot.plugin.process(&mut strip.buf, frames);
|
||||
if contain_failure(slot, &mut strip.buf, frames) {
|
||||
strip.failures.record(i);
|
||||
continue;
|
||||
}
|
||||
latency = latency.saturating_add(slot.plugin.latency());
|
||||
if gr == 0.0 {
|
||||
gr = slot.plugin.gain_reduction_db();
|
||||
@@ -966,6 +1005,20 @@ fn process_strip(
|
||||
}
|
||||
}
|
||||
|
||||
/// Silences a block the plugin filled with NaN or infinity and takes the plugin out until the
|
||||
/// next [`MixEngine::reset`]. True then.
|
||||
fn contain_failure(slot: &mut PluginSlot, buf: &mut [Vec<f32>], frames: usize) -> bool {
|
||||
// A branch-free fold vectorizes; `all` runs about 8 times slower here.
|
||||
if buf.iter().all(|c| c.get(..frames).unwrap_or(c).iter().fold(true, |ok, x| ok & x.is_finite())) {
|
||||
return false;
|
||||
}
|
||||
for c in buf.iter_mut() {
|
||||
c.iter_mut().take(frames).for_each(|x| *x = 0.0);
|
||||
}
|
||||
slot.failed = true;
|
||||
true
|
||||
}
|
||||
|
||||
/// Clip-effect parameters of a clip, or None when it has none or they are bypassed.
|
||||
fn clip_fx_params(s: &Session, clip: u64) -> Option<Vec<(String, f32)>> {
|
||||
if s.edit.values.is_empty() {
|
||||
@@ -2005,6 +2058,11 @@ mod tests {
|
||||
|
||||
fn session_with_dc(level: f32, frames: usize) -> (Session, TrackId) {
|
||||
let mut s = Session::default();
|
||||
let t = add_dc_track(&mut s, level, frames);
|
||||
(s, t)
|
||||
}
|
||||
|
||||
fn add_dc_track(s: &mut Session, level: f32, frames: usize) -> TrackId {
|
||||
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
|
||||
let buf = AudioBuffer { sample_rate: 48_000, channels: vec![vec![level; frames]] };
|
||||
s.pool.insert(SourceId(900), Arc::new(SourceAudio::new(buf)));
|
||||
@@ -2012,7 +2070,7 @@ mod tests {
|
||||
if let Some(pl) = s.track_mut(t).and_then(|t| t.playlist_mut()) {
|
||||
pl.clips.push(Clip::audio(id, "dc", SourceId(900), 0, 0, frames as i64));
|
||||
}
|
||||
(s, t)
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2199,6 +2257,73 @@ mod tests {
|
||||
assert!(eng.drain_plugins().is_empty());
|
||||
}
|
||||
|
||||
struct Nan(Box<dyn Plugin>);
|
||||
|
||||
impl Plugin for Nan {
|
||||
fn info(&self) -> &'static soundcraft_dsp::PluginInfo {
|
||||
self.0.info()
|
||||
}
|
||||
fn prepare(&mut self, sr: f32, mb: usize, ch: usize) {
|
||||
self.0.prepare(sr, mb, ch);
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.0.reset();
|
||||
}
|
||||
fn set_param(&mut self, id: &str, v: f32) -> bool {
|
||||
self.0.set_param(id, v)
|
||||
}
|
||||
fn param(&self, id: &str) -> Option<f32> {
|
||||
self.0.param(id)
|
||||
}
|
||||
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
|
||||
io.iter_mut().for_each(|c| c.iter_mut().take(frames).for_each(|x| *x = f32::NAN));
|
||||
}
|
||||
}
|
||||
|
||||
fn nan(track: TrackId, slot: usize, ch: usize) -> Vec<PreparedInstance> {
|
||||
vec![PreparedInstance { track, slot, id: "clap:test.nan".into(), ch, plugin: Box::new(Nan(soundcraft_dsp::create("gain").unwrap())) }]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_insert_that_puts_out_nan_is_taken_out_until_the_next_reset() {
|
||||
let (mut s, a) = session_with_dc(0.5, 48_000);
|
||||
let b = add_dc_track(&mut s, 0.5, 48_000);
|
||||
s.track_mut(a).unwrap().mixer.inserts[2] = Some(Insert::new("clap:test.nan"));
|
||||
let mut eng = MixEngine::new(48_000.0, 512);
|
||||
eng.set_external_instances(true);
|
||||
let both = render_block(&mut eng, &s, 0);
|
||||
eng.adopt(nan(a, 2, 1));
|
||||
let only_b = render_block(&mut eng, &s, 512);
|
||||
assert!(only_b > 0.3 && (only_b - both / 2.0).abs() < 1e-6, "{only_b} vs {both}");
|
||||
assert_eq!(eng.meters[&a].plugin_failures, PluginFailures { count: 1, last: 2 });
|
||||
assert_eq!(eng.meters[&b].plugin_failures, PluginFailures::default());
|
||||
assert_eq!(render_block(&mut eng, &s, 1024), both);
|
||||
eng.reset();
|
||||
assert_eq!(render_block(&mut eng, &s, 1536), only_b);
|
||||
assert_eq!(eng.meters[&a].plugin_failures.count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nan_from_an_instrument_or_a_master_insert_is_contained() {
|
||||
let mut s = synth_session(-6.0);
|
||||
let synth = s.tracks[0].id;
|
||||
s.track_mut(synth).unwrap().instrument = Some(Insert::new("clap:test.nan"));
|
||||
let b = add_dc_track(&mut s, 0.5, 48_000);
|
||||
let mut eng = MixEngine::new(48_000.0, 512);
|
||||
eng.set_external_instances(true);
|
||||
eng.adopt(nan(synth, INSTRUMENT_SLOT, 2));
|
||||
let only_b = render_block(&mut eng, &s, 0);
|
||||
assert!(only_b > 0.3, "{only_b}");
|
||||
assert_eq!(eng.meters[&synth].plugin_failures, PluginFailures { count: 1, last: INSTRUMENT_SLOT });
|
||||
assert_eq!(eng.meters[&b].plugin_failures, PluginFailures::default());
|
||||
let m = s.add_track(TrackKind::Master, ChannelFormat::Stereo, None);
|
||||
s.track_mut(m).unwrap().mixer.inserts[0] = Some(Insert::new("clap:test.nan"));
|
||||
eng.adopt(nan(m, 0, 2));
|
||||
assert_eq!(render_block(&mut eng, &s, 512), 0.0);
|
||||
assert_eq!(eng.meters[&m].plugin_failures, PluginFailures { count: 1, last: 0 });
|
||||
assert_eq!(render_block(&mut eng, &s, 1024), only_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_state_comes_first_then_session_params() {
|
||||
let (mut s, t) = session_with_dc(0.5, 48_000);
|
||||
|
||||
@@ -760,7 +760,7 @@ mod tests {
|
||||
let keep = TrackId(1);
|
||||
let stale = TrackId(2);
|
||||
let mut source = MeterSnapshot::default();
|
||||
source.tracks.insert(keep, StripMeter { peak: [0.5, 0.25], peaks: vec![0.5, 0.25], gain_reduction_db: 3.0 });
|
||||
source.tracks.insert(keep, StripMeter { peak: [0.5, 0.25], peaks: vec![0.5, 0.25], gain_reduction_db: 3.0, ..StripMeter::default() });
|
||||
source.main.peaks = vec![0.75, 0.5];
|
||||
let mut out = MeterSnapshot::default();
|
||||
out.tracks.insert(stale, StripMeter::default());
|
||||
|
||||
@@ -199,6 +199,8 @@ pub struct MeterDisplay {
|
||||
pub hold_age: f32,
|
||||
pub clip: bool,
|
||||
pub gr: f32,
|
||||
/// [`soundcraft_mix::PluginFailures::count`] as last seen.
|
||||
pub plugin_failures: u32,
|
||||
}
|
||||
|
||||
/// An in-progress mouse gesture in the Edit window.
|
||||
@@ -755,6 +757,12 @@ impl SoundApp {
|
||||
for (id, m) in &snap.tracks {
|
||||
let d = self.meters.entry(*id).or_default();
|
||||
feed_meter(d, m.peak, m.gain_reduction_db, dt);
|
||||
if m.plugin_failures.count != d.plugin_failures {
|
||||
d.plugin_failures = m.plugin_failures.count;
|
||||
if let Some(msg) = failed_plugin_status(self.engine.session(), *id, m.plugin_failures.last) {
|
||||
self.ui.status = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
let main = snap.main.peak;
|
||||
feed_meter(&mut self.main_meter, main, 0.0, dt);
|
||||
@@ -956,6 +964,15 @@ fn feed_meter(d: &mut MeterDisplay, peak: [f32; 2], gr: f32, dt: f32) {
|
||||
d.gr = if gr > d.gr { gr } else { d.gr * fall };
|
||||
}
|
||||
|
||||
/// The status line naming the plugin in `slot` (an insert index or
|
||||
/// [`soundcraft_mix::INSTRUMENT_SLOT`]), which the engine took out of the signal path.
|
||||
fn failed_plugin_status(s: &soundcraft_model::Session, track: TrackId, slot: usize) -> Option<String> {
|
||||
let t = s.track(track)?;
|
||||
let ins = if slot == soundcraft_mix::INSTRUMENT_SLOT { t.instrument.as_ref() } else { t.mixer.inserts.get(slot)?.as_ref() }?;
|
||||
let name = crate::mix_window::plugin_info(&ins.plugin).map_or(ins.plugin.as_str(), |p| p.name);
|
||||
Some(format!("{name} on {} put out invalid audio, so it is bypassed until playback stops or starts again.", t.name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Services, SoundApp, automation_step_due};
|
||||
@@ -988,6 +1005,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_status_names_a_plugin_the_engine_took_out() {
|
||||
use soundcraft_model::{ChannelFormat, Insert, TrackKind};
|
||||
let mut s = soundcraft_model::Session::default();
|
||||
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, Some("Vox"));
|
||||
s.track_mut(t).unwrap().mixer.inserts[3] = Some(Insert::new("gain"));
|
||||
let msg = super::failed_plugin_status(&s, t, 3).unwrap();
|
||||
assert!(msg.starts_with("Gain on Vox put out invalid audio"), "{msg}");
|
||||
assert_eq!(super::failed_plugin_status(&s, t, 0), None);
|
||||
assert_eq!(super::failed_plugin_status(&s, t, soundcraft_mix::INSTRUMENT_SLOT), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_session_cancels_play_queued_before_the_next_frame() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
|
||||
Reference in New Issue
Block a user