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 frame_to_peaks(fr: &SpectFrame) -> [Peak; 9] {
let mut peaks = [Peak::default(); 9];
for ix in 0..7 {
let freq = (fr.ffreq[ix] as i64 * VOICE_FREQ[ix.min(8)] as i64) << 8;
peaks[ix].freq = freq;
let height = (fr.fheight[ix] as i64 * VOICE_HEIGHT[ix.min(8)] as i64) << 6;
peaks[ix].height = height;
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]) -> 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 = if p.left > 0 {
let h = ((fp - p.left) / pitch_hz16) as usize;
h.max(1)
} else {
((fp / pitch_hz16) as usize).max(1)
};
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;
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)
}
struct Xorshift32(u32);
impl Xorshift32 {
fn new() -> Self { Xorshift32(0xBAD_5EED) }
#[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 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::new();
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]);
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]);
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 = 100.0 / voice.speed_percent.max(1) as f64;
let f0_hz = voice.pitch_hz.max(25) as f64;
let global_amp = voice.amplitude.clamp(0, 100) as f64 / 100.0;
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[1] } 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: fr1.clone(),
fr2: fr2.clone(),
n_samples,
voiced,
noise_frac,
amp_factor,
};
let samples = wavegen_segment(&seg, f0_hz, 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 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);
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,
};
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,
};
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,
};
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");
}
}