use alloc::{vec, vec::Vec};
use serde::{Deserialize, Serialize};
use tracing::trace;
use crate::error::Result;
use crate::formant::{Formant, FormantFilter, Vowel, VowelTarget};
use crate::lod::Quality;
use crate::smooth::SmoothedParam;
const NASAL_ANTIFORMANT_FREQ: f32 = 250.0;
const NASAL_ANTIFORMANT_BW: f32 = 100.0;
const DEFAULT_LIP_RADIATION: f32 = 0.97;
use crate::glottal::GlottalSource;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NasalPlace {
Bilabial,
Alveolar,
Velar,
Neutral,
}
impl NasalPlace {
#[must_use]
fn antiformant_frequency(self) -> f32 {
match self {
Self::Neutral => NASAL_ANTIFORMANT_FREQ,
Self::Bilabial => 750.0,
Self::Alveolar => 1450.0,
Self::Velar => 3000.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VocalTract {
filter: FormantFilter,
nasal_coupling: SmoothedParam,
#[cfg(not(feature = "naad-backend"))]
nasal_antiformant: NasalAntiformant,
#[cfg(feature = "naad-backend")]
nasal_antiformant: naad::filter::BiquadFilter,
lip_prev: f32,
lip_radiation: f32,
interaction_strength: f32,
interaction_feedback: f32,
subglottal_coupling: f32,
#[cfg(not(feature = "naad-backend"))]
sg_state: [f32; 4],
#[cfg(not(feature = "naad-backend"))]
sg_coeff: [f32; 4],
#[cfg(feature = "naad-backend")]
subglottal_filter: naad::filter::BiquadFilter,
gain: SmoothedParam,
quality: Quality,
sample_rate: f32,
}
#[cfg(not(feature = "naad-backend"))]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NasalAntiformant {
frequency: f32,
bandwidth: f32,
b0: f32,
b1: f32,
b2: f32,
a1: f32,
a2: f32,
x1: f32,
x2: f32,
y1: f32,
y2: f32,
}
#[cfg(not(feature = "naad-backend"))]
impl NasalAntiformant {
fn new(frequency: f32, bandwidth: f32, sample_rate: f32) -> Self {
let omega = 2.0 * core::f32::consts::PI * frequency / sample_rate;
let cos_omega = crate::math::f32::cos(omega);
let bw_omega = 2.0 * core::f32::consts::PI * bandwidth / sample_rate;
let alpha = crate::math::f32::sinh(bw_omega / 2.0) * crate::math::f32::sin(omega);
let a0 = 1.0 + alpha;
let b0 = 1.0 / a0;
let b1 = (-2.0 * cos_omega) / a0;
let b2 = 1.0 / a0;
let a1 = (-2.0 * cos_omega) / a0;
let a2 = (1.0 - alpha) / a0;
Self {
frequency,
bandwidth,
b0,
b1,
b2,
a1,
a2,
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
}
}
#[inline]
fn process_sample(&mut self, input: f32) -> f32 {
let output = self.b0 * input + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = input;
self.y2 = self.y1;
self.y1 = output;
output
}
fn update(&mut self, frequency: f32, bandwidth: f32, sample_rate: f32) {
let new = Self::new(frequency, bandwidth, sample_rate);
self.frequency = new.frequency;
self.bandwidth = new.bandwidth;
self.b0 = new.b0;
self.b1 = new.b1;
self.b2 = new.b2;
self.a1 = new.a1;
self.a2 = new.a2;
}
fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
self.y1 = 0.0;
self.y2 = 0.0;
}
}
impl VocalTract {
#[must_use]
pub fn new(sample_rate: f32) -> Self {
let target = VowelTarget::from_vowel(Vowel::Schwa);
let formants = target.to_formants();
let filter = FormantFilter::new(&formants, sample_rate).unwrap_or_else(|_| {
FormantFilter::new(&[Formant::new(500.0, 100.0, 1.0)], sample_rate)
.expect("fallback formant filter must succeed")
});
trace!(sample_rate, "created vocal tract with neutral vowel");
let _nasal_q = NASAL_ANTIFORMANT_FREQ / NASAL_ANTIFORMANT_BW;
Self {
filter,
nasal_coupling: SmoothedParam::new(0.0, sample_rate),
#[cfg(not(feature = "naad-backend"))]
nasal_antiformant: NasalAntiformant::new(
NASAL_ANTIFORMANT_FREQ,
NASAL_ANTIFORMANT_BW,
sample_rate,
),
#[cfg(feature = "naad-backend")]
nasal_antiformant: naad::filter::BiquadFilter::new(
naad::filter::FilterType::Notch,
sample_rate,
NASAL_ANTIFORMANT_FREQ,
_nasal_q,
)
.expect("nasal antiformant filter init must succeed"),
lip_prev: 0.0,
lip_radiation: DEFAULT_LIP_RADIATION,
interaction_strength: 0.05,
interaction_feedback: 0.0,
subglottal_coupling: 0.05,
#[cfg(not(feature = "naad-backend"))]
sg_state: [0.0; 4],
#[cfg(not(feature = "naad-backend"))]
sg_coeff: {
let (b0, b2, a1, a2) =
crate::formant::biquad_coefficients(600.0, 80.0, sample_rate);
[b0, b2, a1, a2]
},
#[cfg(feature = "naad-backend")]
subglottal_filter: naad::filter::BiquadFilter::new(
naad::filter::FilterType::BandPass,
sample_rate,
600.0,
600.0 / 80.0, )
.expect("subglottal filter init must succeed"),
gain: SmoothedParam::new(1.0, sample_rate),
quality: Quality::Full,
sample_rate,
}
}
pub fn set_vowel(&mut self, vowel: Vowel) -> Result<()> {
let target = VowelTarget::from_vowel(vowel);
self.set_formants_from_target(&target)
}
pub fn set_formants(&mut self, formants: &[Formant]) -> Result<()> {
self.filter = FormantFilter::new(formants, self.sample_rate)?;
Ok(())
}
pub fn set_formants_from_target(&mut self, target: &VowelTarget) -> Result<()> {
let formants = target.to_formants();
self.set_formants(&formants)
}
pub fn set_nasal_coupling(&mut self, coupling: f32) {
self.nasal_coupling.set_target(coupling.clamp(0.0, 1.0));
}
pub fn set_lip_radiation(&mut self, coefficient: f32) {
self.lip_radiation = coefficient.clamp(0.0, 1.0);
}
pub fn set_nasal_place(&mut self, place: NasalPlace) {
let freq = place.antiformant_frequency();
#[cfg(not(feature = "naad-backend"))]
self.nasal_antiformant
.update(freq, NASAL_ANTIFORMANT_BW, self.sample_rate);
#[cfg(feature = "naad-backend")]
{
let q = freq / NASAL_ANTIFORMANT_BW;
let _ = self.nasal_antiformant.set_params(freq, q, 0.0);
}
}
pub fn set_interaction_strength(&mut self, strength: f32) {
self.interaction_strength = strength.clamp(0.0, 0.3);
}
pub fn set_subglottal_coupling(&mut self, strength: f32) {
self.subglottal_coupling = strength.clamp(0.0, 0.2);
}
pub fn set_gain(&mut self, gain: f32) {
self.gain.set_target(gain.max(0.0));
}
pub fn set_quality(&mut self, quality: Quality) {
self.quality = quality;
}
#[must_use]
pub fn quality(&self) -> Quality {
self.quality
}
#[must_use]
pub fn sample_rate(&self) -> f32 {
self.sample_rate
}
#[inline]
pub fn process_sample(&mut self, input: f32) -> f32 {
let excitation = if self.quality.use_interaction() {
input - self.interaction_strength * self.interaction_feedback
} else {
input
};
let formant_out = self.filter.process_sample(excitation);
let nc = self.nasal_coupling.next();
let output = if self.quality.use_nasal_coupling() && nc > 0.0 {
let nasal = self.nasal_antiformant.process_sample(formant_out);
formant_out * (1.0 - nc) + nasal * nc
} else {
formant_out
};
let output = if self.quality.use_subglottal() && self.subglottal_coupling > 0.0 {
#[cfg(feature = "naad-backend")]
let sg_out = self.subglottal_filter.process_sample(output);
#[cfg(not(feature = "naad-backend"))]
let sg_out = {
let c = &self.sg_coeff;
let s = &mut self.sg_state;
let out = c[0] * output + c[1] * s[1] - c[2] * s[2] - c[3] * s[3];
s[1] = s[0];
s[0] = output;
s[3] = s[2];
s[2] = out;
out
};
output + sg_out * self.subglottal_coupling
} else {
output
};
let radiated = if self.quality.use_lip_radiation() {
let r = output - self.lip_radiation * self.lip_prev;
self.lip_prev = output;
r
} else {
output
};
self.interaction_feedback = radiated;
radiated * self.gain.next()
}
pub fn synthesize(&mut self, glottal: &mut GlottalSource, num_samples: usize) -> Vec<f32> {
let mut output = vec![0.0; num_samples];
self.synthesize_into(glottal, &mut output);
output
}
pub fn synthesize_into(&mut self, glottal: &mut GlottalSource, output: &mut [f32]) {
for sample in output.iter_mut() {
let excitation = glottal.next_sample();
*sample = self.process_sample(excitation);
}
}
pub fn reset(&mut self) {
self.filter.reset();
self.nasal_antiformant.reset();
self.lip_prev = 0.0;
self.interaction_feedback = 0.0;
#[cfg(not(feature = "naad-backend"))]
{
self.sg_state = [0.0; 4];
}
#[cfg(feature = "naad-backend")]
self.subglottal_filter.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vocal_tract_creation() {
let vt = VocalTract::new(44100.0);
assert!((vt.sample_rate() - 44100.0).abs() < f32::EPSILON);
}
#[test]
fn test_synthesize() {
let mut vt = VocalTract::new(44100.0);
vt.set_vowel(Vowel::A).unwrap();
let mut glottal = GlottalSource::new(120.0, 44100.0).unwrap();
let samples = vt.synthesize(&mut glottal, 1024);
assert_eq!(samples.len(), 1024);
assert!(samples.iter().all(|s| s.is_finite()));
assert!(samples.iter().any(|&s| s.abs() > 1e-6));
}
#[test]
fn test_nasal_coupling() {
let mut vt = VocalTract::new(44100.0);
vt.set_vowel(Vowel::A).unwrap();
vt.set_nasal_coupling(0.5);
let mut glottal = GlottalSource::new(120.0, 44100.0).unwrap();
let samples = vt.synthesize(&mut glottal, 512);
assert!(samples.iter().all(|s| s.is_finite()));
}
#[test]
fn test_reset() {
let mut vt = VocalTract::new(44100.0);
let mut glottal = GlottalSource::new(120.0, 44100.0).unwrap();
let _ = vt.synthesize(&mut glottal, 100);
vt.reset();
let out = vt.process_sample(0.0);
assert!(out.abs() < 1e-6);
}
}