Check and Windows preview / check (anvil) (push) Successful in 2m0s
Check and Windows preview / check (crucible) (push) Successful in 1m29s
Check and Windows preview / check (lathe) (push) Successful in 1m27s
Check and Windows preview / check (patina) (push) Successful in 1m53s
Check and Windows preview / check (rivet) (push) Successful in 1m29s
Check and Windows preview / check (temper) (push) Successful in 1m29s
Check and Windows preview / check (weld) (push) Successful in 2m38s
Check and Windows preview / previews (push) Successful in 1m28s
Independent plugin release / plan (push) Successful in 8s
Check and Windows preview / windows (push) Successful in 3m14s
Independent plugin release / build (push) Successful in 3m50s
Independent plugin release / publish (push) Successful in 9s
Weld hears the other bass layer through the sidechain and fits polarity plus up to four RBJ allpass sections to the measured cross-spectrum, so the layers sum instead of cancelling. No latency; the layer on its own keeps its sound. - Worker thread: Welch cross-spectrum and coherence, Nelder-Mead fit with 1/f weighting, a per-band loss penalty and model selection by share of the achievable gain; GCC-PHAT lag detection flags a late layer. - Audio thread: allocation-free chain with a 40 ms warm-up and 40 ms crossfade per new fit; mono blocks go to the worker through a lock-free queue. - Follow / Learn / Hold; the fit is persisted and restored without a crossfade. - Editor: per-band loss and recovery against a perfect alignment, phase before and after, recovered dB, time offset and status. - Tests for flat magnitude, crossfade, fit targets, Follow hysteresis, Learn, silence, realtime allocation and the persisted fit in the host.
81 lines
2.5 KiB
Rust
81 lines
2.5 KiB
Rust
//! Host-independent controls. Gains in dB, frequencies in Hz, times in seconds.
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// What the plugin outputs.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
|
pub enum Listen {
|
|
/// The aligned main layer only. The sidechain layer reaches the mix on its own track.
|
|
#[default]
|
|
Layer,
|
|
/// Aligned main layer plus the sidechain layer, to judge the sum in one place.
|
|
Sum,
|
|
/// Unprocessed main layer plus the sidechain layer, to compare with `Sum`.
|
|
SumDry,
|
|
}
|
|
|
|
/// How the alignment is found.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
|
pub enum Mode {
|
|
/// Keep measuring and refit when a clearly better alignment shows up.
|
|
#[default]
|
|
Follow,
|
|
/// Measure from the moment Learn starts, with equal weight; the result is applied
|
|
/// when Learn ends.
|
|
Learn,
|
|
/// Keep the current alignment. The display keeps measuring.
|
|
Hold,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Settings {
|
|
pub mode: Mode,
|
|
/// Averaging time of the measurement in Follow mode.
|
|
pub response: f32,
|
|
/// Lowest frequency the fit listens to.
|
|
pub low: f32,
|
|
/// Highest frequency the fit listens to.
|
|
pub high: f32,
|
|
/// Largest number of allpass sections the fit may use.
|
|
pub sections: usize,
|
|
/// Frames quieter than this (dBFS RMS) on either layer are not measured.
|
|
pub gate: f32,
|
|
pub listen: Listen,
|
|
pub output: f32,
|
|
pub bypass: bool,
|
|
}
|
|
impl Default for Settings {
|
|
fn default() -> Self {
|
|
Self {
|
|
mode: Mode::Follow,
|
|
response: 1.5,
|
|
low: 60.,
|
|
high: 6000.,
|
|
sections: 4,
|
|
gate: -50.,
|
|
listen: Listen::Layer,
|
|
output: 0.,
|
|
bypass: false,
|
|
}
|
|
}
|
|
}
|
|
impl Settings {
|
|
pub fn validate(&self) -> Result<(), &'static str> {
|
|
let values = [self.response, self.low, self.high, self.gate, self.output];
|
|
if values.iter().any(|v| !v.is_finite()) {
|
|
return Err("non-finite setting");
|
|
}
|
|
if (0.3..=5.).contains(&self.response)
|
|
&& (30.0..=400.).contains(&self.low)
|
|
&& (1000.0..=12000.).contains(&self.high)
|
|
&& (1..=crate::chain::MAX_SECTIONS).contains(&self.sections)
|
|
&& (-70.0..=-30.).contains(&self.gate)
|
|
&& (-12.0..=12.).contains(&self.output)
|
|
{
|
|
Ok(())
|
|
} else {
|
|
Err("setting out of range")
|
|
}
|
|
}
|
|
}
|