pub const N_SEQ_FRAMES: usize = 25;
pub const FRFLAG_KLATT: u16 = 0x01;
pub const FRFLAG_VOWEL_CENTRE: u16 = 0x02;
pub const FRFLAG_BREAK: u16 = 0x10;
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 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)
}
}
#[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::*;
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);
}
}