Skip to main content

oximedia_timecode/vitc/
encoder.rs

1//! VITC Encoder - Generate timecode for video scan lines
2//!
3//! This module implements a complete VITC encoder that:
4//! - Encodes timecode and user bits to VITC bit patterns
5//! - Generates pixel data for embedding in video lines
6//! - Calculates and inserts CRC checksums
7//! - Handles field synchronization
8//! - Supports all standard video formats
9
10use std::collections::HashMap;
11use std::sync::OnceLock;
12
13use super::constants::*;
14use super::{VideoStandard, VitcWriterConfig};
15use crate::{FrameRate, Timecode, TimecodeError};
16
17// ── VITC pattern pre-computation cache ────────────────────────────────────────
18
19/// A pre-computed VITC line-insertion pattern for one frame rate.
20///
21/// The `scan_lines` field lists the standard VBI line numbers used by that
22/// frame rate; `bits_per_sample` is the pixel-clock ratio (how many pixels
23/// represent one VITC bit).
24#[derive(Debug, Clone)]
25pub struct VitcPattern {
26    /// Standard VBI scan lines for this frame rate.
27    pub scan_lines: Vec<u16>,
28    /// Nominal pixels per VITC bit at this standard.
29    pub bits_per_sample: u8,
30}
31
32/// Cache of pre-computed CRC table and per-frame-rate VITC insertion patterns.
33#[derive(Debug)]
34pub struct VitcPatternCache {
35    /// 256-entry CRC lookup table using polynomial `x^8 + x^2 + x + 1`.
36    pub crc_table: [u8; 256],
37    /// Pre-computed patterns for common broadcast frame rates.
38    pub patterns: HashMap<u8, VitcPattern>,
39}
40
41/// Build the 256-entry CRC lookup table for VITC (poly `0x07`).
42fn build_crc_table() -> [u8; 256] {
43    let mut table = [0u8; 256];
44    for (i, entry) in table.iter_mut().enumerate() {
45        let mut byte = i as u8;
46        for _ in 0..8 {
47            let feedback = (byte & 0x80) != 0;
48            byte <<= 1;
49            if feedback {
50                byte ^= 0x07;
51            }
52        }
53        *entry = byte;
54    }
55    table
56}
57
58/// Return a reference to the global (lazily initialised) `VitcPatternCache`.
59///
60/// The first call constructs the cache; subsequent calls return the already-
61/// computed reference.
62pub fn get_vitc_cache() -> &'static VitcPatternCache {
63    static CACHE: OnceLock<VitcPatternCache> = OnceLock::new();
64    CACHE.get_or_init(|| {
65        let crc_table = build_crc_table();
66
67        let mut patterns = HashMap::new();
68
69        // 23.976 / 24 fps — film / cinema, 525-line NTSC timing
70        patterns.insert(
71            24u8,
72            VitcPattern {
73                scan_lines: vec![14, 16],
74                bits_per_sample: 2,
75            },
76        );
77
78        // 25 fps — PAL/SECAM broadcast
79        patterns.insert(
80            25u8,
81            VitcPattern {
82                scan_lines: vec![19, 21],
83                bits_per_sample: 2,
84            },
85        );
86
87        // 29.97 / 30 fps — NTSC broadcast
88        patterns.insert(
89            30u8,
90            VitcPattern {
91                scan_lines: vec![14, 16],
92                bits_per_sample: 2,
93            },
94        );
95
96        // 50 fps — PAL progressive
97        patterns.insert(
98            50u8,
99            VitcPattern {
100                scan_lines: vec![7, 9, 320, 322],
101                bits_per_sample: 2,
102            },
103        );
104
105        // 59.94 / 60 fps — NTSC progressive
106        patterns.insert(
107            60u8,
108            VitcPattern {
109                scan_lines: vec![7, 9, 270, 272],
110                bits_per_sample: 2,
111            },
112        );
113
114        VitcPatternCache {
115            crc_table,
116            patterns,
117        }
118    })
119}
120
121/// VITC encoder
122pub struct VitcEncoder {
123    /// Configuration
124    #[allow(dead_code)]
125    config: VitcWriterConfig,
126    /// Current field being encoded
127    current_field: u8,
128}
129
130impl VitcEncoder {
131    /// Create a new VITC encoder
132    pub fn new(config: VitcWriterConfig) -> Self {
133        VitcEncoder {
134            config,
135            current_field: 1,
136        }
137    }
138
139    /// Encode a timecode to pixel data for a video line
140    pub fn encode_line(
141        &mut self,
142        timecode: &Timecode,
143        field: u8,
144    ) -> Result<Vec<u8>, TimecodeError> {
145        // Create bit array
146        let bits = self.timecode_to_bits(timecode, field)?;
147
148        // Convert bits to pixels
149        let pixels = self.bits_to_pixels(&bits);
150
151        Ok(pixels)
152    }
153
154    /// Convert timecode to VITC bit array
155    fn timecode_to_bits(
156        &self,
157        timecode: &Timecode,
158        field: u8,
159    ) -> Result<[bool; BITS_PER_LINE], TimecodeError> {
160        let mut bits = [false; BITS_PER_LINE];
161
162        // Start sync bits (0-1): 11 (white-white)
163        bits[0] = true;
164        bits[1] = true;
165
166        // Data bits start at position 2
167        let mut data_bits = [false; DATA_BITS];
168
169        // Decompose timecode
170        let frame_units = timecode.frames % 10;
171        let frame_tens = timecode.frames / 10;
172        let second_units = timecode.seconds % 10;
173        let second_tens = timecode.seconds / 10;
174        let minute_units = timecode.minutes % 10;
175        let minute_tens = timecode.minutes / 10;
176        let hour_units = timecode.hours % 10;
177        let hour_tens = timecode.hours / 10;
178
179        // Encode frame units (bits 0-3)
180        self.encode_bcd(&mut data_bits, 0, frame_units);
181
182        // User bits 1 (bits 4-7)
183        self.encode_nibble(&mut data_bits, 4, (timecode.user_bits & 0xF) as u8);
184
185        // Frame tens (bits 8-9)
186        self.encode_bcd(&mut data_bits, 8, frame_tens);
187
188        // Drop frame flag (bit 10)
189        data_bits[10] = timecode.frame_rate.drop_frame;
190
191        // Color frame flag (bit 11)
192        data_bits[11] = false;
193
194        // User bits 2 (bits 12-15)
195        self.encode_nibble(&mut data_bits, 12, ((timecode.user_bits >> 4) & 0xF) as u8);
196
197        // Second units (bits 16-19)
198        self.encode_bcd(&mut data_bits, 16, second_units);
199
200        // User bits 3 (bits 20-23)
201        self.encode_nibble(&mut data_bits, 20, ((timecode.user_bits >> 8) & 0xF) as u8);
202
203        // Second tens (bits 24-26)
204        self.encode_bcd(&mut data_bits, 24, second_tens);
205
206        // Field mark (bit 27) - 0 for field 1, 1 for field 2
207        data_bits[27] = field == 2;
208
209        // User bits 4 (bits 28-31)
210        self.encode_nibble(&mut data_bits, 28, ((timecode.user_bits >> 12) & 0xF) as u8);
211
212        // Minute units (bits 32-35)
213        self.encode_bcd(&mut data_bits, 32, minute_units);
214
215        // User bits 5 (bits 36-39)
216        self.encode_nibble(&mut data_bits, 36, ((timecode.user_bits >> 16) & 0xF) as u8);
217
218        // Minute tens (bits 40-42)
219        self.encode_bcd(&mut data_bits, 40, minute_tens);
220
221        // Binary group flag (bit 43)
222        data_bits[43] = false;
223
224        // User bits 6 (bits 44-47)
225        self.encode_nibble(&mut data_bits, 44, ((timecode.user_bits >> 20) & 0xF) as u8);
226
227        // Hour units (bits 48-51)
228        self.encode_bcd(&mut data_bits, 48, hour_units);
229
230        // User bits 7 (bits 52-55)
231        self.encode_nibble(&mut data_bits, 52, ((timecode.user_bits >> 24) & 0xF) as u8);
232
233        // Hour tens (bits 56-57)
234        self.encode_bcd(&mut data_bits, 56, hour_tens);
235
236        // Reserved bit (58)
237        data_bits[58] = false;
238
239        // User bits 8 (bits 59-73)
240        self.encode_nibble(&mut data_bits, 59, ((timecode.user_bits >> 28) & 0xF) as u8);
241
242        // Calculate and insert CRC (bits 74-81)
243        let crc = self.calculate_crc(&data_bits[0..72]);
244        for i in 0..8 {
245            data_bits[74 + i] = (crc & (1 << i)) != 0;
246        }
247
248        // Copy data bits to main bit array
249        bits[SYNC_START_BITS..(DATA_BITS + SYNC_START_BITS)]
250            .copy_from_slice(&data_bits[..DATA_BITS]);
251
252        // End sync bits (84-89): 001111 (black-black-white-white-white-white)
253        bits[84] = false;
254        bits[85] = false;
255        bits[86] = true;
256        bits[87] = true;
257        bits[88] = true;
258        bits[89] = true;
259
260        Ok(bits)
261    }
262
263    /// Encode a BCD digit
264    fn encode_bcd(&self, bits: &mut [bool; DATA_BITS], start: usize, value: u8) {
265        for i in 0..4 {
266            if start + i < DATA_BITS {
267                bits[start + i] = (value & (1 << i)) != 0;
268            }
269        }
270    }
271
272    /// Encode a 4-bit nibble
273    fn encode_nibble(&self, bits: &mut [bool; DATA_BITS], start: usize, value: u8) {
274        for i in 0..4 {
275            if start + i < DATA_BITS {
276                bits[start + i] = (value & (1 << i)) != 0;
277            }
278        }
279    }
280
281    /// Calculate CRC for VITC
282    fn calculate_crc(&self, bits: &[bool]) -> u8 {
283        let mut crc = 0u8;
284
285        for &bit in bits {
286            let feedback = ((crc & 0x80) != 0) ^ bit;
287            crc <<= 1;
288            if feedback {
289                crc ^= 0x07; // Polynomial: x^8 + x^2 + x^1 + x^0
290            }
291        }
292
293        crc
294    }
295
296    /// Convert bits to pixel data
297    fn bits_to_pixels(&self, bits: &[bool; BITS_PER_LINE]) -> Vec<u8> {
298        let mut pixels = Vec::with_capacity(BITS_PER_LINE * PIXELS_PER_BIT);
299
300        for &bit in bits {
301            let level = if bit { WHITE_LEVEL } else { BLACK_LEVEL };
302
303            // Each bit is PIXELS_PER_BIT pixels wide
304            for _ in 0..PIXELS_PER_BIT {
305                pixels.push(level);
306            }
307        }
308
309        pixels
310    }
311
312    /// Reset encoder state
313    pub fn reset(&mut self) {
314        self.current_field = 1;
315    }
316
317    /// Set current field
318    pub fn set_field(&mut self, field: u8) {
319        self.current_field = field;
320    }
321
322    /// Get current field
323    pub fn field(&self) -> u8 {
324        self.current_field
325    }
326}
327
328/// Multi-line VITC writer for redundancy
329pub struct MultiLineVitcWriter {
330    /// Encoder
331    encoder: VitcEncoder,
332    /// Lines to write
333    lines: Vec<u16>,
334}
335
336impl MultiLineVitcWriter {
337    /// Create a multi-line writer
338    pub fn new(config: VitcWriterConfig) -> Self {
339        let lines = config.scan_lines.clone();
340        MultiLineVitcWriter {
341            encoder: VitcEncoder::new(config),
342            lines,
343        }
344    }
345
346    /// Encode timecode for all configured lines
347    pub fn encode_all_lines(
348        &mut self,
349        timecode: &Timecode,
350        field: u8,
351    ) -> Result<Vec<(u16, Vec<u8>)>, TimecodeError> {
352        let mut results = Vec::new();
353
354        for &line in &self.lines {
355            let pixels = self.encoder.encode_line(timecode, field)?;
356            results.push((line, pixels));
357        }
358
359        Ok(results)
360    }
361}
362
363/// VITC line buffer for video frame integration
364pub struct VitcLineBuffer {
365    /// Video standard
366    #[allow(dead_code)]
367    video_standard: VideoStandard,
368    /// Frame buffer (stores VITC lines only)
369    lines: Vec<(u16, u8, Vec<u8>)>, // (line_number, field, pixels)
370}
371
372impl VitcLineBuffer {
373    /// Create a new line buffer
374    pub fn new(video_standard: VideoStandard) -> Self {
375        VitcLineBuffer {
376            video_standard,
377            lines: Vec::new(),
378        }
379    }
380
381    /// Add a VITC line to the buffer
382    pub fn add_line(&mut self, line_number: u16, field: u8, pixels: Vec<u8>) {
383        // Remove existing line with same line number and field
384        self.lines
385            .retain(|(ln, f, _)| !(*ln == line_number && *f == field));
386
387        self.lines.push((line_number, field, pixels));
388    }
389
390    /// Get all lines for a specific field
391    pub fn get_field_lines(&self, field: u8) -> Vec<(u16, &[u8])> {
392        self.lines
393            .iter()
394            .filter(|(_, f, _)| *f == field)
395            .map(|(ln, _, pixels)| (*ln, pixels.as_slice()))
396            .collect()
397    }
398
399    /// Clear the buffer
400    pub fn clear(&mut self) {
401        self.lines.clear();
402    }
403
404    /// Get total lines
405    pub fn line_count(&self) -> usize {
406        self.lines.len()
407    }
408}
409
410/// Pixel level adjustment for different video standards
411pub struct PixelLevelAdjuster {
412    /// Black level
413    black_level: u8,
414    /// White level
415    white_level: u8,
416}
417
418impl PixelLevelAdjuster {
419    /// Create adjuster for video standard
420    pub fn new(video_standard: VideoStandard) -> Self {
421        match video_standard {
422            VideoStandard::Ntsc => PixelLevelAdjuster {
423                black_level: 16,
424                white_level: 235,
425            },
426            VideoStandard::Pal => PixelLevelAdjuster {
427                black_level: 16,
428                white_level: 235,
429            },
430        }
431    }
432
433    /// Create with custom levels
434    pub fn with_levels(black_level: u8, white_level: u8) -> Self {
435        PixelLevelAdjuster {
436            black_level,
437            white_level,
438        }
439    }
440
441    /// Adjust bit to pixel level
442    pub fn bit_to_pixel(&self, bit: bool) -> u8 {
443        if bit {
444            self.white_level
445        } else {
446            self.black_level
447        }
448    }
449
450    /// Get black level
451    pub fn black_level(&self) -> u8 {
452        self.black_level
453    }
454
455    /// Get white level
456    pub fn white_level(&self) -> u8 {
457        self.white_level
458    }
459}
460
461/// Rise time shaper for cleaner edges
462pub struct RiseTimeShaper {
463    /// Rise time in pixels
464    rise_time_pixels: usize,
465}
466
467impl RiseTimeShaper {
468    /// Create with rise time
469    pub fn new(rise_time_pixels: usize) -> Self {
470        RiseTimeShaper {
471            rise_time_pixels: rise_time_pixels.max(1),
472        }
473    }
474
475    /// Shape pixel transitions
476    pub fn shape_pixels(&self, pixels: &[u8]) -> Vec<u8> {
477        let mut shaped = Vec::with_capacity(pixels.len());
478
479        if pixels.is_empty() {
480            return shaped;
481        }
482
483        shaped.push(pixels[0]);
484
485        for i in 1..pixels.len() {
486            let current = pixels[i];
487            let prev = pixels[i - 1];
488
489            if current != prev {
490                // Transition detected - apply shaping
491                let diff = current as i16 - prev as i16;
492                for j in 0..self.rise_time_pixels.min(pixels.len() - i) {
493                    let progress = (j + 1) as f32 / self.rise_time_pixels as f32;
494                    let value = prev as f32 + diff as f32 * progress;
495                    shaped.push(value as u8);
496                }
497                // Skip the shaped pixels
498                for _ in 0..self.rise_time_pixels.min(pixels.len() - i) {
499                    if i < pixels.len() {
500                        shaped.push(current);
501                    }
502                }
503            } else {
504                shaped.push(current);
505            }
506        }
507
508        shaped.truncate(pixels.len());
509        shaped
510    }
511}
512
513/// Blanking level inserter
514pub struct BlankingInserter {
515    /// Blanking level
516    blanking_level: u8,
517}
518
519impl BlankingInserter {
520    /// Create with blanking level
521    pub fn new(blanking_level: u8) -> Self {
522        BlankingInserter { blanking_level }
523    }
524
525    /// Insert blanking before and after VITC
526    pub fn insert_blanking(
527        &self,
528        vitc_pixels: &[u8],
529        total_width: usize,
530        start_offset: usize,
531    ) -> Vec<u8> {
532        let mut full_line = vec![self.blanking_level; total_width];
533
534        // Copy VITC pixels at the specified offset
535        let end_offset = (start_offset + vitc_pixels.len()).min(total_width);
536        for (i, &pixel) in vitc_pixels.iter().enumerate() {
537            if start_offset + i < end_offset {
538                full_line[start_offset + i] = pixel;
539            }
540        }
541
542        full_line
543    }
544}
545
546/// VITC frame generator for continuous encoding
547pub struct VitcFrameGenerator {
548    /// Writer
549    writer: MultiLineVitcWriter,
550    /// Current timecode
551    current_timecode: Option<Timecode>,
552    /// Frame rate
553    #[allow(dead_code)]
554    frame_rate: FrameRate,
555}
556
557impl VitcFrameGenerator {
558    /// Create a new frame generator
559    pub fn new(config: VitcWriterConfig) -> Self {
560        let frame_rate = config.frame_rate;
561        VitcFrameGenerator {
562            writer: MultiLineVitcWriter::new(config),
563            current_timecode: None,
564            frame_rate,
565        }
566    }
567
568    /// Set starting timecode
569    pub fn set_timecode(&mut self, timecode: Timecode) {
570        self.current_timecode = Some(timecode);
571    }
572
573    /// Generate VITC for next frame
574    pub fn generate_frame(&mut self) -> Result<Vec<(u16, u8, Vec<u8>)>, TimecodeError> {
575        if let Some(ref mut tc) = self.current_timecode {
576            let mut results = Vec::new();
577
578            // Generate for field 1
579            let field1_lines = self.writer.encode_all_lines(tc, 1)?;
580            for (line, pixels) in field1_lines {
581                results.push((line, 1, pixels));
582            }
583
584            // Generate for field 2
585            let field2_lines = self.writer.encode_all_lines(tc, 2)?;
586            for (line, pixels) in field2_lines {
587                results.push((line, 2, pixels));
588            }
589
590            // Increment timecode
591            tc.increment()?;
592
593            Ok(results)
594        } else {
595            Err(TimecodeError::InvalidConfiguration)
596        }
597    }
598
599    /// Get current timecode
600    pub fn current_timecode(&self) -> Option<&Timecode> {
601        self.current_timecode.as_ref()
602    }
603}
604
605/// User bits helpers for VITC
606pub struct VitcUserBitsHelper;
607
608impl VitcUserBitsHelper {
609    /// Validate user bits for VITC
610    pub fn validate_user_bits(user_bits: u32) -> bool {
611        // User bits are 32 bits, all values are valid
612        let _ = user_bits;
613        true
614    }
615
616    /// Extract user bits group
617    pub fn extract_group(user_bits: u32, group: u8) -> u8 {
618        let shift = (group as u32) * 4;
619        ((user_bits >> shift) & 0xF) as u8
620    }
621
622    /// Set user bits group
623    pub fn set_group(user_bits: u32, group: u8, value: u8) -> u32 {
624        let shift = (group as u32) * 4;
625        let mask = !(0xF << shift);
626        (user_bits & mask) | ((value as u32 & 0xF) << shift)
627    }
628}
629
630/// Pixel pattern validator
631pub struct PixelPatternValidator;
632
633impl PixelPatternValidator {
634    /// Validate that pixel pattern is suitable for VITC
635    pub fn validate_pattern(pixels: &[u8]) -> bool {
636        if pixels.len() < BITS_PER_LINE * PIXELS_PER_BIT {
637            return false;
638        }
639
640        // Check that pixels are within valid range
641        for &pixel in pixels {
642            if !(16..=235).contains(&pixel) {
643                return false;
644            }
645        }
646
647        true
648    }
649
650    /// Check sync pattern
651    pub fn check_sync_pattern(pixels: &[u8]) -> bool {
652        if pixels.len() < 4 {
653            return false;
654        }
655
656        // First 4 pixels should be white (start sync)
657        pixels[0] > 200 && pixels[1] > 200 && pixels[2] > 200 && pixels[3] > 200
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn test_encoder_creation() {
667        let config = VitcWriterConfig::default();
668        let encoder = VitcEncoder::new(config);
669        assert_eq!(encoder.field(), 1);
670    }
671
672    #[test]
673    fn test_encode_line() {
674        let config = VitcWriterConfig::default();
675        let mut encoder = VitcEncoder::new(config);
676
677        let timecode = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
678        let pixels = encoder
679            .encode_line(&timecode, 1)
680            .expect("encode should succeed");
681
682        assert_eq!(pixels.len(), BITS_PER_LINE * PIXELS_PER_BIT);
683    }
684
685    #[test]
686    fn test_crc_calculation() {
687        let config = VitcWriterConfig::default();
688        let encoder = VitcEncoder::new(config);
689
690        let bits = [false; 72];
691        let crc = encoder.calculate_crc(&bits);
692        assert_eq!(crc, 0);
693    }
694
695    #[test]
696    fn test_pixel_level_adjuster() {
697        let adjuster = PixelLevelAdjuster::new(VideoStandard::Pal);
698        assert_eq!(adjuster.bit_to_pixel(false), 16);
699        assert_eq!(adjuster.bit_to_pixel(true), 235);
700    }
701
702    #[test]
703    fn test_user_bits_helper() {
704        let user_bits = 0x12345678u32;
705        assert_eq!(VitcUserBitsHelper::extract_group(user_bits, 0), 0x8);
706        assert_eq!(VitcUserBitsHelper::extract_group(user_bits, 1), 0x7);
707
708        let modified = VitcUserBitsHelper::set_group(user_bits, 0, 0xA);
709        assert_eq!(VitcUserBitsHelper::extract_group(modified, 0), 0xA);
710    }
711
712    #[test]
713    fn test_line_buffer() {
714        let mut buffer = VitcLineBuffer::new(VideoStandard::Pal);
715        buffer.add_line(19, 1, vec![16; 180]);
716        buffer.add_line(21, 1, vec![16; 180]);
717
718        assert_eq!(buffer.line_count(), 2);
719
720        let field1_lines = buffer.get_field_lines(1);
721        assert_eq!(field1_lines.len(), 2);
722    }
723
724    // ── VitcPatternCache tests ──────────────────────────────────────────────
725
726    #[test]
727    fn test_vitc_cache_is_computed_on_first_call() {
728        let cache = get_vitc_cache();
729        // CRC table must be populated: table[0] = 0, table[1] = poly value 0x07.
730        assert_eq!(cache.crc_table[0], 0);
731        assert_eq!(cache.crc_table[1], 7);
732    }
733
734    #[test]
735    fn test_vitc_cache_second_call_returns_same_reference() {
736        let ptr1 = get_vitc_cache() as *const VitcPatternCache;
737        let ptr2 = get_vitc_cache() as *const VitcPatternCache;
738        assert_eq!(ptr1, ptr2, "OnceLock must return the same static reference");
739    }
740
741    #[test]
742    fn test_vitc_pattern_25fps_scan_lines() {
743        let cache = get_vitc_cache();
744        let pat = cache
745            .patterns
746            .get(&25)
747            .expect("25fps pattern must be present");
748        assert!(
749            pat.scan_lines.contains(&19),
750            "PAL VITC must include line 19"
751        );
752        assert!(
753            pat.scan_lines.contains(&21),
754            "PAL VITC must include line 21"
755        );
756    }
757
758    #[test]
759    fn test_vitc_pattern_2997fps_scan_lines() {
760        let cache = get_vitc_cache();
761        // 29.97 is stored under nominal fps=30.
762        let pat = cache
763            .patterns
764            .get(&30)
765            .expect("30fps pattern must be present");
766        assert!(
767            !pat.scan_lines.is_empty(),
768            "30fps VITC must have at least one scan line"
769        );
770    }
771
772    #[test]
773    fn test_vitc_crc_table_256_entries() {
774        let cache = get_vitc_cache();
775        assert_eq!(cache.crc_table.len(), 256);
776    }
777
778    #[test]
779    fn test_vitc_crc_table_correct_polynomial() {
780        // Verify a few known values for poly 0x07 CRC:
781        // table[0] = 0 (all-zero input → zero remainder)
782        // table[1] = 7 (0b0000_0001 → remainder 0x07)
783        // table[128] = 0x89 (computed via shift-register for 0b1000_0000)
784        let cache = get_vitc_cache();
785        assert_eq!(cache.crc_table[0], 0);
786        assert_eq!(cache.crc_table[1], 7);
787        assert_eq!(cache.crc_table[128], 0x89);
788    }
789
790    #[test]
791    fn test_vitc_pattern_common_rates_present() {
792        let cache = get_vitc_cache();
793        // All five standard entries must be present.
794        for fps in [24u8, 25, 30, 50, 60] {
795            assert!(
796                cache.patterns.contains_key(&fps),
797                "pattern for {}fps must be in cache",
798                fps
799            );
800        }
801    }
802}