feat(weld): phase alignment for layered bass, release 0.1.0
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.
This commit is contained in:
nvrl committed 2026-11-06 08:31:41 +01:00
1 parent 0a0917ac56
commit 9b3ddeaf56
26 files changed
+2898 -144

No files matched your search

+27 -10
View File
@@ -111,6 +111,20 @@ fn sidechain_port_state_latency_and_audio_lifecycle() {
envelope
.params
.insert("listen".into(), ParamValue::String("sum".into()));
envelope
.params
.insert("mode".into(), ParamValue::String("hold".into()));
envelope
.params
.insert("sections".into(), ParamValue::I32(2));
// A saved alignment that flips polarity.
let flipped = weld_core::Fit {
flip: true,
..weld_core::Fit::NONE
};
envelope
.fields
.insert("weld-fit".into(), serde_json::to_string(&flipped).unwrap());
let encoded = serde_json::to_vec(&envelope).unwrap();
let mut framed = (encoded.len() as u64).to_le_bytes().to_vec();
framed.extend_from_slice(&encoded);
@@ -126,13 +140,16 @@ fn sidechain_port_state_latency_and_audio_lifecycle() {
serde_json::to_value(&restored.params).unwrap(),
serde_json::to_value(&envelope.params).unwrap()
);
// Listen = Sum: a sidechain equal to the main input doubles the output (+6 dB).
// A silent sidechain leaves the main input alone. CLAP hosts pass every port, so
// the test always passes both (nih_plug reads one port past a short count).
let saved: weld_core::Fit = serde_json::from_str(&restored.fields["weld-fit"]).unwrap();
assert_eq!(saved, flipped);
// Listen = Sum with the restored flip: a sidechain equal to the main input cancels
// it from the first sample on. A silent sidechain leaves the level alone. CLAP hosts
// pass every port, so the test always passes both (nih_plug reads one port past a
// short count).
for (rate, with_side, expected_db) in [
(44100., true, 6.02),
(48000., false, 0.),
(96000., true, 6.02),
(44100., true, None),
(48000., false, Some(0.)),
(96000., true, None),
] {
assert!(p.activate.unwrap()(plugin, rate, 1, 512));
assert_eq!((*latency).get.unwrap()(plugin), 0);
@@ -213,10 +230,10 @@ fn sidechain_port_state_latency_and_audio_lifecycle() {
position += frames;
}
let ratio_db = 10. * (energy_out / energy_in).log10();
assert!(
(ratio_db - expected_db).abs() < 0.2,
"{rate} {with_side} {ratio_db}"
);
match expected_db {
Some(db) => assert!((ratio_db - db).abs() < 0.2, "{rate} {with_side} {ratio_db}"),
None => assert!(ratio_db < -60., "{rate} {with_side} {ratio_db}"),
}
p.stop_processing.unwrap()(plugin);
p.deactivate.unwrap()(plugin);
}