feat(ballast): add Ballast switch-up leveller

Ballast gives every part of a bassline one gain that brings it to the target
loudness, with no compression inside a part. Parts start at note starts,
detected from the audio or taken from MIDI, or on the host's beat grid, so a
patch change on the same note counts too.

- Loudness is K-weighted over each part's first 100 ms, in 10 ms blocks with
  a relative gate, and every part is weighted from rest so the last part does
  not ring into it.
- The audio path is delayed by the tracker's lookahead, the window and a
  5 ms glide, so each gain is in place on its part's first sample.
- Learn takes the next part's loudness as the target. Amount, Range, Measure
  from (leave the sub out), Gate, Output, Bypass.
- Editor shows the last 32 parts in and out, the target, and the spread of
  the last 16 parts.
- Registered in the catalog, workflows, editor-check, README, chain and
  development docs, and the website copy.
This commit is contained in:
nvrl committed 2026-11-06 16:05:19 +01:00
1 parent d77b88c7a9
commit d93dd942a8
28 files changed
+4716 -21

No files matched your search

+43 -5
View File
@@ -12,13 +12,13 @@ fn main() {
.unwrap_or("rivet");
if ![
"rivet", "patina", "anvil", "crucible", "lathe", "temper", "weld", "knurl", "planish",
"braze", "quench",
"braze", "quench", "ballast",
]
.contains(&plugin)
{
eprintln!(
"Use --plugin rivet, patina, anvil, crucible, lathe, temper, weld, knurl, planish, \
braze or quench"
braze, quench or ballast"
);
std::process::exit(2);
}
@@ -33,7 +33,10 @@ fn main() {
let planish = plugin == "planish";
let braze = plugin == "braze";
let quench = plugin == "quench";
let (width, height) = if quench {
let ballast = plugin == "ballast";
let (width, height) = 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)
@@ -253,6 +256,37 @@ fn main() {
}
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 {
@@ -382,7 +416,9 @@ fn main() {
let session_motif = session.motifs[0].id;
shared.replace(session).unwrap();
let ctx = egui::Context::default();
if quench {
if ballast {
ballast_ui::style(&ctx);
} else if quench {
quench_ui::style(&ctx);
} else if braze {
braze_ui::style(&ctx);
@@ -425,7 +461,9 @@ fn main() {
},
|ui| {
egui::CentralPanel::default().show(ui, |ui| {
if quench {
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);