pub const N_SEQ_FRAMES: usize = 25;
pub const FRFLAG_KLATT: u16 = 0x01;
pub const FRFLAG_VOWEL_CENTRE: u16 = 0x02;
pub const FRFLAG_BREAK_LF: u16 = 0x08;
pub const FRFLAG_BREAK: u16 = 0x10;
pub const FRFLAG_FORMANT_RATE: u16 = 0x20;
pub const FRAME_T2_SIZE: usize = 44;
pub const FRAME_T_SIZE: usize = 64;
#[derive(Debug, Clone, Default)]
pub struct SpectFrame {
pub frflags: 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],
}
impl SpectFrame {
pub fn from_bytes_t2(data: &[u8]) -> Option<Self> {
if data.len() < FRAME_T2_SIZE { return None; }
let mut f = SpectFrame::default();
f.frflags = u16::from_le_bytes([data[0], data[1]]);
for i in 0..7 {
f.ffreq[i] = i16::from_le_bytes([data[2 + i*2], data[3 + i*2]]);
}
f.length = data[16];
f.rms = data[17];
f.fheight.copy_from_slice(&data[18..26]);
f.fwidth.copy_from_slice(&data[26..32]);
f.fright.copy_from_slice(&data[32..35]);
f.bw.copy_from_slice(&data[35..39]);
f.klattp.copy_from_slice(&data[39..44]);
Some(f)
}
pub fn to_bytes_t2(&self) -> [u8; FRAME_T2_SIZE] {
let mut d = [0u8; FRAME_T2_SIZE];
d[0..2].copy_from_slice(&self.frflags.to_le_bytes());
for i in 0..7 {
d[2 + i * 2..4 + i * 2].copy_from_slice(&self.ffreq[i].to_le_bytes());
}
d[16] = self.length;
d[17] = self.rms;
d[18..26].copy_from_slice(&self.fheight);
d[26..32].copy_from_slice(&self.fwidth);
d[32..35].copy_from_slice(&self.fright);
d[35..39].copy_from_slice(&self.bw);
d[39..44].copy_from_slice(&self.klattp);
d
}
pub fn to_bytes_t(&self) -> [u8; FRAME_T_SIZE] {
let mut d = [0u8; FRAME_T_SIZE];
d[..FRAME_T2_SIZE].copy_from_slice(&self.to_bytes_t2());
d[44..49].copy_from_slice(&self.klattp2);
d[49..56].copy_from_slice(&self.klatt_ap);
d[56..63].copy_from_slice(&self.klatt_bp);
d
}
pub fn from_bytes_t(data: &[u8]) -> Option<Self> {
if data.len() < FRAME_T_SIZE { return None; }
let mut f = SpectFrame::default();
f.frflags = u16::from_le_bytes([data[0], data[1]]);
for i in 0..7 {
f.ffreq[i] = i16::from_le_bytes([data[2 + i*2], data[3 + i*2]]);
}
f.length = data[16];
f.rms = data[17];
f.fheight.copy_from_slice(&data[18..26]);
f.fwidth.copy_from_slice(&data[26..32]);
f.fright.copy_from_slice(&data[32..35]);
f.bw.copy_from_slice(&data[35..39]);
f.klattp.copy_from_slice(&data[39..44]);
f.klattp2.copy_from_slice(&data[44..49]);
f.klatt_ap.copy_from_slice(&data[49..56]);
f.klatt_bp.copy_from_slice(&data[56..63]);
Some(f)
}
#[inline]
pub fn f1_hz(&self) -> f64 { self.ffreq[1] as f64 }
#[inline]
pub fn f2_hz(&self) -> f64 { self.ffreq[2] as f64 }
#[inline]
pub fn f3_hz(&self) -> f64 { self.ffreq[3] as f64 }
#[inline]
pub fn dur_samples(&self, speed_factor: f64) -> usize {
let base = (self.length as usize) * 64;
((base as f64 * speed_factor) as usize).max(1)
}
}
pub fn compile_spect_seq(frames: &[SpectFrame]) -> Vec<u8> {
let is_klatt = frames.first().is_some_and(|f| f.frflags & FRFLAG_KLATT != 0);
let frame_size = if is_klatt { FRAME_T_SIZE } else { FRAME_T2_SIZE };
let length_total: i16 = frames.iter().map(|f| f.length as i16).sum();
let mut out = Vec::with_capacity(4 + frames.len() * frame_size);
out.extend_from_slice(&length_total.to_le_bytes());
out.push(frames.len() as u8);
out.push(0); for f in frames {
if is_klatt {
out.extend_from_slice(&f.to_bytes_t());
} else {
out.extend_from_slice(&f.to_bytes_t2());
}
}
out
}
#[derive(Debug, Clone)]
pub struct SpectSeq {
pub frames: Vec<SpectFrame>,
pub is_klatt: bool,
}
impl SpectSeq {
pub fn parse(phondata: &[u8], offset: usize) -> Option<Self> {
if offset + 4 > phondata.len() { return None; }
let data = &phondata[offset..];
let n_frames = data[2] as usize;
if n_frames == 0 || n_frames > N_SEQ_FRAMES { return None; }
if data.len() < 4 + 2 { return None; } let first_frflags = u16::from_le_bytes([data[4], data[5]]);
let is_klatt = (first_frflags & FRFLAG_KLATT) != 0;
let frame_size = if is_klatt { FRAME_T_SIZE } else { FRAME_T2_SIZE };
if data.len() < 4 + n_frames * frame_size { return None; }
let mut frames = Vec::with_capacity(n_frames);
for i in 0..n_frames {
let base = 4 + i * frame_size;
let frame_data = &data[base..base + frame_size];
let frame = if is_klatt {
SpectFrame::from_bytes_t(frame_data)?
} else {
SpectFrame::from_bytes_t2(frame_data)?
};
frames.push(frame);
}
Some(SpectSeq { frames, is_klatt })
}
pub fn is_voiced(&self) -> bool {
self.frames.iter().any(|f| {
f.klattp[0] > 0 || f.ffreq[1] > 0
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spect_seq_compile_round_trip() {
let mut f1 = SpectFrame::default();
f1.frflags = 0x0002; f1.ffreq = [120, 730, 1090, 2440, 3400, 4000, 4500];
f1.length = 20;
f1.rms = 40;
f1.fheight = [1, 2, 3, 4, 5, 6, 7, 8];
f1.fwidth = [9, 10, 11, 12, 13, 14];
f1.fright = [1, 2, 3];
f1.bw = [4, 5, 6, 7];
f1.klattp = [50, 1, 2, 3, 4];
let mut f2 = f1.clone();
f2.ffreq[1] = 700;
f2.length = 25;
let frames = vec![f1.clone(), f2.clone()];
let bytes = compile_spect_seq(&frames);
let seq = SpectSeq::parse(&bytes, 0).expect("parse compiled SPECT_SEQ");
assert!(!seq.is_klatt);
assert_eq!(seq.frames.len(), 2);
assert_eq!(seq.frames[0].to_bytes_t2(), f1.to_bytes_t2());
assert_eq!(seq.frames[1].to_bytes_t2(), f2.to_bytes_t2());
assert_eq!(seq.frames[0].length, 20);
assert_eq!(seq.frames[1].ffreq[1], 700);
}
#[test]
fn spect_seqk_klatt_compile_round_trip() {
let mut f = SpectFrame::default();
f.frflags = FRFLAG_KLATT;
f.ffreq = [130, 700, 1200, 2500, 3300, 3900, 4400];
f.length = 18;
f.rms = 45;
f.klattp = [50, 1, 2, 3, 4];
f.klattp2 = [5, 6, 7, 8, 9];
f.klatt_ap = [1, 2, 3, 4, 5, 6, 7];
f.klatt_bp = [8, 9, 10, 11, 12, 13, 14];
let frames = vec![f.clone(), f.clone()];
let bytes = compile_spect_seq(&frames);
let seq = SpectSeq::parse(&bytes, 0).expect("parse compiled SPECT_SEQK");
assert!(seq.is_klatt, "should be detected as a Klatt sequence");
assert_eq!(seq.frames.len(), 2);
assert_eq!(seq.frames[0].to_bytes_t(), f.to_bytes_t());
assert_eq!(seq.frames[0].klatt_bp, [8, 9, 10, 11, 12, 13, 14]);
}
fn make_seq_1frame() -> Vec<u8> {
let mut data = vec![0u8; 4 + FRAME_T2_SIZE];
data[2] = 1; let f1 = 500i16.to_le_bytes();
let f2 = 1500i16.to_le_bytes();
data[4+2..4+4].copy_from_slice(&[0,0]); data[4+4..4+6].copy_from_slice(&f1); data[4+6..4+8].copy_from_slice(&f2); data[4+16] = 4; data[4+17] = 80; data
}
#[test]
fn parse_basic_seq() {
let raw = make_seq_1frame();
let seq = SpectSeq::parse(&raw, 0).expect("should parse");
assert_eq!(seq.frames.len(), 1);
assert!(!seq.is_klatt);
assert_eq!(seq.frames[0].f1_hz(), 500.0);
assert_eq!(seq.frames[0].f2_hz(), 1500.0);
assert_eq!(seq.frames[0].length, 4);
assert_eq!(seq.frames[0].rms, 80);
}
#[test]
fn parse_too_short_returns_none() {
let raw = vec![0u8; 3];
assert!(SpectSeq::parse(&raw, 0).is_none());
}
#[test]
fn parse_zero_frames_returns_none() {
let raw = vec![0u8; 8]; assert!(SpectSeq::parse(&raw, 0).is_none());
}
#[test]
fn parse_at_offset() {
let mut raw = vec![0xff_u8; 8]; let seq_data = make_seq_1frame();
raw.extend_from_slice(&seq_data);
let seq = SpectSeq::parse(&raw, 8).expect("should parse at offset 8");
assert_eq!(seq.frames.len(), 1);
assert_eq!(seq.frames[0].f1_hz(), 500.0);
}
#[test]
fn frame_t2_round_trip() {
let mut raw = [0u8; FRAME_T2_SIZE];
raw[0] = 2; raw[1] = 0;
raw[2] = 100; raw[3] = 0;
raw[4] = 244; raw[5] = 1; raw[6] = 220; raw[7] = 5; raw[16] = 8; raw[17] = 60; let frame = SpectFrame::from_bytes_t2(&raw).unwrap();
assert_eq!(frame.frflags, 2);
assert_eq!(frame.ffreq[1], 500);
assert_eq!(frame.ffreq[2], 1500);
assert_eq!(frame.length, 8);
assert_eq!(frame.rms, 60);
}
#[test]
fn frame_t_round_trip() {
let mut raw = [0u8; FRAME_T_SIZE];
raw[0] = 1; let frame = SpectFrame::from_bytes_t(&raw).unwrap();
assert_eq!(frame.frflags, 1);
}
#[test]
fn klatt_seq_detected() {
let mut raw = vec![0u8; 4 + FRAME_T_SIZE];
raw[2] = 1; raw[4] = 1; let seq = SpectSeq::parse(&raw, 0).expect("should parse");
assert!(seq.is_klatt);
assert_eq!(seq.frames.len(), 1);
}
#[test]
fn voiced_detection() {
let mut raw = make_seq_1frame();
raw[4+39] = 0;
let seq_unvoiced = SpectSeq::parse(&raw, 0).unwrap();
assert!(seq_unvoiced.is_voiced(), "non-zero F1 → voiced");
raw[4+4] = 0; raw[4+5] = 0; let seq_v2 = SpectSeq::parse(&raw, 0).unwrap();
assert!(!seq_v2.is_voiced());
}
#[test]
fn dur_samples_speed_factor() {
let mut f = SpectFrame::default();
f.length = 4;
assert_eq!(f.dur_samples(1.0), 256);
assert_eq!(f.dur_samples(0.5), 128);
}
}