use alloc::format;
use alloc::string::ToString;
use serde::{Deserialize, Serialize};
use tracing::trace;
use crate::error::{Result, SvaraError};
use crate::rng::Rng;
const DEFAULT_OPEN_QUOTIENT: f32 = 0.6;
const DEFAULT_JITTER: f32 = 0.01;
const DEFAULT_SHIMMER: f32 = 0.02;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum GlottalModel {
Rosenberg,
LF,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
struct LfParams {
ta: f32,
te: f32,
ee: f32,
}
impl LfParams {
fn from_rd(rd: f32) -> Self {
let rd = rd.clamp(0.3, 2.7);
let te = 0.4 + 0.2 * (rd - 0.3) / 2.4;
let ta = 0.003 + 0.012 * (rd - 0.3) / 2.4;
let ee = 1.0 - 0.3 * (rd - 0.3) / 2.4;
Self { ta, te, ee }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlottalSource {
model: GlottalModel,
rd: f32,
lf_params: LfParams,
f0: f32,
sample_rate: f32,
open_quotient: f32,
spectral_tilt: f32,
jitter: f32,
shimmer: f32,
breathiness: f32,
vibrato_rate: f32,
vibrato_depth: f32,
vibrato_phase: f32,
tilt_state: f32,
phase: f32,
current_period: f32,
current_amplitude: f32,
rng: Rng,
#[cfg(feature = "naad-backend")]
aspiration_noise: naad::noise::NoiseGenerator,
#[cfg(feature = "naad-backend")]
vibrato_lfo: naad::modulation::Lfo,
}
impl GlottalSource {
pub fn new(f0: f32, sample_rate: f32) -> Result<Self> {
if !(20.0..=2000.0).contains(&f0) {
return Err(SvaraError::InvalidPitch(format!(
"f0 must be in [20, 2000] Hz, got {f0}"
)));
}
if sample_rate <= 0.0 || !sample_rate.is_finite() {
return Err(SvaraError::InvalidFormant(
"sample_rate must be positive and finite".to_string(),
));
}
let period = sample_rate / f0;
trace!(f0, sample_rate, period, "created glottal source");
Ok(Self {
model: GlottalModel::Rosenberg,
rd: 1.0,
lf_params: LfParams::from_rd(1.0),
f0,
sample_rate,
open_quotient: DEFAULT_OPEN_QUOTIENT,
spectral_tilt: 0.0,
jitter: DEFAULT_JITTER,
shimmer: DEFAULT_SHIMMER,
breathiness: 0.0,
vibrato_rate: 0.0,
vibrato_depth: 0.0,
vibrato_phase: 0.0,
tilt_state: 0.0,
phase: 0.0,
current_period: period,
current_amplitude: 1.0,
rng: Rng::new(crate::rng::DEFAULT_SEED),
#[cfg(feature = "naad-backend")]
aspiration_noise: naad::noise::NoiseGenerator::new(naad::noise::NoiseType::White, 42),
#[cfg(feature = "naad-backend")]
vibrato_lfo: naad::modulation::Lfo::new(
naad::modulation::LfoShape::Sine,
0.001, sample_rate,
)
.unwrap_or_else(|_| {
naad::modulation::Lfo::new(naad::modulation::LfoShape::Sine, 0.001, sample_rate)
.expect("fallback LFO must succeed")
}),
})
}
pub fn set_f0(&mut self, f0: f32) -> Result<()> {
if !(20.0..=2000.0).contains(&f0) {
return Err(SvaraError::InvalidPitch(format!(
"f0 must be in [20, 2000] Hz, got {f0}"
)));
}
self.f0 = f0;
Ok(())
}
pub fn set_breathiness(&mut self, amount: f32) {
self.breathiness = amount.clamp(0.0, 1.0);
}
pub fn set_open_quotient(&mut self, oq: f32) {
self.open_quotient = oq.clamp(0.4, 0.7);
}
pub fn set_jitter(&mut self, j: f32) {
self.jitter = j.clamp(0.0, 0.05);
}
pub fn set_shimmer(&mut self, s: f32) {
self.shimmer = s.clamp(0.0, 0.1);
}
pub fn set_spectral_tilt(&mut self, tilt: f32) {
self.spectral_tilt = tilt;
}
pub fn set_model(&mut self, model: GlottalModel) {
self.model = model;
}
#[must_use]
pub fn model(&self) -> GlottalModel {
self.model
}
pub fn set_rd(&mut self, rd: f32) {
self.rd = rd.clamp(0.3, 2.7);
self.lf_params = LfParams::from_rd(self.rd);
self.model = GlottalModel::LF;
}
#[must_use]
pub fn rd(&self) -> f32 {
self.rd
}
pub fn set_vibrato(&mut self, rate: f32, depth: f32) {
self.vibrato_rate = rate.max(0.0);
self.vibrato_depth = depth.clamp(0.0, 0.5);
#[cfg(feature = "naad-backend")]
if self.vibrato_rate > 0.0 {
let _ = self.vibrato_lfo.set_frequency(self.vibrato_rate);
self.vibrato_lfo.depth = self.vibrato_depth;
}
}
#[must_use]
#[inline]
pub fn f0(&self) -> f32 {
self.f0
}
#[must_use]
#[inline]
pub fn sample_rate(&self) -> f32 {
self.sample_rate
}
#[must_use]
#[inline]
pub fn period_samples(&self) -> f32 {
self.current_period
}
#[inline]
pub fn next_sample(&mut self) -> f32 {
let t = self.phase / self.current_period;
let pulse = match self.model {
GlottalModel::Rosenberg => self.rosenberg_pulse(t),
GlottalModel::LF => self.lf_pulse(t),
};
let pulse = if self.spectral_tilt > 0.0 {
let alpha = crate::math::f32::exp(
-core::f32::consts::TAU * self.spectral_tilt / self.sample_rate,
);
let filtered = (1.0 - alpha) * pulse + alpha * self.tilt_state;
self.tilt_state = filtered;
filtered
} else {
self.tilt_state = pulse;
pulse
};
let pulse = pulse * self.current_amplitude;
#[cfg(feature = "naad-backend")]
let noise = self.aspiration_noise.next_sample();
#[cfg(not(feature = "naad-backend"))]
let noise = self.rng.next_f32();
let noise_gate = if t < self.open_quotient { 1.0 } else { 0.1 };
let sample = pulse * (1.0 - self.breathiness) + noise * self.breathiness * 0.3 * noise_gate;
self.phase += 1.0;
if self.phase >= self.current_period {
self.phase -= self.current_period;
self.new_period();
}
#[cfg(not(feature = "naad-backend"))]
if self.vibrato_rate > 0.0 {
self.vibrato_phase += core::f32::consts::TAU * self.vibrato_rate / self.sample_rate;
if self.vibrato_phase >= core::f32::consts::TAU {
self.vibrato_phase -= core::f32::consts::TAU;
}
}
sample
}
#[inline]
fn rosenberg_pulse(&self, t: f32) -> f32 {
let oq = self.open_quotient;
if t < oq {
let t_norm = t / oq;
3.0 * t_norm * t_norm - 2.0 * t_norm * t_norm * t_norm
} else {
0.0
}
}
#[inline]
fn lf_pulse(&self, t: f32) -> f32 {
let te = self.lf_params.te;
let ta = self.lf_params.ta;
let ee = self.lf_params.ee;
if t < te {
let phase = core::f32::consts::PI * t / te;
-ee * crate::math::f32::sin(phase)
} else if t < te + ta {
let dt = t - te;
let epsilon = ta.max(0.001);
-ee * crate::math::f32::exp(-dt / epsilon)
} else {
0.0
}
}
fn new_period(&mut self) {
let vibrato_mod = if self.vibrato_rate > 0.0 {
#[cfg(feature = "naad-backend")]
{
let lfo_val = self.vibrato_lfo.next_value();
1.0 + self.vibrato_depth * (lfo_val * 2.0 - 1.0)
}
#[cfg(not(feature = "naad-backend"))]
{
1.0 + self.vibrato_depth * crate::math::f32::sin(self.vibrato_phase)
}
} else {
1.0
};
let effective_f0 = self.f0 * vibrato_mod;
let base_period = self.sample_rate / effective_f0;
let jitter_offset = self.rng.next_f32() * self.jitter * base_period;
self.current_period = (base_period + jitter_offset).max(1.0);
let shimmer_offset = self.rng.next_f32() * self.shimmer;
self.current_amplitude = (1.0 + shimmer_offset).max(0.01);
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
#[test]
fn test_glottal_source_creation() {
let gs = GlottalSource::new(120.0, 44100.0);
assert!(gs.is_ok());
let gs = gs.unwrap();
assert!((gs.f0() - 120.0).abs() < f32::EPSILON);
}
#[test]
fn test_invalid_f0() {
assert!(GlottalSource::new(5.0, 44100.0).is_err());
assert!(GlottalSource::new(3000.0, 44100.0).is_err());
}
#[test]
fn test_generates_samples() {
let mut gs = GlottalSource::new(120.0, 44100.0).unwrap();
let samples: Vec<f32> = (0..1024).map(|_| gs.next_sample()).collect();
assert!(samples.iter().any(|&s| s.abs() > 0.001));
assert!(samples.iter().all(|s| s.is_finite()));
}
#[test]
fn test_period_approximately_correct() {
let mut gs = GlottalSource::new(120.0, 44100.0).unwrap();
gs.set_jitter(0.0);
let expected_period = 44100.0 / 120.0;
assert!((gs.period_samples() - expected_period).abs() < 1.0);
}
#[test]
fn test_breathiness() {
let mut gs = GlottalSource::new(120.0, 44100.0).unwrap();
gs.set_breathiness(1.0);
let samples: Vec<f32> = (0..1024).map(|_| gs.next_sample()).collect();
assert!(samples.iter().all(|s| s.is_finite()));
}
#[test]
fn test_lf_model_produces_output() {
let mut gs = GlottalSource::new(120.0, 44100.0).unwrap();
gs.set_model(GlottalModel::LF);
let samples: Vec<f32> = (0..1024).map(|_| gs.next_sample()).collect();
assert!(samples.iter().any(|&s| s.abs() > 0.001));
assert!(samples.iter().all(|s| s.is_finite()));
}
#[test]
fn test_rd_parameterization() {
let mut pressed = GlottalSource::new(120.0, 44100.0).unwrap();
pressed.set_rd(0.3);
pressed.set_jitter(0.0);
pressed.set_shimmer(0.0);
let pressed_samples: Vec<f32> = (0..4410).map(|_| pressed.next_sample()).collect();
let pressed_energy: f32 = pressed_samples.iter().map(|s| s * s).sum();
let mut breathy = GlottalSource::new(120.0, 44100.0).unwrap();
breathy.set_rd(2.7);
breathy.set_jitter(0.0);
breathy.set_shimmer(0.0);
let breathy_samples: Vec<f32> = (0..4410).map(|_| breathy.next_sample()).collect();
let breathy_energy: f32 = breathy_samples.iter().map(|s| s * s).sum();
assert!(
pressed_energy > breathy_energy,
"pressed voice should have more energy: pressed={pressed_energy}, breathy={breathy_energy}"
);
}
#[test]
fn test_rd_auto_switches_to_lf() {
let mut gs = GlottalSource::new(120.0, 44100.0).unwrap();
assert_eq!(gs.model(), GlottalModel::Rosenberg);
gs.set_rd(1.0);
assert_eq!(gs.model(), GlottalModel::LF);
}
#[test]
fn test_serde_roundtrip() {
let gs = GlottalSource::new(150.0, 44100.0).unwrap();
let json = serde_json::to_string(&gs).unwrap();
let gs2: GlottalSource = serde_json::from_str(&json).unwrap();
assert!((gs2.f0() - 150.0).abs() < f32::EPSILON);
}
}