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:
aiongg
2026-10-27 15:41:22 -04:00
committed by GitHub
14 changed files with 376 additions and 39 deletions
+43 -1
View File
@@ -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
View File
@@ -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;
+18 -1
View File
@@ -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.
+48 -9
View File
@@ -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();
+2 -1
View File
@@ -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.
+91
View File
@@ -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?}");
}
}
+33
View File
@@ -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", &params) {
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");
}
+24
View File
@@ -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:?}");
}
}
+16 -4
View File
@@ -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) }
}
}
+3 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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)
}
+27
View File
@@ -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 {
+4 -5
View File
@@ -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.
+37
View File
@@ -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:?}");
}
}
}
}