use super::*;
use rustfft::num_complex::Complex;
#[derive(Clone, Debug)]
pub struct SpectralShift {
stft: STFT,
fft_size: usize,
sample_rate: f32,
shift_hz: f32,
mix: f32,
enabled: bool,
}
impl SpectralShift {
pub fn new(fft_size: usize, hop_size: usize, window_type: WindowType, sample_rate: f32) -> Self {
assert!(fft_size.is_power_of_two(), "FFT size must be power of 2");
assert!(hop_size <= fft_size, "Hop size must be <= FFT size");
assert!(sample_rate > 0.0, "Sample rate must be positive");
let stft = STFT::new(fft_size, hop_size, window_type);
Self {
stft,
fft_size,
sample_rate,
shift_hz: 0.0,
mix: 1.0,
enabled: true,
}
}
pub fn set_shift_hz(&mut self, shift_hz: f32) {
self.shift_hz = shift_hz;
}
pub fn set_mix(&mut self, mix: f32) {
self.mix = mix.clamp(0.0, 1.0);
}
pub fn shift_hz(&self) -> f32 {
self.shift_hz
}
pub fn mix(&self) -> f32 {
self.mix
}
pub fn process(&mut self, output: &mut [f32], _input: &[f32]) {
if !self.enabled {
return;
}
let shift_hz = self.shift_hz;
let mix = self.mix;
let sample_rate = self.sample_rate;
let fft_size = self.fft_size;
self.stft.process(output, |spectrum| {
Self::apply_shift_static(spectrum, shift_hz, mix, sample_rate, fft_size);
});
}
#[inline]
fn apply_shift_static(
spectrum: &mut [Complex<f32>],
shift_hz: f32,
mix: f32,
sample_rate: f32,
fft_size: usize,
) {
let len = spectrum.len();
let mut dry_spectrum = vec![Complex::new(0.0, 0.0); len];
dry_spectrum.copy_from_slice(spectrum);
let hz_per_bin = sample_rate / fft_size as f32;
let shift_bins = shift_hz / hz_per_bin;
let mut shifted = vec![Complex::new(0.0, 0.0); len];
for (i, shifted_bin) in shifted.iter_mut().enumerate().take(len) {
let src_bin = i as f32 - shift_bins;
if src_bin >= 0.0 && src_bin < (len - 1) as f32 {
let bin_floor = src_bin.floor() as usize;
let bin_ceil = bin_floor + 1;
let frac = src_bin - bin_floor as f32;
let mag_floor = spectrum[bin_floor].norm();
let mag_ceil = spectrum[bin_ceil].norm();
let mag = mag_floor * (1.0 - frac) + mag_ceil * frac;
let phase_floor = spectrum[bin_floor].arg();
let phase_ceil = spectrum[bin_ceil].arg();
let phase = phase_floor * (1.0 - frac) + phase_ceil * frac;
*shifted_bin = Complex::from_polar(mag, phase);
}
}
spectrum.copy_from_slice(&shifted);
if mix < 1.0 {
for i in 0..len {
spectrum[i] = Complex::new(
spectrum[i].re * mix + dry_spectrum[i].re * (1.0 - mix),
spectrum[i].im * mix + dry_spectrum[i].im * (1.0 - mix),
);
}
}
}
pub fn reset(&mut self) {
self.stft.reset();
}
pub fn fft_size(&self) -> usize {
self.fft_size
}
pub fn hop_size(&self) -> usize {
self.stft.hop_size
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
impl SpectralShift {
pub fn subtle() -> Self {
let mut shift = Self::new(2048, 512, WindowType::Hann, 44100.0);
shift.set_shift_hz(7.0);
shift.set_mix(0.5);
shift
}
pub fn metallic() -> Self {
let mut shift = Self::new(2048, 512, WindowType::Hann, 44100.0);
shift.set_shift_hz(75.0);
shift.set_mix(0.8);
shift
}
pub fn bell() -> Self {
let mut shift = Self::new(2048, 512, WindowType::Hann, 44100.0);
shift.set_shift_hz(150.0);
shift.set_mix(0.9);
shift
}
pub fn alien() -> Self {
let mut shift = Self::new(2048, 512, WindowType::Hann, 44100.0);
shift.set_shift_hz(300.0);
shift.set_mix(1.0);
shift
}
pub fn down() -> Self {
let mut shift = Self::new(2048, 512, WindowType::Hann, 44100.0);
shift.set_shift_hz(-100.0);
shift.set_mix(0.8);
shift
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spectral_shift_creation() {
let shift = SpectralShift::new(2048, 512, WindowType::Hann, 44100.0);
assert!(shift.is_enabled());
assert_eq!(shift.fft_size(), 2048);
assert_eq!(shift.hop_size(), 512);
assert_eq!(shift.shift_hz(), 0.0);
assert_eq!(shift.mix(), 1.0);
}
#[test]
#[should_panic(expected = "FFT size must be power of 2")]
fn test_spectral_shift_requires_power_of_two() {
SpectralShift::new(1000, 250, WindowType::Hann, 44100.0);
}
#[test]
#[should_panic(expected = "Hop size must be <= FFT size")]
fn test_spectral_shift_hop_validation() {
SpectralShift::new(512, 1024, WindowType::Hann, 44100.0);
}
#[test]
#[should_panic(expected = "Sample rate must be positive")]
fn test_spectral_shift_sample_rate_validation() {
SpectralShift::new(512, 128, WindowType::Hann, 0.0);
}
#[test]
fn test_set_shift_hz() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_shift_hz(100.0);
assert_eq!(shift.shift_hz(), 100.0);
shift.set_shift_hz(-50.0);
assert_eq!(shift.shift_hz(), -50.0);
shift.set_shift_hz(0.0);
assert_eq!(shift.shift_hz(), 0.0);
}
#[test]
fn test_set_mix() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_mix(0.5);
assert_eq!(shift.mix(), 0.5);
shift.set_mix(1.5);
assert_eq!(shift.mix(), 1.0);
shift.set_mix(-0.5);
assert_eq!(shift.mix(), 0.0);
}
#[test]
fn test_enable_disable() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
assert!(shift.is_enabled());
shift.set_enabled(false);
assert!(!shift.is_enabled());
shift.set_enabled(true);
assert!(shift.is_enabled());
}
#[test]
fn test_process_disabled() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_enabled(false);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
shift.process(&mut output, &input);
assert!(output.iter().all(|&x| x == 0.0));
}
#[test]
fn test_process_basic() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_shift_hz(100.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
shift.process(&mut output, &input);
}
#[test]
fn test_reset() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
shift.process(&mut output, &input);
shift.reset();
shift.process(&mut output, &input);
}
#[test]
fn test_upshift() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_shift_hz(200.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
shift.process(&mut output, &input);
}
#[test]
fn test_downshift() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_shift_hz(-200.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
shift.process(&mut output, &input);
}
#[test]
fn test_preset_subtle() {
let shift = SpectralShift::subtle();
assert_eq!(shift.shift_hz(), 7.0);
assert_eq!(shift.mix(), 0.5);
assert!(shift.is_enabled());
}
#[test]
fn test_preset_metallic() {
let shift = SpectralShift::metallic();
assert_eq!(shift.shift_hz(), 75.0);
assert_eq!(shift.mix(), 0.8);
assert!(shift.is_enabled());
}
#[test]
fn test_preset_bell() {
let shift = SpectralShift::bell();
assert_eq!(shift.shift_hz(), 150.0);
assert_eq!(shift.mix(), 0.9);
assert!(shift.is_enabled());
}
#[test]
fn test_preset_alien() {
let shift = SpectralShift::alien();
assert_eq!(shift.shift_hz(), 300.0);
assert_eq!(shift.mix(), 1.0);
assert!(shift.is_enabled());
}
#[test]
fn test_preset_down() {
let shift = SpectralShift::down();
assert_eq!(shift.shift_hz(), -100.0);
assert_eq!(shift.mix(), 0.8);
assert!(shift.is_enabled());
}
#[test]
fn test_different_fft_sizes() {
let shift_512 = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
let shift_1024 = SpectralShift::new(1024, 256, WindowType::Hann, 44100.0);
let shift_2048 = SpectralShift::new(2048, 512, WindowType::Hann, 44100.0);
assert_eq!(shift_512.fft_size(), 512);
assert_eq!(shift_1024.fft_size(), 1024);
assert_eq!(shift_2048.fft_size(), 2048);
}
#[test]
fn test_zero_shift() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_shift_hz(0.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
shift.process(&mut output, &input);
}
#[test]
fn test_large_shift() {
let mut shift = SpectralShift::new(512, 128, WindowType::Hann, 44100.0);
shift.set_shift_hz(1000.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
shift.process(&mut output, &input);
}
}