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.
423 lines
16 KiB
Rust
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}");
|
|
}
|