pub mod envelopes;
pub mod intonation;
pub mod targets;
pub mod engine;
pub mod phondata;
pub mod bytecode;
pub mod wavegen;
pub mod setlengths;
pub mod sample;
pub mod klatt;
pub mod tempo;
use crate::error::{Error, Result};
use crate::phoneme::{PH_PAUSE, PH_VOWEL};
const LENGTHEN_MS: u16 = 50;
const END_SILENCE_MS: f64 = 300.0;
#[derive(Debug, Clone, Default)]
pub struct Frame {
pub flags: u16,
pub ffreq: [i16; 7],
pub length: u8,
pub rms: u8,
pub fheight: [u8; 8],
pub fwidth: [u8; 6],
pub fright: [u8; 3],
pub bw: [u8; 4],
pub klattp: [u8; 5],
pub klattp2: [u8; 5],
pub klatt_ap: [u8; 7],
pub klatt_bp: [u8; 7],
pub spare: u8,
}
impl Frame {
pub const C_SIZE: usize = 64;
}
#[derive(Debug, Clone, Default)]
pub struct Resonator {
pub a: f64,
pub b: f64,
pub c: f64,
pub x1: f64,
pub x2: f64,
}
impl Resonator {
#[inline]
pub fn tick(&mut self, input: f64) -> f64 {
let y = self.a * input + self.b * self.x1 + self.c * self.x2;
self.x2 = self.x1;
self.x1 = y;
y
}
pub fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
}
}
#[derive(Debug, Clone)]
pub struct VoiceParams {
pub speed_percent: u32,
pub pitch_hz: u32,
pub pitch_range: u32,
pub pitch_range_units: f64,
pub stress_lengths: Option<[u32; 8]>,
pub intonation_group: usize,
pub formant_scale: u32,
pub sample_rate: u32,
pub amplitude: u32,
pub no_final_pause: bool,
pub rand_seed: u32,
pub user_rate_wpm: u32,
pub user_pitch: u32,
pub user_volume: u32,
pub formant_factor: i32,
pub tone_adjust: [u8; wavegen::N_TONE_ADJUST],
pub klatt: bool,
pub formant_freq_pct: [i32; 7],
pub formant_height_pct: [i32; 7],
pub formant_width_pct: [i32; 7],
pub echo_delay_samples: usize,
pub echo_amp: i32,
pub flutter: i32,
pub stress_amps: [i32; 8],
}
impl VoiceParams {
pub fn formant_scaling_active(&self) -> bool {
self.formant_freq_pct.iter()
.chain(self.formant_height_pct.iter())
.chain(self.formant_width_pct.iter())
.any(|&p| p != 100)
}
pub fn stress_amp_ratio(&self, level: u8) -> f64 {
let i = (level as usize).min(7);
let base = setlengths::STRESS_AMPS_EN[i] as f64;
if self.stress_amps[i] <= 0 || base <= 0.0 {
return 1.0;
}
self.stress_amps[i] as f64 / base
}
}
impl Default for VoiceParams {
fn default() -> Self {
VoiceParams {
speed_percent: 100,
pitch_hz: 118, pitch_range_units: 3888.0, stress_lengths: None,
intonation_group: 1,
pitch_range: 100,
formant_scale: 100,
sample_rate: 22050,
amplitude: 80,
no_final_pause: false,
rand_seed: 0,
user_rate_wpm: 175,
user_pitch: 50,
user_volume: 100,
formant_factor: 256,
tone_adjust: wavegen::set_tone_adjust(&wavegen::DEFAULT_TONE_POINTS),
klatt: false,
formant_freq_pct: [100; 7],
formant_height_pct: [100; 7],
formant_width_pct: [100; 7],
echo_delay_samples: 0,
echo_amp: 0,
flutter: 0,
stress_amps: [0; 8],
}
}
}
pub fn flutter_factor(idx: usize, flutter: i32) -> f64 {
if flutter <= 0 {
return 1.0;
}
let i = idx as f64;
let lfo = (0.9 * i).sin().mul_add(0.5, 0.5 * (2.3 * i).sin()); 1.0 + (flutter as f64 / 1000.0) * lfo
}
pub fn apply_echo(pcm: &mut [i16], delay: usize, amp: i32) {
if delay == 0 || amp == 0 || delay >= pcm.len() {
return;
}
for i in delay..pcm.len() {
let echo = (pcm[i - delay] as i32 * amp) >> 8;
pcm[i] = (pcm[i] as i32 + echo).clamp(i16::MIN as i32, i16::MAX as i32) as i16;
}
}
pub type PcmBuffer = Vec<i16>;
pub struct Synthesizer {
pub voice: VoiceParams,
}
impl Synthesizer {
pub fn new(voice: VoiceParams) -> Self {
Synthesizer { voice }
}
pub fn synthesize(&self, phonemes: &str) -> Result<PcmBuffer> {
if phonemes.is_empty() {
return Ok(Vec::new());
}
let segments = engine::parse_ipa(phonemes, &self.voice);
if segments.is_empty() {
return Err(Error::InvalidData(
format!("no recognisable phonemes in {:?}", phonemes)
));
}
let pcm = engine::synthesize_segments(&segments, &self.voice);
Ok(pcm)
}
pub fn synthesize_codes(
&self,
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
) -> Result<PcmBuffer> {
self.synthesize_codes_marked(codes, phdata, &mut Vec::new(), &mut Vec::new())
}
pub fn mbrola_phonemes(
&self,
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
) -> Vec<crate::mbrola::MbrPhoneme> {
use crate::mbrola::MbrPhoneme;
let speed_factor = 100.0 / self.voice.speed_percent.max(1) as f64;
let sr = self.voice.sample_rate as f64;
let ms = |samples: usize| -> u32 { (samples as f64 * 1000.0 / sr).round() as u32 };
let annotated = annotate_codes(codes, phdata);
let clause_type = match codes.iter().rev().find_map(|c| c.clause_char) {
Some('.') => 0usize,
Some(',') | Some(';') | Some(':') => 1,
Some('?') => 2,
Some('!') => 3,
_ => 0,
};
let mut syllables: Vec<intonation::Syllable> = annotated
.iter()
.filter_map(|a| match a {
AnnCode::Phoneme(info) if info.ph_type == PH_VOWEL => {
Some(intonation::Syllable::new(info.stress_level))
}
_ => None,
})
.collect();
intonation::calc_pitches(
&mut syllables,
intonation::tone_for_clause(self.voice.intonation_group, clause_type),
None,
);
let voice_pitch_base = (self.voice.pitch_hz as i32) << 12;
let voice_pitch_range = self.voice.pitch_range_units as i32;
let mut out: Vec<MbrPhoneme> = Vec::new();
let mut syllable_idx = 0usize;
let mut newword = true;
let mut word_start = 0usize;
let push_pause = |out: &mut Vec<MbrPhoneme>, ms: u32| {
out.push(MbrPhoneme {
mnemonic: "_".into(),
ph_type: PH_PAUSE,
duration_ms: ms,
newword: true,
..Default::default()
});
};
for ann in &annotated {
match ann {
AnnCode::Pause(d) | AnnCode::ClauseBoundary(d) => {
push_pause(&mut out, (d * speed_factor).round() as u32);
newword = true;
}
AnnCode::PrepauseSamples(n) => {
push_pause(&mut out, ms(*n));
}
AnnCode::Embedded(_) => {}
AnnCode::WordBoundary => {
let top = out[word_start..].iter().map(|p| p.stress_level).max().unwrap_or(0);
for p in &mut out[word_start..] {
p.word_stress = top;
}
word_start = out.len();
newword = true;
}
AnnCode::Phoneme(info) => {
let syllable = if info.ph_type == PH_VOWEL {
let syl = syllables.get(syllable_idx).copied();
syllable_idx += 1;
syl
} else {
None
};
let syl = syllable.unwrap_or_default();
let ph = phdata.get(info.code);
let (samples, prepause) = match synthesize_phoneme_info(
info, phdata, speed_factor, self.voice.formant_factor,
self.voice.stress_lengths.as_ref(),
)
{
PhonemeRender::Frames { frames, lead_silence, trail_silence, .. } => (
frames.iter().map(|f| f.length as usize * 64).sum::<usize>()
+ trail_silence,
lead_silence,
),
PhonemeRender::Pcm(pcm) => (pcm.len(), 0),
};
out.push(MbrPhoneme {
mnemonic: ph.map(|p| p.mnemonic_display()).unwrap_or_default(),
ph_type: info.ph_type,
stress_level: info.stress_level,
word_stress: info.stress_level,
newword,
lengthen: info.lengthen,
std_length: u16::from(info.std_length) * 2,
lengthen_ms: LENGTHEN_MS,
length: 256,
duration_ms: match ms(samples) {
0 => (80.0 * speed_factor).round() as u32,
d => d,
},
prepause_ms: ms(prepause) as u16,
env: syl.env,
pitch1: syl.pitch1,
pitch2: syl.pitch2,
voice_pitch_base,
voice_pitch_range,
});
newword = false;
}
}
}
let top = out[word_start..].iter().map(|p| p.stress_level).max().unwrap_or(0);
for p in &mut out[word_start..] {
p.word_stress = top;
}
if !out.is_empty() && !self.voice.no_final_pause {
push_pause(&mut out, (END_SILENCE_MS * speed_factor).round() as u32);
}
out
}
pub fn synthesize_codes_with_marks(
&self,
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
) -> Result<(PcmBuffer, Vec<usize>, Vec<usize>)> {
let mut word_marks = Vec::new();
let mut sentence_marks = Vec::new();
let pcm = self.synthesize_codes_marked(codes, phdata, &mut word_marks, &mut sentence_marks)?;
Ok((pcm, word_marks, sentence_marks))
}
fn synthesize_codes_marked(
&self,
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
word_marks: &mut Vec<usize>,
sentence_marks: &mut Vec<usize>,
) -> Result<PcmBuffer> {
self.render(codes, phdata, word_marks, sentence_marks, &mut None)
}
pub fn synthesize_codes_streaming(
&self,
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
sink: &mut dyn FnMut(&[i16], bool) -> bool,
) -> Result<()> {
let mut stopped = false;
let (tail, _, _) = {
let mut watch = |chunk: &[i16], is_final: bool| -> bool {
let stop = sink(chunk, is_final);
stopped |= stop;
stop
};
self.synthesize_codes_streaming_with_marks(codes, phdata, &mut watch)?
};
if !stopped {
sink(&tail, true);
}
Ok(())
}
pub fn synthesize_codes_streaming_with_marks(
&self,
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
sink: &mut dyn FnMut(&[i16], bool) -> bool,
) -> Result<(PcmBuffer, Vec<usize>, Vec<usize>)> {
let mut word_marks = Vec::new();
let mut sentence_marks = Vec::new();
let mut sink = Some(sink);
let tail = self.render(codes, phdata, &mut word_marks, &mut sentence_marks, &mut sink)?;
Ok((tail, word_marks, sentence_marks))
}
fn render(
&self,
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
word_marks: &mut Vec<usize>,
sentence_marks: &mut Vec<usize>,
sink: &mut Option<&mut dyn FnMut(&[i16], bool) -> bool>,
) -> Result<PcmBuffer> {
if codes.is_empty() {
return Ok(Vec::new());
}
let speed_factor = 100.0 / self.voice.speed_percent.max(1) as f64;
let annotated = annotate_codes(codes, phdata);
let mut output_i16: Vec<i16> = Vec::new();
let mut wavephase: i32 = i32::MAX;
let mut run_frames: Vec<phondata::SpectFrame> = Vec::new();
let mut run_amps: Vec<f64> = Vec::new();
let mut run_pitch: Vec<f64> = Vec::new();
let mut run_speed: Vec<f64> = Vec::new();
let mut run_centres: Vec<usize> = Vec::new();
let total_phonemes = annotated
.iter()
.filter(|a| matches!(a, AnnCode::Phoneme(_)))
.count();
let clause_type = match codes.iter().rev().find_map(|c| c.clause_char) {
Some('.') => 0usize,
Some(',') | Some(';') | Some(':') => 1,
Some('?') => 2,
Some('!') => 3,
_ => 0,
};
let mut syllables: Vec<intonation::Syllable> = annotated
.iter()
.filter_map(|a| match a {
AnnCode::Phoneme(info) if info.ph_type == 2 => {
Some(intonation::Syllable::new(info.stress_level))
}
_ => None,
})
.collect();
intonation::calc_pitches(
&mut syllables,
intonation::tone_for_clause(self.voice.intonation_group, clause_type),
None,
);
let mut syllable_idx = 0usize;
let mut phoneme_idx = 0usize;
let base_pitch = self.voice.pitch_hz as f64;
let mut carried_pitch = base_pitch;
let sil_samples = |ms: f64, embed: f64| -> usize {
((ms / 1000.0) * 22050.0 * speed_factor * embed) as usize
};
const WORD_GAP_MS: f64 = 10.0;
const NONFINAL_LEN: f64 = 0.85;
let total_words = annotated
.iter()
.filter(|a| matches!(a, AnnCode::WordBoundary))
.count()
+ 1;
let mut word_idx = 0usize;
word_marks.push(0);
let mut flushed = 0usize;
let mut stopped = false;
let mut embed_speed = 1.0f64;
let mut embed_pitch = 1.0f64;
let mut embed_amp = 1.0f64;
let mut embed_rate = self.voice.user_rate_wpm as i32;
let mut embed_pitch_v = self.voice.user_pitch as i32;
let mut embed_volume = self.voice.user_volume as i32;
for ann in &annotated {
match ann {
AnnCode::Embedded(cmd) => {
let next = |cur: i32| -> i32 {
let v = cmd.value.max(0);
match cmd.relative {
1 => cur + v,
-1 => cur - v,
_ => v,
}
};
match cmd.letter {
'S' => {
embed_rate = next(embed_rate).clamp(80, 450);
let f = setlengths::speed_duration_factor(embed_rate as u32);
let base =
setlengths::speed_duration_factor(self.voice.user_rate_wpm);
embed_speed = if base > 0.0 { f / base } else { 1.0 };
}
'P' => {
embed_pitch_v = next(embed_pitch_v).clamp(0, 100);
let base = intonation::base_pitch_hz(82.0, self.voice.user_pitch);
let now = intonation::base_pitch_hz(82.0, embed_pitch_v as u32);
embed_pitch = if base > 0.0 { now / base } else { 1.0 };
}
'A' => {
embed_volume = next(embed_volume).clamp(0, 200);
embed_amp = if self.voice.user_volume > 0 {
embed_volume as f64 / self.voice.user_volume as f64
} else {
1.0
};
}
_ => {}
}
}
AnnCode::Pause(ms) => {
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch, &mut run_speed, &mut run_centres,
&mut output_i16, &self.voice, &mut wavephase);
let n = sil_samples(*ms, embed_speed);
output_i16.extend(std::iter::repeat(0i16).take(n));
}
AnnCode::ClauseBoundary(ms) => {
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch, &mut run_speed, &mut run_centres,
&mut output_i16, &self.voice, &mut wavephase);
let n = sil_samples(*ms, embed_speed);
output_i16.extend(std::iter::repeat(0i16).take(n));
let start = flushed + output_i16.len();
sentence_marks.push(start);
word_marks.push(start);
if let Some(sink) = sink.as_mut().filter(|_| self.voice.echo_amp == 0) {
if sink(&output_i16, false) {
stopped = true;
break;
}
flushed += output_i16.len();
output_i16.clear();
}
}
AnnCode::WordBoundary => {
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch, &mut run_speed, &mut run_centres,
&mut output_i16, &self.voice, &mut wavephase);
let n = sil_samples(WORD_GAP_MS, embed_speed);
output_i16.extend(std::iter::repeat(0i16).take(n));
word_idx += 1;
word_marks.push(flushed + output_i16.len());
}
AnnCode::PrepauseSamples(n) => {
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch, &mut run_speed, &mut run_centres,
&mut output_i16, &self.voice, &mut wavephase);
output_i16.extend(std::iter::repeat(0i16).take(*n));
}
AnnCode::Phoneme(info) => {
let _ = total_phonemes;
phoneme_idx += 1;
let syllable = if info.ph_type == 2 {
let syl = syllables.get(syllable_idx).copied();
syllable_idx += 1;
syl
} else {
None
};
if let Some(syl) = syllable {
carried_pitch = intonation::pitch_to_hz(
syl.pitch_at(0.5),
base_pitch,
self.voice.pitch_range_units,
);
}
let glide = syllable;
let ph_pitch = carried_pitch.max(25.0)
* flutter_factor(phoneme_idx, self.voice.flutter)
* embed_pitch;
let ph_speed = if word_idx + 1 < total_words {
speed_factor * NONFINAL_LEN
} else {
speed_factor
} * embed_speed;
let is_vowel = info.ph_type == 2;
match synthesize_phoneme_info(
info, phdata, ph_speed, self.voice.formant_factor,
self.voice.stress_lengths.as_ref(),
) {
PhonemeRender::Frames { frames, amp_factor, lead_silence, trail_silence } => {
if lead_silence > 0 {
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch,
&mut run_speed, &mut run_centres, &mut output_i16,
&self.voice, &mut wavephase);
output_i16.extend(std::iter::repeat(0i16).take(lead_silence));
}
let amp_factor = amp_factor
* self.voice.stress_amp_ratio(info.stress_level)
* embed_amp;
if is_vowel && !frames.is_empty() {
let (rel, _) = frames
.iter()
.enumerate()
.max_by_key(|(_, f)| f.rms)
.unwrap();
run_centres.push(run_frames.len() + rel);
}
run_amps.extend(std::iter::repeat(amp_factor).take(frames.len()));
match glide {
Some(syl) if frames.len() > 1 => {
let last = (frames.len() - 1) as f64;
let scale = flutter_factor(phoneme_idx, self.voice.flutter)
* embed_pitch;
for i in 0..frames.len() {
let hz = intonation::pitch_to_hz(
syl.pitch_at(i as f64 / last),
base_pitch,
self.voice.pitch_range_units,
);
run_pitch.push(hz.max(25.0) * scale);
}
carried_pitch = intonation::pitch_to_hz(
syl.pitch_at(1.0),
base_pitch,
self.voice.pitch_range_units,
);
}
_ => run_pitch
.extend(std::iter::repeat(ph_pitch).take(frames.len())),
}
run_speed.extend(std::iter::repeat(embed_speed).take(frames.len()));
run_frames.extend(frames);
if trail_silence > 0 {
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch,
&mut run_speed, &mut run_centres, &mut output_i16,
&self.voice, &mut wavephase);
output_i16.extend(std::iter::repeat(0i16).take(trail_silence));
}
}
PhonemeRender::Pcm(pcm) => {
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch, &mut run_speed, &mut run_centres,
&mut output_i16, &self.voice, &mut wavephase);
output_i16.extend_from_slice(&pcm);
}
}
}
}
}
flush_run(&mut run_frames, &mut run_amps, &mut run_pitch, &mut run_speed, &mut run_centres,
&mut output_i16, &self.voice, &mut wavephase);
if (!output_i16.is_empty() || flushed > 0) && !self.voice.no_final_pause && !stopped {
output_i16.extend(std::iter::repeat(0i16).take(sil_samples(END_SILENCE_MS, embed_speed)));
}
apply_echo(&mut output_i16, self.voice.echo_delay_samples, self.voice.echo_amp);
Ok(output_i16)
}
pub fn sample_rate(&self) -> u32 {
self.voice.sample_rate
}
}
struct PhonemeInfo {
code: u8,
ph_type: u8,
stress_level: u8,
lengthen: bool,
next_lm: u8,
next2_lm: u8,
more_syllables: bool,
end_of_clause: bool,
std_length: u8,
#[allow(dead_code)]
prev_type: u8,
#[allow(dead_code)]
prev_code: u8,
#[allow(dead_code)]
next_type: u8,
#[allow(dead_code)]
next_code: u8,
next2_code: u8,
this_wordstart: bool,
prev_wordstart: bool,
next_wordstart: bool,
next2_wordstart: bool,
}
enum AnnCode {
Pause(f64),
ClauseBoundary(f64),
WordBoundary,
PrepauseSamples(usize),
Phoneme(PhonemeInfo),
Embedded(crate::translate::EmbeddedCmd),
}
fn annotate_codes(
codes: &[crate::translate::PhonemeCode],
phdata: &crate::phoneme::PhonemeData,
) -> Vec<AnnCode> {
let mut result = Vec::new();
let ph_info = |c: u8| -> (u8, u8, u8) {
if let Some(ph) = phdata.get(c) {
(ph.typ, ph.length_mod, ph.std_length)
} else {
(0, 0, 0)
}
};
let mut pending_embedded: Vec<(usize, crate::translate::EmbeddedCmd)> = Vec::new();
let codes: Vec<crate::translate::PhonemeCode> = {
let mut kept = Vec::with_capacity(codes.len());
for c in codes {
match &c.marker {
Some(crate::translate::CodeMarker::Embedded(cmd)) => {
pending_embedded.push((kept.len(), *cmd));
}
Some(_) => {}
None => kept.push(c.clone()),
}
}
kept
};
let codes = &codes[..];
let flat: Vec<(u8, bool)> = codes.iter().map(|c| (c.code, c.is_boundary)).collect();
let n = flat.len();
let mut i = 0;
let mut next_embedded = 0usize;
let mut pending_stress: u8 = 0;
let mut pending_lengthen = false;
let mut prev_code: u8 = 0;
let mut prev_type: u8 = 0;
let mut word_start_pending = true; let mut prev_wordstart = false;
while i < n {
while pending_embedded.get(next_embedded).is_some_and(|(ix, _)| *ix <= i) {
result.push(AnnCode::Embedded(pending_embedded[next_embedded].1));
next_embedded += 1;
}
let (code, is_boundary) = flat[i];
i += 1;
match code {
0 if is_boundary => {
result.push(AnnCode::ClauseBoundary(200.0));
pending_stress = 0;
prev_code = 0; prev_type = 0; word_start_pending = true;
}
0 => {
}
1..=7 if !is_boundary => {
pending_stress = code;
}
9 => {
result.push(AnnCode::Pause(80.0));
prev_code = 0; prev_type = 0;
word_start_pending = true;
}
12 => {
pending_lengthen = true;
}
15 if is_boundary => {
result.push(AnnCode::WordBoundary);
pending_stress = 0;
word_start_pending = true;
}
_ => {
let (ph_type, ph_lm, std_length) = ph_info(code);
let mut next_code = 0u8;
let mut next_is_boundary = false;
let mut next_wordstart = false;
let mut j = i;
while j < n {
let (nc, nb) = flat[j];
j += 1;
if nb || nc == 9 { next_wordstart = true; continue; }
if nc == 12 { continue; }
if nc >= 1 && nc <= 7 { continue; }
next_code = nc;
next_is_boundary = nb;
break;
}
let mut next2_code = 0u8;
let mut next2_wordstart = false;
while j < n {
let (nc, nb) = flat[j];
j += 1;
if nb || nc == 9 { next2_wordstart = true; continue; }
if nc == 12 { continue; }
if nc >= 1 && nc <= 7 { continue; }
next2_code = nc;
break;
}
let this_wordstart = word_start_pending;
let (next_type, next_lm, _) = ph_info(next_code);
let (next2_type, next2_lm, _) = ph_info(next2_code);
let end_of_clause = next_code == 0 || (next_code == 15 && next_is_boundary);
let more_syllables = {
let mut has_more = false;
for jj in i..n {
let (c2, b2) = flat[jj];
if b2 { break; } if c2 == 0 || c2 == 15 { break; }
if c2 >= 1 && c2 <= 7 { continue; }
if let Some(ph) = phdata.get(c2) {
if ph.typ == 2 { has_more = true; break; }
}
}
has_more
};
let prepause_samples = compute_prepause(
ph_type, next_type, next2_type, ph_lm, code,
&mut result,
);
let stress_level = setlengths::stress_code_to_level(pending_stress);
pending_stress = 0;
if prepause_samples > 0 {
result.push(AnnCode::PrepauseSamples(prepause_samples));
}
result.push(AnnCode::Phoneme(PhonemeInfo {
code,
ph_type,
stress_level,
lengthen: pending_lengthen,
next_lm,
next2_lm,
more_syllables,
end_of_clause,
std_length,
prev_type,
prev_code,
next_type,
next_code,
next2_code,
this_wordstart,
prev_wordstart,
next_wordstart,
next2_wordstart,
}));
pending_lengthen = false;
prev_type = ph_type;
prev_code = code;
prev_wordstart = this_wordstart;
word_start_pending = false;
let _ = next2_type; }
}
}
for &(_, cmd) in &pending_embedded[next_embedded..] {
result.push(AnnCode::Embedded(cmd));
}
result
}
fn compute_prepause(
ph_type: u8,
_next_type: u8,
_next2_type: u8,
_ph_lm: u8,
_code: u8,
_result: &mut Vec<AnnCode>,
) -> usize {
let prepause_ms: f64 = match ph_type {
4 => 48.0,
_ => 0.0,
};
if prepause_ms > 0.0 {
(prepause_ms / 1000.0 * 22050.0) as usize
} else {
0
}
}
struct Transition {
rms: i32,
flags: i32,
f2: i32,
f2_min: i32,
f2_max: i32,
f3_adj: i32,
f3_amp: i32,
f1: i32,
}
fn unpack_transition(data1: u32, data2: u32) -> Transition {
Transition {
rms: ((data1 >> 6) & 0x3f) as i32,
flags: (data1 >> 12) as i32,
f2: (data2 & 0x3f) as i32 * 50,
f2_min: (((data2 >> 6) & 0x1f) as i32 - 15) * 50,
f2_max: (((data2 >> 11) & 0x1f) as i32 - 15) * 50,
f3_adj: (((data2 >> 16) & 0x1f) as i32 - 15) * 50,
f3_amp: ((data2 >> 21) & 0x1f) as i32 * 8,
f1: ((data2 >> 26) & 0x7) as i32,
}
}
fn set_frame_rms(fr: &mut phondata::SpectFrame, new_rms: i32) {
if new_rms <= 0 {
return;
}
let old = fr.rms.max(1) as f64;
let scale = ((new_rms as f64) / old).sqrt();
for h in fr.fheight.iter_mut() {
*h = ((*h as f64 * scale).round() as i32).clamp(0, 255) as u8;
}
fr.rms = new_rms.clamp(0, 255) as u8;
}
fn adjust_formants(
fr: &mut phondata::SpectFrame,
target: i32, min: i32, max: i32,
f1_adj: i32, mut f3_adj: i32, hf_reduce: i32, flags: i32,
formant_factor: i32,
) {
let target = target * formant_factor / 256;
let mut x = (target - fr.ffreq[2] as i32) / 2;
x = x.clamp(min, max);
fr.ffreq[2] = (fr.ffreq[2] as i32 + x).clamp(0, i16::MAX as i32) as i16;
fr.ffreq[3] = (fr.ffreq[3] as i32 + f3_adj).clamp(0, i16::MAX as i32) as i16;
if flags & 0x20 != 0 {
f3_adj = -f3_adj;
}
fr.ffreq[4] = (fr.ffreq[4] as i32 + f3_adj).clamp(0, i16::MAX as i32) as i16;
fr.ffreq[5] = (fr.ffreq[5] as i32 + f3_adj).clamp(0, i16::MAX as i32) as i16;
if f1_adj == 1 {
let x = (235 - fr.ffreq[1] as i32).clamp(-100, -60);
fr.ffreq[1] = (fr.ffreq[1] as i32 + x).max(0) as i16;
} else if f1_adj == 2 {
let x = (235 - fr.ffreq[1] as i32).clamp(-300, -150);
fr.ffreq[1] = (fr.ffreq[1] as i32 + x).max(0) as i16;
fr.ffreq[0] = (fr.ffreq[0] as i32 + x).max(0) as i16;
} else if f1_adj == 3 {
let x = (100 - fr.ffreq[1] as i32).max(-400); fr.ffreq[1] = (fr.ffreq[1] as i32 + x).max(0) as i16;
fr.ffreq[0] = (fr.ffreq[0] as i32 + x).max(0) as i16;
}
if hf_reduce > 0 {
for ix in 2..8 {
fr.fheight[ix] = (fr.fheight[ix] as i32 * hf_reduce / 100).clamp(0, 255) as u8;
}
}
}
fn smooth_spect(frames: &mut [phondata::SpectFrame], centre: usize) {
const FORMANT_RATE: [i32; 6] = [240, 170, 170, 170, 170, 170];
let n = frames.len();
if n < 2 {
return;
}
let centre = centre.min(n - 1);
let limit = |frames: &mut [phondata::SpectFrame], cur: usize, anchor: usize, len: i32, break_lf: bool| {
for pk in 0..6 {
if break_lf && pk < 3 {
continue;
}
let f1 = frames[anchor].ffreq[pk] as i32; let f2 = frames[cur].ffreq[pk] as i32;
let diff = f2 - f1;
let mut allowed = if diff > 0 { f1 * 2 + f2 } else { f1 + f2 * 2 };
allowed = allowed * FORMANT_RATE[pk] / 3000;
allowed = allowed * len / 256;
if diff > allowed {
frames[cur].ffreq[pk] = (f1 + allowed).clamp(0, i16::MAX as i32) as i16;
} else if diff < -allowed {
frames[cur].ffreq[pk] = (f1 - allowed).clamp(0, i16::MAX as i32) as i16;
}
}
};
for i in (0..centre).rev() {
let fl = frames[i].frflags;
if fl & phondata::FRFLAG_BREAK != 0 {
break;
}
let mut len = (frames[i].length as i32) * 64;
if fl & phondata::FRFLAG_FORMANT_RATE != 0 {
len = len * 12 / 10;
}
limit(frames, i, i + 1, len, fl & phondata::FRFLAG_BREAK_LF != 0);
}
for i in (centre + 1)..n {
let fl = frames[i - 1].frflags;
if fl & phondata::FRFLAG_BREAK != 0 {
break;
}
let mut len = (frames[i - 1].length as i32) * 64;
if fl & phondata::FRFLAG_FORMANT_RATE != 0 {
len = len * 6 / 5;
}
limit(frames, i, i - 1, len, false);
}
}
fn apply_vowel_transition(
frames: &mut Vec<phondata::SpectFrame>,
data1: u32,
data2: u32,
which: u8,
formant_factor: i32,
) {
const RMS_START: i32 = 28;
if frames.len() < 2 {
return;
}
let t = unpack_transition(data1, data2);
if which == 1 {
let next_rms = frames[1].rms as i32;
let fr = &mut frames[0];
if t.f2 != 0 {
if t.rms & 0x20 != 0 {
set_frame_rms(fr, next_rms * (t.rms & 0x1f) / 30);
}
adjust_formants(fr, t.f2, t.f2_min, t.f2_max, t.f1, t.f3_adj, t.f3_amp, t.flags,
formant_factor);
if t.rms & 0x20 == 0 {
set_frame_rms(fr, t.rms * 2);
}
} else {
set_frame_rms(fr, RMS_START);
}
} else {
if t.f2 != 0 || t.flags != 0 {
let mut fr = frames[frames.len() - 1].clone();
if t.f2 != 0 {
adjust_formants(&mut fr, t.f2, t.f2_min, t.f2_max, t.f1, t.f3_adj, t.f3_amp,
t.flags, formant_factor);
}
set_frame_rms(&mut fr, t.rms * 2);
frames.push(fr);
}
}
}
enum PhonemeRender {
Frames {
frames: Vec<phondata::SpectFrame>,
amp_factor: f64,
lead_silence: usize,
trail_silence: usize,
},
Pcm(Vec<i16>),
}
fn flush_run(
run_frames: &mut Vec<phondata::SpectFrame>,
run_amps: &mut Vec<f64>,
run_pitch: &mut Vec<f64>,
run_speed: &mut Vec<f64>,
run_centres: &mut Vec<usize>,
output: &mut Vec<i16>,
voice: &VoiceParams,
wavephase: &mut i32,
) {
if run_frames.is_empty() {
run_centres.clear();
return;
}
if !run_centres.is_empty() {
let n = run_frames.len();
let k = run_centres.len();
let mid = |a: usize, b: usize| (a + b) / 2;
for i in 0..k {
let seg_start = if i == 0 { 0 } else { mid(run_centres[i - 1], run_centres[i]) };
let seg_end = if i == k - 1 { n } else { mid(run_centres[i], run_centres[i + 1]) };
let c = run_centres[i];
if seg_end > seg_start + 1 && c >= seg_start && c < seg_end {
smooth_spect(&mut run_frames[seg_start..seg_end], c - seg_start);
}
}
}
let raw = if voice.klatt {
klatt::synthesize_frames_klatt(run_frames, run_amps, run_pitch, voice.sample_rate)
} else {
wavegen::synthesize_frames_seq(
run_frames, run_amps, run_pitch, run_speed, voice, wavephase,
)
};
output.extend_from_slice(&agc_clip(&raw));
run_frames.clear();
run_amps.clear();
run_pitch.clear();
run_speed.clear();
run_centres.clear();
}
fn synthesize_phoneme_info(
info: &PhonemeInfo,
phdata: &crate::phoneme::PhonemeData,
speed_factor: f64,
formant_factor: i32,
stress_lengths: Option<&[u32; 8]>,
) -> PhonemeRender {
use setlengths::{calc_vowel_length_mod, length_mod_to_samples};
const SAMPLERATE: u32 = 22050;
let ph_tab = match phdata.get(info.code) {
Some(p) => p,
None => return PhonemeRender::Pcm(Vec::new()),
};
let nb = bytecode::Neighbours {
prev: info.prev_code,
this: info.code,
next: info.next_code,
next2: info.next2_code,
stress: info.stress_level,
this_wordstart: info.this_wordstart,
prev_wordstart: info.prev_wordstart,
next_wordstart: info.next_wordstart,
next2_wordstart: info.next2_wordstart,
translation_given: false,
..Default::default()
};
let mut extract = bytecode::interpret_phoneme(
ph_tab.program,
&phdata.phonindex,
&nb,
|c| phdata.get(c).cloned(),
);
if extract.fmt_addr.is_none() && extract.wav_addr.is_none() {
if let Some(target_code) = extract.change_phoneme_code {
if let Some(target_ph) = phdata.get(target_code) {
if target_ph.program > 0 {
let sub = bytecode::scan_phoneme(target_ph.program, &phdata.phonindex);
if extract.fmt_addr.is_none() { extract.fmt_addr = sub.fmt_addr; extract.fmt_param = sub.fmt_param; }
if extract.wav_addr.is_none() { extract.wav_addr = sub.wav_addr; extract.wav_param = sub.wav_param; }
}
}
}
}
let lead_silence = (extract.pause_before_ms as f64 / 1000.0 * SAMPLERATE as f64
* speed_factor) as usize;
let trail_silence = (extract.pause_after_ms as f64 / 1000.0 * SAMPLERATE as f64
* speed_factor) as usize;
if info.ph_type == 4 || info.ph_type == 6 {
if let Some(wav_addr) = extract.wav_addr {
if let Some(pcm) = sample::parse_wav_sample(
wav_addr, &phdata.phondata, speed_factor, extract.wav_param as i32,
) {
if lead_silence == 0 && trail_silence == 0 {
return PhonemeRender::Pcm(pcm);
}
let mut out = vec![0i16; lead_silence];
out.extend_from_slice(&pcm);
out.extend(std::iter::repeat(0i16).take(trail_silence));
return PhonemeRender::Pcm(out);
}
}
let n = (50.0 / 1000.0 * SAMPLERATE as f64 * speed_factor) as usize;
return PhonemeRender::Pcm(vec![0i16; n]);
}
let fmt_addr = match extract.fmt_addr {
Some(a) => a as usize,
None => return PhonemeRender::Pcm(Vec::new()),
};
let mut seq = match phondata::SpectSeq::parse(&phdata.phondata, fmt_addr) {
Some(s) => s,
None => return PhonemeRender::Pcm(Vec::new()),
};
if seq.frames.is_empty() {
return PhonemeRender::Pcm(Vec::new());
}
let mut onset_count = 0usize;
let mut tail_count = 0usize;
if info.ph_type == 2 {
let seq_break = seq
.frames
.iter()
.enumerate()
.filter(|(_, f)| f.frflags & phondata::FRFLAG_VOWEL_CENTRE != 0)
.map(|(i, _)| i)
.next_back()
.unwrap_or(0);
let pre_centre: Vec<_> = if seq_break > 0 {
seq.frames[..seq_break].to_vec()
} else {
Vec::new()
};
if seq_break > 0 {
seq.frames.drain(..seq_break); }
let prev_extract = if info.prev_code != 0 {
phdata.get(info.prev_code).map(|prev| {
let prev_nb = bytecode::Neighbours {
this: info.prev_code,
next: info.code,
next2: info.next_code,
..Default::default()
};
bytecode::interpret_phoneme(
prev.program, &phdata.phonindex, &prev_nb,
|c| phdata.get(c).cloned(),
)
})
} else {
None
};
let onset_addr = if extract.vwlstart_addr.is_some() && !extract.pd_fornextph {
extract.vwlstart_addr
} else {
prev_extract.as_ref().and_then(|pe| {
if pe.pd_fornextph { pe.vwlstart_addr } else { None }
})
};
let mut used_external_onset = false;
if let Some(vs_addr) = onset_addr {
if let Some(vs) = phondata::SpectSeq::parse(&phdata.phondata, vs_addr as usize) {
if !vs.frames.is_empty() {
onset_count = vs.frames.len();
used_external_onset = true;
let mut combined = vs.frames;
combined.append(&mut seq.frames);
seq.frames = combined;
}
}
}
if !used_external_onset {
if !pre_centre.is_empty() {
onset_count = pre_centre.len();
let mut combined = pre_centre;
combined.append(&mut seq.frames);
seq.frames = combined;
}
let prev_vt = prev_extract
.as_ref()
.map(|pe| pe.vowel_transition)
.unwrap_or([0; 4]);
if (prev_vt[0] != 0 || prev_vt[1] != 0) && seq.frames.len() >= 2 {
apply_vowel_transition(&mut seq.frames, prev_vt[0], prev_vt[1], 1, formant_factor);
}
}
if !seq.frames.is_empty() && info.next_code != 0 {
let next_extract = phdata.get(info.next_code).map(|next| {
let next_nb = bytecode::Neighbours {
this: info.next_code,
prev: info.code,
next: info.next2_code,
..Default::default()
};
bytecode::interpret_phoneme(
next.program, &phdata.phonindex, &next_nb,
|c| phdata.get(c).cloned(),
)
});
let vwlend_addr = extract
.vwlending_addr
.or_else(|| next_extract.as_ref().and_then(|ne| ne.vwlending_addr));
if let Some(ve_addr) = vwlend_addr {
if let Some(ve) = phondata::SpectSeq::parse(&phdata.phondata, ve_addr as usize) {
if !ve.frames.is_empty() {
if let Some(last) = seq.frames.last_mut() {
last.length = ve.frames[0].length; }
let appended: Vec<_> = ve.frames.into_iter().skip(1).collect();
tail_count = appended.len();
seq.frames.extend(appended);
}
}
} else if seq.frames.len() >= 2 {
let next_vt = next_extract
.as_ref()
.map(|ne| ne.vowel_transition)
.unwrap_or([0; 4]);
if next_vt[2] != 0 || next_vt[3] != 0 {
apply_vowel_transition(&mut seq.frames, next_vt[2], next_vt[3], 2, formant_factor);
}
}
}
}
if info.ph_type == 2 {
let std_length = extract
.set_length
.unwrap_or(info.std_length)
.saturating_add_signed(extract.add_length);
let length_mod = calc_vowel_length_mod(
info.stress_level,
info.next_lm,
info.next2_lm,
info.more_syllables,
info.end_of_clause,
std_length,
stress_lengths,
);
let length_mod = if info.lengthen { length_mod * 4 / 3 } else { length_mod };
let target_samples = length_mod_to_samples(length_mod, SAMPLERATE, speed_factor);
if onset_count > 0 {
let ms_to_stepsize = (SAMPLERATE as f64 / 1000.0) / 64.0;
let end = onset_count.min(seq.frames.len());
for fr in &mut seq.frames[..end] {
fr.length = ((fr.length as f64 * ms_to_stepsize * speed_factor).round() as usize)
.clamp(1, 255) as u8;
}
}
if target_samples > 0 {
let n = seq.frames.len();
let body_end = n.saturating_sub(tail_count);
let body_start = onset_count.min(body_end.saturating_sub(1));
if body_end > body_start + 1 {
let raw_sum: usize = seq.frames[body_start..body_end-1].iter()
.map(|f| f.length as usize).sum::<usize>().max(1);
let scaled_sum = (raw_sum as f64 * 64.0 * speed_factor) as usize;
if scaled_sum > 0 {
let scale256 = target_samples * 256 / scaled_sum.max(1);
for fr in &mut seq.frames[body_start..body_end-1] {
let new_len = ((fr.length as usize * scale256 / 256).max(1) as u8).min(255);
fr.length = new_len;
}
}
}
if tail_count > 0 && body_end >= 1 {
let ms_to_stepsize = (SAMPLERATE as f64 / 1000.0) / 64.0;
for fr in &mut seq.frames[body_end - 1..n] {
fr.length = ((fr.length as f64 * ms_to_stepsize * speed_factor).round() as usize)
.clamp(1, 255) as u8;
}
}
}
} else {
let ms_to_stepsize = (SAMPLERATE as f64 / 1000.0) / 64.0;
for fr in &mut seq.frames {
let new_len = ((fr.length as f64 * ms_to_stepsize * speed_factor).round() as usize).max(1);
fr.length = new_len.min(255) as u8;
}
}
if info.lengthen && seq.frames.len() > 1 {
let mid = seq.frames.len() / 2;
let extra = seq.frames[mid].clone();
seq.frames.insert(mid, extra);
}
let stress_amp = setlengths::STRESS_AMPS_EN
.get(info.stress_level as usize)
.copied()
.unwrap_or(20) as f64;
let general_amp = 55.0f64; let wdata_amplitude = stress_amp * general_amp / 16.0;
let amp_primary = 22.0 * 55.0 / 16.0;
let amp_factor = wdata_amplitude / amp_primary;
if let Ok(path) = std::env::var("ESPEAK_RS_DUMP_FRAMES") {
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
let mnem = phdata.get(info.code).map(|p| p.mnemonic_str()).unwrap_or_default();
let _ = writeln!(f, "PH {} nf={}", mnem, seq.frames.len());
for fr in &seq.frames {
let _ = writeln!(
f, " len={} rms={} ffreq={},{},{},{},{},{},{}",
fr.length, fr.rms,
fr.ffreq[0], fr.ffreq[1], fr.ffreq[2], fr.ffreq[3],
fr.ffreq[4], fr.ffreq[5], fr.ffreq[6],
);
}
}
}
PhonemeRender::Frames {
frames: seq.frames,
amp_factor,
lead_silence,
trail_silence,
}
}
fn agc_clip(samples: &[i32]) -> Vec<i16> {
if samples.is_empty() {
return Vec::new();
}
let mut agc: i64 = 256;
let mut out = Vec::with_capacity(samples.len());
for &z1 in samples {
let z = (z1 as i64 * agc) >> 8;
if z >= 32768 {
let ov = if z1 != 0 { 8_388_608i64 / (z1 as i64).abs() - 1 } else { 0 };
if ov < agc { agc = ov.max(1); }
let z2 = (z1 as i64 * agc) >> 8;
out.push(z2.clamp(-32767, 32767) as i16);
} else if z <= -32768 {
let ov = if z1 != 0 { 8_388_608i64 / (z1 as i64).abs() - 1 } else { 0 };
if ov < agc { agc = ov.max(1); }
let z2 = (z1 as i64 * agc) >> 8;
out.push(z2.clamp(-32767, 32767) as i16);
} else {
out.push(z.clamp(-32767, 32767) as i16);
}
if agc < 256 { agc += 1; }
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stress_amp_ratio_unset_is_identity() {
let v = VoiceParams::default(); for level in 0..8u8 {
assert_eq!(v.stress_amp_ratio(level), 1.0);
}
}
#[test]
fn stress_amp_ratio_scales_against_baseline() {
let base5 = setlengths::STRESS_AMPS_EN[5] as i32; let mut v = VoiceParams::default();
v.stress_amps[5] = base5 * 2;
assert!((v.stress_amp_ratio(5) - 2.0).abs() < 1e-9, "primary stress should double");
assert_eq!(v.stress_amp_ratio(0), 1.0);
let _ = v.stress_amp_ratio(200);
}
#[test]
fn flutter_factor_is_identity_when_off() {
for idx in 0..20 {
assert_eq!(flutter_factor(idx, 0), 1.0);
assert_eq!(flutter_factor(idx, -5), 1.0);
}
}
#[test]
fn flutter_factor_wobbles_within_bounds() {
let flutter = 20; let vals: Vec<f64> = (0..40).map(|i| flutter_factor(i, flutter)).collect();
assert!(vals.iter().all(|&v| (v - 1.0).abs() <= 0.020 + 1e-9), "flutter exceeds bound");
assert!(vals.iter().any(|&v| v > 1.001), "flutter never raises pitch");
assert!(vals.iter().any(|&v| v < 0.999), "flutter never lowers pitch");
assert_eq!(flutter_factor(7, flutter), flutter_factor(7, flutter));
}
#[test]
fn apply_echo_adds_decaying_repeats() {
let mut pcm = vec![0i16; 1000];
for s in pcm.iter_mut().take(100) {
*s = 10_000;
}
apply_echo(&mut pcm, 200, 128);
assert_eq!(pcm[250], 5000, "first echo hop");
assert_eq!(pcm[450], 2500, "second echo hop should decay");
assert_eq!(pcm[50], 10_000);
}
#[test]
fn apply_echo_zero_params_are_noop() {
let orig = vec![100i16, -200, 300, -400, 500];
let mut a = orig.clone();
apply_echo(&mut a, 0, 128); assert_eq!(a, orig);
let mut b = orig.clone();
apply_echo(&mut b, 2, 0); assert_eq!(b, orig);
}
#[test]
fn apply_echo_saturates_instead_of_diverging() {
let mut pcm = vec![0i16; 500];
pcm[0] = 30_000;
apply_echo(&mut pcm, 50, 10_000); assert!(pcm.iter().all(|&s| s >= i16::MIN && s <= i16::MAX));
assert_eq!(pcm[50], i16::MAX, "over-unity echo saturates, not overflows");
}
#[test]
fn klatt_frame_run_auto_selects_cascade() {
let mut fr = phondata::SpectFrame::default();
fr.frflags = phondata::FRFLAG_KLATT;
fr.ffreq = [120, 730, 1090, 2440, 3400, 4000, 4500];
fr.length = 20;
fr.rms = 40;
let mut frames = vec![fr; 4];
let mut amps = vec![1.0; 4];
let mut pitch = vec![120.0; 4];
let mut centres = Vec::new();
let mut out = Vec::new();
let mut phase = i32::MAX;
let voice = VoiceParams::default(); flush_run(&mut frames, &mut amps, &mut pitch, &mut Vec::new(), &mut centres, &mut out, &voice, &mut phase);
assert!(!out.is_empty(), "Klatt-flagged run produced no audio");
}
#[test]
fn resonator_tick_accumulates() {
let mut r = Resonator { a: 1.0, b: 0.0, c: 0.0, x1: 0.0, x2: 0.0 };
assert!((r.tick(1.0) - 1.0).abs() < 1e-12);
assert!((r.tick(2.0) - 2.0).abs() < 1e-12);
}
#[test]
fn resonator_tick_with_feedback() {
let mut r = Resonator { a: 0.0, b: 0.5, c: 0.0, x1: 1.0, x2: 0.0 };
let y0 = r.tick(0.0);
assert!((y0 - 0.5).abs() < 1e-12);
let y1 = r.tick(0.0);
assert!((y1 - 0.25).abs() < 1e-12);
}
#[test]
fn resonator_reset_clears_state() {
let mut r = Resonator { a: 1.0, b: 0.5, c: 0.0, x1: 99.0, x2: 99.0 };
r.reset();
assert_eq!(r.x1, 0.0);
assert_eq!(r.x2, 0.0);
}
#[test]
fn frame_c_size() {
assert_eq!(Frame::C_SIZE, 64,
"Frame::C_SIZE must match the C struct frame_t");
}
#[test]
fn voice_params_default_sample_rate() {
let v = VoiceParams::default();
assert_eq!(v.sample_rate, 22050);
}
#[test]
fn synthesize_empty_string_returns_empty() {
let s = Synthesizer::new(VoiceParams::default());
let pcm = s.synthesize("").unwrap();
assert!(pcm.is_empty());
}
#[test]
fn synthesize_ipa_the() {
let s = Synthesizer::new(VoiceParams::default());
let pcm = s.synthesize("ðə").expect("should synthesise 'the'");
assert!(!pcm.is_empty());
assert!(pcm.iter().all(|&x| x >= i16::MIN + 1));
}
#[test]
fn synthesize_hello() {
let s = Synthesizer::new(VoiceParams::default());
let pcm = s.synthesize("hɛloʊ").expect("should synthesise 'hello'");
assert!(!pcm.is_empty());
assert!(pcm.len() > 5_000, "too short: {} samples", pcm.len());
}
#[test]
fn synthesize_produces_nonzero_audio() {
let s = Synthesizer::new(VoiceParams::default());
let pcm = s.synthesize("iː").unwrap();
let peak = pcm.iter().map(|&x| x.unsigned_abs()).max().unwrap_or(0);
assert!(peak > 1000, "expected non-trivial audio, got peak = {peak}");
}
#[test]
fn synthesize_stress_words() {
let s = Synthesizer::new(VoiceParams::default());
let pcm = s.synthesize("ˈhɛloʊ ˌwɜːld").unwrap();
assert!(!pcm.is_empty());
}
#[test]
fn synthesize_unknown_phonemes_error() {
let s = Synthesizer::new(VoiceParams::default());
let result = s.synthesize("☺☻♥");
assert!(result.is_err(), "expected error for all-unrecognised input");
}
#[test]
fn sample_rate_is_22050() {
let s = Synthesizer::new(VoiceParams::default());
assert_eq!(s.sample_rate(), 22050);
}
#[test]
fn synthesize_speed_affects_duration() {
let mut fast_voice = VoiceParams::default();
fast_voice.speed_percent = 200;
let s_normal = Synthesizer::new(VoiceParams::default());
let s_fast = Synthesizer::new(fast_voice);
let pcm_normal = s_normal.synthesize("hɛloʊ").unwrap();
let pcm_fast = s_fast.synthesize("hɛloʊ").unwrap();
assert!(pcm_fast.len() < pcm_normal.len(),
"fast speech must be shorter: fast={}, normal={}",
pcm_fast.len(), pcm_normal.len());
}
}