Skip to main content

oximedia_timecode/
reader.rs

1//! Timecode reader/decoder module.
2//!
3//! Provides VITC parsing, LTC decode approximation, and timecode validation.
4
5#![allow(dead_code)]
6#![allow(clippy::cast_precision_loss)]
7
8use crate::{FrameRate, FrameRateInfo, Timecode, TimecodeError};
9
10/// Result of a VITC line parse attempt.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct VitcParseResult {
13    /// Parsed timecode, if successful.
14    pub timecode: Option<Timecode>,
15    /// Whether the CRC matched.
16    pub crc_ok: bool,
17    /// Raw nibbles extracted from the line.
18    pub raw_nibbles: Vec<u8>,
19}
20
21/// VITC (Vertical Interval Timecode) parser.
22///
23/// Parses timecode data embedded in the vertical blanking interval of a video signal.
24#[derive(Debug, Clone)]
25pub struct VitcParser {
26    frame_rate: FrameRate,
27    clock_period_samples: usize,
28    sync_threshold: f32,
29}
30
31impl VitcParser {
32    /// Create a new VITC parser for the given frame rate and pixel clock.
33    pub fn new(frame_rate: FrameRate, pixels_per_line: usize) -> Self {
34        // VITC uses 90 bits per line: 2 sync bits + 8 groups of 9 bits + 2 sync bits
35        let clock_period_samples = pixels_per_line / 90;
36        Self {
37            frame_rate,
38            clock_period_samples: clock_period_samples.max(1),
39            sync_threshold: 0.5,
40        }
41    }
42
43    /// Set the sync detection threshold (0.0–1.0).
44    pub fn set_sync_threshold(&mut self, threshold: f32) {
45        self.sync_threshold = threshold.clamp(0.0, 1.0);
46    }
47
48    /// Parse a VITC scan line represented as normalized pixel values (0.0–1.0).
49    pub fn parse_line(&self, pixels: &[f32]) -> VitcParseResult {
50        if pixels.len() < 90 {
51            return VitcParseResult {
52                timecode: None,
53                crc_ok: false,
54                raw_nibbles: Vec::new(),
55            };
56        }
57
58        let bits = self.sample_bits(pixels);
59        if bits.len() < 90 {
60            return VitcParseResult {
61                timecode: None,
62                crc_ok: false,
63                raw_nibbles: Vec::new(),
64            };
65        }
66
67        // Check sync pattern: bits 0-1 should be 1,0 and bits 88-89 should be 0,1
68        let sync_ok = bits[0] == 1 && bits[1] == 0 && bits[88] == 0 && bits[89] == 1;
69        if !sync_ok {
70            return VitcParseResult {
71                timecode: None,
72                crc_ok: false,
73                raw_nibbles: Vec::new(),
74            };
75        }
76
77        // Extract 8 groups of 9 bits (bits 2..89)
78        let mut nibbles = Vec::with_capacity(16);
79        let mut crc_accum: u8 = 0;
80        for group in 0..8 {
81            let base = 2 + group * 9;
82            let lo_nibble =
83                bits[base] | (bits[base + 1] << 1) | (bits[base + 2] << 2) | (bits[base + 3] << 3);
84            let hi_nibble = bits[base + 4]
85                | (bits[base + 5] << 1)
86                | (bits[base + 6] << 2)
87                | (bits[base + 7] << 3);
88            // bit 8 of each group is the CRC bit for that group
89            let _group_crc = bits[base + 8];
90            nibbles.push(lo_nibble);
91            nibbles.push(hi_nibble);
92            crc_accum ^= lo_nibble ^ hi_nibble;
93        }
94
95        let crc_ok = crc_accum == 0;
96
97        // Decode timecode from nibbles
98        // Group 0: frames units/tens
99        // Group 1: seconds units/tens
100        // Group 2: minutes units/tens
101        // Group 3: hours units/tens
102        let frames_units = nibbles[0] & 0x0F;
103        let frames_tens = nibbles[1] & 0x03;
104        let seconds_units = nibbles[2] & 0x0F;
105        let seconds_tens = nibbles[3] & 0x07;
106        let minutes_units = nibbles[4] & 0x0F;
107        let minutes_tens = nibbles[5] & 0x07;
108        let hours_units = nibbles[6] & 0x0F;
109        let hours_tens = nibbles[7] & 0x03;
110
111        let frames = frames_tens * 10 + frames_units;
112        let seconds = seconds_tens * 10 + seconds_units;
113        let minutes = minutes_tens * 10 + minutes_units;
114        let hours = hours_tens * 10 + hours_units;
115
116        let timecode = Timecode::new(hours, minutes, seconds, frames, self.frame_rate).ok();
117
118        VitcParseResult {
119            timecode,
120            crc_ok,
121            raw_nibbles: nibbles,
122        }
123    }
124
125    /// Sample bits from pixel array at clock-period intervals.
126    fn sample_bits(&self, pixels: &[f32]) -> Vec<u8> {
127        let period = self.clock_period_samples.max(1);
128        let count = pixels.len() / period;
129        pixels
130            .chunks(period)
131            .take(count)
132            .map(|chunk| {
133                let avg = chunk.iter().sum::<f32>() / chunk.len() as f32;
134                if avg >= self.sync_threshold {
135                    1u8
136                } else {
137                    0u8
138                }
139            })
140            .collect()
141    }
142}
143
144/// LTC (Linear Timecode) decoder state.
145#[derive(Debug, Clone)]
146pub struct LtcDecoder {
147    frame_rate: FrameRate,
148    sample_rate: u32,
149    /// Internal ring buffer for edge detection.
150    buffer: Vec<f32>,
151    buffer_pos: usize,
152    half_period_samples: usize,
153    decoded_bits: Vec<u8>,
154    last_sample: f32,
155    bit_count: usize,
156}
157
158impl LtcDecoder {
159    /// Create a new LTC decoder.
160    pub fn new(frame_rate: FrameRate, sample_rate: u32) -> Self {
161        let fps = frame_rate.frames_per_second() as f64;
162        let bits_per_frame = 80usize;
163        let samples_per_frame = sample_rate as f64 / fps;
164        let samples_per_bit = samples_per_frame / bits_per_frame as f64;
165        let half_period = (samples_per_bit / 2.0).round() as usize;
166
167        Self {
168            frame_rate,
169            sample_rate,
170            buffer: vec![0.0; half_period * 2],
171            buffer_pos: 0,
172            half_period_samples: half_period.max(1),
173            decoded_bits: Vec::with_capacity(80),
174            last_sample: 0.0,
175            bit_count: 0,
176        }
177    }
178
179    /// Feed audio samples and return any decoded timecodes.
180    pub fn feed(&mut self, samples: &[f32]) -> Vec<Timecode> {
181        let mut results = Vec::new();
182
183        for &s in samples {
184            // Detect zero crossings for biphase mark decoding
185            let crossed = (s >= 0.0) != (self.last_sample >= 0.0);
186            self.last_sample = s;
187
188            if crossed {
189                self.bit_count += 1;
190                // Every two half-periods is one bit
191                if self.bit_count.is_multiple_of(2) {
192                    self.decoded_bits.push(1);
193                } else {
194                    self.decoded_bits.push(0);
195                }
196
197                if self.decoded_bits.len() >= 80 {
198                    if let Some(tc) = self.try_decode_frame() {
199                        results.push(tc);
200                    }
201                    self.decoded_bits.clear();
202                    self.bit_count = 0;
203                }
204            }
205
206            self.buffer[self.buffer_pos] = s;
207            self.buffer_pos = (self.buffer_pos + 1) % self.buffer.len();
208        }
209
210        results
211    }
212
213    /// Attempt to decode a timecode frame from accumulated bits.
214    fn try_decode_frame(&self) -> Option<Timecode> {
215        if self.decoded_bits.len() < 80 {
216            return None;
217        }
218
219        // LTC sync word: bits 64-79 = 0011111111111101
220        let sync_pattern = [0u8, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1];
221        let sync_ok = self.decoded_bits[64..80]
222            .iter()
223            .zip(sync_pattern.iter())
224            .all(|(a, b)| a == b);
225
226        if !sync_ok {
227            return None;
228        }
229
230        let frames = self.extract_bcd(0, 4, 2);
231        let seconds = self.extract_bcd(8, 4, 3);
232        let minutes = self.extract_bcd(24, 4, 3);
233        let hours = self.extract_bcd(40, 4, 2);
234
235        Timecode::new(hours, minutes, seconds, frames, self.frame_rate).ok()
236    }
237
238    /// Extract BCD value from bit array.
239    fn extract_bcd(&self, offset: usize, unit_bits: usize, tens_bits: usize) -> u8 {
240        let mut units = 0u8;
241        for i in 0..unit_bits {
242            if offset + i < self.decoded_bits.len() {
243                units |= self.decoded_bits[offset + i] << i;
244            }
245        }
246        let mut tens = 0u8;
247        for i in 0..tens_bits {
248            let idx = offset + unit_bits + 1 + i; // +1 skips user bit
249            if idx < self.decoded_bits.len() {
250                tens |= self.decoded_bits[idx] << i;
251            }
252        }
253        tens * 10 + units
254    }
255
256    /// Get the configured frame rate.
257    pub fn frame_rate(&self) -> FrameRate {
258        self.frame_rate
259    }
260
261    /// Get the configured sample rate.
262    pub fn sample_rate(&self) -> u32 {
263        self.sample_rate
264    }
265}
266
267/// Timecode validator.
268#[derive(Debug, Clone)]
269pub struct TimecodeValidator {
270    frame_rate: FrameRate,
271    last_tc: Option<Timecode>,
272    discontinuity_count: u32,
273    validate_continuity: bool,
274}
275
276impl TimecodeValidator {
277    /// Create a new timecode validator.
278    pub fn new(frame_rate: FrameRate) -> Self {
279        Self {
280            frame_rate,
281            last_tc: None,
282            discontinuity_count: 0,
283            validate_continuity: true,
284        }
285    }
286
287    /// Enable or disable continuity validation.
288    pub fn set_validate_continuity(&mut self, enabled: bool) {
289        self.validate_continuity = enabled;
290    }
291
292    /// Validate a timecode value.
293    pub fn validate(&self, tc: &Timecode) -> Result<(), TimecodeError> {
294        let fps = self.frame_rate.frames_per_second() as u8;
295        if tc.hours > 23 {
296            return Err(TimecodeError::InvalidHours);
297        }
298        if tc.minutes > 59 {
299            return Err(TimecodeError::InvalidMinutes);
300        }
301        if tc.seconds > 59 {
302            return Err(TimecodeError::InvalidSeconds);
303        }
304        if tc.frames >= fps {
305            return Err(TimecodeError::InvalidFrames);
306        }
307        if self.frame_rate.is_drop_frame()
308            && tc.seconds == 0
309            && tc.frames < 2
310            && !tc.minutes.is_multiple_of(10)
311        {
312            return Err(TimecodeError::InvalidDropFrame);
313        }
314        Ok(())
315    }
316
317    /// Validate and check continuity with the previous timecode.
318    pub fn validate_sequence(&mut self, tc: Timecode) -> Result<bool, TimecodeError> {
319        self.validate(&tc)?;
320
321        let is_continuous = if let Some(ref last) = self.last_tc {
322            if self.validate_continuity {
323                let expected = last.to_frames() + 1;
324                tc.to_frames() == expected
325            } else {
326                true
327            }
328        } else {
329            true
330        };
331
332        if !is_continuous {
333            self.discontinuity_count += 1;
334        }
335
336        self.last_tc = Some(tc);
337        Ok(is_continuous)
338    }
339
340    /// Get the number of discontinuities detected.
341    pub fn discontinuity_count(&self) -> u32 {
342        self.discontinuity_count
343    }
344
345    /// Reset state.
346    pub fn reset(&mut self) {
347        self.last_tc = None;
348        self.discontinuity_count = 0;
349    }
350
351    /// Get last validated timecode.
352    pub fn last_timecode(&self) -> Option<&Timecode> {
353        self.last_tc.as_ref()
354    }
355}
356
357/// Parse a timecode string in HH:MM:SS:FF or HH:MM:SS;FF format.
358pub fn parse_timecode_string(s: &str, frame_rate: FrameRate) -> Result<Timecode, TimecodeError> {
359    let bytes = s.as_bytes();
360    if bytes.len() < 11 {
361        return Err(TimecodeError::InvalidConfiguration);
362    }
363
364    let parse_two = |b: &[u8]| -> Result<u8, TimecodeError> {
365        if b.len() < 2 {
366            return Err(TimecodeError::InvalidConfiguration);
367        }
368        let hi = (b[0] as char)
369            .to_digit(10)
370            .ok_or(TimecodeError::InvalidConfiguration)? as u8;
371        let lo = (b[1] as char)
372            .to_digit(10)
373            .ok_or(TimecodeError::InvalidConfiguration)? as u8;
374        Ok(hi * 10 + lo)
375    };
376
377    let hours = parse_two(&bytes[0..2])?;
378    if bytes[2] != b':' {
379        return Err(TimecodeError::InvalidConfiguration);
380    }
381    let minutes = parse_two(&bytes[3..5])?;
382    if bytes[5] != b':' {
383        return Err(TimecodeError::InvalidConfiguration);
384    }
385    let seconds = parse_two(&bytes[6..8])?;
386    let sep = bytes[8];
387    if sep != b':' && sep != b';' {
388        return Err(TimecodeError::InvalidConfiguration);
389    }
390    let frames = parse_two(&bytes[9..11])?;
391
392    Timecode::new(hours, minutes, seconds, frames, frame_rate)
393}
394
395/// VITC line number selector for standard formats.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum VitcLine {
398    /// Line 14 (common for 525-line systems).
399    Line14,
400    /// Line 16 (alternate for 525-line systems).
401    Line16,
402    /// Line 19 (common for 625-line systems).
403    Line19,
404    /// Line 21 (alternate for 625-line systems).
405    Line21,
406    /// Custom line number.
407    Custom(u16),
408}
409
410impl VitcLine {
411    /// Get the line number.
412    pub fn line_number(&self) -> u16 {
413        match self {
414            VitcLine::Line14 => 14,
415            VitcLine::Line16 => 16,
416            VitcLine::Line19 => 19,
417            VitcLine::Line21 => 21,
418            VitcLine::Custom(n) => *n,
419        }
420    }
421}
422
423/// Decoded timecode with metadata about source and confidence.
424#[derive(Debug, Clone)]
425pub struct DecodedTimecode {
426    /// The timecode value.
427    pub timecode: Timecode,
428    /// Frame rate information.
429    pub frame_rate_info: FrameRateInfo,
430    /// Confidence score 0.0–1.0.
431    pub confidence: f32,
432    /// Source type.
433    pub source: TimecodeSource,
434}
435
436/// Source of a decoded timecode.
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum TimecodeSource {
439    /// Linear Timecode from audio track.
440    Ltc,
441    /// Vertical Interval Timecode from video.
442    Vitc,
443    /// MIDI Timecode.
444    Mtc,
445    /// Embedded metadata.
446    Metadata,
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_parse_timecode_string_25fps() {
455        let tc = parse_timecode_string("01:02:03:04", FrameRate::Fps25).unwrap();
456        assert_eq!(tc.hours, 1);
457        assert_eq!(tc.minutes, 2);
458        assert_eq!(tc.seconds, 3);
459        assert_eq!(tc.frames, 4);
460    }
461
462    #[test]
463    fn test_parse_timecode_string_drop_frame() {
464        let tc = parse_timecode_string("01:02:03;04", FrameRate::Fps2997DF).unwrap();
465        assert_eq!(tc.hours, 1);
466        assert_eq!(tc.seconds, 3);
467        assert_eq!(tc.frames, 4);
468    }
469
470    #[test]
471    fn test_parse_timecode_string_invalid() {
472        assert!(parse_timecode_string("bad", FrameRate::Fps25).is_err());
473        assert!(parse_timecode_string("01:02:03X04", FrameRate::Fps25).is_err());
474    }
475
476    #[test]
477    fn test_vitc_parser_new() {
478        let parser = VitcParser::new(FrameRate::Fps25, 720);
479        assert_eq!(parser.frame_rate, FrameRate::Fps25);
480        assert!(parser.clock_period_samples >= 1);
481    }
482
483    #[test]
484    fn test_vitc_parser_short_line() {
485        let parser = VitcParser::new(FrameRate::Fps25, 720);
486        let short = vec![0.5f32; 10];
487        let result = parser.parse_line(&short);
488        assert!(result.timecode.is_none());
489        assert!(!result.crc_ok);
490    }
491
492    #[test]
493    fn test_vitc_parser_all_zeros() {
494        let parser = VitcParser::new(FrameRate::Fps25, 720);
495        let pixels = vec![0.0f32; 720];
496        let result = parser.parse_line(&pixels);
497        // No sync pattern so should fail
498        assert!(result.timecode.is_none());
499    }
500
501    #[test]
502    fn test_ltc_decoder_new() {
503        let dec = LtcDecoder::new(FrameRate::Fps25, 48000);
504        assert_eq!(dec.frame_rate(), FrameRate::Fps25);
505        assert_eq!(dec.sample_rate(), 48000);
506    }
507
508    #[test]
509    fn test_ltc_decoder_feed_silence() {
510        let mut dec = LtcDecoder::new(FrameRate::Fps25, 48000);
511        let silence = vec![0.0f32; 48000];
512        let results = dec.feed(&silence);
513        // Silence produces no crossings, no timecodes
514        assert!(results.is_empty());
515    }
516
517    #[test]
518    fn test_timecode_validator_valid() {
519        let validator = TimecodeValidator::new(FrameRate::Fps25);
520        let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).unwrap();
521        assert!(validator.validate(&tc).is_ok());
522    }
523
524    #[test]
525    fn test_timecode_validator_invalid_frames() {
526        let validator = TimecodeValidator::new(FrameRate::Fps25);
527        let tc = Timecode {
528            hours: 0,
529            minutes: 0,
530            seconds: 0,
531            frames: 30, // invalid for 25fps
532            frame_rate: FrameRateInfo {
533                fps: 25,
534                drop_frame: false,
535            },
536            user_bits: 0,
537        };
538        assert!(validator.validate(&tc).is_err());
539    }
540
541    #[test]
542    fn test_timecode_validator_sequence_continuity() {
543        let mut validator = TimecodeValidator::new(FrameRate::Fps25);
544        let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).unwrap();
545        let tc2 = Timecode::new(0, 0, 0, 1, FrameRate::Fps25).unwrap();
546        assert!(validator.validate_sequence(tc1).unwrap());
547        assert!(validator.validate_sequence(tc2).unwrap());
548        assert_eq!(validator.discontinuity_count(), 0);
549    }
550
551    #[test]
552    fn test_timecode_validator_sequence_discontinuity() {
553        let mut validator = TimecodeValidator::new(FrameRate::Fps25);
554        let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).unwrap();
555        let tc2 = Timecode::new(0, 0, 1, 5, FrameRate::Fps25).unwrap();
556        validator.validate_sequence(tc1).unwrap();
557        let cont = validator.validate_sequence(tc2).unwrap();
558        assert!(!cont);
559        assert_eq!(validator.discontinuity_count(), 1);
560    }
561
562    #[test]
563    fn test_timecode_validator_reset() {
564        let mut validator = TimecodeValidator::new(FrameRate::Fps25);
565        let tc = Timecode::new(0, 0, 1, 5, FrameRate::Fps25).unwrap();
566        validator.validate_sequence(tc).unwrap();
567        validator.reset();
568        assert_eq!(validator.discontinuity_count(), 0);
569        assert!(validator.last_timecode().is_none());
570    }
571
572    #[test]
573    fn test_vitc_line_numbers() {
574        assert_eq!(VitcLine::Line14.line_number(), 14);
575        assert_eq!(VitcLine::Line19.line_number(), 19);
576        assert_eq!(VitcLine::Custom(22).line_number(), 22);
577    }
578
579    #[test]
580    fn test_timecode_source_eq() {
581        assert_eq!(TimecodeSource::Ltc, TimecodeSource::Ltc);
582        assert_ne!(TimecodeSource::Ltc, TimecodeSource::Vitc);
583    }
584}