use std::sync::Arc;
use crate::analysis::preanalysis::analyze_for_dj;
use crate::core::preanalysis::PreAnalysisArtifact;
use crate::core::resample::STREAM_SINC_HALF_TAPS;
use crate::core::window::WindowType;
use crate::engine::{Engine, EngineConfig, EngineProfile};
use crate::error::StretchError;
use crate::stretch::phase_locking::PhaseLockingMode;
use crate::stretch::phase_vocoder::PhaseVocoder;
pub const OFFLINE_GRAPH_MAX_DEV: f64 = 0.20;
const WIDE_PV_FFT: usize = 2_048;
pub fn stretch_offline(
input: &[f32],
channels: usize,
sample_rate: u32,
ratio: f64,
pre_analysis: Option<Arc<PreAnalysisArtifact>>,
) -> Result<Vec<f32>, StretchError> {
if input.is_empty() {
return Ok(vec![]);
}
debug_assert_eq!(input.len() % channels.max(1), 0);
let rate = 1.0 / ratio;
if (1.0 - rate).abs() <= OFFLINE_GRAPH_MAX_DEV {
stretch_via_graph(input, channels, sample_rate, ratio, pre_analysis)
} else {
stretch_wide_pv(input, channels, sample_rate, ratio)
}
}
fn stretch_via_graph(
input: &[f32],
channels: usize,
sample_rate: u32,
ratio: f64,
pre_analysis: Option<Arc<PreAnalysisArtifact>>,
) -> Result<Vec<f32>, StretchError> {
let rate = 1.0 / ratio;
let frames = input.len() / channels;
let expected_frames = (frames as f64 * ratio).round() as usize;
let artifact: Arc<PreAnalysisArtifact> = match pre_analysis {
Some(artifact) => artifact,
None => {
let mid: Vec<f32> = if channels == 1 {
input.to_vec()
} else {
crate::downmix_to_mid(input, channels)
};
Arc::new(analyze_for_dj(&mid, sample_rate))
}
};
let handles = Engine::build(EngineConfig {
sample_rate,
channels,
profile: EngineProfile::Keylock,
initial_tempo_rate: rate,
pre_analysis: Some(artifact),
..EngineConfig::default()
})?;
let (controller, mut processor, mut source) =
(handles.controller, handles.processor, handles.source);
source.set_track_position(0);
let latency = processor.pipeline_latency_frames();
let flush_frames = (latency as f64 * rate).ceil() as usize + 4 * STREAM_SINC_HALF_TAPS;
let flush: Vec<f32> = vec![0.0; flush_frames * channels];
let mut feed = 0usize;
let mut flush_fed = 0usize;
let mut finished = false;
let mut out = vec![0.0f32; 1_024 * channels];
let total_needed = (expected_frames + latency) * channels;
let mut collected: Vec<f32> = Vec::with_capacity(total_needed + out.len());
while collected.len() < total_needed {
while feed < input.len() && source.free_frames() > 0 {
let end = (feed + 8_192 * channels).min(input.len());
feed += source.push(&input[feed..end]) * channels;
}
if feed >= input.len() {
while flush_fed < flush.len() && source.free_frames() > 0 {
flush_fed += source.push(&flush[flush_fed..]) * channels;
}
if flush_fed >= flush.len() && !finished {
finished = source.finish();
}
}
let underruns_before = controller.underrun_frames();
processor.process(&mut out);
collected.extend_from_slice(&out);
if finished && controller.underrun_frames() > underruns_before {
collected.resize(total_needed, 0.0);
break;
}
}
collected.drain(..latency * channels);
collected.truncate(expected_frames * channels);
debug_assert_eq!(collected.len(), expected_frames * channels);
Ok(collected)
}
fn stretch_wide_pv(
input: &[f32],
channels: usize,
sample_rate: u32,
ratio: f64,
) -> Result<Vec<f32>, StretchError> {
let frames = input.len() / channels;
let expected_frames = (frames as f64 * ratio).round() as usize;
let hop = WIDE_PV_FFT / 4;
if frames < WIDE_PV_FFT {
let mut out = vec![0.0f32; expected_frames * channels];
for ch in 0..channels {
let mono: Vec<f32> = input.iter().skip(ch).step_by(channels).copied().collect();
let resampled = crate::core::resample::resample_linear(&mono, expected_frames.max(1));
for (i, &sample) in resampled.iter().take(expected_frames).enumerate() {
out[i * channels + ch] = sample;
}
}
return Ok(out);
}
let mut outs: Vec<Vec<f32>> = Vec::with_capacity(channels);
for ch in 0..channels {
let mono: Vec<f32> = input.iter().skip(ch).step_by(channels).copied().collect();
let mut pv = PhaseVocoder::with_options(
WIDE_PV_FFT,
hop,
ratio,
sample_rate,
100.0,
WindowType::Hann,
PhaseLockingMode::Identity,
);
let mut rendered = pv.process(&mono)?;
rendered.resize(expected_frames, 0.0);
outs.push(rendered);
}
let mut interleaved = vec![0.0f32; expected_frames * channels];
for (ch, data) in outs.iter().enumerate() {
for (i, &s) in data.iter().enumerate() {
interleaved[i * channels + ch] = s;
}
}
Ok(interleaved)
}
#[cfg(test)]
mod tests {
use super::*;
fn sine(freq: f64, len: usize) -> Vec<f32> {
(0..len)
.map(|i| 0.5 * (2.0 * std::f64::consts::PI * freq * i as f64 / 44_100.0).sin() as f32)
.collect()
}
#[test]
fn graph_path_output_length_is_exact() {
let input = sine(440.0, 44_100 * 3);
for ratio in [0.85f64, 0.926, 1.0417, 1.08, 1.2] {
let out = stretch_offline(&input, 1, 44_100, ratio, None).unwrap();
let expected = (input.len() as f64 * ratio).round() as usize;
assert_eq!(out.len(), expected, "ratio {ratio}");
}
}
#[test]
fn wide_path_output_length_is_exact() {
let input = sine(440.0, 44_100 * 2);
for ratio in [0.5f64, 1.5, 2.0] {
let out = stretch_offline(&input, 1, 44_100, ratio, None).unwrap();
let expected = (input.len() as f64 * ratio).round() as usize;
assert_eq!(out.len(), expected, "ratio {ratio}");
}
}
#[test]
fn graph_path_preserves_pitch_and_level() {
let input = sine(440.0, 44_100 * 4);
let out = stretch_offline(&input, 1, 44_100, 1.06, None).unwrap();
let scan = &out[44_100..44_100 * 3];
let (mut first, mut last, mut count) = (None, None, 0usize);
for i in 1..scan.len() {
let (a, b) = (scan[i - 1] as f64, scan[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;
}
}
let freq = (count - 1) as f64 * 44_100.0 / (last.unwrap() - first.unwrap());
let cents = 1_200.0 * (freq / 440.0).log2();
assert!(cents.abs() < 6.0, "pitch off by {cents:.2} cents");
let rms =
(scan.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>() / scan.len() as f64).sqrt();
assert!((rms - 0.3535).abs() < 0.05, "level off: rms {rms:.4}");
}
#[test]
fn stereo_stays_interleaved_and_exact() {
let mono = sine(440.0, 44_100 * 2);
let mut input = Vec::with_capacity(mono.len() * 2);
for &s in &mono {
input.push(s);
input.push(-0.5 * s);
}
let out = stretch_offline(&input, 2, 44_100, 1.05, None).unwrap();
let expected = ((mono.len()) as f64 * 1.05).round() as usize * 2;
assert_eq!(out.len(), expected);
for i in (88_200..out.len() - 88_200).step_by(2) {
assert!(
(out[i + 1] + 0.5 * out[i]).abs() < 1e-3,
"stereo relationship broken at frame {}",
i / 2
);
}
}
}