Files
bass-foundry/tools/editor-check/src/main.rs
T
nvrl e3286f9094 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.
2026-11-06 07:53:20 +01:00

423 lines
16 KiB
Rust

//! Offline compilation and software-rendered visual QA for the shared editor.
pub use egui;
#[path = "../../../plugins/rivet/plugin/src/editor.rs"]
mod editor;
use std::sync::Arc;
fn main() {
let args: Vec<_> = std::env::args().collect();
let plugin = args
.windows(2)
.find(|a| a[0] == "--plugin")
.map(|a| a[1].as_str())
.unwrap_or("rivet");
if !["rivet", "patina", "anvil", "crucible", "lathe", "temper", "weld"].contains(&plugin) {
eprintln!("Use --plugin rivet, patina, anvil, crucible, lathe, temper or weld");
std::process::exit(2);
}
let patina = plugin == "patina" || args.iter().any(|arg| arg == "--resonance");
let compact = std::env::args().any(|arg| arg == "--compact");
let anvil = plugin == "anvil";
let crucible = plugin == "crucible";
let lathe = plugin == "lathe";
let temper = plugin == "temper";
let weld = plugin == "weld";
let (width, height) = if weld {
(weld_ui::WIDTH, weld_ui::HEIGHT)
} else if temper {
(temper_ui::WIDTH, temper_ui::HEIGHT)
} else if lathe {
(lathe_ui::WIDTH, lathe_ui::HEIGHT)
} else if crucible {
(crucible_ui::WIDTH, crucible_ui::HEIGHT)
} else if anvil {
(anvil_ui::WIDTH, anvil_ui::HEIGHT)
} else if patina {
if compact {
(1040, 700)
} else {
(1180, 760)
}
} else {
(1100, 850)
};
let mut patina_settings = patina_core::Settings::preset(1);
patina_settings.focus[0] = patina_core::Focus {
enabled: true,
frequency: 380.,
width: 0.5,
sensitivity: -12.,
};
patina_settings.focus[1] = patina_core::Focus {
enabled: true,
frequency: 3000.,
width: 0.3,
sensitivity: 6.,
};
let mut patina_state = patina_ui::State::default();
patina_state.selected = Some(1);
patina_state.preset = 1;
patina_state.delta = std::env::args().any(|arg| arg == "--delta");
patina_settings.bypass = std::env::args().any(|arg| arg == "--bypass");
if patina && !std::env::args().any(|arg| arg == "--silent") {
let mut engine = patina_core::Engine::new(48000., 2).unwrap();
engine.set_settings(patina_settings).unwrap();
for i in 0..24000 {
let t = i as f32 / 48000.;
// A 220 Hz harmonic tone with a ring around 3 kHz.
let x = (1..80)
.map(|k| {
let f = 220. * k as f32;
let ring = 1. + 5. * (-((f / 3000.).log2() / 0.12).powi(2)).exp();
(std::f32::consts::TAU * f * t).sin() * ring / k as f32
})
.sum::<f32>()
* 0.12;
engine.process_sample([x, x * 0.85], patina_core::Monitor::default());
if let Some(frame) = engine.take_analyzer() {
patina_state.accept_frame(frame);
}
}
}
let mut anvil_settings = anvil_core::Settings::default();
let mut anvil_state = anvil_ui::State::default();
if anvil {
let mut engine = anvil_core::Engine::new(48000., 2).unwrap();
engine.set_settings(anvil_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..36000 {
// A distorted F1 bass whose own sub drifts in phase and level.
let x = (saw.sample() * 1.6 + 0.3 * (i as f32 * 0.0009).sin()).tanh() * 0.4;
engine.process_sample([x, x], anvil_core::Monitor::default());
if i % 1600 == 0 {
anvil_state.accept_frame(engine.take_analyzer());
}
}
anvil_state.accept_frame(engine.take_analyzer());
}
let mut lathe_settings = lathe_core::Settings::default();
let mut lathe_state = lathe_ui::State::default();
if lathe {
// A growl-like map: cut the hollow region, lift the 5th, darker top.
lathe_settings.gains[1] = -4.;
lathe_settings.gains[2] = -12.;
lathe_settings.gains[4] = 6.;
lathe_settings.gains[6] = 3.;
lathe_settings.gains[10] = -8.;
lathe_settings.gains[11] = -8.;
lathe_settings.tilt = -1.5;
lathe_settings.between = -6.;
let mut engine = lathe_core::Engine::new(48000., 2).unwrap();
engine.set_settings(lathe_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..36000 {
let x = (saw.sample() * 1.6).tanh() * 0.4;
engine.process_sample([x, x]);
if i % 1600 == 0 {
lathe_state.accept_frame(engine.take_analyzer());
}
}
lathe_state.accept_frame(engine.take_analyzer());
}
let mut temper_settings = temper_core::Settings {
drive: 20.,
curve: temper_core::Curve::Hard,
bias: 20.,
floor: 0.5,
push_harmonic: 5,
push_gain: 6.,
bright: 12.,
..Default::default()
};
let mut temper_state = temper_ui::State::default();
if temper {
let mut engine = temper_core::Engine::new(48000., 2).unwrap();
engine.set_settings(temper_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..36000 {
let x = saw.sample() * 0.3;
engine.process_sample([x, x]);
if i % 1600 == 0 {
temper_state.accept_frame(engine.take_analyzer());
}
}
temper_state.accept_frame(engine.take_analyzer());
}
let mut weld_settings = weld_core::Settings::default();
let mut weld_state = weld_ui::State::default();
if weld {
// Two layers of an F1 saw bass; the sidechain layer is polarity-flipped.
let mut engine = weld_core::Engine::new(48000., 2).unwrap();
engine.set_settings(weld_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..36000 {
let x = (saw.sample() * 1.6).tanh() * 0.4;
engine.process_sample([x, x], [-0.7 * x, -0.7 * x]);
if i % 1600 == 0 {
weld_state.accept_frame(engine.take_analyzer());
}
}
weld_state.accept_frame(engine.take_analyzer());
}
let mut crucible_settings = crucible_core::Settings {
folder: "C:\\Users\\you\\Documents\\Crucible".into(),
..Default::default()
};
let mut crucible_state = crucible_ui::State::default();
let crucible_link = crucible_ui::Link {
ring: Some(Arc::new(crucible_core::Ring::new(
48000.,
crucible_core::HISTORY,
))),
queues: Arc::new(crucible_core::Queues::default()),
};
if crucible {
// Four bars of a 150 BPM riddim pattern: square-ish bass hits on varied notes.
let rate = 48000.;
let beat = 60. / 150. * rate;
let frames = (16. * beat) as usize;
let mut audio = vec![0f32; frames * 2];
let pattern = [
(0., 29),
(1.5, 29),
(2.75, 32),
(4., 27),
(5.5, 29),
(6.5, 34),
(8., 29),
(9.5, 29),
(10.75, 36),
(12., 27),
(13.5, 32),
(14.5, 29),
];
for (at, note) in pattern {
let start = (at * beat) as usize;
let hz = common_dsp::midi_to_hz(note as f32);
let mut saw = common_dsp::Saw::new(rate, hz);
for i in 0..(0.6 * beat) as usize {
let f = start + i;
if f >= frames {
break;
}
let env = (1. - i as f32 / (0.6 * beat)).powf(0.4);
let v = (saw.sample() * 2.5).tanh() * 0.6 * env;
audio[2 * f] += v;
audio[2 * f + 1] += v;
}
}
let transport = crucible_core::Transport {
playing: true,
tempo: 150.,
beats: 0.,
numerator: 4,
denominator: 4,
};
let segments = vec![crucible_core::Segment {
frame: 0.,
quarters: 0.,
tempo: 150.,
}];
let take = crucible_core::Take::from_audio(rate, audio, 4, transport, segments, true);
crucible_settings.mode = crucible_core::Mode::Transient;
crucible_state.add(take.clone(), &crucible_settings);
crucible_state.add(take, &crucible_settings);
crucible_state.slice = Some(2);
crucible_state.takes[0].slices[5].enabled = false;
crucible_state.transport = Some(transport);
crucible_state.status = "Captured 4 bars on the bar grid.".into();
}
let shared = Arc::new(rivet_app::Shared::default());
let mut session = rivet_core::Session::demo();
session.motifs[0].phrase.notes[0].locked = true;
session.realize_all();
session.blocks[2].kept = true;
let creative = std::env::args().any(|arg| arg == "--creative");
if creative {
session.performance.mode = rivet_core::RoleMode::CallResponse;
session.performance.voices[0].name = "Glass question".into();
session.performance.voices[1].name = "Warm answer".into();
for (i, voice) in session.performance.voices.iter_mut().enumerate() {
voice.controller.enabled = true;
voice.controller.cc = 20 + i as u8;
voice.controller.shape(2);
voice.attack_ms = 40;
}
}
let session_motif = session.motifs[0].id;
shared.replace(session).unwrap();
let ctx = egui::Context::default();
if weld {
weld_ui::style(&ctx);
} else if temper {
temper_ui::style(&ctx);
} else if lathe {
lathe_ui::style(&ctx);
} else if crucible {
crucible_ui::style(&ctx);
} else if anvil {
anvil_ui::style(&ctx);
} else if patina {
patina_ui::style(&ctx);
} else {
editor::style(&ctx);
}
let mut state = editor::EditorState::default();
if creative {
state.tab = editor::Tab::Sound;
}
if std::env::args().any(|arg| arg == "--motif") {
state.focus = Some(editor::Focus::Motif(session_motif));
}
let mut textures = std::collections::HashMap::new();
let mut output = None;
for _ in 0..3 {
let frame = ctx.run(
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(width as f32, height as f32),
)),
..Default::default()
},
|ui| {
egui::CentralPanel::default().show(ui, |ui| {
if weld {
weld_ui::show(ui, &mut weld_settings, &mut weld_state);
} else if temper {
temper_ui::show(ui, &mut temper_settings, &mut temper_state);
} else if lathe {
lathe_ui::show(ui, &mut lathe_settings, &mut lathe_state);
} else if crucible {
crucible_ui::show(
ui,
&mut crucible_settings,
&mut crucible_state,
&crucible_link,
);
} else if anvil {
anvil_ui::show(ui, &mut anvil_settings, &mut anvil_state);
} else if patina {
patina_ui::show(ui, &mut patina_settings, &mut patina_state);
} else {
editor::show(ui, &shared, &mut state);
}
});
},
);
for (id, delta) in &frame.textures_delta.set {
let image = match &delta.image {
egui::ImageData::Color(image) => image.as_ref().clone(),
egui::ImageData::Font(image) => egui::ColorImage {
size: image.size,
pixels: image.srgba_pixels(None).collect(),
},
};
if let Some([x, y]) = delta.pos {
let full = textures.get_mut(id).unwrap();
let full: &mut egui::ColorImage = full;
for row in 0..image.size[1] {
for col in 0..image.size[0] {
full.pixels[(y + row) * full.size[0] + x + col] =
image.pixels[row * image.size[0] + col];
}
}
} else {
textures.insert(*id, image.clone());
}
}
output = Some(frame);
}
let output = output.unwrap();
let meshes = ctx.tessellate(output.shapes, 1.);
let mut image = image::RgbaImage::from_pixel(width, height, image::Rgba([23, 34, 51, 255]));
for clipped in meshes {
let egui::epaint::Primitive::Mesh(mesh) = clipped.primitive else {
continue;
};
let Some(texture) = textures.get(&mesh.texture_id) else {
continue;
};
for tri in mesh.indices.chunks_exact(3) {
let [a, b, c] = [
mesh.vertices[tri[0] as usize],
mesh.vertices[tri[1] as usize],
mesh.vertices[tri[2] as usize],
];
let min_x = a
.pos
.x
.min(b.pos.x)
.min(c.pos.x)
.max(clipped.clip_rect.left())
.max(0.) as u32;
let min_y = a
.pos
.y
.min(b.pos.y)
.min(c.pos.y)
.max(clipped.clip_rect.top())
.max(0.) as u32;
let max_x = a
.pos
.x
.max(b.pos.x)
.max(c.pos.x)
.min(clipped.clip_rect.right())
.min((width - 1) as f32) as u32;
let max_y = a
.pos
.y
.max(b.pos.y)
.max(c.pos.y)
.min(clipped.clip_rect.bottom())
.min((height - 1) as f32) as u32;
let cross = |u: egui::Vec2, v: egui::Vec2| u.x * v.y - u.y * v.x;
let area = cross(b.pos - a.pos, c.pos - a.pos);
if area.abs() < 0.0001 {
continue;
}
for y in min_y..=max_y {
for x in min_x..=max_x {
let p = egui::pos2(x as f32 + 0.5, y as f32 + 0.5);
let wa = cross(b.pos - p, c.pos - p) / area;
let wb = cross(c.pos - p, a.pos - p) / area;
let wc = 1. - wa - wb;
if wa < 0. || wb < 0. || wc < 0. {
continue;
}
let uv = a.uv.to_vec2() * wa + b.uv.to_vec2() * wb + c.uv.to_vec2() * wc;
let tx = (uv.x * texture.size[0] as f32)
.floor()
.clamp(0., texture.size[0] as f32 - 1.)
as usize;
let ty = (uv.y * texture.size[1] as f32)
.floor()
.clamp(0., texture.size[1] as f32 - 1.)
as usize;
let tex = texture.pixels[ty * texture.size[0] + tx].to_array();
let ac = a.color.to_array();
let bc = b.color.to_array();
let cc = c.color.to_array();
let mut color = [0.; 4];
for i in 0..4 {
color[i] = (ac[i] as f32 * wa + bc[i] as f32 * wb + cc[i] as f32 * wc)
/ 255.
* tex[i] as f32;
}
let pixel = image.get_pixel_mut(x, y);
let alpha = color[3] / 255.;
for i in 0..3 {
pixel[i] =
(color[i] + pixel[i] as f32 * (1. - alpha)).clamp(0., 255.) as u8;
}
}
}
}
}
let path = std::env::args()
.nth(1)
.unwrap_or_else(|| "/tmp/rivet-editor.png".into());
image.save(&path).unwrap();
println!("Rendered {path}");
}