use crate::engine::stage::{OnsetEvent, BLOCK_FRAMES};
const READ_HALF_TAPS: usize = 32;
const READ_PHASES: usize = 512;
const READ_KAISER_BETA: f64 = 9.0;
#[derive(Debug)]
struct ReadInterpTable {
taps: Vec<f32>,
}
impl ReadInterpTable {
fn new() -> Self {
fn bessel_i0(x: f64) -> f64 {
let mut sum = 1.0f64;
let mut term = 1.0f64;
let half_x = x * 0.5;
for k in 1..=25 {
term *= (half_x / k as f64) * (half_x / k as f64);
sum += term;
if term < sum * 1e-16 {
break;
}
}
sum
}
let entries = READ_HALF_TAPS * READ_PHASES;
let mut taps = vec![0.0f32; entries + 2];
let bessel_beta = bessel_i0(READ_KAISER_BETA);
for (i, tap) in taps.iter_mut().enumerate().take(entries + 1) {
let u = i as f64 / READ_PHASES as f64;
let sinc_val = if u < 1e-12 {
1.0
} else {
let pi_u = std::f64::consts::PI * u;
pi_u.sin() / pi_u
};
let t = u / READ_HALF_TAPS as f64;
let window = if t <= 1.0 {
bessel_i0(READ_KAISER_BETA * (1.0 - t * t).max(0.0).sqrt()) / bessel_beta
} else {
0.0
};
*tap = (sinc_val * window) as f32;
}
Self { taps }
}
#[inline]
fn weight(&self, u_abs: f64) -> f32 {
if u_abs >= READ_HALF_TAPS as f64 {
return 0.0;
}
let x = u_abs * READ_PHASES as f64;
let i = x as usize;
let frac = (x - i as f64) as f32;
let a = self.taps[i];
let b = self.taps[i + 1];
a + (b - a) * frac
}
}
const RING_LEN: usize = 4_096;
const RING_MASK: usize = RING_LEN - 1;
const DRIFT_TRIGGER: f64 = 192.0;
const HARD_TRIGGER: f64 = 320.0;
const SEARCH_RANGE: isize = 160;
const CORR_WINDOW: usize = 320;
const XFADE_FRAMES: usize = 96;
const MIN_READ_MARGIN: f64 = (READ_HALF_TAPS + 4) as f64;
const FINE_SEARCH_RADIUS: f64 = 0.5;
const FINE_SEARCH_STEPS: isize = 4;
const TRANSIENT_POSTPONE_RATIO: f64 = 3.0;
const ONSET_PROTECT_PRE: f64 = 96.0;
const ONSET_PROTECT_POST: f64 = 512.0;
const OPPORTUNISTIC_DRIFT: f64 = 96.0;
const MASKED_WINDOW_START: f64 = 768.0;
const MASKED_WINDOW_END: f64 = 2_048.0;
const REST_DEV: f64 = 0.003;
const REST_DWELL_BLOCKS: u32 = 206;
const REST_SPLICE_DRIFT: f64 = 48.0;
const REST_TRIM_MAX: f64 = 0.0008;
#[derive(Debug)]
struct SolaChannel {
ring: Vec<f32>,
}
#[derive(Debug)]
pub(crate) struct SolaCorrector {
channels: Vec<SolaChannel>,
table: ReadInterpTable,
write_abs: u64,
read_pos: f64,
xfade_from: f64,
xfade_remaining: usize,
transposition: f64,
rate_slope: f64,
nominal_lag: f64,
energy_avg: f64,
splice_count: u64,
recenter_requested: bool,
rest_blocks: u32,
}
impl SolaCorrector {
pub(crate) fn new(num_channels: usize, nominal_lag_frames: usize) -> Self {
assert!(
nominal_lag_frames as f64 + HARD_TRIGGER + (SEARCH_RANGE as f64) + MIN_READ_MARGIN
< (RING_LEN - CORR_WINDOW - BLOCK_FRAMES) as f64,
"SOLA ring too small for nominal lag {nominal_lag_frames}"
);
Self {
channels: (0..num_channels)
.map(|_| SolaChannel {
ring: vec![0.0; RING_LEN],
})
.collect(),
table: ReadInterpTable::new(),
write_abs: 0,
read_pos: -(nominal_lag_frames as f64),
xfade_from: 0.0,
xfade_remaining: 0,
transposition: 1.0,
rate_slope: 0.0,
nominal_lag: nominal_lag_frames as f64,
energy_avg: 0.0,
splice_count: 0,
recenter_requested: false,
rest_blocks: 0,
}
}
pub(crate) fn set_transposition(&mut self, transposition: f64) {
self.transposition = if transposition.is_finite() {
transposition.clamp(0.75, 1.35)
} else {
1.0
};
}
pub(crate) fn set_rate_slope(&mut self, slope: f64) {
self.rate_slope = if slope.is_finite() { slope } else { 0.0 };
}
pub(crate) fn latency_frames(&self) -> usize {
self.nominal_lag as usize
}
#[cfg(test)]
pub(crate) fn splice_count(&self) -> u64 {
self.splice_count
}
pub(crate) fn lag_error_frames(&self) -> f64 {
(self.write_abs as f64 - self.read_pos) - self.nominal_lag
}
pub(crate) fn recenter_hard(&mut self) {
self.read_pos = self.write_abs as f64 - self.nominal_lag;
self.xfade_remaining = 0;
self.recenter_requested = false;
}
pub(crate) fn request_recenter_splice(&mut self) {
self.recenter_requested = true;
}
pub(crate) fn is_recentered(&self) -> bool {
!self.recenter_requested && self.xfade_remaining == 0
}
pub(crate) fn reset(&mut self) {
for ch in &mut self.channels {
ch.ring.fill(0.0);
}
self.write_abs = 0;
self.read_pos = -self.nominal_lag;
self.xfade_from = 0.0;
self.xfade_remaining = 0;
self.energy_avg = 0.0;
self.splice_count = 0;
self.recenter_requested = false;
self.rest_blocks = 0;
self.rate_slope = 0.0;
}
fn span_hits_onset(&self, onsets: &[OnsetEvent], start: f64) -> bool {
let span = XFADE_FRAMES as f64 * self.transposition.max(1.0);
onsets.iter().any(|event| {
start - FINE_SEARCH_RADIUS < event.stage_frame + ONSET_PROTECT_POST
&& start + span + FINE_SEARCH_RADIUS > event.stage_frame - ONSET_PROTECT_PRE
})
}
fn in_masked_window(&self, onsets: &[OnsetEvent]) -> bool {
onsets.iter().any(|event| {
let since = self.read_pos - event.stage_frame;
(MASKED_WINDOW_START..MASKED_WINDOW_END).contains(&since)
})
}
pub(crate) fn process_block(&mut self, io: &mut [[f32; BLOCK_FRAMES]], onsets: &[OnsetEvent]) {
debug_assert_eq!(io.len(), self.channels.len());
let mut block_energy = 0.0f64;
for (ch, input) in self.channels.iter_mut().zip(io.iter()) {
for (i, &sample) in input.iter().enumerate() {
ch.ring[(self.write_abs as usize + i) & RING_MASK] = sample;
block_energy += (sample as f64) * (sample as f64);
}
}
self.write_abs += BLOCK_FRAMES as u64;
let block_rms = (block_energy / (BLOCK_FRAMES * io.len()) as f64).sqrt();
self.energy_avg = 0.98 * self.energy_avg + 0.02 * block_rms;
let deviation = (self.transposition - 1.0).abs();
let settled_drift = self.lag_error_frames() - BLOCK_FRAMES as f64;
let at_rest = if deviation < REST_DEV {
self.rest_blocks = self.rest_blocks.saturating_add(1);
self.rest_blocks >= REST_DWELL_BLOCKS
} else {
self.rest_blocks = 0;
false
};
if self.xfade_remaining == 0 {
let drift = self.lag_error_frames();
if self.recenter_requested {
if drift.abs() < 8.0 || self.try_splice_forced(drift) {
self.recenter_requested = false;
}
} else if drift.abs() > DRIFT_TRIGGER {
self.try_splice(drift, onsets);
} else if drift.abs() > OPPORTUNISTIC_DRIFT && self.in_masked_window(onsets) {
self.try_splice(drift, onsets);
} else if at_rest && settled_drift.abs() > REST_SPLICE_DRIFT {
self.try_splice(drift, onsets);
}
}
let trim = if at_rest {
(settled_drift * 0.001).clamp(-REST_TRIM_MAX, REST_TRIM_MAX)
} else {
0.0
};
let t = if self.rate_slope != 0.0 {
let rate_at_cursor =
1.0 / self.transposition - (self.rate_slope * settled_drift).clamp(-0.02, 0.02);
if rate_at_cursor > 0.5 {
(1.0 / rate_at_cursor).clamp(0.75, 1.35) + trim
} else {
self.transposition + trim
}
} else {
self.transposition + trim
};
for i in 0..BLOCK_FRAMES {
if self.xfade_remaining > 0 {
let progress =
1.0 - (self.xfade_remaining as f64 - 1.0) / (XFADE_FRAMES as f64 - 1.0);
let g_in = (0.5 - 0.5 * (std::f64::consts::PI * progress).cos()) as f32;
let g_out = 1.0 - g_in;
for (ch, out) in self.channels.iter().zip(io.iter_mut()) {
let a = sinc_read(&ch.ring, self.xfade_from, &self.table);
let b = sinc_read(&ch.ring, self.read_pos, &self.table);
out[i] = g_out * a + g_in * b;
}
self.xfade_from += t;
self.read_pos += t;
self.xfade_remaining -= 1;
} else {
for (ch, out) in self.channels.iter().zip(io.iter_mut()) {
out[i] = sinc_read(&ch.ring, self.read_pos, &self.table);
}
self.read_pos += t;
}
}
let newest_read = if self.xfade_remaining > 0 {
self.read_pos.max(self.xfade_from)
} else {
self.read_pos
};
debug_assert!(
self.write_abs as f64 - newest_read >= MIN_READ_MARGIN - 1.0,
"SOLA read overtook the write head"
);
}
fn try_splice(&mut self, drift: f64, onsets: &[OnsetEvent]) {
let force = drift.abs() >= HARD_TRIGGER;
let _ = self.plan_splice(drift, force, onsets);
}
fn try_splice_forced(&mut self, drift: f64) -> bool {
self.plan_splice(drift, true, &[])
}
fn plan_splice(&mut self, drift: f64, force: bool, onsets: &[OnsetEvent]) -> bool {
if !force && self.span_hits_onset(onsets, self.read_pos) {
return false;
}
let nominal_jump = drift;
let (mut best_jump, mut best_score) = (nominal_jump, f64::MIN);
let base = self.read_pos;
let lo = nominal_jump as isize - SEARCH_RANGE;
let hi = nominal_jump as isize + SEARCH_RANGE;
for jump in lo..=hi {
let candidate = base + jump as f64;
if !self.readable_span(candidate, CORR_WINDOW + XFADE_FRAMES) {
continue;
}
if !force && self.span_hits_onset(onsets, candidate) {
continue;
}
let distance = (jump as f64 - nominal_jump).abs() / SEARCH_RANGE as f64;
let score = self.mix_correlation(base, candidate, CORR_WINDOW) - 0.02 * distance;
if score > best_score {
best_score = score;
best_jump = jump as f64;
}
}
if best_score == f64::MIN {
return false; }
if self.readable_span(base + best_jump - 1.0, CORR_WINDOW + XFADE_FRAMES)
&& self.readable_span(base + best_jump + 1.0, CORR_WINDOW + XFADE_FRAMES)
{
const GRID: usize = 2 * FINE_SEARCH_STEPS as usize + 1;
let step = FINE_SEARCH_RADIUS / FINE_SEARCH_STEPS as f64;
let mut cs = [0.0f64; GRID];
let (mut best_k, mut best_c) = (0usize, f64::MIN);
for (k, c) in cs.iter_mut().enumerate() {
let frac = (k as f64 - FINE_SEARCH_STEPS as f64) * step;
*c = self.mix_correlation_frac(base, base + best_jump + frac, XFADE_FRAMES);
if *c > best_c {
best_c = *c;
best_k = k;
}
}
let mut best_frac = (best_k as f64 - FINE_SEARCH_STEPS as f64) * step;
if best_k > 0 && best_k + 1 < GRID {
let (cm, c0, cp) = (cs[best_k - 1], cs[best_k], cs[best_k + 1]);
let denom = cm - 2.0 * c0 + cp;
if denom < -1e-12 {
best_frac += step * (0.5 * (cm - cp) / denom).clamp(-0.5, 0.5);
}
}
best_jump += best_frac;
}
let target = base + best_jump;
if !self.readable_span(target, CORR_WINDOW + XFADE_FRAMES) {
return false; }
if !force
&& self.energy_avg > 1e-6
&& self.region_rms(target, CORR_WINDOW) > TRANSIENT_POSTPONE_RATIO * self.energy_avg
{
return false;
}
self.xfade_from = self.read_pos;
self.read_pos = target;
self.xfade_remaining = XFADE_FRAMES;
self.splice_count += 1;
true
}
fn readable_span(&self, pos: f64, span: usize) -> bool {
let end = pos + span as f64 * self.transposition.max(1.0);
let newest_ok = end <= self.write_abs as f64 - MIN_READ_MARGIN;
let oldest_ok = pos
>= (self.write_abs as f64 - RING_LEN as f64) + MIN_READ_MARGIN + BLOCK_FRAMES as f64;
let started = pos >= MIN_READ_MARGIN;
newest_ok && oldest_ok && started
}
fn mix_correlation(&self, a_pos: f64, b_pos: f64, len: usize) -> f64 {
let a0 = a_pos.floor() as usize;
let b0 = b_pos.floor() as usize;
let (mut dot, mut a_sq, mut b_sq) = (0.0f64, 0.0f64, 0.0f64);
for i in 0..len {
let (mut a, mut b) = (0.0f64, 0.0f64);
for ch in &self.channels {
a += ch.ring[(a0 + i) & RING_MASK] as f64;
b += ch.ring[(b0 + i) & RING_MASK] as f64;
}
dot += a * b;
a_sq += a * a;
b_sq += b * b;
}
let norm = (a_sq * b_sq).sqrt();
if norm < 1e-12 {
0.0
} else {
dot / norm
}
}
fn mix_correlation_frac(&self, a_pos: f64, b_pos: f64, len: usize) -> f64 {
let a0 = a_pos.floor() as usize;
let (mut dot, mut a_sq, mut b_sq) = (0.0f64, 0.0f64, 0.0f64);
for i in 0..len {
let (mut a, mut b) = (0.0f64, 0.0f64);
for ch in &self.channels {
a += ch.ring[(a0 + i) & RING_MASK] as f64;
b += sinc_read(&ch.ring, b_pos + i as f64, &self.table) as f64;
}
dot += a * b;
a_sq += a * a;
b_sq += b * b;
}
let norm = (a_sq * b_sq).sqrt();
if norm < 1e-12 {
0.0
} else {
dot / norm
}
}
fn region_rms(&self, pos: f64, len: usize) -> f64 {
let p0 = pos.floor() as usize;
let mut acc = 0.0f64;
for i in 0..len {
let mut mix = 0.0f64;
for ch in &self.channels {
mix += ch.ring[(p0 + i) & RING_MASK] as f64;
}
let mix = mix / self.channels.len() as f64;
acc += mix * mix;
}
(acc / len as f64).sqrt()
}
}
#[inline]
fn sinc_read(ring: &[f32], pos: f64, table: &ReadInterpTable) -> f32 {
let center = pos.floor();
let frac = pos - center;
let center = center as isize;
let half = READ_HALF_TAPS as isize;
let mut acc = 0.0f64;
let mut wsum = 0.0f64;
for j in (1 - half)..=half {
let w = table.weight((j as f64 - frac).abs()) as f64;
if w != 0.0 {
let idx = (center + j) as usize & RING_MASK;
acc += ring[idx] as f64 * w;
wsum += w;
}
}
if wsum.abs() > 1e-12 {
(acc / wsum) as f32
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f64 = 44_100.0;
const LAG: usize = 560;
fn sine(freq: f64, len: usize, amp: f32) -> Vec<f32> {
(0..len)
.map(|i| amp * (2.0 * std::f64::consts::PI * freq * i as f64 / SR).sin() as f32)
.collect()
}
fn run(corrector: &mut SolaCorrector, input: &[f32], t: f64) -> Vec<f32> {
corrector.set_transposition(t);
let mut out = Vec::with_capacity(input.len());
let mut block = [[0.0f32; BLOCK_FRAMES]; 1];
for chunk in input.chunks_exact(BLOCK_FRAMES) {
block[0].copy_from_slice(chunk);
corrector.process_block(&mut block, &[]);
out.extend_from_slice(&block[0]);
}
out
}
fn measure_freq(window: &[f32]) -> f64 {
let (mut first, mut last, mut count) = (None, None, 0usize);
for i in 1..window.len() {
let (a, b) = (window[i - 1] as f64, window[i] as f64);
if a <= 0.0 && b > 0.0 {
let t = (i - 1) as f64 + a / (a - b);
if first.is_none() {
first = Some(t);
}
last = Some(t);
count += 1;
}
}
match (first, last) {
(Some(f), Some(l)) if count >= 2 => (count - 1) as f64 * SR / (l - f),
_ => 0.0,
}
}
#[test]
fn unity_is_pure_delay_with_no_splices() {
let mut corrector = SolaCorrector::new(1, LAG);
let input = sine(700.0, 44_100, 0.5);
let out = run(&mut corrector, &input, 1.0);
assert_eq!(corrector.splice_count(), 0, "unity must never splice");
for i in 8_192..40_000 {
assert!(
(out[i] - input[i - LAG]).abs() < 1e-4,
"unity SOLA deviates at {i}"
);
}
}
#[test]
fn shifts_pitch_by_transposition() {
let mut corrector = SolaCorrector::new(1, LAG);
let input = sine(600.0, 44_100 * 4, 0.5);
let out = run(&mut corrector, &input, 1.04);
let f = measure_freq(&out[44_100..88_200]);
assert!(
(f - 624.0).abs() < 2.0,
"expected ~624 Hz at T=1.04, measured {f:.1}"
);
assert!(corrector.splice_count() > 0, "non-unity must splice");
}
#[test]
fn splices_are_click_free_on_tone() {
let freq = 330.0;
let amp = 0.5;
let mut corrector = SolaCorrector::new(1, LAG);
let input = sine(freq, 44_100 * 6, amp);
let out = run(&mut corrector, &input, 1.05);
let bound = amp * 2.0 * std::f32::consts::PI * (freq * 1.05) as f32 / SR as f32 * 1.35;
let mut worst = (0usize, 0.0f32);
for (i, w) in out[4_096..].windows(2).enumerate() {
let d = (w[1] - w[0]).abs();
if d > worst.1 {
worst = (i + 4_096, d);
}
}
println!(
"sola tone splice: {} splices, max diff {:.5} (bound {bound:.5})",
corrector.splice_count(),
worst.1
);
assert!(
worst.1 <= bound,
"splice click at {}: {:.5} > {bound:.5}",
worst.0,
worst.1
);
}
#[test]
fn lag_stays_bounded_over_long_runs() {
let mut corrector = SolaCorrector::new(1, LAG);
let input = sine(500.0, 44_100 * 20, 0.4);
let _ = run(&mut corrector, &input, 1.05);
let error = corrector.lag_error_frames().abs();
assert!(
error <= HARD_TRIGGER + BLOCK_FRAMES as f64,
"lag error {error:.0} frames escaped the elastic band"
);
}
#[test]
fn splice_fades_avoid_artifact_onsets() {
let mut corrector = SolaCorrector::new(1, LAG);
corrector.set_transposition(1.05);
let input = sine(500.0, 44_100 * 8, 0.4);
let mut block = [[0.0f32; BLOCK_FRAMES]; 1];
let mut fade_spans: Vec<(f64, f64)> = Vec::new();
let mut events: Vec<OnsetEvent> = Vec::new();
let mut was_fading = false;
for (bi, chunk) in input.chunks_exact(BLOCK_FRAMES).enumerate() {
let stage_now = (bi * BLOCK_FRAMES) as f64;
events.clear();
let mut onset = ((stage_now - 2_048.0).max(0.0) / 4_410.0).floor() * 4_410.0;
while onset <= stage_now + 2_048.0 {
if onset > 0.0 {
events.push(OnsetEvent {
stage_frame: onset,
strength: 0.9,
beat: false,
});
}
onset += 4_410.0;
}
block[0].copy_from_slice(chunk);
corrector.process_block(&mut block, &events);
let fading = corrector.xfade_remaining > 0;
if fading && !was_fading {
let elapsed = (XFADE_FRAMES - corrector.xfade_remaining) as f64 * 1.05;
let span = XFADE_FRAMES as f64 * 1.05;
let out_start = corrector.xfade_from - elapsed;
let in_start = corrector.read_pos - elapsed;
fade_spans.push((out_start, out_start + span));
fade_spans.push((in_start, in_start + span));
}
was_fading = fading;
}
assert!(
corrector.splice_count() > 10,
"fixture must splice ({} splices)",
corrector.splice_count()
);
for &(lo, hi) in &fade_spans {
let mut onset = 4_410.0;
while onset < 44_100.0 * 8.0 {
assert!(
hi <= onset - ONSET_PROTECT_PRE || lo >= onset + ONSET_PROTECT_POST,
"fade span [{lo:.0}, {hi:.0}] overlaps onset at {onset:.0}"
);
onset += 4_410.0;
}
}
}
#[test]
fn stereo_channels_splice_in_lockstep() {
let mut corrector = SolaCorrector::new(2, LAG);
corrector.set_transposition(1.05);
let mono = sine(440.0, 44_100 * 4, 0.5);
let mut left = Vec::new();
let mut right = Vec::new();
let mut block = [[0.0f32; BLOCK_FRAMES]; 2];
for chunk in mono.chunks_exact(BLOCK_FRAMES) {
for (i, &s) in chunk.iter().enumerate() {
block[0][i] = s;
block[1][i] = -0.8 * s;
}
corrector.process_block(&mut block, &[]);
left.extend_from_slice(&block[0]);
right.extend_from_slice(&block[1]);
}
for i in 4_096..left.len() {
assert!(
(right[i] + 0.8 * left[i]).abs() < 1e-4,
"stereo lockstep broken at {i}: L={} R={}",
left[i],
right[i]
);
}
}
}