use serde::{Deserialize, Serialize};
use crate::error::Result;
const MAX_PARTIALS: usize = 64;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Partial {
pub frequency_ratio: f32,
pub amplitude: f32,
pub phase: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdditiveSynth {
fundamental: f32,
partials: Vec<Partial>,
phases: Vec<f32>,
sample_rate: f32,
}
impl AdditiveSynth {
pub fn new(fundamental: f32, num_partials: usize, sample_rate: f32) -> Result<Self> {
if sample_rate <= 0.0 || !sample_rate.is_finite() {
return Err(crate::error::NaadError::InvalidSampleRate { sample_rate });
}
if fundamental <= 0.0 || !fundamental.is_finite() {
return Err(crate::error::NaadError::InvalidFrequency {
frequency: fundamental,
nyquist: sample_rate / 2.0,
});
}
let count = num_partials.clamp(1, MAX_PARTIALS);
let mut partials = Vec::with_capacity(count);
let nyquist = sample_rate / 2.0;
for i in 0..count {
let ratio = (i + 1) as f32;
let amp = if fundamental * ratio < nyquist {
1.0 / ratio
} else {
0.0
};
partials.push(Partial {
frequency_ratio: ratio,
amplitude: amp,
phase: 0.0,
});
}
let phases = vec![0.0; count];
Ok(Self {
fundamental,
partials,
phases,
sample_rate,
})
}
pub fn set_fundamental(&mut self, freq: f32) -> Result<()> {
if freq <= 0.0 || !freq.is_finite() {
return Err(crate::error::NaadError::InvalidFrequency {
frequency: freq,
nyquist: self.sample_rate / 2.0,
});
}
self.fundamental = freq;
let nyquist = self.sample_rate / 2.0;
for p in &mut self.partials {
if freq * p.frequency_ratio >= nyquist {
p.amplitude = 0.0;
}
}
Ok(())
}
pub fn set_partial(&mut self, index: usize, freq_ratio: f32, amplitude: f32) {
if let Some(p) = self.partials.get_mut(index) {
p.frequency_ratio = freq_ratio;
let nyquist = self.sample_rate / 2.0;
if self.fundamental * freq_ratio >= nyquist {
p.amplitude = 0.0;
} else {
p.amplitude = amplitude.clamp(0.0, 1.0);
}
}
}
#[must_use]
pub fn num_partials(&self) -> usize {
self.partials.len()
}
#[must_use]
pub fn fundamental(&self) -> f32 {
self.fundamental
}
#[inline]
#[must_use]
pub fn next_sample(&mut self) -> f32 {
let mut sum = 0.0f32;
for (i, partial) in self.partials.iter().enumerate() {
if partial.amplitude <= 0.0 {
continue;
}
let phase = self.phases[i];
sum += (phase * std::f32::consts::TAU).sin() * partial.amplitude;
let freq = self.fundamental * partial.frequency_ratio;
let inc = freq / self.sample_rate;
let new_phase = self.phases[i] + inc;
self.phases[i] = new_phase - new_phase.floor();
}
sum
}
#[inline]
pub fn fill_buffer(&mut self, buffer: &mut [f32]) {
for s in buffer.iter_mut() {
*s = self.next_sample();
}
}
#[must_use]
pub fn is_active(&self) -> bool {
self.partials.iter().any(|p| p.amplitude > 0.0)
}
pub fn compress_amplitudes_dct(&self, num_coeffs: usize) -> Result<Vec<f64>> {
let amps: Vec<f64> = self.partials.iter().map(|p| p.amplitude as f64).collect();
let coeffs =
hisab::num::dct(&s).map_err(|e| crate::error::NaadError::ComputationError {
message: format!("DCT failed: {e:?}"),
})?;
let keep = num_coeffs.clamp(1, coeffs.len());
Ok(coeffs.into_iter().take(keep).collect())
}
pub fn restore_amplitudes_dct(&mut self, coeffs: &[f64]) -> Result<()> {
let n = self.partials.len();
if coeffs.is_empty() || coeffs.len() > n {
return Err(crate::error::NaadError::InvalidParameter {
name: "coeffs".to_string(),
reason: format!("must be 1..={n} long, got {}", coeffs.len()),
});
}
let mut padded = vec![0.0f64; n];
padded[..coeffs.len()].copy_from_slice(coeffs);
let restored =
hisab::num::idct(&padded).map_err(|e| crate::error::NaadError::ComputationError {
message: format!("IDCT failed: {e:?}"),
})?;
let nyquist = self.sample_rate / 2.0;
for (p, &) in self.partials.iter_mut().zip(restored.iter()) {
p.amplitude = if self.fundamental * p.frequency_ratio >= nyquist {
0.0
} else {
(amp as f32).clamp(0.0, 1.0)
};
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_partial_is_sine() {
let mut synth = AdditiveSynth::new(440.0, 1, 44100.0).unwrap();
let mut buf = [0.0f32; 1024];
synth.fill_buffer(&mut buf);
assert!(
buf.iter().any(|&s| s.abs() > 0.5),
"single partial should produce sine output"
);
assert!(buf.iter().all(|s| s.is_finite()));
let peak = buf.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!((peak - 1.0).abs() < 0.05, "peak should be near 1.0");
}
#[test]
fn test_harmonic_series() {
let mut synth = AdditiveSynth::new(440.0, 8, 44100.0).unwrap();
let mut buf = [0.0f32; 1024];
synth.fill_buffer(&mut buf);
assert!(buf.iter().any(|&s| s.abs() > 0.5));
assert!(buf.iter().all(|s| s.is_finite()));
}
#[test]
fn test_set_partial() {
let mut synth = AdditiveSynth::new(440.0, 4, 44100.0).unwrap();
synth.set_partial(0, 1.0, 0.0);
synth.set_partial(1, 2.0, 1.0);
let mut buf = [0.0f32; 1024];
synth.fill_buffer(&mut buf);
assert!(buf.iter().any(|&s| s.abs() > 0.5));
}
#[test]
fn test_serde_roundtrip() {
let synth = AdditiveSynth::new(440.0, 16, 44100.0).unwrap();
let json = serde_json::to_string(&synth).unwrap();
let back: AdditiveSynth = serde_json::from_str(&json).unwrap();
assert_eq!(synth.partials.len(), back.partials.len());
assert!((synth.fundamental - back.fundamental).abs() < f32::EPSILON);
}
#[test]
fn test_nyquist_filtering() {
let synth = AdditiveSynth::new(1000.0, 8, 8000.0).unwrap();
for i in 4..synth.partials.len() {
assert!(
synth.partials[i].amplitude == 0.0,
"partial {} should be zeroed (above Nyquist)",
i + 1
);
}
}
#[test]
fn test_dct_full_roundtrip_preserves_amplitudes() {
let mut synth = AdditiveSynth::new(440.0, 16, 44100.0).unwrap();
let original: Vec<f32> = synth.partials.iter().map(|p| p.amplitude).collect();
let coeffs = synth.compress_amplitudes_dct(synth.num_partials()).unwrap();
assert_eq!(coeffs.len(), 16);
synth.restore_amplitudes_dct(&coeffs).unwrap();
for (i, (orig, p)) in original.iter().zip(synth.partials.iter()).enumerate() {
assert!(
(orig - p.amplitude).abs() < 1e-4,
"partial {i}: orig={orig}, restored={}",
p.amplitude
);
}
}
#[test]
fn test_dct_truncated_is_lossy_approximation() {
let mut synth = AdditiveSynth::new(440.0, 16, 44100.0).unwrap();
let original: Vec<f32> = synth.partials.iter().map(|p| p.amplitude).collect();
let coeffs = synth.compress_amplitudes_dct(4).unwrap();
assert_eq!(coeffs.len(), 4);
synth.restore_amplitudes_dct(&coeffs).unwrap();
let mse: f32 = original
.iter()
.zip(synth.partials.iter())
.map(|(o, p)| (o - p.amplitude).powi(2))
.sum::<f32>()
/ original.len() as f32;
let rmse = mse.sqrt();
assert!(rmse < 0.15, "truncated DCT roundtrip RMSE = {rmse}");
assert!(
rmse > 1e-6,
"with truncation RMSE shouldn't be exactly zero"
);
}
#[test]
fn test_dct_compress_clamps_num_coeffs() {
let synth = AdditiveSynth::new(440.0, 8, 44100.0).unwrap();
let coeffs = synth.compress_amplitudes_dct(100).unwrap();
assert_eq!(coeffs.len(), 8);
let coeffs = synth.compress_amplitudes_dct(0).unwrap();
assert_eq!(coeffs.len(), 1);
}
#[test]
fn test_dct_restore_rejects_invalid_lengths() {
let mut synth = AdditiveSynth::new(440.0, 8, 44100.0).unwrap();
assert!(synth.restore_amplitudes_dct(&[]).is_err());
assert!(synth.restore_amplitudes_dct(&vec![0.5; 99]).is_err());
}
#[test]
fn test_dct_restore_respects_nyquist() {
let mut synth = AdditiveSynth::new(1000.0, 8, 8000.0).unwrap();
let pretend_full = vec![0.5f64; 8];
let coeffs = hisab::num::dct(&pretend_full).unwrap();
synth.restore_amplitudes_dct(&coeffs).unwrap();
for (i, p) in synth.partials.iter().enumerate() {
if 1000.0 * p.frequency_ratio >= 4000.0 {
assert!(
p.amplitude == 0.0,
"partial {i} above Nyquist must remain 0.0 after restore"
);
}
}
}
}