mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-28 04:26:51 -06:00
Merge pull request #1144 from Silvertree2010/perf/export-speed
Speed up mix bounce by about 20 % with byte-identical output
This commit is contained in:
@@ -159,10 +159,17 @@ impl Quantizer {
|
||||
v += self.uniform() - self.uniform();
|
||||
}
|
||||
let max = scale - 1.0;
|
||||
v.round().clamp(-scale, max) as i32
|
||||
round_half_away(v.clamp(-scale, max)) as i32
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_half_away(v: f64) -> f64 {
|
||||
let t = v as i64;
|
||||
let f = v - t as f64;
|
||||
(t + i64::from(f >= 0.5) - i64::from(f <= -0.5)) as f64
|
||||
}
|
||||
|
||||
/// Bits per sample at `depth`, and whether they are float.
|
||||
pub(crate) fn sample_format(depth: BitDepth) -> (u16, bool) {
|
||||
match depth {
|
||||
@@ -235,3 +242,38 @@ pub(crate) fn validate_layout(channels: usize, sample_rate: u32) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_half_away_matches_f64_round_on_the_quantizer_range() {
|
||||
let edges = [
|
||||
0.0,
|
||||
-0.0,
|
||||
0.5,
|
||||
-0.5,
|
||||
1.5,
|
||||
-1.5,
|
||||
2.5,
|
||||
0.499_999_999_999_999_94,
|
||||
-0.499_999_999_999_999_94,
|
||||
8_388_607.5,
|
||||
-8_388_608.0,
|
||||
2_147_483_647.0,
|
||||
-2_147_483_648.0,
|
||||
];
|
||||
let mut x = 0x2545_f491_4f6c_dd1du64;
|
||||
let spread = (0..10_000).map(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
(x >> 11) as f64 / (1u64 << 53) as f64 * 4_294_967_295.0 - 2_147_483_648.0
|
||||
});
|
||||
for v in edges.into_iter().chain(spread) {
|
||||
assert!(round_half_away(v) == v.round(), "{v}");
|
||||
assert_eq!(round_half_away(v) as i32, v.round() as i32, "{v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-15
@@ -150,8 +150,9 @@ pub struct LoudnessMeter {
|
||||
sub_filled: usize,
|
||||
hist_count: Vec<u64>,
|
||||
hist_energy: Vec<f64>,
|
||||
tp_coefs: [[f32; TP_TAPS]; TP_PHASES],
|
||||
tp_hist: Vec<[f32; TP_TAPS]>,
|
||||
tp_coefs: [[f32; TP_PHASES]; TP_TAPS],
|
||||
tp_hist: Vec<[f32; 2 * TP_TAPS]>,
|
||||
tp_pos: Vec<usize>,
|
||||
true_peak: f32,
|
||||
max_momentary: f32,
|
||||
}
|
||||
@@ -197,7 +198,7 @@ impl LoudnessMeter {
|
||||
let weights = if channels == 6 { vec![1.0, 1.0, 1.0, 0.0, 1.41, 1.41] } else { vec![1.0; channels] };
|
||||
let [c1, c2] = k_weighting(sr);
|
||||
// Interpolation filter: windowed sinc at the original Nyquist, split into 4 phases.
|
||||
let mut tp_coefs = [[0.0f32; TP_TAPS]; TP_PHASES];
|
||||
let mut tp_coefs = [[0.0f32; TP_PHASES]; TP_TAPS];
|
||||
let total = TP_TAPS * TP_PHASES;
|
||||
let center = (total as f64 - 1.0) / 2.0;
|
||||
for i in 0..total {
|
||||
@@ -205,14 +206,14 @@ impl LoudnessMeter {
|
||||
let sinc = if t.abs() < 1e-12 { 1.0 } else { (std::f64::consts::PI * t).sin() / (std::f64::consts::PI * t) };
|
||||
let x = i as f64 / (total as f64 - 1.0);
|
||||
let w = 0.42 - 0.5 * (2.0 * std::f64::consts::PI * x).cos() + 0.08 * (4.0 * std::f64::consts::PI * x).cos();
|
||||
if let Some(slot) = tp_coefs.get_mut(i % TP_PHASES).and_then(|ph| ph.get_mut(i / TP_PHASES)) {
|
||||
if let Some(slot) = tp_coefs.get_mut(i / TP_PHASES).and_then(|tap| tap.get_mut(i % TP_PHASES)) {
|
||||
*slot = (sinc * w) as f32;
|
||||
}
|
||||
}
|
||||
for ph in &mut tp_coefs {
|
||||
let s: f32 = ph.iter().sum();
|
||||
for p in 0..TP_PHASES {
|
||||
let s: f32 = tp_coefs.iter().filter_map(|tap| tap.get(p)).sum();
|
||||
if s.abs() > 1e-6 {
|
||||
ph.iter_mut().for_each(|c| *c /= s);
|
||||
tp_coefs.iter_mut().filter_map(|tap| tap.get_mut(p)).for_each(|c| *c /= s);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
@@ -228,7 +229,8 @@ impl LoudnessMeter {
|
||||
hist_count: vec![0; HIST_BINS],
|
||||
hist_energy: vec![0.0; HIST_BINS],
|
||||
tp_coefs,
|
||||
tp_hist: vec![[0.0; TP_TAPS]; channels],
|
||||
tp_hist: vec![[0.0; 2 * TP_TAPS]; channels],
|
||||
tp_pos: vec![0; channels],
|
||||
true_peak: 0.0,
|
||||
max_momentary: MIN_DB,
|
||||
}
|
||||
@@ -257,7 +259,8 @@ impl LoudnessMeter {
|
||||
self.sub_filled = 0;
|
||||
self.hist_count.iter_mut().for_each(|v| *v = 0);
|
||||
self.hist_energy.iter_mut().for_each(|v| *v = 0.0);
|
||||
self.tp_hist.iter_mut().for_each(|h| *h = [0.0; TP_TAPS]);
|
||||
self.tp_hist.iter_mut().for_each(|h| *h = [0.0; 2 * TP_TAPS]);
|
||||
self.tp_pos.iter_mut().for_each(|p| *p = 0);
|
||||
self.true_peak = 0.0;
|
||||
self.max_momentary = MIN_DB;
|
||||
}
|
||||
@@ -285,13 +288,24 @@ impl LoudnessMeter {
|
||||
}
|
||||
|
||||
fn true_peak_sample(&mut self, c: usize, x: f32) {
|
||||
let Some(h) = self.tp_hist.get_mut(c) else { return };
|
||||
h.copy_within(0..TP_TAPS - 1, 1);
|
||||
h[0] = x;
|
||||
let (Some(h), Some(pos)) = (self.tp_hist.get_mut(c), self.tp_pos.get_mut(c)) else { return };
|
||||
*pos = (*pos + TP_TAPS - 1) % TP_TAPS;
|
||||
let p = *pos;
|
||||
for slot in [p, p + TP_TAPS] {
|
||||
if let Some(v) = h.get_mut(slot) {
|
||||
*v = x;
|
||||
}
|
||||
}
|
||||
let Some(w) = h.get(p..p + TP_TAPS).and_then(|w| <&[f32; TP_TAPS]>::try_from(w).ok()) else { return };
|
||||
let mut y = [-0.0f32; TP_PHASES];
|
||||
for (cs, s) in self.tp_coefs.iter().zip(w) {
|
||||
for (acc, k) in y.iter_mut().zip(cs) {
|
||||
*acc += k * s;
|
||||
}
|
||||
}
|
||||
let mut peak = x.abs();
|
||||
for ph in &self.tp_coefs {
|
||||
let y: f32 = ph.iter().zip(h.iter()).map(|(a, b)| a * b).sum();
|
||||
peak = peak.max(y.abs());
|
||||
for v in y {
|
||||
peak = peak.max(v.abs());
|
||||
}
|
||||
if peak > self.true_peak {
|
||||
self.true_peak = peak;
|
||||
|
||||
@@ -54,7 +54,24 @@ fn max_len(ch: &[Vec<f32>]) -> usize {
|
||||
|
||||
/// The largest finite sample magnitude in any channel.
|
||||
pub fn peak(ch: &[Vec<f32>]) -> f32 {
|
||||
ch.iter().flatten().filter(|v| v.is_finite()).fold(0.0f32, |m, v| m.max(v.abs()))
|
||||
ch.iter().map(|c| channel_peak(c)).fold(0.0f32, f32::max)
|
||||
}
|
||||
|
||||
/// Largest finite absolute sample of one channel (0 for none).
|
||||
pub fn channel_peak(x: &[f32]) -> f32 {
|
||||
let mut lanes = [0.0f32; 8];
|
||||
let (chunks, rest) = x.as_chunks::<8>();
|
||||
for c in chunks {
|
||||
for (m, v) in lanes.iter_mut().zip(c) {
|
||||
let a = v.abs();
|
||||
*m = if a < f32::INFINITY && a > *m { a } else { *m };
|
||||
}
|
||||
}
|
||||
for (m, v) in lanes.iter_mut().zip(rest) {
|
||||
let a = v.abs();
|
||||
*m = if a < f32::INFINITY && a > *m { a } else { *m };
|
||||
}
|
||||
lanes.into_iter().fold(0.0f32, f32::max)
|
||||
}
|
||||
|
||||
/// Peak-normalizes to `target_db` dBFS, either all channels together or each on its own.
|
||||
|
||||
@@ -502,7 +502,10 @@ pub struct Maximizer {
|
||||
sr: f32,
|
||||
la: usize,
|
||||
delay: Vec<DelayLine>,
|
||||
req: Vec<f32>,
|
||||
win: Vec<(u64, f32)>,
|
||||
win_head: usize,
|
||||
win_len: usize,
|
||||
clock: u64,
|
||||
smooth: Vec<f32>,
|
||||
idx: usize,
|
||||
sum: f64,
|
||||
@@ -526,7 +529,10 @@ impl Maximizer {
|
||||
sr: 48_000.0,
|
||||
la: 1,
|
||||
delay: Vec::new(),
|
||||
req: vec![1.0],
|
||||
win: vec![(0, 1.0)],
|
||||
win_head: 0,
|
||||
win_len: 0,
|
||||
clock: 0,
|
||||
smooth: vec![1.0],
|
||||
idx: 0,
|
||||
sum: 1.0,
|
||||
@@ -547,6 +553,40 @@ impl Maximizer {
|
||||
self.ceiling.snap();
|
||||
self.rel = time_coef(self.p.v(2), self.sr);
|
||||
}
|
||||
|
||||
fn window_min(&mut self, req: f32, la: usize) -> f32 {
|
||||
let cap = self.win.len();
|
||||
let t = self.clock;
|
||||
self.clock = t.saturating_add(1);
|
||||
if cap == 0 {
|
||||
return req.min(1.0);
|
||||
}
|
||||
while self.win_len > 0 && self.win.get(self.win_head).is_some_and(|&(at, _)| at.saturating_add(la as u64) <= t) {
|
||||
self.win_head = wrap(self.win_head + 1, cap);
|
||||
self.win_len -= 1;
|
||||
}
|
||||
if !req.is_nan() {
|
||||
while self.win_len > 0 && self.win.get(wrap(self.win_head + self.win_len - 1, cap)).is_some_and(|&(_, v)| v >= req) {
|
||||
self.win_len -= 1;
|
||||
}
|
||||
if self.win_len == cap {
|
||||
self.win_head = wrap(self.win_head + 1, cap);
|
||||
self.win_len -= 1;
|
||||
}
|
||||
if let Some(slot) = self.win.get_mut(wrap(self.win_head + self.win_len, cap)) {
|
||||
*slot = (t, req);
|
||||
self.win_len += 1;
|
||||
}
|
||||
}
|
||||
match self.win.get(self.win_head) {
|
||||
Some(&(_, v)) if self.win_len > 0 => v.min(1.0),
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap(i: usize, n: usize) -> usize {
|
||||
if i < n { i } else { i % n }
|
||||
}
|
||||
|
||||
impl Plugin for Maximizer {
|
||||
@@ -562,7 +602,7 @@ impl Plugin for Maximizer {
|
||||
d
|
||||
})
|
||||
.collect();
|
||||
self.req = vec![1.0; self.la];
|
||||
self.win = vec![(0, 1.0); self.la];
|
||||
self.smooth = vec![1.0; self.la];
|
||||
self.drive.set_time(20.0, self.sr);
|
||||
self.ceiling.set_time(20.0, self.sr);
|
||||
@@ -572,7 +612,9 @@ impl Plugin for Maximizer {
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.delay.iter_mut().for_each(DelayLine::clear);
|
||||
self.req.iter_mut().for_each(|v| *v = 1.0);
|
||||
self.win_head = 0;
|
||||
self.win_len = 0;
|
||||
self.clock = 0;
|
||||
self.smooth.iter_mut().for_each(|v| *v = 1.0);
|
||||
self.sum = self.la as f64;
|
||||
self.held = 1.0;
|
||||
@@ -598,16 +640,13 @@ impl Plugin for Maximizer {
|
||||
peak = peak.max(x.abs());
|
||||
}
|
||||
let req = if peak > ceil { ceil / peak } else { 1.0 };
|
||||
if let Some(r) = self.req.get_mut(self.idx) {
|
||||
*r = req;
|
||||
}
|
||||
let hold = self.req.iter().copied().fold(1.0f32, f32::min);
|
||||
let hold = self.window_min(req, la);
|
||||
self.held = if hold < self.held { hold } else { hold + (self.held - hold) * self.rel };
|
||||
if let Some(s) = self.smooth.get_mut(self.idx) {
|
||||
self.sum += f64::from(self.held) - f64::from(*s);
|
||||
*s = self.held;
|
||||
}
|
||||
self.idx = (self.idx + 1) % la;
|
||||
self.idx = wrap(self.idx + 1, la);
|
||||
if self.idx == 0 {
|
||||
// Re-sum once per window to cancel floating-point drift.
|
||||
self.sum = self.smooth.iter().map(|&v| f64::from(v)).sum();
|
||||
|
||||
@@ -34,7 +34,8 @@ pub(crate) fn time_coef(ms: f32, sr: f32) -> f32 {
|
||||
/// Flushes denormals and non-finite values in feedback paths to zero.
|
||||
#[inline]
|
||||
pub(crate) fn flush(x: f32) -> f32 {
|
||||
if x.is_finite() && x.abs() > 1.0e-25 { x } else { 0.0 }
|
||||
let a = x.abs();
|
||||
if (a > 1.0e-25) & (a < f32::INFINITY) { x } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Number of frames a plugin may safely touch.
|
||||
|
||||
@@ -298,3 +298,94 @@ fn spectrum_edge_cases() {
|
||||
let m = magnitude_db(&[f32::NAN; 64], 64);
|
||||
assert!(m.iter().all(|v| v.is_finite()));
|
||||
}
|
||||
|
||||
const PIN_LUFS: u32 = 3_215_227_935;
|
||||
const PIN_TRUE_PEAK: u32 = 3_162_870_746;
|
||||
fn noise(len: usize, seed: u64, amp: f32) -> Vec<f32> {
|
||||
let mut x = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
|
||||
(0..len)
|
||||
.map(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
((x >> 40) as f32 / (1u64 << 24) as f32 * 2.0 - 1.0) * amp
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn bits_hash(chans: &[Vec<f32>]) -> u64 {
|
||||
chans.iter().flatten().fold(0xcbf2_9ce4_8422_2325u64, |h, v| (h ^ u64::from(v.to_bits())).wrapping_mul(0x0100_0000_01b3))
|
||||
}
|
||||
fn run_blocks(p: &mut dyn soundcraft_dsp::Plugin, input: &[Vec<f32>], block: usize) -> Vec<Vec<f32>> {
|
||||
let len = input[0].len();
|
||||
let mut out = vec![Vec::with_capacity(len); input.len()];
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let n = block.min(len - i);
|
||||
let mut io: Vec<Vec<f32>> = input.iter().map(|c| c[i..i + n].to_vec()).collect();
|
||||
p.process(&mut io, n);
|
||||
for (o, c) in out.iter_mut().zip(io) {
|
||||
o.extend(c);
|
||||
}
|
||||
i += n;
|
||||
}
|
||||
out
|
||||
}
|
||||
#[test]
|
||||
fn loudness_and_true_peak_do_not_depend_on_block_size() {
|
||||
let sr = 48_000.0;
|
||||
let len = 96_000;
|
||||
let l: Vec<f32> = sine(997.0, 0.5, sr, len).iter().zip(noise(len, 1, 0.3)).map(|(a, b)| a + b).collect();
|
||||
let r: Vec<f32> = sine(11_025.0, 0.7, sr, len).iter().zip(noise(len, 2, 0.2)).map(|(a, b)| a + b).collect();
|
||||
let io = [l, r];
|
||||
let run = |block: usize| {
|
||||
let mut m = LoudnessMeter::new(sr, 2);
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let n = block.min(len - i);
|
||||
let chunk: Vec<Vec<f32>> = io.iter().map(|c| c[i..i + n].to_vec()).collect();
|
||||
m.process(&chunk, n);
|
||||
i += n;
|
||||
}
|
||||
(m.integrated_lufs().to_bits(), m.true_peak_db().to_bits(), m.momentary_lufs().to_bits())
|
||||
};
|
||||
let whole = run(len);
|
||||
for block in [1, 7, 1024, 4801] {
|
||||
assert_eq!(run(block), whole, "block {block}");
|
||||
}
|
||||
let near = |got: u32, pin: u32| (f32::from_bits(got) - f32::from_bits(pin)).abs() < 1e-3;
|
||||
assert!(near(whole.0, PIN_LUFS) && near(whole.1, PIN_TRUE_PEAK), "lufs {} dB, true peak {} dB", f32::from_bits(whole.0), f32::from_bits(whole.1));
|
||||
}
|
||||
#[test]
|
||||
fn peak_skips_non_finite_samples_at_any_length() {
|
||||
for len in 0..40 {
|
||||
let mut x = noise(len, len as u64 + 3, 0.9);
|
||||
for (at, v) in [(1, f32::NAN), (len.saturating_sub(1), f32::INFINITY), (len / 2, f32::NEG_INFINITY)] {
|
||||
if len > 3 {
|
||||
x[at] = v;
|
||||
}
|
||||
}
|
||||
let naive = x.iter().filter(|v| v.is_finite()).fold(0.0f32, |m, v| m.max(v.abs()));
|
||||
assert_eq!(offline::channel_peak(&x).to_bits(), naive.to_bits(), "len {len}");
|
||||
let y = noise(len, 99, 0.5);
|
||||
let both = y.iter().fold(naive, |m, v| m.max(v.abs()));
|
||||
assert_eq!(offline::peak(&[x, y]).to_bits(), both.to_bits(), "len {len}");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn maximizer_and_plate_reverb_output_does_not_depend_on_block_size() {
|
||||
let len = 48_000;
|
||||
let loud = [noise(len, 5, 1.6), noise(len, 6, 1.2)];
|
||||
let mut burst = [noise(len, 7, 0.5), noise(len, 8, 0.5)];
|
||||
for c in &mut burst {
|
||||
c[4_800..].iter_mut().for_each(|v| *v = 0.0);
|
||||
}
|
||||
for (id, input) in [("maximizer", &loud), ("plate_reverb", &burst)] {
|
||||
let mut hashes = Vec::new();
|
||||
for block in [64, 1000, 4096] {
|
||||
let mut p = create(id).unwrap();
|
||||
p.prepare(48_000.0, 4096, 2);
|
||||
hashes.push(bits_hash(&run_blocks(p.as_mut(), input, block)));
|
||||
}
|
||||
assert!(hashes.iter().all(|h| *h == hashes[0]), "{id}: {hashes:x?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use serde_json::{Value, json};
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let Some(out) = args.first() else {
|
||||
eprintln!("usage: bounce_bench OUT.wav [reps] [JSON params]");
|
||||
std::process::exit(2);
|
||||
};
|
||||
let reps = args.get(1).and_then(|r| r.parse().ok()).unwrap_or(5usize);
|
||||
let mut params = args.get(2).and_then(|p| serde_json::from_str::<Value>(p).ok()).unwrap_or_else(|| json!({}));
|
||||
if let Some(m) = params.as_object_mut() {
|
||||
m.insert("path".into(), json!(out));
|
||||
}
|
||||
let mut e = soundcraft_engine::demo::demo_engine();
|
||||
let mut times = Vec::new();
|
||||
let mut last = Value::Null;
|
||||
for _ in 0..reps {
|
||||
let t0 = Instant::now();
|
||||
match e.execute("file.bounce_mix", ¶ms) {
|
||||
Ok(v) => last = v,
|
||||
Err(err) => {
|
||||
eprintln!("bounce failed: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
times.push(t0.elapsed().as_secs_f64());
|
||||
}
|
||||
let total: f64 = times.iter().sum();
|
||||
let min = times.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
println!("{last}");
|
||||
println!("total {total:.4} s, min {min:.4} s over {reps} bounces");
|
||||
}
|
||||
@@ -137,3 +137,27 @@ fn dropping_an_unfinished_bounce_cancels_it_and_keeps_the_old_file() {
|
||||
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
const PINNED: [[u32; 3]; 2] = [[0xc195_6706, 0xc093_0031, 0xc093_0031], [0xc162_e790, 0xbdcc_ccbf, 0xbdcc_ccbf]];
|
||||
|
||||
#[test]
|
||||
fn demo_bounce_levels_are_pinned() {
|
||||
let mut e = soundcraft_engine::demo::demo_engine();
|
||||
let dir = scratch("bounce-demo-pinned");
|
||||
let mut got = Vec::new();
|
||||
for (name, extra) in [("Mix.wav", serde_json::json!({})), ("Norm.wav", serde_json::json!({"normalize": true, "bit_depth": 16}))] {
|
||||
let path = dir.join(name);
|
||||
let mut p = serde_json::json!({"path": path, "start": 1_296_000, "end": 1_440_000});
|
||||
if let (Some(m), Some(x)) = (p.as_object_mut(), extra.as_object()) {
|
||||
m.extend(x.clone());
|
||||
}
|
||||
let r = e.execute("file.bounce_mix", &p).unwrap();
|
||||
assert!(std::fs::metadata(&path).unwrap().len() > 44, "{name} has audio");
|
||||
let level = |k: &str| r[k].as_f64().unwrap() as f32;
|
||||
got.push([level("lufs"), level("true_peak_db"), level("peak_db")]);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
for (g, pin) in got.iter().zip(PINNED) {
|
||||
assert!(g.iter().zip(pin).all(|(v, p)| (v - f32::from_bits(p)).abs() < 1e-3), "{got:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +70,20 @@ pub(crate) fn send_level(t: &Track, i: usize, base: f32, pos: Samples) -> f32 {
|
||||
base
|
||||
}
|
||||
|
||||
pub(crate) fn lerp_gain(db0: f32, db1: f32, i: usize, n: usize) -> f32 {
|
||||
let g0 = db_to_gain(db0);
|
||||
let g1 = db_to_gain(db1);
|
||||
if n == 0 { g0 } else { g0 + (g1 - g0) * (i as f32 / n as f32) }
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct GainRamp {
|
||||
g0: f32,
|
||||
g1: f32,
|
||||
n: usize,
|
||||
}
|
||||
|
||||
impl GainRamp {
|
||||
pub(crate) fn new(db0: f32, db1: f32, n: usize) -> Self {
|
||||
GainRamp { g0: db_to_gain(db0), g1: db_to_gain(db1), n }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn at(&self, i: usize) -> f32 {
|
||||
if self.n == 0 { self.g0 } else { self.g0 + (self.g1 - self.g0) * (i as f32 / self.n as f32) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ mod routing;
|
||||
mod strip;
|
||||
mod threads;
|
||||
|
||||
use automation::{lerp_gain, pan_at, pan_segment, send_level, transport_at, volume_db_at};
|
||||
use automation::{GainRamp, pan_at, pan_segment, send_level, transport_at, volume_db_at};
|
||||
pub use instances::{INSTRUMENT_SLOT, InstanceSpec, NewInstance, PluginFailures, PreparedInstance, create_instance, instance_specs, is_third_party};
|
||||
pub use meter::StripMeter;
|
||||
use midi::sync_sample;
|
||||
@@ -506,10 +506,11 @@ impl MixEngine {
|
||||
let last = pos.saturating_add(frames.saturating_sub(1) as i64);
|
||||
let v0 = volume_db_at(t, pos);
|
||||
let v1 = volume_db_at(t, last);
|
||||
let ramp = GainRamp::new(v0, v1, frames.saturating_sub(1));
|
||||
for (ch, src) in strip.buf.iter().enumerate().take(self.main.len()) {
|
||||
if let Some(m) = self.main.get_mut(ch) {
|
||||
for i in 0..frames {
|
||||
let g = lerp_gain(v0, v1, i, frames.saturating_sub(1));
|
||||
let g = ramp.at(i);
|
||||
if let (Some(d), Some(x)) = (m.get_mut(i), src.get(i)) {
|
||||
*d = x * g;
|
||||
}
|
||||
|
||||
@@ -75,5 +75,5 @@ fn max_level(a: f32, b: f32) -> f32 {
|
||||
}
|
||||
|
||||
fn peak(x: &[f32]) -> f32 {
|
||||
x.iter().fold(0.0f32, |m, v| if v.is_finite() { m.max(v.abs()) } else { m })
|
||||
soundcraft_dsp::offline::channel_peak(x)
|
||||
}
|
||||
|
||||
@@ -251,6 +251,33 @@ pub(crate) fn apply_matrix_span(src: &[Vec<f32>], span: std::ops::Range<usize>,
|
||||
if taps.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let end = span.end.min(out.len());
|
||||
let Some(dst) = out.get_mut(span.start..end) else { continue };
|
||||
let lane = |s: usize| src.get(s).and_then(|c| c.get(span.start..end));
|
||||
match *taps {
|
||||
[(s0, g0)] => {
|
||||
if let Some(x0) = lane(s0) {
|
||||
for (o, a) in dst.iter_mut().zip(x0) {
|
||||
let mut acc = 0.0f32;
|
||||
acc += a * gain * g0;
|
||||
*o += acc;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
[(s0, g0), (s1, g1)] => {
|
||||
if let (Some(x0), Some(x1)) = (lane(s0), lane(s1)) {
|
||||
for ((o, a), b) in dst.iter_mut().zip(x0).zip(x1) {
|
||||
let mut acc = 0.0f32;
|
||||
acc += a * gain * g0;
|
||||
acc += b * gain * g1;
|
||||
*o += acc;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for (i, o) in out.iter_mut().enumerate().take(span.end).skip(span.start) {
|
||||
let mut acc = 0.0f32;
|
||||
for &(s, g) in taps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::automation::{lerp_gain, transport_at, volume_db_at};
|
||||
use super::automation::{GainRamp, transport_at, volume_db_at};
|
||||
use super::clips::{ClipFx, clip_fx_params, render_clip};
|
||||
use super::instances::{INSTRUMENT_SLOT, PluginFailures, PreparedInstance, create_instance, is_third_party};
|
||||
use super::midi::{chase_controllers, schedule_notes};
|
||||
@@ -380,11 +380,10 @@ pub(crate) fn process_strip(
|
||||
}
|
||||
}
|
||||
let (g0, g1) = if strip.muted { (-144.0, -144.0) } else { (v0.min(12.0), v1.min(12.0)) };
|
||||
let ramp = GainRamp::new(g0, g1, frames.saturating_sub(1));
|
||||
for c in strip.buf.iter_mut() {
|
||||
for i in 0..frames {
|
||||
if let Some(x) = c.get_mut(i) {
|
||||
*x *= lerp_gain(g0, g1, i, frames.saturating_sub(1));
|
||||
}
|
||||
for (i, x) in c.iter_mut().enumerate().take(frames) {
|
||||
*x *= ramp.at(i);
|
||||
}
|
||||
}
|
||||
// Track mute gates the audio and its pre-fader tap without splitting processor blocks.
|
||||
|
||||
@@ -1150,3 +1150,40 @@ fn surround_upmix_and_downmix_between_layouts() {
|
||||
let e_ls = out[4][100].powi(2) + out[6][100].powi(2);
|
||||
assert!((e_ls - x(4).powi(2)).abs() < 1e-5, "Ls energy is preserved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_matrix_span_matches_the_plain_per_sample_sum() {
|
||||
let src: Vec<Vec<f32>> = (0..3).map(|c| (0..300).map(|i| ((i * 7 + c * 13) % 23) as f32 * 0.037 - 0.4).collect()).collect();
|
||||
let short = vec![src[0].clone(), src[1][..150].to_vec(), src[2].clone()];
|
||||
let mut layouts: Vec<Vec<(usize, usize, f32)>> =
|
||||
vec![vec![(0, 0, 0.7)], vec![(0, 0, 0.7), (1, 1, -0.3)], vec![(0, 0, 0.6), (1, 0, 0.45), (1, 1, 1.0)]];
|
||||
layouts.push(vec![(0, 0, 0.5), (1, 0, 0.25), (2, 0, 0.125), (2, 1, 0.9)]);
|
||||
for taps in &layouts {
|
||||
let mut m = [[0.0f32; MAX_CHANNELS]; MAX_CHANNELS];
|
||||
for &(s, d, g) in taps {
|
||||
m[s][d] = g;
|
||||
}
|
||||
for input in [&src, &short] {
|
||||
for span in [0..300, 17..211, 140..160, 250..400, 5..5] {
|
||||
let mut got: Vec<Vec<f32>> = (0..2).map(|d| (0..300).map(|i| (i + d) as f32 * 1e-3).collect()).collect();
|
||||
let mut want = got.clone();
|
||||
pan::apply_matrix_span(input, span.clone(), &mut got, &m, 0.8);
|
||||
for (d, out) in want.iter_mut().enumerate() {
|
||||
for (i, o) in out.iter_mut().enumerate().take(span.end).skip(span.start) {
|
||||
let mut acc = 0.0f32;
|
||||
for (s, row) in m.iter().enumerate().take(input.len()) {
|
||||
if row[d] != 0.0 {
|
||||
acc += input[s].get(i).copied().unwrap_or(0.0) * 0.8 * row[d];
|
||||
}
|
||||
}
|
||||
if m.iter().take(input.len()).any(|row| row[d] != 0.0) {
|
||||
*o += acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
let bits = |v: &Vec<Vec<f32>>| v.iter().flatten().map(|x| x.to_bits()).collect::<Vec<_>>();
|
||||
assert_eq!(bits(&got), bits(&want), "taps {taps:?}, span {span:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user