Skip to main content

oximedia_timecode/ltc/
decoder.rs

1//! LTC Decoder - Biphase Mark Code decoding from audio
2//!
3//! This module implements a complete LTC decoder that:
4//! - Analyzes audio waveforms for biphase mark code transitions
5//! - Detects and validates SMPTE sync words
6//! - Extracts timecode and user bits
7//! - Handles variable playback speeds
8//! - Provides error correction and validation
9
10use super::constants::*;
11use crate::{FrameRate, Timecode, TimecodeError};
12
13/// LTC decoder state machine states
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[allow(dead_code)]
16enum DecoderState {
17    /// Searching for sync word
18    Searching,
19    /// Locked to sync, decoding bits
20    Locked,
21    /// Lost sync, attempting to reacquire
22    LostSync,
23}
24
25/// Biphase mark code decoder
26pub struct LtcDecoder {
27    /// Sample rate
28    #[allow(dead_code)]
29    sample_rate: u32,
30    /// Frame rate
31    frame_rate: FrameRate,
32    /// Minimum signal amplitude
33    min_amplitude: f32,
34    /// Current decoder state
35    state: DecoderState,
36    /// Bit buffer (80 bits)
37    bit_buffer: [bool; BITS_PER_FRAME],
38    /// Current bit position in buffer
39    bit_position: usize,
40    /// Zero crossing detector state
41    zero_crossing: ZeroCrossingDetector,
42    /// Bit synchronizer
43    bit_sync: BitSynchronizer,
44    /// Last decoded timecode
45    last_timecode: Option<Timecode>,
46    /// Sync confidence counter
47    sync_confidence: u32,
48    /// Error counter
49    error_count: u32,
50}
51
52impl LtcDecoder {
53    /// Create a new LTC decoder
54    pub fn new(sample_rate: u32, frame_rate: FrameRate, min_amplitude: f32) -> Self {
55        LtcDecoder {
56            sample_rate,
57            frame_rate,
58            min_amplitude,
59            state: DecoderState::Searching,
60            bit_buffer: [false; BITS_PER_FRAME],
61            bit_position: 0,
62            zero_crossing: ZeroCrossingDetector::new(sample_rate, frame_rate),
63            bit_sync: BitSynchronizer::new(sample_rate, frame_rate),
64            last_timecode: None,
65            sync_confidence: 0,
66            error_count: 0,
67        }
68    }
69
70    /// Process audio samples and decode timecode
71    pub fn process_samples(&mut self, samples: &[f32]) -> Result<Option<Timecode>, TimecodeError> {
72        let mut result = None;
73
74        for &sample in samples {
75            // Detect zero crossings
76            if let Some(transition) = self
77                .zero_crossing
78                .process_sample(sample, self.min_amplitude)
79            {
80                // Process the transition
81                if let Some(bit) = self.bit_sync.process_transition(transition) {
82                    // Store the bit
83                    if let Some(tc) = self.process_bit(bit)? {
84                        result = Some(tc);
85                    }
86                }
87            }
88        }
89
90        Ok(result)
91    }
92
93    /// Process a decoded bit
94    fn process_bit(&mut self, bit: bool) -> Result<Option<Timecode>, TimecodeError> {
95        // Store bit in buffer
96        self.bit_buffer[self.bit_position] = bit;
97        self.bit_position += 1;
98
99        // Check if we have a complete frame
100        if self.bit_position >= BITS_PER_FRAME {
101            self.bit_position = 0;
102
103            // Try to decode the frame
104            match self.decode_frame() {
105                Ok(timecode) => {
106                    self.state = DecoderState::Locked;
107                    self.sync_confidence = self.sync_confidence.saturating_add(1).min(100);
108                    self.error_count = 0;
109                    self.last_timecode = Some(timecode);
110                    return Ok(Some(timecode));
111                }
112                Err(_) => {
113                    self.error_count += 1;
114                    if self.error_count > 10 {
115                        self.state = DecoderState::LostSync;
116                        self.sync_confidence = 0;
117                    }
118                }
119            }
120        }
121
122        Ok(None)
123    }
124
125    /// Decode a complete LTC frame from the bit buffer
126    fn decode_frame(&self) -> Result<Timecode, TimecodeError> {
127        // Find sync word position
128        let sync_pos = self.find_sync_word()?;
129
130        // Extract data bits (64 bits before sync word)
131        let mut data_bits = [false; DATA_BITS];
132        for (i, data_bit) in data_bits.iter_mut().enumerate().take(DATA_BITS) {
133            let pos = (sync_pos + BITS_PER_FRAME - SYNC_BITS - DATA_BITS + i) % BITS_PER_FRAME;
134            *data_bit = self.bit_buffer[pos];
135        }
136
137        // Decode timecode from data bits
138        self.decode_timecode_from_bits(&data_bits)
139    }
140
141    /// Find the sync word in the bit buffer
142    fn find_sync_word(&self) -> Result<usize, TimecodeError> {
143        // Convert sync word to bit pattern
144        let sync_bits = self.u16_to_bits(SYNC_WORD);
145
146        // Search for sync word in buffer
147        for start_pos in 0..BITS_PER_FRAME {
148            let mut match_count = 0;
149            for (i, &sync_bit) in sync_bits.iter().enumerate().take(SYNC_BITS) {
150                let pos = (start_pos + i) % BITS_PER_FRAME;
151                if self.bit_buffer[pos] == sync_bit {
152                    match_count += 1;
153                }
154            }
155
156            // Allow up to 2 bit errors in sync word
157            if match_count >= SYNC_BITS - 2 {
158                return Ok(start_pos);
159            }
160        }
161
162        Err(TimecodeError::SyncNotFound)
163    }
164
165    /// Convert u16 to bit array (LSB first, as per LTC spec)
166    fn u16_to_bits(&self, value: u16) -> [bool; 16] {
167        let mut bits = [false; 16];
168        for (i, bit) in bits.iter_mut().enumerate() {
169            *bit = (value & (1 << i)) != 0;
170        }
171        bits
172    }
173
174    /// Decode timecode from 64 data bits
175    fn decode_timecode_from_bits(
176        &self,
177        bits: &[bool; DATA_BITS],
178    ) -> Result<Timecode, TimecodeError> {
179        // LTC bit layout (SMPTE 12M):
180        // Bits 0-3: Frame units
181        // Bits 4-7: User bits 1
182        // Bits 8-9: Frame tens
183        // Bit 10: Drop frame flag
184        // Bit 11: Color frame flag
185        // Bits 12-15: User bits 2
186        // Bits 16-19: Second units
187        // Bits 20-23: User bits 3
188        // Bits 24-26: Second tens
189        // Bit 27: Biphase mark correction (even parity)
190        // Bits 28-31: User bits 4
191        // Bits 32-35: Minute units
192        // Bits 36-39: User bits 5
193        // Bits 40-42: Minute tens
194        // Bit 43: Binary group flag
195        // Bits 44-47: User bits 6
196        // Bits 48-51: Hour units
197        // Bits 52-55: User bits 7
198        // Bits 56-57: Hour tens
199        // Bit 58-63: User bits 8 and flags
200
201        let frame_units = self.bits_to_u8(&bits[0..4]);
202        let frame_tens = self.bits_to_u8(&bits[8..10]);
203        let frames = frame_tens * 10 + frame_units;
204
205        let second_units = self.bits_to_u8(&bits[16..20]);
206        let second_tens = self.bits_to_u8(&bits[24..27]);
207        let seconds = second_tens * 10 + second_units;
208
209        let minute_units = self.bits_to_u8(&bits[32..36]);
210        let minute_tens = self.bits_to_u8(&bits[40..43]);
211        let minutes = minute_tens * 10 + minute_units;
212
213        let hour_units = self.bits_to_u8(&bits[48..52]);
214        let hour_tens = self.bits_to_u8(&bits[56..58]);
215        let hours = hour_tens * 10 + hour_units;
216
217        // Extract drop frame flag
218        let drop_frame = bits[10];
219
220        // Extract user bits
221        let user_bits = self.extract_user_bits(bits);
222
223        // Create timecode
224        let frame_rate = if drop_frame && self.frame_rate == FrameRate::Fps2997NDF {
225            FrameRate::Fps2997DF
226        } else {
227            self.frame_rate
228        };
229
230        let mut timecode = Timecode::new(hours, minutes, seconds, frames, frame_rate)?;
231        timecode.user_bits = user_bits;
232
233        Ok(timecode)
234    }
235
236    /// Convert bit slice to u8 (LSB first)
237    fn bits_to_u8(&self, bits: &[bool]) -> u8 {
238        let mut value = 0u8;
239        for (i, &bit) in bits.iter().enumerate() {
240            if bit {
241                value |= 1 << i;
242            }
243        }
244        value
245    }
246
247    /// Extract user bits from data bits
248    fn extract_user_bits(&self, bits: &[bool; DATA_BITS]) -> u32 {
249        let mut user_bits = 0u32;
250
251        // User bits are scattered throughout the LTC frame
252        // UB1: bits 4-7
253        user_bits |= self.bits_to_u8(&bits[4..8]) as u32;
254        // UB2: bits 12-15
255        user_bits |= (self.bits_to_u8(&bits[12..16]) as u32) << 4;
256        // UB3: bits 20-23
257        user_bits |= (self.bits_to_u8(&bits[20..24]) as u32) << 8;
258        // UB4: bits 28-31
259        user_bits |= (self.bits_to_u8(&bits[28..32]) as u32) << 12;
260        // UB5: bits 36-39
261        user_bits |= (self.bits_to_u8(&bits[36..40]) as u32) << 16;
262        // UB6: bits 44-47
263        user_bits |= (self.bits_to_u8(&bits[44..48]) as u32) << 20;
264        // UB7: bits 52-55
265        user_bits |= (self.bits_to_u8(&bits[52..56]) as u32) << 24;
266        // UB8: bits 59-62 (4 bits)
267        user_bits |= (self.bits_to_u8(&bits[59..63]) as u32) << 28;
268
269        user_bits
270    }
271
272    /// Reset decoder state
273    pub fn reset(&mut self) {
274        self.state = DecoderState::Searching;
275        self.bit_position = 0;
276        self.sync_confidence = 0;
277        self.error_count = 0;
278        self.zero_crossing.reset();
279        self.bit_sync.reset();
280    }
281
282    /// Check if decoder is synchronized
283    pub fn is_synchronized(&self) -> bool {
284        self.state == DecoderState::Locked && self.sync_confidence >= 10
285    }
286
287    /// Get sync confidence (0.0 to 1.0)
288    pub fn sync_confidence(&self) -> f32 {
289        (self.sync_confidence as f32) / 100.0
290    }
291}
292
293/// Zero crossing detector
294#[allow(dead_code)]
295struct ZeroCrossingDetector {
296    /// Previous sample value
297    prev_sample: f32,
298    /// Sample counter
299    sample_count: u64,
300    /// Expected samples per bit (nominal)
301    samples_per_bit: f32,
302}
303
304impl ZeroCrossingDetector {
305    fn new(sample_rate: u32, frame_rate: FrameRate) -> Self {
306        let fps = frame_rate.as_float();
307        let bits_per_second = fps * BITS_PER_FRAME as f64;
308        let samples_per_bit = sample_rate as f64 / bits_per_second;
309
310        ZeroCrossingDetector {
311            prev_sample: 0.0,
312            sample_count: 0,
313            samples_per_bit: samples_per_bit as f32,
314        }
315    }
316
317    /// Process a sample and detect zero crossings
318    fn process_sample(&mut self, sample: f32, min_amplitude: f32) -> Option<Transition> {
319        self.sample_count += 1;
320
321        // Detect zero crossing with hysteresis
322        let transition = if self.prev_sample < -min_amplitude && sample >= min_amplitude {
323            Some(Transition {
324                sample_index: self.sample_count,
325                rising: true,
326            })
327        } else if self.prev_sample > min_amplitude && sample <= -min_amplitude {
328            Some(Transition {
329                sample_index: self.sample_count,
330                rising: false,
331            })
332        } else {
333            None
334        };
335
336        self.prev_sample = sample;
337        transition
338    }
339
340    fn reset(&mut self) {
341        self.prev_sample = 0.0;
342        self.sample_count = 0;
343    }
344}
345
346/// Transition event
347#[derive(Debug, Clone, Copy)]
348#[allow(dead_code)]
349struct Transition {
350    sample_index: u64,
351    rising: bool,
352}
353
354/// Bit synchronizer - converts transitions to bits
355struct BitSynchronizer {
356    /// Last transition time
357    last_transition: Option<u64>,
358    /// Expected samples per bit cell
359    samples_per_bit: f32,
360    /// Current bit cell phase
361    bit_phase: f32,
362    /// Bit clock accumulator
363    bit_clock: f32,
364    /// Phase locked loop filter
365    pll_filter: PllFilter,
366}
367
368impl BitSynchronizer {
369    fn new(sample_rate: u32, frame_rate: FrameRate) -> Self {
370        let fps = frame_rate.as_float();
371        let bits_per_second = fps * BITS_PER_FRAME as f64;
372        let samples_per_bit = sample_rate as f64 / bits_per_second;
373
374        BitSynchronizer {
375            last_transition: None,
376            samples_per_bit: samples_per_bit as f32,
377            bit_phase: 0.0,
378            bit_clock: 0.0,
379            pll_filter: PllFilter::new(0.1),
380        }
381    }
382
383    /// Process a transition and decode bits
384    fn process_transition(&mut self, transition: Transition) -> Option<bool> {
385        let sample_index = transition.sample_index;
386
387        if let Some(last_idx) = self.last_transition {
388            let samples_since_last = (sample_index - last_idx) as f32;
389
390            // Update PLL
391            let phase_error = samples_since_last - self.samples_per_bit;
392            let correction = self.pll_filter.update(phase_error);
393            self.samples_per_bit += correction;
394
395            // Determine if this is a half-bit or full-bit transition
396            let is_half_bit = samples_since_last < (self.samples_per_bit * 0.75);
397
398            self.last_transition = Some(sample_index);
399
400            if is_half_bit {
401                // This is a mid-bit transition (bit = 1)
402                self.bit_clock = 0.5;
403                return Some(true);
404            } else {
405                // This is a full-bit transition (bit = 0)
406                self.bit_clock = 0.0;
407                return Some(false);
408            }
409        }
410
411        self.last_transition = Some(sample_index);
412        None
413    }
414
415    fn reset(&mut self) {
416        self.last_transition = None;
417        self.bit_phase = 0.0;
418        self.bit_clock = 0.0;
419        self.pll_filter.reset();
420    }
421}
422
423/// Phase-Locked Loop filter for bit clock recovery
424struct PllFilter {
425    /// Loop gain
426    gain: f32,
427    /// Integrator state
428    integrator: f32,
429}
430
431impl PllFilter {
432    fn new(gain: f32) -> Self {
433        PllFilter {
434            gain,
435            integrator: 0.0,
436        }
437    }
438
439    /// Update filter with phase error
440    fn update(&mut self, phase_error: f32) -> f32 {
441        // Proportional + Integral controller
442        let proportional = phase_error * self.gain;
443        self.integrator += phase_error * self.gain * 0.01;
444
445        // Clamp integrator to prevent windup
446        self.integrator = self.integrator.clamp(-10.0, 10.0);
447
448        proportional + self.integrator
449    }
450
451    fn reset(&mut self) {
452        self.integrator = 0.0;
453    }
454}
455
456/// Signal filter for noise reduction
457#[allow(dead_code)]
458struct SignalFilter {
459    /// Filter coefficients (simple low-pass IIR)
460    alpha: f32,
461    /// Previous filtered value
462    prev_value: f32,
463}
464
465impl SignalFilter {
466    #[allow(dead_code)]
467    fn new(cutoff_freq: f32, sample_rate: f32) -> Self {
468        let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff_freq);
469        let dt = 1.0 / sample_rate;
470        let alpha = dt / (rc + dt);
471
472        SignalFilter {
473            alpha,
474            prev_value: 0.0,
475        }
476    }
477
478    /// Apply low-pass filter to sample
479    #[allow(dead_code)]
480    fn process(&mut self, sample: f32) -> f32 {
481        let filtered = self.alpha * sample + (1.0 - self.alpha) * self.prev_value;
482        self.prev_value = filtered;
483        filtered
484    }
485
486    #[allow(dead_code)]
487    fn reset(&mut self) {
488        self.prev_value = 0.0;
489    }
490}
491
492/// Waveform analyzer for signal quality assessment
493#[allow(dead_code)]
494struct WaveformAnalyzer {
495    /// RMS calculator
496    rms_accumulator: f32,
497    /// Sample count for RMS
498    rms_count: u32,
499    /// Peak detector
500    peak_positive: f32,
501    /// Negative peak
502    peak_negative: f32,
503}
504
505impl WaveformAnalyzer {
506    #[allow(dead_code)]
507    fn new() -> Self {
508        WaveformAnalyzer {
509            rms_accumulator: 0.0,
510            rms_count: 0,
511            peak_positive: 0.0,
512            peak_negative: 0.0,
513        }
514    }
515
516    /// Process a sample and update statistics
517    #[allow(dead_code)]
518    fn process_sample(&mut self, sample: f32) {
519        self.rms_accumulator += sample * sample;
520        self.rms_count += 1;
521
522        if sample > self.peak_positive {
523            self.peak_positive = sample;
524        }
525        if sample < self.peak_negative {
526            self.peak_negative = sample;
527        }
528    }
529
530    /// Get RMS value
531    #[allow(dead_code)]
532    fn get_rms(&self) -> f32 {
533        if self.rms_count > 0 {
534            (self.rms_accumulator / self.rms_count as f32).sqrt()
535        } else {
536            0.0
537        }
538    }
539
540    /// Get peak-to-peak amplitude
541    #[allow(dead_code)]
542    fn get_peak_to_peak(&self) -> f32 {
543        self.peak_positive - self.peak_negative
544    }
545
546    #[allow(dead_code)]
547    fn reset(&mut self) {
548        self.rms_accumulator = 0.0;
549        self.rms_count = 0;
550        self.peak_positive = 0.0;
551        self.peak_negative = 0.0;
552    }
553}
554
555/// Drop frame calculator
556#[allow(dead_code)]
557struct DropFrameCalculator;
558
559impl DropFrameCalculator {
560    /// Check if a timecode is valid for drop frame
561    #[allow(dead_code)]
562    fn is_valid_drop_frame(minutes: u8, seconds: u8, frames: u8) -> bool {
563        // Frames 0 and 1 are dropped at the start of each minute except 0, 10, 20, 30, 40, 50
564        if seconds == 0 && frames < 2 && !minutes.is_multiple_of(10) {
565            return false;
566        }
567        true
568    }
569
570    /// Adjust timecode for drop frame
571    #[allow(dead_code)]
572    fn adjust_for_drop_frame(minutes: u8, seconds: u8, frames: u8) -> (u8, u8, u8) {
573        if seconds == 0 && frames < 2 && !minutes.is_multiple_of(10) {
574            // Skip to frame 2
575            (minutes, seconds, 2)
576        } else {
577            (minutes, seconds, frames)
578        }
579    }
580}
581
582/// Error correction using redundancy
583#[allow(dead_code)]
584struct ErrorCorrector {
585    /// History of recent timecodes
586    history: Vec<Timecode>,
587    /// Maximum history size
588    max_history: usize,
589}
590
591impl ErrorCorrector {
592    #[allow(dead_code)]
593    fn new(max_history: usize) -> Self {
594        ErrorCorrector {
595            history: Vec::with_capacity(max_history),
596            max_history,
597        }
598    }
599
600    /// Add timecode to history
601    #[allow(dead_code)]
602    fn add_timecode(&mut self, timecode: Timecode) {
603        self.history.push(timecode);
604        if self.history.len() > self.max_history {
605            self.history.remove(0);
606        }
607    }
608
609    /// Try to correct errors using history
610    #[allow(dead_code)]
611    fn correct_timecode(&self, timecode: &Timecode) -> Option<Timecode> {
612        if self.history.is_empty() {
613            return Some(*timecode);
614        }
615
616        // Check if timecode is sequential with last one
617        if let Some(last) = self.history.last() {
618            let mut expected = *last;
619            if expected.increment().is_ok() {
620                // Check if current timecode is close to expected
621                if Self::is_close(timecode, &expected) {
622                    return Some(*timecode);
623                }
624            }
625        }
626
627        // If not sequential, return the timecode anyway
628        Some(*timecode)
629    }
630
631    /// Check if two timecodes are close (within a few frames)
632    #[allow(dead_code)]
633    fn is_close(tc1: &Timecode, tc2: &Timecode) -> bool {
634        let diff = (tc1.to_frames() as i64 - tc2.to_frames() as i64).abs();
635        diff <= 5
636    }
637
638    #[allow(dead_code)]
639    fn reset(&mut self) {
640        self.history.clear();
641    }
642}
643
644/// Bit pattern validator
645#[allow(dead_code)]
646struct BitPatternValidator;
647
648impl BitPatternValidator {
649    /// Validate that bit patterns make sense for timecode
650    #[allow(dead_code)]
651    fn validate_timecode_bits(bits: &[bool; DATA_BITS]) -> bool {
652        // Check that tens digits are in valid range
653        let frame_tens = Self::bits_to_u8(&bits[8..10]);
654        let second_tens = Self::bits_to_u8(&bits[24..27]);
655        let minute_tens = Self::bits_to_u8(&bits[40..43]);
656        let hour_tens = Self::bits_to_u8(&bits[56..58]);
657
658        // Frame tens should be 0-5 (for 60fps max)
659        if frame_tens > 5 {
660            return false;
661        }
662
663        // Second tens should be 0-5
664        if second_tens > 5 {
665            return false;
666        }
667
668        // Minute tens should be 0-5
669        if minute_tens > 5 {
670            return false;
671        }
672
673        // Hour tens should be 0-2
674        if hour_tens > 2 {
675            return false;
676        }
677
678        true
679    }
680
681    #[allow(dead_code)]
682    fn bits_to_u8(bits: &[bool]) -> u8 {
683        let mut value = 0u8;
684        for (i, &bit) in bits.iter().enumerate() {
685            if bit {
686                value |= 1 << i;
687            }
688        }
689        value
690    }
691}
692
693/// Speed variation detector
694#[allow(dead_code)]
695struct SpeedDetector {
696    /// History of bit periods
697    bit_periods: Vec<f32>,
698    /// Maximum history
699    max_history: usize,
700}
701
702impl SpeedDetector {
703    #[allow(dead_code)]
704    fn new(max_history: usize) -> Self {
705        SpeedDetector {
706            bit_periods: Vec::with_capacity(max_history),
707            max_history,
708        }
709    }
710
711    /// Add a bit period measurement
712    #[allow(dead_code)]
713    fn add_period(&mut self, period: f32) {
714        self.bit_periods.push(period);
715        if self.bit_periods.len() > self.max_history {
716            self.bit_periods.remove(0);
717        }
718    }
719
720    /// Get average period
721    #[allow(dead_code)]
722    fn get_average_period(&self) -> Option<f32> {
723        if self.bit_periods.is_empty() {
724            return None;
725        }
726
727        let sum: f32 = self.bit_periods.iter().sum();
728        Some(sum / self.bit_periods.len() as f32)
729    }
730
731    /// Get speed ratio (1.0 = nominal speed)
732    #[allow(dead_code)]
733    fn get_speed_ratio(&self, nominal_period: f32) -> f32 {
734        if let Some(avg) = self.get_average_period() {
735            nominal_period / avg
736        } else {
737            1.0
738        }
739    }
740
741    #[allow(dead_code)]
742    fn reset(&mut self) {
743        self.bit_periods.clear();
744    }
745}
746
747#[cfg(test)]
748mod tests {
749    use super::*;
750
751    #[test]
752    fn test_decoder_creation() {
753        let decoder = LtcDecoder::new(48000, FrameRate::Fps25, 0.1);
754        assert!(!decoder.is_synchronized());
755    }
756
757    #[test]
758    fn test_zero_crossing_detector() {
759        let mut detector = ZeroCrossingDetector::new(48000, FrameRate::Fps25);
760
761        // Test rising edge
762        let t1 = detector.process_sample(-0.5, 0.1);
763        assert!(t1.is_none());
764
765        let t2 = detector.process_sample(0.5, 0.1);
766        assert!(t2.is_some());
767    }
768
769    #[test]
770    fn test_bits_to_u8() {
771        let decoder = LtcDecoder::new(48000, FrameRate::Fps25, 0.1);
772        let bits = [true, false, true, false]; // Binary: 0101 = 5
773        assert_eq!(decoder.bits_to_u8(&bits), 5);
774    }
775
776    #[test]
777    fn test_drop_frame_validator() {
778        assert!(!DropFrameCalculator::is_valid_drop_frame(1, 0, 0));
779        assert!(!DropFrameCalculator::is_valid_drop_frame(1, 0, 1));
780        assert!(DropFrameCalculator::is_valid_drop_frame(1, 0, 2));
781        assert!(DropFrameCalculator::is_valid_drop_frame(10, 0, 0));
782    }
783}