Files
bass-foundry/tools/editor-check/src/main.rs
T
nvrl e7f3346077 feat(anvil): add Ghost harmonics and Phone listen
Ghost adds the sub's harmonics 2–10 above Ghost from, so phones hear the
note. The band above the split is measured per harmonic and only missing
harmonics are filled, so added ones cannot cancel the patch's own. The sub
level does not change. Phone monitors the output in mono, 400 Hz–7 kHz.
2026-11-06 13:46:15 +01:00

505 lines
19 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",
]
.contains(&plugin)
{
eprintln!(
"Use --plugin rivet, patina, anvil, crucible, lathe, temper, weld, knurl or planish"
);
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 (width, height) = 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.;
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 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 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 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 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 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}");
}