use super::phondata::SpectFrame;
use super::VoiceParams;
pub const MAX_HARMONIC: usize = 200;
const N_LOWHARM: usize = 30;
pub const STEPSIZE: usize = 64;
const PHASE_INC_FACTOR: i32 = 6088;
include!("sintab_data.rs");
static PK_SHAPE2: [u8; 257] = [
255, 254, 254, 254, 254, 254, 254, 254, 254, 254, 253, 253, 253, 253, 252, 252,
252, 251, 251, 251, 250, 250, 249, 249, 248, 248, 247, 247, 246, 245, 245, 244,
243, 243, 242, 241, 239, 237, 235, 233, 231, 229, 227, 225, 223, 221, 218, 216,
213, 211, 208, 205, 203, 200, 197, 194, 191, 187, 184, 181, 178, 174, 171, 167,
163, 160, 156, 152, 148, 144, 140, 136, 132, 127, 123, 119, 114, 110, 105, 100,
96, 94, 91, 88, 86, 83, 81, 78, 76, 74, 71, 69, 66, 64, 62, 60,
57, 55, 53, 51, 49, 47, 44, 42, 40, 38, 36, 34, 32, 30, 29, 27,
25, 23, 21, 19, 18, 16, 14, 12, 11, 9, 7, 6, 4, 3, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0,
];
static WAVEMULT: [u8; 128] = [
0, 0, 0, 2, 3, 5, 8, 11, 14, 18, 22, 27, 32, 37, 43, 49,
55, 62, 69, 76, 83, 90, 98, 105, 113, 121, 128, 136, 144, 152, 159, 166,
174, 181, 188, 194, 201, 207, 213, 218, 224, 228, 233, 237, 240, 244, 246, 249,
251, 252, 253, 253, 253, 253, 252, 251, 249, 246, 244, 240, 237, 233, 228, 224,
218, 213, 207, 201, 194, 188, 181, 174, 166, 159, 152, 144, 136, 128, 121, 113,
105, 98, 90, 83, 76, 69, 62, 55, 49, 43, 37, 32, 27, 22, 18, 14,
11, 8, 5, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
const WAVEMULT_MAX: usize = (22050 * 60) / (256 * 50);
const WAVEMULT_OFFSET: i32 = (WAVEMULT_MAX / 2) as i32;
const VOICE_FREQ: [i32; 9] = [256, 256, 256, 256, 256, 256, 256, 256, 256];
const VOICE_HEIGHT: [i64; 9] = [65, 64, 60, 58, 50, 50, 64, 64, 64];
const VOICE_WIDTH: [i64; 9] = [96, 80, 80, 80, 80, 80, 80, 80, 80];
const N_HARMONIC_PEAKS: usize = 5;
#[derive(Debug, Clone, Copy, Default)]
struct Peak {
freq: i64, height: i64, left: i64, right: i64, }
fn scale_frame_formants(fr: &SpectFrame, voice: &VoiceParams) -> SpectFrame {
let scale_u8 = |v: u8, pct: i32| (v as i32 * pct / 100).clamp(0, u8::MAX as i32) as u8;
let mut out = fr.clone();
for i in 0..7 {
out.ffreq[i] = (fr.ffreq[i] as i32 * voice.formant_freq_pct[i] / 100)
.clamp(0, i16::MAX as i32) as i16;
}
for i in 0..8 {
let pct = voice.formant_height_pct.get(i).copied().unwrap_or(100);
out.fheight[i] = scale_u8(fr.fheight[i], pct);
}
for i in 0..6 {
out.fwidth[i] = scale_u8(fr.fwidth[i], voice.formant_width_pct[i]);
}
for i in 0..3 {
out.fright[i] = scale_u8(fr.fright[i], voice.formant_width_pct[i]);
}
out
}
fn frame_to_peaks(fr: &SpectFrame) -> [Peak; 9] {
let mut peaks = [Peak::default(); 9];
for ix in 0..8 {
if ix < 7 {
peaks[ix].freq = (fr.ffreq[ix] as i64 * VOICE_FREQ[ix] as i64) << 8;
}
peaks[ix].height = (fr.fheight[ix] as i64 * VOICE_HEIGHT[ix] as i64) << 6;
if ix <= 5 {
let width = (fr.fwidth[ix] as i64 * VOICE_WIDTH[ix.min(8)] as i64) << 10;
peaks[ix].left = width;
if ix < 3 {
let rw = (fr.fright[ix] as i64 * VOICE_WIDTH[ix.min(8)] as i64) << 10;
peaks[ix].right = rw;
} else {
peaks[ix].right = width;
}
}
}
peaks[7].freq = 7800_i64 << 16;
peaks[8].freq = 9000_i64 << 16;
peaks
}
fn interpolate_peaks(p1: &[Peak; 9], p2: &[Peak; 9], t: f64) -> [Peak; 9] {
let mut out = [Peak::default(); 9];
for i in 0..9 {
out[i].freq = (p1[i].freq as f64 + (p2[i].freq - p1[i].freq) as f64 * t) as i64;
out[i].height = (p1[i].height as f64 + (p2[i].height - p1[i].height) as f64 * t) as i64;
out[i].left = (p1[i].left as f64 + (p2[i].left - p1[i].left) as f64 * t) as i64;
out[i].right = (p1[i].right as f64 + (p2[i].right - p1[i].right) as f64 * t) as i64;
}
out
}
fn peaks_to_harmspect(
peaks: &[Peak; 9],
pitch_hz16: i64,
htab: &mut [i32; MAX_HARMONIC],
tone_adjust: &[u8; N_TONE_ADJUST],
) -> usize {
htab.fill(0);
if pitch_hz16 <= 0 {
return 0;
}
let nyquist_limit = (22050_i64 * 19 / 40) << 16; let hmax_samplerate = (nyquist_limit / pitch_hz16) as usize;
let hmax = {
let top_peak = &peaks[N_HARMONIC_PEAKS];
let top_freq = top_peak.freq + top_peak.right;
let hmax_peak = if pitch_hz16 > 0 { (top_freq / pitch_hz16) as usize } else { 0 };
hmax_peak.min(hmax_samplerate).min(MAX_HARMONIC - 1)
};
for pk in 0..=N_HARMONIC_PEAKS {
let p = &peaks[pk];
if p.height == 0 || p.freq == 0 {
continue;
}
let fp = p.freq; let fhi = fp + p.right;
let h_start = {
let edge = if p.left > 0 { fp - p.left } else { fp };
let h = edge / pitch_hz16 + 1;
if h <= 0 { 1usize } else { h as usize }
};
let mut h = h_start;
let mut f = pitch_hz16 * h as i64;
while f < fp && h < MAX_HARMONIC {
let diff = fp - f;
let bw = p.left >> 8;
if bw > 0 {
let idx = ((diff / bw) as usize).min(256);
let contrib = (PK_SHAPE2[idx] as i64 * p.height).min(i32::MAX as i64) as i32;
htab[h] = htab[h].saturating_add(contrib);
}
h += 1;
f += pitch_hz16;
}
while f < fhi && h < MAX_HARMONIC {
let diff = f - fp;
let bw = p.right >> 8;
if bw > 0 {
let idx = ((diff / bw) as usize).min(256);
let contrib = (PK_SHAPE2[idx] as i64 * p.height).min(i32::MAX as i64) as i32;
htab[h] = htab[h].saturating_add(contrib);
}
h += 1;
f += pitch_hz16;
}
}
let bass_h = (1000_i64 << 16) / pitch_hz16; if bass_h > 0 {
let bass_y0 = peaks[1].height * 10;
let bass_step = (bass_y0 / bass_h).max(1);
let mut y = bass_y0;
let mut h = 1usize;
while y > 0 && h < MAX_HARMONIC {
htab[h] = htab[h].saturating_add(y.min(i32::MAX as i64) as i32);
y -= bass_step;
h += 1;
}
}
let mut effective_hmax = 0;
for h in 1..=hmax {
let x = htab[h] >> 15;
htab[h] = (x * x) >> 8;
let ix = ((pitch_hz16 * h as i64) >> 19) as usize;
if ix < N_TONE_ADJUST {
htab[h] = ((htab[h] as i64 * tone_adjust[ix] as i64) / TONE_REFERENCE) as i32;
}
if htab[h] > 0 {
effective_hmax = h;
}
}
if hmax >= 1 {
htab[1] = htab[1] * 10 / 8;
if htab[1] > 0 { effective_hmax = effective_hmax.max(1); }
}
effective_hmax.max(1)
}
const TONE_REFERENCE: i64 = 170;
pub const N_TONE_ADJUST: usize = 1000;
pub const DEFAULT_TONE_POINTS: [i32; 12] =
[600, 170, 1200, 135, 2000, 110, 3000, 110, -1, 0, 0, 0];
pub static DEFAULT_TONE_ADJUST: std::sync::LazyLock<[u8; N_TONE_ADJUST]> =
std::sync::LazyLock::new(|| set_tone_adjust(&DEFAULT_TONE_POINTS));
pub fn set_tone_adjust(tone_pts: &[i32; 12]) -> [u8; N_TONE_ADJUST] {
let mut out = [0u8; N_TONE_ADJUST];
let mut pts = *tone_pts;
let mut freq1 = 0usize;
let mut height1 = pts[1];
let mut pt = 0usize;
while pt < 12 {
if pts[pt] == -1 {
pts[pt] = (N_TONE_ADJUST * 8) as i32;
if pt > 0 {
pts[pt + 1] = pts[pt - 1];
}
}
let freq2 = (pts[pt] / 8) as usize; let height2 = pts[pt + 1];
if freq2 > freq1 {
for ix in freq1..freq2.min(N_TONE_ADJUST) {
let y = height1
+ ((ix - freq1) as i32 * (height2 - height1)) / (freq2 - freq1) as i32;
out[ix] = y.clamp(0, 255) as u8;
}
}
freq1 = freq2;
height1 = height2;
pt += 2;
}
out
}
struct Xorshift32(u32);
impl Xorshift32 {
fn new() -> Self { Xorshift32(0xBAD_5EED) }
fn seeded(seed: u32) -> Self {
if seed == 0 { Self::new() } else { Xorshift32(seed) }
}
#[inline]
fn next_i32(&mut self) -> i32 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 17;
self.0 ^= self.0 << 5;
self.0 as i32
}
}
#[derive(Debug, Clone)]
pub struct SynthSegment {
pub fr1: SpectFrame,
pub fr2: SpectFrame,
pub n_samples: usize,
pub voiced: bool,
pub noise_frac: f64,
pub amp_factor: f64,
pub rand_seed: u32,
pub tone_adjust: [u8; N_TONE_ADJUST],
}
pub fn wavegen_segment(
seg: &SynthSegment,
f0_hz: f64,
global_amp: f64,
wavephase: &mut i32,
) -> Vec<i32> {
let n = seg.n_samples;
if n == 0 {
return Vec::new();
}
let mut output = vec![0i32; n];
let pitch_i32: i32 = ((f0_hz as i64 * 4096) as i32).max(25 * 4096);
let phaseinc: i32 = ((pitch_i32 >> 7) as i64 * PHASE_INC_FACTOR as i64) as i32;
let pitch_hz16: i64 = (f0_hz as i64) << 16;
let peaks1 = frame_to_peaks(&seg.fr1);
let peaks2 = frame_to_peaks(&seg.fr2);
let amp_scale: i64 = (global_amp * 7500.0 * seg.amp_factor) as i64;
let mut htab = [[0i32; MAX_HARMONIC]; 2];
let mut harm_inc = [0i32; N_LOWHARM];
let mut hswitch = 0usize;
let mut maxh = 0usize;
let mut maxh2;
let mut rng = Xorshift32::seeded(seg.rand_seed);
let tone_adjust = seg.tone_adjust;
let mut amplitude2: i64 = 0;
const SAMPLERATE: f64 = 22050.0;
let cycle_samples = (SAMPLERATE / f0_hz) as i32;
let hf_factor = ((f0_hz as i64 * 4096) >> 11).max(1); const N_HF: usize = 3; let mut peak_harmonic = [0u32; N_HF];
let mut peak_height = [0i64; N_HF];
let hmax_sr = ((22050 * 19 / 40) as f64 / f0_hz) as u32;
{
let t = 0.0f64;
let interp = interpolate_peaks(&frame_to_peaks(&seg.fr1), &frame_to_peaks(&seg.fr2), t);
for (k, fk) in (N_HARMONIC_PEAKS + 1..9).enumerate() {
let freq_hz16 = interp[fk].freq;
let pitch = (f0_hz as i64) * 4096; let h = if pitch > 0 { ((freq_hz16 / (pitch * 8) + 1) / 2).max(0) as u32 } else { 0 };
peak_harmonic[k] = if h <= hmax_sr { h } else { 0 };
let x = (interp[fk].height >> 14) as i64;
peak_height[k] = if peak_harmonic[k] > 0 { (x * x * 5) / 2 } else { 0 };
}
}
let mut cbytes: i32 = WAVEMULT_OFFSET - cycle_samples / 2;
for i in 0..n {
if (i & 63) == 0 {
let t = i as f64 / n as f64;
let interp_peaks = interpolate_peaks(&peaks1, &peaks2, t);
if i == 0 {
hswitch = 0;
let hm = peaks_to_harmspect(&interp_peaks, pitch_hz16, &mut htab[0], &tone_adjust);
hswitch ^= 1;
maxh = hm;
amplitude2 = (amp_scale * ((pitch_i32 >> 8) as i64)) / (10000 * 8);
} else {
maxh2 = peaks_to_harmspect(&interp_peaks, pitch_hz16, &mut htab[hswitch], &tone_adjust);
let other = hswitch ^ 1;
let n_lo = N_LOWHARM.min(maxh2).min(maxh);
for h in 1..n_lo {
harm_inc[h] = (htab[hswitch][h] - htab[other][h]) >> 3;
}
hswitch ^= 1;
maxh = maxh2;
}
} else if (i & 7) == 0 {
for h in 1..N_LOWHARM.min(maxh) {
htab[hswitch][h] = htab[hswitch][h].saturating_add(harm_inc[h]);
}
}
let old_phase = *wavephase;
*wavephase = wavephase.wrapping_add(phaseinc);
if old_phase > 0 && *wavephase < 0 {
amplitude2 = (amp_scale * ((pitch_i32 >> 8) as i64)) / (10000 * 8);
cbytes = WAVEMULT_OFFSET - cycle_samples / 2;
}
let waveph = (*wavephase >> 16) as u16;
let mut total: i64 = 0;
if seg.voiced {
cbytes += 1;
if cbytes >= 0 && (cbytes as usize) < WAVEMULT_MAX {
let mut hf_total: i64 = 0;
for k in 0..N_HF {
let h = peak_harmonic[k];
if h > 0 && peak_height[k] > 0 {
let theta = (waveph as u32).wrapping_mul(h) as u16;
let idx = (theta >> 5) as usize;
hf_total += SIN_TAB[idx] as i64 * peak_height[k];
}
}
total = (hf_total / hf_factor) * WAVEMULT[cbytes as usize] as i64;
} else {
total = 0;
}
let mut theta = waveph;
let mh = maxh.min(MAX_HARMONIC - 1);
let h_switch_sign = (890_u32 / (f0_hz as u32).max(1)) as usize;
let h_switch_sign = h_switch_sign.min(mh);
for h in 1..=h_switch_sign {
let idx = (theta >> 5) as usize;
total += SIN_TAB[idx] as i64 * htab[hswitch][h] as i64;
theta = theta.wrapping_add(waveph);
}
for h in (h_switch_sign + 1)..=mh {
let idx = (theta >> 5) as usize;
total -= SIN_TAB[idx] as i64 * htab[hswitch][h] as i64;
theta = theta.wrapping_add(waveph);
}
}
if seg.noise_frac > 0.0 {
let noise_amp = (seg.noise_frac * amplitude2 as f64) as i64;
total += rng.next_i32() as i64 * noise_amp / (1 << 20);
}
let z = (total >> 8) * amplitude2 / (1 << 13);
output[i] = z.clamp(i32::MIN as i64 + 1, i32::MAX as i64) as i32;
}
output
}
pub fn synthesize_frames(
seq: &super::phondata::SpectSeq,
voice: &VoiceParams,
amp_factor: f64,
wavephase: &mut i32,
) -> Vec<i32> {
if seq.frames.is_empty() {
return Vec::new();
}
let speed_factor = 1.0;
let f0_hz = voice.pitch_hz.max(25) as f64;
let global_amp = voice.amplitude.clamp(0, 100) as f64 / 100.0;
let fscale = voice.formant_scaling_active();
let mut output = Vec::new();
let n = seq.frames.len();
for i in 0..n {
let fr1 = &seq.frames[i];
let fr2 = if i + 1 < n { &seq.frames[i + 1] } else { fr1 };
let n_samples = fr1.dur_samples(speed_factor);
let av = fr1.klattp[0]; let fric = if seq.is_klatt { fr1.klattp2[2] } else { 0 };
let (voiced, noise_frac) = if av > 0 && fr1.ffreq[1] > 0 {
(true, fric as f64 / 255.0)
} else if fr1.ffreq[1] == 0 && av == 0 {
(false, 0.5) } else {
(av > 0, fric as f64 / 255.0)
};
let seg = SynthSegment {
fr1: if fscale { scale_frame_formants(fr1, voice) } else { fr1.clone() },
fr2: if fscale { scale_frame_formants(fr2, voice) } else { fr2.clone() },
n_samples,
voiced,
noise_frac,
amp_factor,
rand_seed: voice.rand_seed,
tone_adjust: voice.tone_adjust,
};
let samples = wavegen_segment(&seg, f0_hz, global_amp, wavephase);
output.extend_from_slice(&samples);
}
output
}
pub fn synthesize_frames_seq(
frames: &[super::phondata::SpectFrame],
amps: &[f64],
pitches_hz: &[f64],
speeds: &[f64],
voice: &VoiceParams,
wavephase: &mut i32,
) -> Vec<i32> {
if frames.is_empty() {
return Vec::new();
}
let speed_factor = 100.0 / voice.speed_percent.max(1) as f64;
let global_amp = voice.amplitude.clamp(0, 100) as f64 / 100.0;
let fscale = voice.formant_scaling_active();
let mut output = Vec::new();
let n = frames.len();
for i in 0..n {
let fr1 = &frames[i];
let fr2 = if i + 1 < n { &frames[i + 1] } else { fr1 };
let n_samples =
fr1.dur_samples(speed_factor * speeds.get(i).copied().unwrap_or(1.0));
let av = fr1.klattp[0];
let (voiced, noise_frac) = if av > 0 && fr1.ffreq[1] > 0 {
(true, 0.0)
} else if fr1.ffreq[1] == 0 && av == 0 {
(false, 0.5)
} else {
(av > 0, 0.0)
};
let seg = SynthSegment {
fr1: if fscale { scale_frame_formants(fr1, voice) } else { fr1.clone() },
fr2: if fscale { scale_frame_formants(fr2, voice) } else { fr2.clone() },
n_samples,
voiced,
noise_frac,
amp_factor: amps.get(i).copied().unwrap_or(1.0),
rand_seed: voice.rand_seed,
tone_adjust: voice.tone_adjust,
};
let f0 = pitches_hz.get(i).copied().unwrap_or(120.0).max(25.0);
let samples = wavegen_segment(&seg, f0, global_amp, wavephase);
output.extend_from_slice(&samples);
}
output
}
pub fn i32_to_i16(samples: &[i32]) -> Vec<i16> {
if samples.is_empty() {
return Vec::new();
}
let peak = samples.iter().map(|&x| x.unsigned_abs()).max().unwrap_or(0);
let target = (0.90 * 32767.0) as i64;
let scale: i64 = if peak > 0 { target * 4096 / peak as i64 } else { 0 };
samples.iter()
.map(|&x| ((x as i64 * scale) >> 12).clamp(-32767, 32767) as i16)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn voiced_frame(f1: i16, f2: i16, f3: i16, len: u8) -> SpectFrame {
let mut f = SpectFrame::default();
f.ffreq[1] = f1;
f.ffreq[2] = f2;
f.ffreq[3] = f3;
f.fwidth[0] = 25; f.fwidth[1] = 50; f.fwidth[2] = 80; f.fright[0] = 25;
f.fright[1] = 50;
f.fright[2] = 80;
f.fheight = [100, 60, 40, 20, 10, 5, 2, 0];
f.rms = 50;
f.klattp[0] = 50; f.length = len;
f
}
fn default_voice() -> VoiceParams { VoiceParams::default() }
#[test]
fn scale_frame_formants_scales_freqs() {
let mut fr = voiced_frame(500, 1500, 2500, 4);
fr.fheight = [10, 20, 40, 60, 80, 100, 120, 0];
fr.fwidth = [10, 20, 30, 40, 50, 60];
let v = VoiceParams {
formant_freq_pct: [150; 7],
formant_height_pct: [150; 7],
formant_width_pct: [50; 7],
..VoiceParams::default()
};
let up = scale_frame_formants(&fr, &v);
assert_eq!(up.ffreq[2], 2250);
assert_eq!(up.fheight[2], 60); assert_eq!(up.fwidth[2], 15); let same = scale_frame_formants(&fr, &VoiceParams::default());
assert_eq!(same.ffreq, fr.ffreq);
assert_eq!(same.fheight, fr.fheight);
assert_eq!(same.fwidth, fr.fwidth);
}
fn goertzel_power(s: &[i32], f_hz: f64, sr: f64) -> f64 {
let w = 2.0 * std::f64::consts::PI * f_hz / sr;
let coeff = 2.0 * w.cos();
let (mut s1, mut s2) = (0.0f64, 0.0f64);
for &x in s {
let s0 = x as f64 + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
(s1 * s1 + s2 * s2 - coeff * s1 * s2).max(0.0)
}
fn full_voiced_frame(f1: i16, f2: i16, f3: i16) -> SpectFrame {
let mut f = SpectFrame::default();
f.ffreq = [0, f1, f2, f3, 3300, 4300, 5300];
f.fheight = [0, 100, 80, 60, 45, 30, 15, 0];
f.fwidth = [40, 55, 70, 90, 110, 130];
f.fright = [40, 55, 70];
f.rms = 50;
f.klattp[0] = 50; f.length = 40;
f
}
#[test]
fn formant_scaling_shifts_energy_upward() {
let frames: Vec<SpectFrame> = (0..8).map(|_| full_voiced_frame(400, 1400, 2400)).collect();
let amps = vec![1.0; frames.len()];
let pitch = vec![120.0; frames.len()];
const SR: f64 = 22050.0;
let probe_energy = |pct: [i32; 7], f_hz: f64| -> f64 {
let voice = VoiceParams { formant_freq_pct: pct, ..VoiceParams::default() };
let mut phase = i32::MAX;
let s = synthesize_frames_seq(&frames, &s, &pitch, &[], &voice, &mut phase);
let total: f64 = s.iter().map(|&x| (x as f64).powi(2)).sum::<f64>().max(1.0);
goertzel_power(&s, f_hz, SR) / total
};
let base = probe_energy([100; 7], 2240.0);
let raised = probe_energy([160; 7], 2240.0);
assert!(
raised > base * 2.0,
"raising formants should push energy to the higher band: base {base:.3e} vs raised {raised:.3e}"
);
}
#[test]
fn formant_height_scaling_boosts_its_band() {
let frames: Vec<SpectFrame> = (0..8).map(|_| full_voiced_frame(400, 1400, 2400)).collect();
let amps = vec![1.0; frames.len()];
let pitch = vec![120.0; frames.len()];
const SR: f64 = 22050.0;
let power = |height: [i32; 7]| -> f64 {
let voice = VoiceParams { formant_height_pct: height, ..VoiceParams::default() };
let mut phase = i32::MAX;
let s = synthesize_frames_seq(&frames, &s, &pitch, &[], &voice, &mut phase);
goertzel_power(&s, 1400.0, SR)
};
let base = power([100; 7]);
let boosted = power([100, 100, 220, 100, 100, 100, 100]); assert!(
boosted > base * 1.5,
"boosting F2 height should raise its band's power: base {base:.3e} vs boosted {boosted:.3e}"
);
}
#[test]
fn formant_scaling_active_flag() {
assert!(!VoiceParams::default().formant_scaling_active());
let freq = VoiceParams { formant_freq_pct: [100, 100, 130, 100, 100, 100, 100], ..VoiceParams::default() };
assert!(freq.formant_scaling_active());
let height = VoiceParams { formant_height_pct: [80, 100, 100, 100, 100, 100, 100], ..VoiceParams::default() };
assert!(height.formant_scaling_active(), "height scale must count as active");
let width = VoiceParams { formant_width_pct: [100, 100, 100, 100, 100, 100, 150], ..VoiceParams::default() };
assert!(width.formant_scaling_active(), "width scale must count as active");
}
#[test]
fn peaks_to_harmspect_nonzero() {
let frame = voiced_frame(500, 1500, 2500, 4);
let peaks = frame_to_peaks(&frame);
let mut htab = [0i32; MAX_HARMONIC];
let hmax = peaks_to_harmspect(&peaks, 118_i64 << 16, &mut htab, &set_tone_adjust(&DEFAULT_TONE_POINTS));
assert!(hmax > 0);
let total: i32 = htab[1..].iter().sum();
assert!(total > 0, "harmonic table should be non-zero");
}
#[test]
fn wavegen_segment_produces_audio() {
let fr = voiced_frame(500, 1500, 2500, 4);
let seg = SynthSegment {
fr1: fr.clone(),
fr2: fr.clone(),
n_samples: 512,
voiced: true,
noise_frac: 0.0,
amp_factor: 1.0,
rand_seed: 0,
tone_adjust: set_tone_adjust(&DEFAULT_TONE_POINTS)
};
let mut phase = i32::MAX;
let samples = wavegen_segment(&seg, 118.0, 0.8, &mut phase);
assert_eq!(samples.len(), 512);
let max = samples.iter().map(|x| x.unsigned_abs()).max().unwrap_or(0);
assert!(max > 0, "voiced segment must produce non-zero audio");
}
#[test]
fn wavegen_segment_silence_when_zero_peaks() {
let fr = SpectFrame::default(); let seg = SynthSegment {
fr1: fr.clone(),
fr2: fr.clone(),
n_samples: 256,
voiced: false,
noise_frac: 0.0,
amp_factor: 1.0,
rand_seed: 0,
tone_adjust: set_tone_adjust(&DEFAULT_TONE_POINTS)
};
let mut phase = i32::MAX;
let samples = wavegen_segment(&seg, 118.0, 0.8, &mut phase);
assert_eq!(samples.len(), 256);
let max = samples.iter().map(|x| x.unsigned_abs()).max().unwrap_or(0);
assert_eq!(max, 0, "zero frame + no voice → silence");
}
#[test]
fn synthesize_frames_uses_all_frames() {
let seq = super::super::phondata::SpectSeq {
frames: vec![
voiced_frame(500, 1500, 2500, 2),
voiced_frame(510, 1480, 2480, 2),
voiced_frame(520, 1460, 2460, 2),
],
is_klatt: false,
};
let mut phase = i32::MAX;
let samples = synthesize_frames(&seq, &default_voice(), 1.0, &mut phase);
let expected = 3 * 2 * 64;
assert_eq!(samples.len(), expected,
"expected {expected} samples, got {}", samples.len());
}
#[test]
fn i32_to_i16_normalises() {
let big = vec![100_000i32, -200_000i32, 150_000i32];
let out = i32_to_i16(&big);
assert_eq!(out.len(), 3);
let peak = out.iter().map(|&x| x.unsigned_abs()).max().unwrap();
assert!(peak > 28000, "should normalise close to full scale: peak={peak}");
}
#[test]
fn i32_to_i16_empty() {
assert!(i32_to_i16(&[]).is_empty());
}
#[test]
fn phase_carries_over() {
let fr = voiced_frame(500, 1500, 2500, 1);
let seg = SynthSegment {
fr1: fr.clone(),
fr2: fr.clone(),
n_samples: 64,
voiced: true,
noise_frac: 0.0,
amp_factor: 1.0,
rand_seed: 0,
tone_adjust: set_tone_adjust(&DEFAULT_TONE_POINTS)
};
let mut phase = i32::MAX;
let _ = wavegen_segment(&seg, 118.0, 0.8, &mut phase);
let phase_after_first = phase;
let _ = wavegen_segment(&seg, 118.0, 0.8, &mut phase);
assert_ne!(phase, phase_after_first,
"phase should evolve between calls");
}
#[test]
fn tone_curve_interpolates_between_points() {
let t = set_tone_adjust(&DEFAULT_TONE_POINTS);
assert_eq!(t[0], 170);
assert_eq!(t[74], 170);
assert_eq!(t[150], 135);
assert_eq!(t[112] as i32, 170 + (112 - 75) * (135 - 170) / (150 - 75));
assert_eq!(t[250], 110);
assert_eq!(t[N_TONE_ADJUST - 1], 110);
}
#[test]
fn default_tone_curve_is_a_no_op() {
let peaks = frame_to_peaks(&SpectFrame {
ffreq: [100, 700, 1200, 2500, 3300, 3700, 7000],
fheight: [200; 8],
fwidth: [40; 6],
fright: [40; 3],
..Default::default()
});
let mut with_default = [0i32; MAX_HARMONIC];
let mut without = [0i32; MAX_HARMONIC];
let flat = [128u8; N_TONE_ADJUST];
peaks_to_harmspect(&peaks, 118_i64 << 16, &mut with_default,
&set_tone_adjust(&DEFAULT_TONE_POINTS));
peaks_to_harmspect(&peaks, 118_i64 << 16, &mut without, &DEFAULT_TONE_ADJUST);
assert_eq!(with_default, without, "the default curve must not change the spectrum");
let mut flattened = [0i32; MAX_HARMONIC];
peaks_to_harmspect(&peaks, 118_i64 << 16, &mut flattened, &flat);
assert_ne!(flattened, with_default, "a different curve must reshape the spectrum");
}
}