use alloc::vec::Vec;
use crate::error::Result;
use crate::species::Species;
use crate::tract::{CreatureTract, SynthesisOptions};
use crate::vocalization::{CallIntent, Vocalization};
use crate::voice::CreatureVoice;
pub struct SynthStream {
voice: CreatureVoice,
vocalization: Vocalization,
intent: CallIntent,
sample_rate: f32,
total_samples: usize,
samples_rendered: usize,
tract: Option<CreatureTract>,
f0: f32,
amplitude_scale: f32,
effort_amp: f32,
spectral_tilt: f32,
is_cat_purr: bool,
}
impl SynthStream {
pub fn new(
voice: CreatureVoice,
vocalization: Vocalization,
intent: CallIntent,
sample_rate: f32,
duration: f32,
) -> Result<Self> {
if !voice.species().supports_vocalization(&vocalization) {
return Err(crate::error::PraniError::InvalidVocalization(
alloc::format!(
"{:?} cannot produce {:?} — incompatible vocal apparatus",
voice.species(),
vocalization
),
));
}
let modifiers = intent.modifiers();
let f0 = voice.effective_f0() * modifiers.pitch_scale;
let effective_duration = duration * modifiers.duration_scale;
let total_samples = (effective_duration * sample_rate) as usize;
let effort = voice.vocal_effort();
let effort_amp = 0.3 + effort * 1.2;
let effort_tilt_offset = (effort - 0.5) * 6.0;
let vocalization_tilt = vocalization_spectral_offset(&vocalization);
let species_params = voice.species().params();
let spectral_tilt = species_params.spectral_tilt + vocalization_tilt + effort_tilt_offset;
let is_cat_purr = vocalization == Vocalization::Purr && voice.species() == Species::Cat;
Ok(Self {
voice,
vocalization,
intent,
sample_rate,
total_samples,
samples_rendered: 0,
tract: None,
f0,
amplitude_scale: modifiers.amplitude_scale,
effort_amp,
spectral_tilt,
is_cat_purr,
})
}
#[must_use]
pub fn is_finished(&self) -> bool {
self.samples_rendered >= self.total_samples
}
#[must_use]
pub fn total_samples(&self) -> usize {
self.total_samples
}
#[must_use]
pub fn samples_rendered(&self) -> usize {
self.samples_rendered
}
pub fn fill_buffer(&mut self, buffer: &mut [f32]) -> usize {
if self.is_finished() {
return 0;
}
let remaining = self.total_samples - self.samples_rendered;
let to_render = buffer.len().min(remaining);
let tract = self.tract.get_or_insert_with(|| {
let params = self.voice.species().params();
CreatureTract::new(¶ms, self.sample_rate)
});
if self.is_cat_purr {
let purr_f0 = (27.0 / 1.0_f32).clamp(20.0, 35.0); if let Ok(block) = tract.synthesize_purr(to_render, purr_f0) {
buffer[..to_render].copy_from_slice(&block[..to_render]);
}
} else {
let t = self.samples_rendered as f32 / self.total_samples.max(1) as f32;
let contour = pitch_contour_at(&self.vocalization, self.f0, t);
let modifiers = self.intent.modifiers();
let boundary_boost = if t < 0.1 {
1.0 + (1.0 - t / 0.1) * 0.5
} else if t > 0.85 {
1.0 + (t - 0.85) / 0.15 * 0.5
} else {
1.0
};
let perturbation_scale = boundary_boost + modifiers.urgency;
let options = SynthesisOptions {
perturbation_scale,
..SynthesisOptions::default()
};
if let Ok(block) = tract.synthesize(contour, to_render, &options) {
buffer[..to_render].copy_from_slice(&block[..to_render]);
}
}
CreatureTract::apply_spectral_tilt(
&mut buffer[..to_render],
self.spectral_tilt,
self.sample_rate,
);
let amp = self.amplitude_scale * self.effort_amp;
for s in buffer[..to_render].iter_mut() {
*s *= amp;
}
self.samples_rendered += to_render;
to_render
}
pub fn next_block(&mut self, block_size: usize) -> Vec<f32> {
let remaining = self.total_samples.saturating_sub(self.samples_rendered);
let actual = block_size.min(remaining);
if actual == 0 {
return Vec::new();
}
let mut buf = alloc::vec![0.0f32; actual];
self.fill_buffer(&mut buf);
buf
}
}
fn pitch_contour_at(v: &Vocalization, base_f0: f32, t: f32) -> f32 {
static FLAT: &[(f32, f32)] = &[(0.0, 1.0), (1.0, 1.0)];
static HOWL: &[(f32, f32)] = &[(0.0, 0.8), (0.3, 1.3), (0.7, 1.2), (1.0, 0.6)];
static BARK: &[(f32, f32)] = &[(0.0, 1.2), (0.1, 1.0), (1.0, 0.8)];
static ROAR: &[(f32, f32)] = &[(0.0, 0.9), (0.15, 1.2), (0.5, 1.0), (1.0, 0.7)];
static WHINE: &[(f32, f32)] = &[(0.0, 1.0), (0.5, 1.4), (1.0, 1.6)];
static SCREECH: &[(f32, f32)] = &[(0.0, 1.5), (0.3, 1.2), (1.0, 0.7)];
static TRILL: &[(f32, f32)] = &[(0.0, 1.0), (0.25, 1.1), (0.5, 0.9), (0.75, 1.1), (1.0, 1.0)];
let points = match v {
Vocalization::Howl => HOWL,
Vocalization::Bark | Vocalization::Yelp => BARK,
Vocalization::Roar | Vocalization::Rumble => ROAR,
Vocalization::Whine => WHINE,
Vocalization::Screech => SCREECH,
Vocalization::Trill => TRILL,
_ => FLAT,
};
let t = t.clamp(0.0, 1.0);
if points.is_empty() {
return base_f0;
}
for i in 0..points.len() - 1 {
let (t0, v0) = points[i];
let (t1, v1) = points[i + 1];
if t >= t0 && t <= t1 {
let frac = if (t1 - t0).abs() < f32::EPSILON {
0.0
} else {
(t - t0) / (t1 - t0)
};
return base_f0 * (v0 + (v1 - v0) * frac);
}
}
base_f0 * points.last().map_or(1.0, |p| p.1)
}
fn vocalization_spectral_offset(v: &Vocalization) -> f32 {
match v {
Vocalization::Growl | Vocalization::Rumble => -2.0,
Vocalization::Roar => -1.0,
Vocalization::Screech | Vocalization::Chirp => 1.5,
Vocalization::Hiss => 2.0,
Vocalization::Trill => 0.5,
_ => 0.0,
}
}