Skip to main content

rgpre/import/
smacker.rs

1//! Smacker header metadata; compressed video/audio decoding is external.
2/// One movie's source dimensions, timing and vertical display flag.
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct SmackerHeader {
5    pub width: u32,
6    pub height: u32,
7    pub frames: u32,
8    pub frame_micros: u32,
9    pub double_height: bool,
10    /// At least one of the seven audio stream descriptors has a sample rate.
11    pub has_audio: bool,
12}
13impl SmackerHeader {
14    /// Read the fixed header of an SMK2/SMK4 stream. See docs/formats/SMK.md.
15    pub fn parse(bytes: &[u8]) -> Result<Self, String> {
16        if bytes.len() < 104 || !matches!(&bytes[..4], b"SMK2" | b"SMK4") {
17            return Err("SMK: missing or invalid header".into());
18        }
19        let number = |i| u32::from_le_bytes(bytes[i..i + 4].try_into().unwrap());
20        let width = number(4);
21        let height = number(8);
22        let frames = number(12);
23        let timing = number(16) as i32;
24        let frame_micros = if timing < 0 {
25            timing.unsigned_abs().checked_mul(10)
26        } else {
27            (timing as u32).checked_mul(1000)
28        }
29        .filter(|v| *v > 0)
30        .ok_or("SMK: invalid frame timing")?;
31        if width == 0 || height == 0 || width > 4096 || height > 4096 || frames == 0 {
32            return Err("SMK: invalid dimensions/frame count".into());
33        }
34        Ok(Self {
35            width,
36            height,
37            frames,
38            frame_micros,
39            double_height: number(20) & 6 != 0,
40            has_audio: (72..100).step_by(4).any(|i| number(i) & 0x00ff_ffff != 0),
41        })
42    }
43}
44#[cfg(test)]
45mod tests {
46    use super::*;
47    #[test]
48    fn negative_timing_is_hundredths_of_a_millisecond() {
49        let mut header = [0; 104];
50        header[..4].copy_from_slice(b"SMK2");
51        for (offset, value) in [
52            (4, 640u32),
53            (8, 240),
54            (12, 6895),
55            (16, (-6666i32) as u32),
56            (20, 2),
57        ] {
58            header[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
59        }
60        assert_eq!(
61            SmackerHeader::parse(&header).unwrap(),
62            SmackerHeader {
63                width: 640,
64                height: 240,
65                frames: 6895,
66                frame_micros: 66660,
67                double_height: true,
68                has_audio: false,
69            }
70        );
71        assert!(SmackerHeader::parse(&header[..24]).is_err());
72        header[16..20].fill(0);
73        assert!(SmackerHeader::parse(&header).is_err());
74    }
75}