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.
228 lines
6.7 KiB
Rust
228 lines
6.7 KiB
Rust
use nih_plug::prelude::*;
|
|
use nih_plug_egui::EguiState;
|
|
use std::sync::{Arc, RwLock};
|
|
use weld_core::{Fit, Listen, Mode, Settings, MAX_SECTIONS};
|
|
|
|
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum ModeParam {
|
|
#[id = "follow"]
|
|
Follow,
|
|
#[id = "learn"]
|
|
Learn,
|
|
#[id = "hold"]
|
|
Hold,
|
|
}
|
|
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum ListenParam {
|
|
#[id = "layer"]
|
|
Layer,
|
|
#[id = "sum"]
|
|
Sum,
|
|
#[id = "sum-dry"]
|
|
#[name = "Sum dry"]
|
|
SumDry,
|
|
}
|
|
/// Continuous controls, in the order of the gesture slots.
|
|
const FLOATS: usize = 5;
|
|
|
|
#[derive(Params)]
|
|
pub struct WeldParams {
|
|
#[persist = "editor"]
|
|
pub editor: Arc<EguiState>,
|
|
/// The applied alignment, so a project reopens with it.
|
|
#[persist = "weld-fit"]
|
|
pub fit: Arc<RwLock<Fit>>,
|
|
#[id = "mode"]
|
|
pub mode: EnumParam<ModeParam>,
|
|
#[id = "response"]
|
|
pub response: FloatParam,
|
|
#[id = "low"]
|
|
pub low: FloatParam,
|
|
#[id = "high"]
|
|
pub high: FloatParam,
|
|
#[id = "sections"]
|
|
pub sections: IntParam,
|
|
#[id = "gate"]
|
|
pub gate: FloatParam,
|
|
#[id = "listen"]
|
|
pub listen: EnumParam<ListenParam>,
|
|
#[id = "output"]
|
|
pub output: FloatParam,
|
|
#[id = "bypass"]
|
|
pub bypass: BoolParam,
|
|
}
|
|
fn hz(value: f32) -> String {
|
|
if value >= 1000. {
|
|
format!("{:.2} kHz", value / 1000.)
|
|
} else {
|
|
format!("{value:.0} Hz")
|
|
}
|
|
}
|
|
impl Default for WeldParams {
|
|
fn default() -> Self {
|
|
let d = Settings::default();
|
|
Self {
|
|
editor: EguiState::from_size(weld_ui::WIDTH, weld_ui::HEIGHT),
|
|
fit: Arc::new(RwLock::new(Fit::NONE)),
|
|
mode: EnumParam::new("Mode", ModeParam::Follow),
|
|
response: FloatParam::new(
|
|
"Response",
|
|
d.response,
|
|
FloatRange::Skewed {
|
|
min: 0.3,
|
|
max: 5.,
|
|
factor: FloatRange::skew_factor(-1.),
|
|
},
|
|
)
|
|
.with_unit(" s")
|
|
.with_value_to_string(Arc::new(|v| format!("{v:.2}"))),
|
|
low: FloatParam::new(
|
|
"Low",
|
|
d.low,
|
|
FloatRange::Skewed {
|
|
min: 30.,
|
|
max: 400.,
|
|
factor: FloatRange::skew_factor(-1.5),
|
|
},
|
|
)
|
|
.with_value_to_string(Arc::new(hz)),
|
|
high: FloatParam::new(
|
|
"High",
|
|
d.high,
|
|
FloatRange::Skewed {
|
|
min: 1000.,
|
|
max: 12000.,
|
|
factor: FloatRange::skew_factor(-1.),
|
|
},
|
|
)
|
|
.with_value_to_string(Arc::new(hz)),
|
|
sections: IntParam::new(
|
|
"Sections",
|
|
d.sections as i32,
|
|
IntRange::Linear {
|
|
min: 1,
|
|
max: MAX_SECTIONS as i32,
|
|
},
|
|
),
|
|
gate: FloatParam::new(
|
|
"Gate",
|
|
d.gate,
|
|
FloatRange::Linear {
|
|
min: -70.,
|
|
max: -30.,
|
|
},
|
|
)
|
|
.with_unit(" dB"),
|
|
listen: EnumParam::new("Listen", ListenParam::Layer),
|
|
output: FloatParam::new(
|
|
"Output",
|
|
d.output,
|
|
FloatRange::Linear {
|
|
min: -12.,
|
|
max: 12.,
|
|
},
|
|
)
|
|
.with_unit(" dB"),
|
|
bypass: BoolParam::new("Bypass", false).make_bypass(),
|
|
}
|
|
}
|
|
}
|
|
fn mode_param(mode: Mode) -> ModeParam {
|
|
match mode {
|
|
Mode::Follow => ModeParam::Follow,
|
|
Mode::Learn => ModeParam::Learn,
|
|
Mode::Hold => ModeParam::Hold,
|
|
}
|
|
}
|
|
fn listen_param(listen: Listen) -> ListenParam {
|
|
match listen {
|
|
Listen::Layer => ListenParam::Layer,
|
|
Listen::Sum => ListenParam::Sum,
|
|
Listen::SumDry => ListenParam::SumDry,
|
|
}
|
|
}
|
|
impl WeldParams {
|
|
pub fn settings(&self) -> Settings {
|
|
Settings {
|
|
mode: match self.mode.value() {
|
|
ModeParam::Follow => Mode::Follow,
|
|
ModeParam::Learn => Mode::Learn,
|
|
ModeParam::Hold => Mode::Hold,
|
|
},
|
|
response: self.response.value(),
|
|
low: self.low.value(),
|
|
high: self.high.value(),
|
|
sections: self.sections.value().clamp(1, MAX_SECTIONS as i32) as usize,
|
|
gate: self.gate.value(),
|
|
listen: match self.listen.value() {
|
|
ListenParam::Layer => Listen::Layer,
|
|
ListenParam::Sum => Listen::Sum,
|
|
ListenParam::SumDry => Listen::SumDry,
|
|
},
|
|
output: self.output.value(),
|
|
bypass: self.bypass.value(),
|
|
}
|
|
}
|
|
pub fn stored_fit(&self) -> Fit {
|
|
self.fit.read().map(|f| *f).unwrap_or(Fit::NONE)
|
|
}
|
|
fn floats(&self) -> [&FloatParam; FLOATS] {
|
|
[
|
|
&self.response,
|
|
&self.low,
|
|
&self.high,
|
|
&self.gate,
|
|
&self.output,
|
|
]
|
|
}
|
|
/// Pushes every changed editor value to the host. Continuous controls hold one gesture
|
|
/// while the pointer is down; switches are set in a single gesture.
|
|
pub fn apply(
|
|
&self,
|
|
setter: &ParamSetter,
|
|
old: &Settings,
|
|
new: &Settings,
|
|
dragging: bool,
|
|
open: &mut [bool; FLOATS],
|
|
) {
|
|
let values = [
|
|
(old.response, new.response),
|
|
(old.low, new.low),
|
|
(old.high, new.high),
|
|
(old.gate, new.gate),
|
|
(old.output, new.output),
|
|
];
|
|
for (i, (p, (a, b))) in self.floats().into_iter().zip(values).enumerate() {
|
|
if a != b {
|
|
if !open[i] {
|
|
open[i] = true;
|
|
setter.begin_set_parameter(p);
|
|
}
|
|
setter.set_parameter(p, b);
|
|
}
|
|
if !dragging && open[i] {
|
|
setter.end_set_parameter(p);
|
|
open[i] = false;
|
|
}
|
|
}
|
|
fn switch<P: Param>(setter: &ParamSetter, p: &P, v: P::Plain) {
|
|
setter.begin_set_parameter(p);
|
|
setter.set_parameter(p, v);
|
|
setter.end_set_parameter(p);
|
|
}
|
|
if new.bypass != old.bypass {
|
|
switch(setter, &self.bypass, new.bypass);
|
|
}
|
|
if new.mode != old.mode {
|
|
switch(setter, &self.mode, mode_param(new.mode));
|
|
}
|
|
if new.listen != old.listen {
|
|
switch(setter, &self.listen, listen_param(new.listen));
|
|
}
|
|
if new.sections != old.sections {
|
|
switch(setter, &self.sections, new.sections as i32);
|
|
}
|
|
}
|
|
}
|
|
pub const GESTURES: usize = FLOATS;
|