use crate::{Formant, VoiceActivity, VoiceFrame, VoiceParams};
#[derive(Debug, Clone)]
pub struct SynthesisConfig {
pub sample_rate: u32,
pub frame_size: usize,
pub use_formants: bool,
pub use_noise: bool,
}
impl Default for SynthesisConfig {
fn default() -> Self {
Self {
sample_rate: 16000,
frame_size: 320, use_formants: true,
use_noise: true,
}
}
}
#[derive(Debug)]
pub struct VoiceSynthesizer {
config: SynthesisConfig,
phase: f32,
noise_state: u32,
last_output: f32,
}
impl VoiceSynthesizer {
pub fn new(config: SynthesisConfig) -> Self {
Self {
config,
phase: 0.0,
noise_state: 12345,
last_output: 0.0,
}
}
pub fn synthesize_params(&mut self, params: &VoiceParams) -> Vec<f32> {
let mut samples = Vec::with_capacity(self.config.frame_size);
let pitch_inc = params.pitch / self.config.sample_rate as f32;
for _ in 0..self.config.frame_size {
let mut sample = 0.0;
if params.voicing > 0.5 {
sample = self.generate_pulse(pitch_inc);
}
if self.config.use_noise {
let noise = self.generate_noise() * params.breathiness;
sample = sample * (1.0 - params.breathiness) + noise;
}
if self.config.use_formants {
sample = self.apply_formants(sample, ¶ms.formants);
}
sample *= params.energy;
sample = self.last_output * 0.1 + sample * 0.9;
self.last_output = sample;
samples.push(sample);
}
samples
}
pub fn synthesize_frame(&mut self, frame: &VoiceFrame) -> Vec<f32> {
if !frame.voiced || frame.energy < 0.01 {
return vec![0.0; self.config.frame_size];
}
let params = VoiceParams {
pitch: frame.pitch,
pitch_variation: 0.0,
energy: frame.energy,
formants: self.spectral_index_to_formants(frame.spectral_index),
voicing: if frame.voiced { 1.0 } else { 0.0 },
rate: 1.0,
contour: crate::PitchContour::Flat,
emotion: crate::SpeechEmotion::Neutral,
breathiness: 0.1,
nasality: 0.1,
};
self.synthesize_params(¶ms)
}
fn generate_pulse(&mut self, pitch_inc: f32) -> f32 {
self.phase += pitch_inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
let saw = 2.0 * self.phase - 1.0;
saw - saw.powi(3) / 3.0
}
fn generate_noise(&mut self) -> f32 {
self.noise_state = self
.noise_state
.wrapping_mul(1103515245)
.wrapping_add(12345);
(self.noise_state as f32 / u32::MAX as f32) * 2.0 - 1.0
}
fn apply_formants(&self, input: f32, formants: &[Formant; 4]) -> f32 {
let mut output = input;
for formant in formants {
let boost = formant.amplitude * 0.5;
output *= 1.0 + boost;
}
output.clamp(-1.0, 1.0)
}
fn spectral_index_to_formants(&self, index: u16) -> [Formant; 4] {
let f1_ratio = (index & 0xFF) as f32 / 255.0;
let f2_ratio = ((index >> 8) & 0xFF) as f32 / 255.0;
[
Formant::new(f1_ratio * 1000.0, 100.0, 1.0),
Formant::new(f2_ratio * 3000.0, 150.0, 0.8),
Formant::new(2500.0, 200.0, 0.6),
Formant::new(3500.0, 250.0, 0.4),
]
}
pub fn reset(&mut self) {
self.phase = 0.0;
self.noise_state = 12345;
self.last_output = 0.0;
}
}
#[derive(Debug)]
pub struct VoiceActivityDetector {
threshold: f32,
smoothed_energy: f32,
hangover: u32,
hangover_count: u32,
}
impl VoiceActivityDetector {
pub fn new(threshold: f32, hangover: u32) -> Self {
Self {
threshold,
smoothed_energy: 0.0,
hangover,
hangover_count: 0,
}
}
pub fn process(&mut self, samples: &[f32]) -> VoiceActivity {
let energy: f32 = samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32;
self.smoothed_energy = self.smoothed_energy * 0.9 + energy * 0.1;
if self.smoothed_energy > self.threshold {
self.hangover_count = self.hangover;
VoiceActivity::Speaking
} else if self.hangover_count > 0 {
self.hangover_count -= 1;
VoiceActivity::Speaking
} else {
VoiceActivity::Silent
}
}
pub fn energy(&self) -> f32 {
self.smoothed_energy
}
pub fn reset(&mut self) {
self.smoothed_energy = 0.0;
self.hangover_count = 0;
}
}
#[derive(Debug, Clone)]
pub struct VoicePipelineEvaluation {
pub params_samples: usize,
pub frame_samples: usize,
pub params_peak: f32,
pub frame_peak: f32,
pub params_rms: f32,
pub frame_rms: f32,
}
impl VoicePipelineEvaluation {
pub fn evaluate(params: &VoiceParams, frame: &VoiceFrame, config: SynthesisConfig) -> Self {
let mut synth = VoiceSynthesizer::new(config);
let params_samples = synth.synthesize_params(params);
let frame_samples = synth.synthesize_frame(frame);
let (params_peak, params_rms) = sample_stats(¶ms_samples);
let (frame_peak, frame_rms) = sample_stats(&frame_samples);
Self {
params_samples: params_samples.len(),
frame_samples: frame_samples.len(),
params_peak,
frame_peak,
params_rms,
frame_rms,
}
}
}
fn sample_stats(samples: &[f32]) -> (f32, f32) {
if samples.is_empty() {
return (0.0, 0.0);
}
let mut peak = 0.0;
let mut sum_sq = 0.0;
for &sample in samples {
let abs = sample.abs();
if abs > peak {
peak = abs;
}
sum_sq += sample * sample;
}
let rms = (sum_sq / samples.len() as f32).sqrt();
(peak, rms)
}
#[cfg(test)]
mod tests {
use super::*;
use elara_core::{NodeId, StateTime};
#[test]
fn test_synthesizer() {
let config = SynthesisConfig::default();
let mut synth = VoiceSynthesizer::new(config);
let params = VoiceParams::new();
let samples = synth.synthesize_params(¶ms);
assert_eq!(samples.len(), 320);
for s in &samples {
assert!(*s >= -1.0 && *s <= 1.0);
}
}
#[test]
fn test_voice_pipeline_evaluation() {
let config = SynthesisConfig::default();
let params = VoiceParams::new();
let frame = VoiceFrame::voiced(NodeId::new(1), StateTime::from_millis(0), 1, 120.0, 0.5);
let eval = VoicePipelineEvaluation::evaluate(¶ms, &frame, config);
assert_eq!(eval.params_samples, 320);
assert_eq!(eval.frame_samples, 320);
assert!(eval.params_peak >= 0.0);
assert!(eval.frame_peak >= 0.0);
}
#[test]
fn test_vad() {
let mut vad = VoiceActivityDetector::new(0.01, 5);
let silent = vec![0.0; 320];
assert_eq!(vad.process(&silent), VoiceActivity::Silent);
let active: Vec<f32> = (0..320).map(|i| (i as f32 * 0.1).sin() * 0.5).collect();
assert_eq!(vad.process(&active), VoiceActivity::Speaking);
}
}