feat(weld): add sidechain test build

Weld will phase-align a bass layer to another layer it hears through the
sidechain. This first build only checks that FL Studio delivers the
sidechain to a CLAP aux port: it passes the main layer through unchanged,
meters the sidechain and offers Layer/Sum/Sum dry listening. No latency.
This commit is contained in:
nvrl committed 2026-11-06 07:53:20 +01:00
1 parent b3ebe4d952
commit e3286f9094
21 files changed
+3471 -9

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
//! Weld editor, shared by the CLAP wrapper and the headless renderer.
use common_ui::*;
use egui::{self, FontId, RichText};
use weld_core::{AnalyzerFrame, Listen, Settings};
pub use common_ui::style;
pub const WIDTH: u32 = 1040;
pub const HEIGHT: u32 = 650;
pub struct State {
pub analyzer: AnalyzerFrame,
/// True while a pointer button is held, so knob drags become one host gesture.
pub gesture_active: bool,
pub help: String,
}
impl Default for State {
fn default() -> Self {
Self {
analyzer: AnalyzerFrame::default(),
gesture_active: false,
help: DEFAULT_HELP.into(),
}
}
}
const DEFAULT_HELP: &str =
"Put Weld on one bass layer and send the other layer to its sidechain input.";
impl State {
pub fn accept_frame(&mut self, frame: AnalyzerFrame) {
let old = self.analyzer;
self.analyzer = frame;
// Meters fall smoothly between frames.
self.analyzer.main_db = frame.main_db.max(old.main_db - 3.);
self.analyzer.side_db = frame.side_db.max(old.side_db - 3.);
self.analyzer.side_peak_db = frame.side_peak_db.max(old.side_peak_db - 3.);
}
}
fn hint(r: &egui::Response, state: &mut State, text: &str) {
if r.hovered() || r.has_focus() {
state.help = text.into();
}
}
pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
backdrop(ui);
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "WELD", "LAYER ALIGN", |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Pass the main layer through unchanged.");
if r.clicked() {
settings.bypass = !settings.bypass;
}
});
ui.add_space(6.);
let frame = state.analyzer;
panel(ui, |ui| {
ui.set_width(ui.available_width());
ui.set_min_height(380.);
ui.vertical(|ui| {
caption(ui, "SIDECHAIN");
let (text, color) = if frame.side_db > -90. {
(format!("sidechain: {:.0} dBFS", frame.side_db), TEXT)
} else if frame.side_seen {
("sidechain: silent".to_string(), MUTED)
} else {
("no sidechain signal".to_string(), DIM)
};
ui.label(
RichText::new(text)
.font(FontId::new(40., egui::FontFamily::Name("medium".into())))
.color(color),
);
ui.add_space(12.);
meter(ui, "MAIN", frame.main_db, STEEL, 520.);
meter(ui, "SIDECHAIN", frame.side_db, ACCENT, 520.);
meter(ui, "SIDE PEAK", frame.side_peak_db, FOCUS, 520.);
ui.add_space(18.);
ui.label(
RichText::new(
"FL Studio: on the other layer's mixer track, right-click the route arrow \
to this track and choose \"Sidechain to this track\". Then open Weld's \
wrapper settings and set the sidechain input to that track.",
)
.size(13.)
.color(MUTED),
);
});
});
ui.add_space(6.);
panel(ui, |ui| {
ui.set_width(ui.available_width());
ui.horizontal(|ui| {
caption(ui, "LISTEN");
for (listen, label, help) in [
(Listen::Layer, "LAYER", "Output the main layer only. Use this in the mix."),
(Listen::Sum, "SUM", "Output main + sidechain layer. Mute the other layer's own send while listening."),
(Listen::SumDry, "SUM DRY", "Output the unprocessed main + sidechain layer, to compare with SUM."),
] {
let r = ui.selectable_label(settings.listen == listen, RichText::new(label).size(13.));
if r.clicked() {
settings.listen = listen;
}
hint(&r, state, help);
}
ui.add_space(40.);
let d = Settings::default();
let r = rotary(ui, "Output", &mut settings.output, -12. ..=12., d.output, " dB", 54., ACCENT);
hint(&r, state, "Output trim.");
});
});
ui.separator();
ui.label(RichText::new(&state.help).color(MUTED));
}