use super::*;
use rustfft::num_complex::Complex;
#[derive(Clone, Debug)]
pub struct SpectralRobotize {
stft: STFT,
target_phase: f32,
mix: f32,
enabled: bool,
}
impl SpectralRobotize {
pub fn new(fft_size: usize, hop_size: usize, window_type: WindowType) -> 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");
Self {
stft: STFT::new(fft_size, hop_size, window_type),
target_phase: 0.0, mix: 1.0, enabled: true,
}
}
pub fn set_target_phase(&mut self, phase: f32) {
self.target_phase = phase;
}
pub fn set_mix(&mut self, mix: f32) {
self.mix = mix.clamp(0.0, 1.0);
}
pub fn target_phase(&self) -> f32 {
self.target_phase
}
pub fn mix(&self) -> f32 {
self.mix
}
pub fn process(&mut self, output: &mut [f32], input: &[f32]) {
if !self.enabled {
output.copy_from_slice(input);
return;
}
self.stft.add_input(input);
let target_phase = self.target_phase;
let mix = self.mix;
self.stft.process(output, |spectrum| {
Self::robotize_spectrum(spectrum, target_phase, mix);
});
}
#[inline]
fn robotize_spectrum(spectrum: &mut [Complex<f32>], target_phase: f32, mix: f32) {
let _target_re = target_phase.cos();
let _target_im = target_phase.sin();
for bin in spectrum.iter_mut() {
let magnitude = (bin.re * bin.re + bin.im * bin.im).sqrt();
let original_phase = bin.im.atan2(bin.re);
let new_phase = if mix >= 1.0 {
target_phase
} else if mix <= 0.0 {
original_phase
} else {
original_phase * (1.0 - mix) + target_phase * mix
};
bin.re = magnitude * new_phase.cos();
bin.im = magnitude * new_phase.sin();
}
}
pub fn reset(&mut self) {
self.stft.reset();
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spectral_robotize_creation() {
let robotize = SpectralRobotize::new(2048, 512, WindowType::Hann);
assert_eq!(robotize.target_phase(), 0.0);
assert_eq!(robotize.mix(), 1.0);
assert!(robotize.is_enabled());
}
#[test]
#[should_panic(expected = "FFT size must be power of 2")]
fn test_spectral_robotize_requires_power_of_two() {
SpectralRobotize::new(1000, 250, WindowType::Hann);
}
#[test]
#[should_panic(expected = "Hop size must be <= FFT size")]
fn test_spectral_robotize_hop_validation() {
SpectralRobotize::new(512, 1024, WindowType::Hann);
}
#[test]
fn test_spectral_robotize_set_target_phase() {
let mut robotize = SpectralRobotize::new(512, 128, WindowType::Hann);
robotize.set_target_phase(std::f32::consts::PI / 2.0);
assert_eq!(robotize.target_phase(), std::f32::consts::PI / 2.0);
}
#[test]
fn test_spectral_robotize_set_mix() {
let mut robotize = SpectralRobotize::new(512, 128, WindowType::Hann);
robotize.set_mix(0.5);
assert_eq!(robotize.mix(), 0.5);
robotize.set_mix(1.5);
assert_eq!(robotize.mix(), 1.0);
robotize.set_mix(-0.5);
assert_eq!(robotize.mix(), 0.0);
}
#[test]
fn test_spectral_robotize_process_silent() {
let mut robotize = SpectralRobotize::new(512, 128, WindowType::Hann);
let input = vec![0.0; 256];
let mut output = vec![0.0; 256];
robotize.process(&mut output, &input);
for &sample in &output {
assert!(sample.abs() < 1e-6);
}
}
#[test]
fn test_spectral_robotize_process_with_signal() {
let mut robotize = SpectralRobotize::new(2048, 512, WindowType::Hann);
let mut input = vec![0.0; 2048];
for i in 0..2048 {
let t = i as f32 / 44100.0;
input[i] = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 0.5;
}
let mut output = vec![0.0; 2048];
robotize.process(&mut output, &input);
let output_energy: f32 = output.iter().map(|x| x * x).sum();
assert!(output_energy > 0.0);
}
#[test]
fn test_spectral_robotize_disabled() {
let mut robotize = SpectralRobotize::new(512, 128, WindowType::Hann);
robotize.set_enabled(false);
let input = vec![0.5; 256];
let mut output = vec![0.0; 256];
robotize.process(&mut output, &input);
for i in 0..256 {
assert_eq!(output[i], input[i]);
}
}
#[test]
fn test_spectral_robotize_reset() {
let mut robotize = SpectralRobotize::new(512, 128, WindowType::Hann);
let input = vec![0.5; 256];
let mut output = vec![0.0; 256];
robotize.process(&mut output, &input);
robotize.reset();
robotize.process(&mut output, &input);
assert_eq!(output.len(), 256);
}
#[test]
fn test_spectral_robotize_all_window_types() {
for window_type in [WindowType::Hann, WindowType::Hamming, WindowType::Blackman, WindowType::Rectangular] {
let mut robotize = SpectralRobotize::new(512, 128, window_type);
let input = vec![0.0; 256];
let mut output = vec![0.0; 256];
robotize.process(&mut output, &input);
assert_eq!(output.len(), 256);
}
}
#[test]
fn test_spectral_robotize_various_fft_sizes() {
for fft_size in [512, 1024, 2048, 4096] {
let hop_size = fft_size / 4;
let mut robotize = SpectralRobotize::new(fft_size, hop_size, WindowType::Hann);
let input = vec![0.0; 512];
let mut output = vec![0.0; 512];
robotize.process(&mut output, &input);
assert_eq!(output.len(), 512);
}
}
#[test]
fn test_spectral_robotize_enable_disable() {
let mut robotize = SpectralRobotize::new(512, 128, WindowType::Hann);
assert!(robotize.is_enabled());
robotize.set_enabled(false);
assert!(!robotize.is_enabled());
robotize.set_enabled(true);
assert!(robotize.is_enabled());
}
#[test]
fn test_spectral_robotize_zero_mix() {
let mut robotize = SpectralRobotize::new(2048, 512, WindowType::Hann);
robotize.set_mix(0.0);
let mut input = vec![0.0; 2048];
for i in 0..2048 {
let t = i as f32 / 44100.0;
input[i] = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 0.3;
}
let mut output = vec![0.0; 2048];
robotize.process(&mut output, &input);
let output_energy: f32 = output.iter().map(|x| x * x).sum();
assert!(output_energy > 0.0);
}
#[test]
fn test_spectral_robotize_full_mix() {
let mut robotize = SpectralRobotize::new(2048, 512, WindowType::Hann);
robotize.set_mix(1.0);
let mut input = vec![0.0; 2048];
for i in 0..2048 {
let t = i as f32 / 44100.0;
input[i] = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 0.3;
}
let mut output = vec![0.0; 2048];
robotize.process(&mut output, &input);
let output_energy: f32 = output.iter().map(|x| x * x).sum();
assert!(output_energy > 0.0);
}
}