use super::*;
use rustfft::num_complex::Complex;
#[derive(Clone, Debug)]
pub struct HarmonyVoice {
pub semitones: f32,
pub mix: f32,
}
impl HarmonyVoice {
pub fn new(semitones: f32, mix: f32) -> Self {
Self {
semitones,
mix: mix.clamp(0.0, 1.0),
}
}
}
#[derive(Clone, Debug)]
pub struct SpectralHarmonizer {
stft: STFT,
fft_size: usize,
sample_rate: f32,
voices: Vec<HarmonyVoice>,
dry_mix: f32,
enabled: bool,
}
impl SpectralHarmonizer {
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,
voices: Vec::new(),
dry_mix: 1.0,
enabled: true,
}
}
pub fn add_voice(&mut self, voice: HarmonyVoice) {
self.voices.push(voice);
}
pub fn clear_voices(&mut self) {
self.voices.clear();
}
pub fn set_voices(&mut self, voices: Vec<HarmonyVoice>) {
self.voices = voices;
}
pub fn voices(&self) -> &[HarmonyVoice] {
&self.voices
}
pub fn set_dry_mix(&mut self, mix: f32) {
self.dry_mix = mix.clamp(0.0, 1.0);
}
pub fn dry_mix(&self) -> f32 {
self.dry_mix
}
pub fn process(&mut self, output: &mut [f32], _input: &[f32]) {
if !self.enabled || self.voices.is_empty() {
return;
}
let voices = self.voices.clone(); let dry_mix = self.dry_mix;
let sample_rate = self.sample_rate;
self.stft.process(output, |spectrum| {
Self::apply_harmonizer_static(spectrum, &voices, dry_mix, sample_rate);
});
}
#[inline]
fn apply_harmonizer_static(
spectrum: &mut [Complex<f32>],
voices: &[HarmonyVoice],
dry_mix: f32,
_sample_rate: f32,
) {
let len = spectrum.len();
let dry_spectrum: Vec<Complex<f32>> = spectrum.to_vec();
for bin in spectrum.iter_mut() {
*bin = Complex::new(0.0, 0.0);
}
if dry_mix > 0.0 {
for i in 0..len {
spectrum[i] = dry_spectrum[i] * dry_mix;
}
}
for voice in voices {
if voice.mix <= 0.0 {
continue;
}
let pitch_ratio = 2.0f32.powf(voice.semitones / 12.0);
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 / pitch_ratio;
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).min(len - 1);
let frac = src_bin - bin_floor as f32;
let mag_floor = dry_spectrum[bin_floor].norm();
let mag_ceil = dry_spectrum[bin_ceil].norm();
let mag = mag_floor * (1.0 - frac) + mag_ceil * frac;
let phase_floor = dry_spectrum[bin_floor].arg();
let phase_ceil = dry_spectrum[bin_ceil].arg();
let mut phase_diff = phase_ceil - phase_floor;
if phase_diff > std::f32::consts::PI {
phase_diff -= 2.0 * std::f32::consts::PI;
} else if phase_diff < -std::f32::consts::PI {
phase_diff += 2.0 * std::f32::consts::PI;
}
let phase = phase_floor + phase_diff * frac;
*shifted_bin = Complex::from_polar(mag, phase);
}
}
for i in 0..len {
spectrum[i] += shifted[i] * voice.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 SpectralHarmonizer {
pub fn major_chord() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_voices(vec![
HarmonyVoice::new(4.0, 0.7), HarmonyVoice::new(7.0, 0.7), ]);
harmonizer.set_dry_mix(0.8);
harmonizer
}
pub fn minor_chord() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_voices(vec![
HarmonyVoice::new(3.0, 0.7), HarmonyVoice::new(7.0, 0.7), ]);
harmonizer.set_dry_mix(0.8);
harmonizer
}
pub fn fifth() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.add_voice(HarmonyVoice::new(7.0, 0.8));
harmonizer.set_dry_mix(0.9);
harmonizer
}
pub fn octave_up() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.add_voice(HarmonyVoice::new(12.0, 0.7));
harmonizer.set_dry_mix(1.0);
harmonizer
}
pub fn octave_down() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.add_voice(HarmonyVoice::new(-12.0, 0.7));
harmonizer.set_dry_mix(1.0);
harmonizer
}
pub fn octave_double() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_voices(vec![
HarmonyVoice::new(-12.0, 0.5), HarmonyVoice::new(12.0, 0.5), ]);
harmonizer.set_dry_mix(1.0);
harmonizer
}
pub fn choir() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_voices(vec![
HarmonyVoice::new(-12.0, 0.4), HarmonyVoice::new(3.0, 0.5), HarmonyVoice::new(7.0, 0.5), HarmonyVoice::new(12.0, 0.4), ]);
harmonizer.set_dry_mix(0.7);
harmonizer
}
pub fn barbershop() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_voices(vec![
HarmonyVoice::new(-12.0, 0.6), HarmonyVoice::new(-5.0, 0.6), HarmonyVoice::new(4.0, 0.6), HarmonyVoice::new(12.0, 0.5), ]);
harmonizer.set_dry_mix(0.8);
harmonizer
}
pub fn thick() -> Self {
let mut harmonizer = Self::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_voices(vec![
HarmonyVoice::new(-0.1, 0.6), HarmonyVoice::new(0.1, 0.6), ]);
harmonizer.set_dry_mix(1.0);
harmonizer
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spectral_harmonizer_creation() {
let harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
assert_eq!(harmonizer.fft_size(), 2048);
assert_eq!(harmonizer.hop_size(), 512);
assert_eq!(harmonizer.dry_mix(), 1.0);
assert_eq!(harmonizer.voices().len(), 0);
assert!(harmonizer.is_enabled());
}
#[test]
#[should_panic(expected = "FFT size must be power of 2")]
fn test_spectral_harmonizer_requires_power_of_two() {
SpectralHarmonizer::new(1000, 250, WindowType::Hann, 44100.0);
}
#[test]
#[should_panic(expected = "Hop size must be <= FFT size")]
fn test_spectral_harmonizer_hop_validation() {
SpectralHarmonizer::new(1024, 2048, WindowType::Hann, 44100.0);
}
#[test]
#[should_panic(expected = "Sample rate must be positive")]
fn test_spectral_harmonizer_sample_rate_validation() {
SpectralHarmonizer::new(1024, 256, WindowType::Hann, 0.0);
}
#[test]
fn test_harmony_voice_creation() {
let voice = HarmonyVoice::new(7.0, 0.8);
assert_eq!(voice.semitones, 7.0);
assert_eq!(voice.mix, 0.8);
}
#[test]
fn test_harmony_voice_mix_clamps() {
let voice = HarmonyVoice::new(7.0, 1.5);
assert_eq!(voice.mix, 1.0);
let voice = HarmonyVoice::new(7.0, -0.5);
assert_eq!(voice.mix, 0.0);
}
#[test]
fn test_add_voice() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
assert_eq!(harmonizer.voices().len(), 0);
harmonizer.add_voice(HarmonyVoice::new(7.0, 0.8));
assert_eq!(harmonizer.voices().len(), 1);
assert_eq!(harmonizer.voices()[0].semitones, 7.0);
}
#[test]
fn test_clear_voices() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.add_voice(HarmonyVoice::new(7.0, 0.8));
harmonizer.add_voice(HarmonyVoice::new(4.0, 0.7));
assert_eq!(harmonizer.voices().len(), 2);
harmonizer.clear_voices();
assert_eq!(harmonizer.voices().len(), 0);
}
#[test]
fn test_set_voices() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_voices(vec![
HarmonyVoice::new(4.0, 0.7),
HarmonyVoice::new(7.0, 0.8),
]);
assert_eq!(harmonizer.voices().len(), 2);
assert_eq!(harmonizer.voices()[0].semitones, 4.0);
assert_eq!(harmonizer.voices()[1].semitones, 7.0);
}
#[test]
fn test_set_dry_mix() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.set_dry_mix(0.5);
assert_eq!(harmonizer.dry_mix(), 0.5);
harmonizer.set_dry_mix(1.5);
assert_eq!(harmonizer.dry_mix(), 1.0);
harmonizer.set_dry_mix(-0.5);
assert_eq!(harmonizer.dry_mix(), 0.0);
}
#[test]
fn test_enable_disable() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
assert!(harmonizer.is_enabled());
harmonizer.set_enabled(false);
assert!(!harmonizer.is_enabled());
harmonizer.set_enabled(true);
assert!(harmonizer.is_enabled());
}
#[test]
fn test_process_doesnt_crash() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.add_voice(HarmonyVoice::new(7.0, 0.8));
let mut output = vec![0.0; 512];
let input = vec![0.5; 512];
harmonizer.process(&mut output, &input);
}
#[test]
fn test_process_disabled() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.add_voice(HarmonyVoice::new(7.0, 0.8));
harmonizer.set_enabled(false);
let mut output = vec![1.0; 512];
let input = vec![0.5; 512];
harmonizer.process(&mut output, &input);
for sample in output.iter() {
assert_eq!(*sample, 1.0);
}
}
#[test]
fn test_process_no_voices() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
let mut output = vec![1.0; 512];
let input = vec![0.5; 512];
harmonizer.process(&mut output, &input);
for sample in output.iter() {
assert_eq!(*sample, 1.0);
}
}
#[test]
fn test_reset() {
let mut harmonizer = SpectralHarmonizer::new(2048, 512, WindowType::Hann, 44100.0);
harmonizer.add_voice(HarmonyVoice::new(7.0, 0.8));
let mut output = vec![0.0; 512];
let input = vec![0.5; 512];
harmonizer.process(&mut output, &input);
harmonizer.reset();
harmonizer.process(&mut output, &input);
}
#[test]
fn test_major_chord_preset() {
let harmonizer = SpectralHarmonizer::major_chord();
assert_eq!(harmonizer.voices().len(), 2);
assert_eq!(harmonizer.voices()[0].semitones, 4.0);
assert_eq!(harmonizer.voices()[1].semitones, 7.0);
assert_eq!(harmonizer.dry_mix(), 0.8);
}
#[test]
fn test_minor_chord_preset() {
let harmonizer = SpectralHarmonizer::minor_chord();
assert_eq!(harmonizer.voices().len(), 2);
assert_eq!(harmonizer.voices()[0].semitones, 3.0);
assert_eq!(harmonizer.voices()[1].semitones, 7.0);
}
#[test]
fn test_fifth_preset() {
let harmonizer = SpectralHarmonizer::fifth();
assert_eq!(harmonizer.voices().len(), 1);
assert_eq!(harmonizer.voices()[0].semitones, 7.0);
}
#[test]
fn test_octave_up_preset() {
let harmonizer = SpectralHarmonizer::octave_up();
assert_eq!(harmonizer.voices().len(), 1);
assert_eq!(harmonizer.voices()[0].semitones, 12.0);
}
#[test]
fn test_octave_down_preset() {
let harmonizer = SpectralHarmonizer::octave_down();
assert_eq!(harmonizer.voices().len(), 1);
assert_eq!(harmonizer.voices()[0].semitones, -12.0);
}
#[test]
fn test_octave_double_preset() {
let harmonizer = SpectralHarmonizer::octave_double();
assert_eq!(harmonizer.voices().len(), 2);
assert_eq!(harmonizer.voices()[0].semitones, -12.0);
assert_eq!(harmonizer.voices()[1].semitones, 12.0);
}
#[test]
fn test_choir_preset() {
let harmonizer = SpectralHarmonizer::choir();
assert_eq!(harmonizer.voices().len(), 4);
}
#[test]
fn test_barbershop_preset() {
let harmonizer = SpectralHarmonizer::barbershop();
assert_eq!(harmonizer.voices().len(), 4);
}
#[test]
fn test_thick_preset() {
let harmonizer = SpectralHarmonizer::thick();
assert_eq!(harmonizer.voices().len(), 2);
assert!(harmonizer.voices()[0].semitones.abs() < 1.0);
assert!(harmonizer.voices()[1].semitones.abs() < 1.0);
}
}