Files
bass-foundry/tools/editor-check/src/main.rs
T
nvrl 7b989f7685 feat(forge): add Forge tracked channel strip
One note tracker reads the input and Lathe, Temper and Knurl follow it as
modules, in any of six orders, each on or off. Every module gets the
tracker's events delayed by the latency of the modules in front of it.

- One module alone matches its standalone plugin sample for sample in
  every order.
- With Knurl's sub-octave pattern in front, Lathe in Forge follows the
  played note; a standalone Lathe after Knurl reads it an octave low.
- About 72 ms latency against about 206 ms for the three in a row.

The module pages reuse the standalone editors. Register Forge in the
catalog, CI, README, docs and website, and drop the built Ballast from
the site's idea list.
2026-11-06 16:30:23 +01:00

653 lines
25 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", "knurl", "planish",
"braze", "quench", "ballast", "forge",
]
.contains(&plugin)
{
eprintln!(
"Use --plugin rivet, patina, anvil, crucible, lathe, temper, weld, knurl, planish, \
braze, quench, ballast or forge"
);
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 knurl = plugin == "knurl";
let planish = plugin == "planish";
let braze = plugin == "braze";
let quench = plugin == "quench";
let ballast = plugin == "ballast";
let forge = plugin == "forge";
let (width, height) = if forge {
(forge_ui::WIDTH, forge_ui::HEIGHT)
} else if ballast {
(ballast_ui::WIDTH, ballast_ui::HEIGHT)
} else if quench {
(quench_ui::WIDTH, quench_ui::HEIGHT)
} else if braze {
(braze_ui::WIDTH, braze_ui::HEIGHT)
} else if planish {
(planish_ui::WIDTH, planish_ui::HEIGHT)
} else if knurl {
(knurl_ui::WIDTH, knurl_ui::HEIGHT)
} else 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 {
anvil_settings.ghost = 60.;
anvil_settings.kick_lock = true;
anvil_state.audition = true;
let monitor = anvil_core::Monitor {
audition: true,
..Default::default()
};
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);
let total = 300000;
for i in 0..total {
// A distorted F1 bass whose own sub drifts in phase and level, and a kick every
// 0.2 s in the sidechain while the audition runs.
let x = (saw.sample() * 1.6 + 0.3 * (i as f32 * 0.0009).sin()).tanh() * 0.4;
let t = (i % 9600) as f32 / 48000.;
let kick = 0.8 * (std::f32::consts::TAU * 50. * t).sin() * (-t / 0.06).exp();
engine.process_with_kick([x, x], kick, monitor);
if i < total - 36000 {
continue;
}
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 knurl_settings = knurl_core::Settings::default();
let mut knurl_state = knurl_ui::State::default();
if knurl {
// An F1 saw bass with the default Fry pattern, jitter and grit.
let mut engine = knurl_core::Engine::new(48000., 2).unwrap();
engine.set_settings(knurl_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..36000 {
let x = (saw.sample() * 2.2).tanh() * 0.5;
engine.process_sample([x, x]);
if i % 1600 == 0 {
knurl_state.accept_frame(engine.take_analyzer());
}
}
knurl_state.accept_frame(engine.take_analyzer());
}
let mut forge_settings = forge_core::Settings::default();
forge_settings.lathe.gains[1] = -9.;
forge_settings.lathe.gains[3] = 4.;
forge_settings.temper.drive = 9.;
forge_settings.order = [
forge_core::Stage::Knurl,
forge_core::Stage::Lathe,
forge_core::Stage::Temper,
];
let mut forge_state = forge_ui::State::default();
forge_state.page = match args
.iter()
.position(|a| a == "--page")
.map(|i| args[i + 1].as_str())
{
Some("temper") => forge_core::Stage::Temper,
Some("knurl") => forge_core::Stage::Knurl,
_ => forge_core::Stage::Lathe,
};
if forge {
// An F1 saw bass through Knurl's Fry pattern first: Lathe after it still follows F1.
let mut engine = forge_core::Engine::new(48000., 2).unwrap();
engine.set_settings(forge_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..36000 {
let x = (saw.sample() * 2.2).tanh() * 0.5;
engine.process_sample([x, x]);
if i % 1600 == 0 {
forge_state.accept_frame(engine.take_analyzer());
}
}
forge_state.accept_frame(engine.take_analyzer());
}
let mut planish_settings = planish_core::Settings {
depth: 80.,
gains: [0., 2., 3., -2.],
..Default::default()
};
let mut planish_state = planish_ui::State::default();
if planish {
// A distorted F1 saw bass: loud body, thin top, so both directions work.
let mut engine = planish_core::Engine::new(48000., 2).unwrap();
engine.set_settings(planish_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 {
planish_state.accept_frame(engine.take_analyzer());
}
}
planish_state.accept_frame(engine.take_analyzer());
}
let mut braze_settings = braze_core::Settings {
width: 30.,
..Default::default()
};
let mut braze_state = braze_ui::State::default();
if braze {
// A reese-like F1 saw at 140 BPM, 2.3 beats into the bar.
let mut engine = braze_core::Engine::new(48000., 2).unwrap();
engine.set_settings(braze_settings).unwrap();
engine.set_transport(Some(140.), Some(0.));
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..47300 {
let x = (saw.sample() * 1.6).tanh() * 0.4;
engine.process_sample([x, x]);
if i % 1600 == 0 {
braze_state.accept_frame(engine.take_analyzer());
}
}
braze_state.accept_frame(engine.take_analyzer());
}
let mut quench_settings = quench_core::Settings {
drive: 9.,
..Default::default()
};
let mut quench_state = quench_ui::State::default();
if quench {
// A 49 Hz sub under a distorted saw top two octaves up, driven 9 dB into the clipper.
let mut engine = quench_core::Engine::new(48000., 2).unwrap();
engine.set_settings(quench_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 196.);
for i in 0..48000 {
let t = i as f32 / 48000.;
let sub = 0.18 * (std::f32::consts::TAU * 49. * t).sin();
let top = (saw.sample() * 3.).tanh() * 0.3;
engine.process_sample([sub + top, sub + top], quench_core::Monitor::Quench);
if i % 1600 == 0 {
quench_state.accept_frame(engine.take_analyzer());
}
}
quench_state.accept_frame(engine.take_analyzer());
}
let mut ballast_settings = ballast_core::Settings {
segment: ballast_core::Segment::Grid,
target: -16.,
..Default::default()
};
let mut ballast_state = ballast_ui::State::default();
if ballast {
// A switch-up every quarter at 140 BPM, cycling four patches of different loudness.
let mut engine = ballast_core::Engine::new(48000., 2).unwrap();
engine.set_settings(ballast_settings).unwrap();
let mut saw = common_dsp::Saw::new(48000., 43.65);
let quarter = 48000. * 60. / 140.;
for i in 0..48000 * 9 {
if i % 512 == 0 {
engine.set_transport(Some(140.), Some(i as f64 / 48000. * 140. / 60.));
}
let part = (i as f32 / quarter) as usize;
let s = saw.sample();
let x = match part % 4 {
0 => (s * 4.).tanh() * 0.6,
1 => s * 0.15,
2 => (s * 2.).tanh() * 0.35,
_ => (s * 8.).tanh() * 0.9,
};
engine.process_sample([x, x]);
if i % 1600 == 0 {
ballast_state.accept_frame(engine.take_analyzer());
}
}
ballast_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 and runs
// through an allpass at 400 Hz, as an EQ in its chain would leave it. The real
// analyzer measures them and its fits go back to the engine, as on the worker.
let mut engine = weld_core::Engine::new(48000., 2).unwrap();
engine.set_settings(weld_settings).unwrap();
let link = engine.link();
let mut analyzer = weld_core::Analyzer::new(48000., weld_core::Fit::NONE);
let mut other = weld_core::Chain::new(
weld_core::Fit {
flip: true,
count: 1,
sections: [weld_core::Section { fc: 400., q: 0.7 }; weld_core::MAX_SECTIONS],
},
48000.,
);
let mut saw = common_dsp::Saw::new(48000., 43.65);
for i in 0..48000 * 4 {
let x = (saw.sample() * 1.6).tanh() * 0.4;
let b = 0.8 * other.process(0, x);
engine.process_sample([x, x], [b, b]);
if i % 1600 == 0 {
weld_state.accept_frame(engine.take_analyzer());
}
while let Some(settings) = link.pop_settings() {
analyzer.set_settings(settings);
}
while let Some(block) = link.pop_block() {
analyzer.push(&block.a, &block.b);
if let Some(update) = analyzer.poll() {
if let Some(fit) = update.fit {
link.push_fit(fit);
}
weld_state.accept_report(update.report);
}
}
}
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 forge {
forge_ui::style(&ctx);
} else if ballast {
ballast_ui::style(&ctx);
} else if quench {
quench_ui::style(&ctx);
} else if braze {
braze_ui::style(&ctx);
} else if planish {
planish_ui::style(&ctx);
} else if knurl {
knurl_ui::style(&ctx);
} else 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 forge {
forge_ui::show(ui, &mut forge_settings, &mut forge_state);
} else if ballast {
ballast_ui::show(ui, &mut ballast_settings, &mut ballast_state);
} else if quench {
quench_ui::show(ui, &mut quench_settings, &mut quench_state);
} else if braze {
braze_ui::show(ui, &mut braze_settings, &mut braze_state);
} else if planish {
planish_ui::show(ui, &mut planish_settings, &mut planish_state);
} else if knurl {
knurl_ui::show(ui, &mut knurl_settings, &mut knurl_state);
} else 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}");
}