Files
bass-foundry/plugins/weld/core/tests/realtime.rs
T
nvrl 9b3ddeaf56
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
feat(weld): phase alignment for layered bass, release 0.1.0
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.
2026-11-06 08:31:41 +01:00

71 lines
2.3 KiB
Rust

use std::{
alloc::{GlobalAlloc, Layout, System},
cell::Cell,
};
use weld_core::{Engine, Fit, Listen, Section, Settings};
struct CheckedAllocator;
thread_local! {
static CHECK:Cell<bool>=const {Cell::new(false)};
static OPERATIONS:Cell<usize>=const {Cell::new(0)};
}
fn record() {
if CHECK.try_with(Cell::get).unwrap_or(false) {
let _ = OPERATIONS.try_with(|n| n.set(n.get() + 1));
}
}
unsafe impl GlobalAlloc for CheckedAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
record();
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
record();
unsafe { System.dealloc(pointer, layout) }
}
unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 {
record();
unsafe { System.realloc(pointer, layout, size) }
}
}
#[global_allocator]
static ALLOCATOR: CheckedAllocator = CheckedAllocator;
#[test]
fn processing_fits_crossfades_and_a_full_queue_do_not_allocate_or_free() {
let mut engine = Engine::new(48000., 2).unwrap();
let mut settings = Settings::default();
engine.set_settings(settings).unwrap();
let link = engine.link();
let fits: Vec<Fit> = (0..4)
.map(|k| Fit {
flip: k % 2 == 1,
count: k,
sections: [Section {
fc: 200. * (k + 1) as f32,
q: 0.7,
}; 4],
})
.collect();
CHECK.with(|v| v.set(true));
// Nothing takes blocks, so the queue fills up and later blocks are dropped.
for i in 0..96000 {
if i % 4800 == 0 {
settings.output = ((i / 4800) % 5) as f32 - 2.;
settings.listen = [Listen::Layer, Listen::Sum, Listen::SumDry][(i / 4800) % 3];
engine.set_settings(settings).unwrap();
link.push_fit(fits[(i / 4800) % 4]);
}
let t = i as f32 / 48000.;
let x = (std::f32::consts::TAU * 55. * t).sin() * 0.5;
std::hint::black_box(engine.process_sample([x, -x], [x * 0.5, x]));
if i % 1600 == 0 {
std::hint::black_box(engine.take_analyzer());
}
}
engine.reset();
CHECK.with(|v| v.set(false));
assert_eq!(OPERATIONS.with(Cell::get), 0);
assert!(link.dropped() > 0);
}