use std::f64::consts::PI;
#[derive(Clone, Copy, Default)]
struct Resonator {
a: f64,
b: f64,
c: f64,
p1: f64,
p2: f64,
}
impl Resonator {
fn set(&mut self, freq: f64, bw: f64, sr: f64) {
let r = (-PI * bw / sr).exp();
self.b = 2.0 * r * (2.0 * PI * freq / sr).cos();
self.c = -(r * r);
self.a = 1.0 - self.b - self.c; }
#[inline]
fn resonate(&mut self, input: f64) -> f64 {
let out = self.a * input + self.b * self.p1 + self.c * self.p2;
self.p2 = self.p1;
self.p1 = out;
out
}
}
#[derive(Debug, Clone)]
pub struct KlattFrame {
pub f0: f64,
pub formants: Vec<(f64, f64)>,
pub voicing: f64,
pub aspiration: f64,
}
impl KlattFrame {
pub fn vowel(f0: f64, formants: &[(f64, f64)]) -> Self {
KlattFrame { f0, formants: formants.to_vec(), voicing: 1.0, aspiration: 0.0 }
}
}
pub fn synthesize(frame: &KlattFrame, n_samples: usize, sample_rate: u32) -> Vec<i16> {
let sr = sample_rate as f64;
let mut cascade: Vec<Resonator> = frame
.formants
.iter()
.map(|&(f, bw)| {
let mut r = Resonator::default();
r.set(f, bw, sr);
r
})
.collect();
let mut rng: u32 = 0x1234_5678;
let mut noise = || {
rng = rng.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((rng >> 9) as f64 / (1u32 << 23) as f64) * 2.0 - 1.0
};
let mut phase = 0.0f64; let mut raw = Vec::with_capacity(n_samples);
for _ in 0..n_samples {
let voiced = if frame.f0 > 0.0 {
phase += frame.f0 / sr;
if phase >= 1.0 {
phase -= 1.0;
frame.voicing
} else {
0.0
}
} else {
0.0
};
let source = voiced + frame.aspiration * noise();
let mut s = source;
for r in &mut cascade {
s = r.resonate(s);
}
raw.push(s);
}
let peak = raw.iter().fold(0.0f64, |m, &v| m.max(v.abs())).max(1e-9);
let scale = 0.6 * 32767.0 / peak;
raw.iter().map(|&v| (v * scale).clamp(-32767.0, 32767.0) as i16).collect()
}
pub fn synthesize_frames_klatt(
frames: &[super::phondata::SpectFrame],
amps: &[f64],
pitch: &[f64],
sample_rate: u32,
) -> Vec<i32> {
let sr = sample_rate as f64;
let mut cascade = [Resonator::default(); 6];
let mut phase = 0.0f64;
let mut raw: Vec<f64> = Vec::new();
let formant = |fr: &super::phondata::SpectFrame, k: usize| -> (f64, f64) {
let f = fr.ffreq[k + 1] as f64;
let bw = fr.fwidth.get(k + 1).copied().unwrap_or(10) as f64 * 4.0;
(f, bw)
};
for (fi, fr) in frames.iter().enumerate() {
let next = frames.get(fi + 1).unwrap_or(fr);
let f0 = pitch.get(fi).copied().unwrap_or(120.0).max(50.0);
let amp = amps.get(fi).copied().unwrap_or(1.0) * (fr.rms as f64 / 64.0).max(0.05);
let n = (fr.length as usize) * 64; if n == 0 {
continue;
}
for j in 0..n {
let t = j as f64 / n as f64;
for (k, res) in cascade.iter_mut().enumerate() {
let (fa, ba) = formant(fr, k);
let (fb, bb) = formant(next, k);
let f = fa + (fb - fa) * t;
if f > 20.0 {
let bw = (ba + (bb - ba) * t).clamp(30.0, 1000.0);
res.set(f, bw, sr);
}
}
phase += f0 / sr;
let src = if phase >= 1.0 {
phase -= 1.0;
amp
} else {
0.0
};
let mut s = src;
for r in &mut cascade {
s = r.resonate(s);
}
raw.push(s);
}
}
let peak = raw.iter().fold(0.0f64, |m, &v| m.max(v.abs())).max(1e-9);
let scale = 0.5 * 32767.0 / peak;
raw.iter().map(|&v| (v * scale) as i32).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn output_is_finite_and_bounded() {
let frame = KlattFrame::vowel(120.0, &[(730.0, 50.0), (1090.0, 60.0), (2440.0, 120.0)]);
let pcm = synthesize(&frame, 4410, 22_050); assert_eq!(pcm.len(), 4410);
assert!(pcm.iter().any(|&s| s != 0), "silent output");
assert!(pcm.iter().all(|&s| s.abs() < 32767), "output clipped");
}
#[test]
fn unvoiced_frame_uses_noise_not_pitch() {
let frame = KlattFrame { f0: 0.0, formants: vec![(1500.0, 200.0)], voicing: 0.0, aspiration: 1.0 };
let pcm = synthesize(&frame, 2205, 22_050);
assert!(pcm.iter().any(|&s| s != 0), "noise source produced silence");
}
}