Skip to main content

oximedia_timecode/ltc/
encoder.rs

1//! LTC Encoder - Biphase Mark Code encoding to audio
2//!
3//! This module implements a complete LTC encoder that:
4//! - Encodes timecode and user bits to 80-bit LTC frames
5//! - Generates biphase mark code audio waveforms
6//! - Inserts SMPTE sync words
7//! - Handles drop frame encoding
8//! - Supports variable amplitude and sample rates
9
10use super::constants::*;
11use crate::{FrameRate, Timecode, TimecodeError};
12
13/// LTC encoder
14pub struct LtcEncoder {
15    /// Sample rate
16    #[allow(dead_code)]
17    sample_rate: u32,
18    /// Frame rate
19    #[allow(dead_code)]
20    frame_rate: FrameRate,
21    /// Output amplitude (0.0 to 1.0)
22    amplitude: f32,
23    /// Samples per bit (for nominal speed)
24    samples_per_bit: f32,
25    /// Current phase (0.0 to 1.0)
26    #[allow(dead_code)]
27    phase: f32,
28    /// Current waveform polarity
29    polarity: bool,
30}
31
32impl LtcEncoder {
33    /// Create a new LTC encoder
34    pub fn new(sample_rate: u32, frame_rate: FrameRate, amplitude: f32) -> Self {
35        let fps = frame_rate.as_float();
36        let bits_per_second = fps * BITS_PER_FRAME as f64;
37        let samples_per_bit = sample_rate as f64 / bits_per_second;
38
39        LtcEncoder {
40            sample_rate,
41            frame_rate,
42            amplitude: amplitude.clamp(0.0, 1.0),
43            samples_per_bit: samples_per_bit as f32,
44            phase: 0.0,
45            polarity: false,
46        }
47    }
48
49    /// Encode a timecode frame to audio samples
50    pub fn encode_frame(&mut self, timecode: &Timecode) -> Result<Vec<f32>, TimecodeError> {
51        // Create bit array
52        let bits = self.timecode_to_bits(timecode)?;
53
54        // Encode bits to audio
55        let samples = self.bits_to_audio(&bits);
56
57        Ok(samples)
58    }
59
60    /// Convert timecode to 80-bit LTC frame
61    fn timecode_to_bits(
62        &self,
63        timecode: &Timecode,
64    ) -> Result<[bool; BITS_PER_FRAME], TimecodeError> {
65        let mut bits = [false; BITS_PER_FRAME];
66
67        // Decompose timecode
68        let frame_units = timecode.frames % 10;
69        let frame_tens = timecode.frames / 10;
70        let second_units = timecode.seconds % 10;
71        let second_tens = timecode.seconds / 10;
72        let minute_units = timecode.minutes % 10;
73        let minute_tens = timecode.minutes / 10;
74        let hour_units = timecode.hours % 10;
75        let hour_tens = timecode.hours / 10;
76
77        // Encode frame units (bits 0-3)
78        self.encode_bcd(&mut bits, 0, frame_units);
79
80        // User bits 1 (bits 4-7)
81        self.encode_nibble(&mut bits, 4, (timecode.user_bits & 0xF) as u8);
82
83        // Frame tens (bits 8-9)
84        self.encode_bcd(&mut bits, 8, frame_tens);
85
86        // Drop frame flag (bit 10)
87        bits[10] = timecode.frame_rate.drop_frame;
88
89        // Color frame flag (bit 11) - assume 0
90        bits[11] = false;
91
92        // User bits 2 (bits 12-15)
93        self.encode_nibble(&mut bits, 12, ((timecode.user_bits >> 4) & 0xF) as u8);
94
95        // Second units (bits 16-19)
96        self.encode_bcd(&mut bits, 16, second_units);
97
98        // User bits 3 (bits 20-23)
99        self.encode_nibble(&mut bits, 20, ((timecode.user_bits >> 8) & 0xF) as u8);
100
101        // Second tens (bits 24-26)
102        self.encode_bcd(&mut bits, 24, second_tens);
103
104        // Even parity (bit 27)
105        bits[27] = self.calculate_even_parity(&bits[0..27]);
106
107        // User bits 4 (bits 28-31)
108        self.encode_nibble(&mut bits, 28, ((timecode.user_bits >> 12) & 0xF) as u8);
109
110        // Minute units (bits 32-35)
111        self.encode_bcd(&mut bits, 32, minute_units);
112
113        // User bits 5 (bits 36-39)
114        self.encode_nibble(&mut bits, 36, ((timecode.user_bits >> 16) & 0xF) as u8);
115
116        // Minute tens (bits 40-42)
117        self.encode_bcd(&mut bits, 40, minute_tens);
118
119        // Binary group flag (bit 43)
120        bits[43] = false;
121
122        // User bits 6 (bits 44-47)
123        self.encode_nibble(&mut bits, 44, ((timecode.user_bits >> 20) & 0xF) as u8);
124
125        // Hour units (bits 48-51)
126        self.encode_bcd(&mut bits, 48, hour_units);
127
128        // User bits 7 (bits 52-55)
129        self.encode_nibble(&mut bits, 52, ((timecode.user_bits >> 24) & 0xF) as u8);
130
131        // Hour tens (bits 56-57)
132        self.encode_bcd(&mut bits, 56, hour_tens);
133
134        // Reserved bits (58)
135        bits[58] = false;
136
137        // User bits 8 (bits 59-62)
138        self.encode_nibble(&mut bits, 59, ((timecode.user_bits >> 28) & 0xF) as u8);
139
140        // Reserved bit (63)
141        bits[63] = false;
142
143        // Sync word (bits 64-79)
144        self.encode_sync_word(&mut bits);
145
146        Ok(bits)
147    }
148
149    /// Encode a BCD digit (4 bits, but may use fewer)
150    fn encode_bcd(&self, bits: &mut [bool; BITS_PER_FRAME], start: usize, value: u8) {
151        for i in 0..4 {
152            if start + i < BITS_PER_FRAME {
153                bits[start + i] = (value & (1 << i)) != 0;
154            }
155        }
156    }
157
158    /// Encode a 4-bit nibble
159    fn encode_nibble(&self, bits: &mut [bool; BITS_PER_FRAME], start: usize, value: u8) {
160        for i in 0..4 {
161            if start + i < BITS_PER_FRAME {
162                bits[start + i] = (value & (1 << i)) != 0;
163            }
164        }
165    }
166
167    /// Calculate even parity
168    fn calculate_even_parity(&self, bits: &[bool]) -> bool {
169        let count = bits.iter().filter(|&&b| b).count();
170        count % 2 != 0
171    }
172
173    /// Encode sync word (0x3FFD)
174    fn encode_sync_word(&self, bits: &mut [bool; BITS_PER_FRAME]) {
175        let sync_word = SYNC_WORD;
176        for i in 0..SYNC_BITS {
177            bits[DATA_BITS + i] = (sync_word & (1 << i)) != 0;
178        }
179    }
180
181    /// Convert bit array to audio samples using biphase mark code
182    fn bits_to_audio(&mut self, bits: &[bool; BITS_PER_FRAME]) -> Vec<f32> {
183        let total_samples = (self.samples_per_bit * BITS_PER_FRAME as f32) as usize;
184        let mut samples = Vec::with_capacity(total_samples);
185
186        for &bit in bits.iter() {
187            // Generate audio for one bit using biphase mark code
188            let bit_samples = self.encode_bit_bmc(bit);
189            samples.extend_from_slice(&bit_samples);
190        }
191
192        samples
193    }
194
195    /// Encode a single bit using biphase mark code
196    fn encode_bit_bmc(&mut self, bit: bool) -> Vec<f32> {
197        let samples_per_bit = self.samples_per_bit as usize;
198        let mut samples = Vec::with_capacity(samples_per_bit);
199
200        if bit {
201            // Bit 1: Transition at start and middle
202            // First half
203            for _ in 0..(samples_per_bit / 2) {
204                samples.push(if self.polarity {
205                    self.amplitude
206                } else {
207                    -self.amplitude
208                });
209            }
210            self.polarity = !self.polarity;
211
212            // Second half
213            for _ in (samples_per_bit / 2)..samples_per_bit {
214                samples.push(if self.polarity {
215                    self.amplitude
216                } else {
217                    -self.amplitude
218                });
219            }
220            self.polarity = !self.polarity;
221        } else {
222            // Bit 0: Transition only at start
223            for _ in 0..samples_per_bit {
224                samples.push(if self.polarity {
225                    self.amplitude
226                } else {
227                    -self.amplitude
228                });
229            }
230            self.polarity = !self.polarity;
231        }
232
233        samples
234    }
235
236    /// Encode multiple timecodes in a single batch call.
237    ///
238    /// Returns one inner `Vec<i16>` per timecode, each holding one LTC frame
239    /// worth of PCM samples at the given `sample_rate`.  The `i16` samples
240    /// are scaled from `amplitude` (0.0 – 1.0): `+amplitude → i16::MAX`,
241    /// `−amplitude → i16::MIN`.
242    ///
243    /// A fresh encoder polarity state is used for each timecode, matching the
244    /// per-frame reset semantics that many LTC editors expect.
245    pub fn encode_batch(timecodes: &[Timecode], sample_rate: u32) -> Vec<Vec<i16>> {
246        timecodes
247            .iter()
248            .map(|tc| {
249                let frame_rate = crate::frame_rate_from_info(&tc.frame_rate);
250                let mut enc = LtcEncoder::new(sample_rate, frame_rate, 1.0);
251                let f32_samples = enc.encode_frame(tc).unwrap_or_default();
252                f32_to_i16_samples(&f32_samples)
253            })
254            .collect()
255    }
256
257    /// Encode multiple timecodes and interleave all resulting PCM samples into
258    /// a single flat `Vec<i16>`.
259    ///
260    /// Equivalent to calling [`encode_batch`](Self::encode_batch) and
261    /// flattening the result, but avoids an intermediate allocation per frame.
262    pub fn encode_batch_interleaved(timecodes: &[Timecode], sample_rate: u32) -> Vec<i16> {
263        let mut out = Vec::new();
264        for tc in timecodes {
265            let frame_rate = crate::frame_rate_from_info(&tc.frame_rate);
266            let mut enc = LtcEncoder::new(sample_rate, frame_rate, 1.0);
267            let f32_samples = enc.encode_frame(tc).unwrap_or_default();
268            out.extend(f32_to_i16_samples(&f32_samples));
269        }
270        out
271    }
272
273    /// Reset encoder state
274    pub fn reset(&mut self) {
275        self.phase = 0.0;
276        self.polarity = false;
277    }
278
279    /// Set output amplitude
280    pub fn set_amplitude(&mut self, amplitude: f32) {
281        self.amplitude = amplitude.clamp(0.0, 1.0);
282    }
283
284    /// Get current amplitude
285    pub fn amplitude(&self) -> f32 {
286        self.amplitude
287    }
288}
289
290/// Scale f32 samples (`-1.0 …+1.0`) to i16 PCM.
291fn f32_to_i16_samples(samples: &[f32]) -> Vec<i16> {
292    samples
293        .iter()
294        .map(|&s| {
295            let clamped = s.clamp(-1.0, 1.0);
296            (clamped * i16::MAX as f32) as i16
297        })
298        .collect()
299}
300
301/// Waveform shaper for improved signal quality
302#[allow(dead_code)]
303struct WaveformShaper {
304    /// Rise time (in samples)
305    rise_time: usize,
306    /// Current transition progress
307    transition_progress: usize,
308    /// Target level
309    target_level: f32,
310    /// Current level
311    current_level: f32,
312}
313
314impl WaveformShaper {
315    #[allow(dead_code)]
316    fn new(sample_rate: u32, rise_time_us: f32) -> Self {
317        let rise_time = ((rise_time_us / 1_000_000.0) * sample_rate as f32) as usize;
318
319        WaveformShaper {
320            rise_time: rise_time.max(1),
321            transition_progress: 0,
322            target_level: 0.0,
323            current_level: 0.0,
324        }
325    }
326
327    /// Set target level for transition
328    #[allow(dead_code)]
329    fn set_target(&mut self, level: f32) {
330        if (level - self.target_level).abs() > 0.001 {
331            self.target_level = level;
332            self.transition_progress = 0;
333        }
334    }
335
336    /// Get next shaped sample
337    #[allow(dead_code)]
338    fn next_sample(&mut self) -> f32 {
339        if self.transition_progress < self.rise_time {
340            // Linear interpolation during transition
341            let progress = self.transition_progress as f32 / self.rise_time as f32;
342            self.current_level =
343                self.current_level * (1.0 - progress) + self.target_level * progress;
344            self.transition_progress += 1;
345        } else {
346            self.current_level = self.target_level;
347        }
348
349        self.current_level
350    }
351
352    #[allow(dead_code)]
353    fn reset(&mut self) {
354        self.transition_progress = 0;
355        self.current_level = 0.0;
356    }
357}
358
359/// Pre-emphasis filter for tape recording
360#[allow(dead_code)]
361struct PreEmphasisFilter {
362    /// Filter coefficient
363    alpha: f32,
364    /// Previous input
365    prev_input: f32,
366    /// Previous output
367    prev_output: f32,
368}
369
370impl PreEmphasisFilter {
371    #[allow(dead_code)]
372    fn new(time_constant_us: f32, sample_rate: u32) -> Self {
373        let tc = time_constant_us / 1_000_000.0;
374        let dt = 1.0 / sample_rate as f32;
375        let alpha = tc / (tc + dt);
376
377        PreEmphasisFilter {
378            alpha,
379            prev_input: 0.0,
380            prev_output: 0.0,
381        }
382    }
383
384    /// Apply pre-emphasis to sample
385    #[allow(dead_code)]
386    fn process(&mut self, input: f32) -> f32 {
387        let output = self.alpha * (self.prev_output + input - self.prev_input);
388        self.prev_input = input;
389        self.prev_output = output;
390        output
391    }
392
393    #[allow(dead_code)]
394    fn reset(&mut self) {
395        self.prev_input = 0.0;
396        self.prev_output = 0.0;
397    }
398}
399
400/// DC offset remover
401#[allow(dead_code)]
402struct DcBlocker {
403    /// Filter coefficient
404    alpha: f32,
405    /// Previous input
406    prev_input: f32,
407    /// Previous output
408    prev_output: f32,
409}
410
411impl DcBlocker {
412    #[allow(dead_code)]
413    fn new(cutoff_hz: f32, sample_rate: u32) -> Self {
414        let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff_hz);
415        let dt = 1.0 / sample_rate as f32;
416        let alpha = rc / (rc + dt);
417
418        DcBlocker {
419            alpha,
420            prev_input: 0.0,
421            prev_output: 0.0,
422        }
423    }
424
425    /// Remove DC offset from sample
426    #[allow(dead_code)]
427    fn process(&mut self, input: f32) -> f32 {
428        let output = self.alpha * (self.prev_output + input - self.prev_input);
429        self.prev_input = input;
430        self.prev_output = output;
431        output
432    }
433
434    #[allow(dead_code)]
435    fn reset(&mut self) {
436        self.prev_input = 0.0;
437        self.prev_output = 0.0;
438    }
439}
440
441/// Amplitude limiter
442#[allow(dead_code)]
443struct Limiter {
444    /// Threshold (0.0 to 1.0)
445    threshold: f32,
446    /// Attack time (in samples)
447    attack_samples: usize,
448    /// Release time (in samples)
449    release_samples: usize,
450    /// Current gain reduction
451    gain_reduction: f32,
452}
453
454impl Limiter {
455    #[allow(dead_code)]
456    fn new(threshold: f32, attack_ms: f32, release_ms: f32, sample_rate: u32) -> Self {
457        let attack_samples = ((attack_ms / 1000.0) * sample_rate as f32) as usize;
458        let release_samples = ((release_ms / 1000.0) * sample_rate as f32) as usize;
459
460        Limiter {
461            threshold,
462            attack_samples: attack_samples.max(1),
463            release_samples: release_samples.max(1),
464            gain_reduction: 1.0,
465        }
466    }
467
468    /// Apply limiting to sample
469    #[allow(dead_code)]
470    fn process(&mut self, input: f32) -> f32 {
471        let abs_input = input.abs();
472
473        if abs_input > self.threshold {
474            // Attack: reduce gain quickly
475            let target_gain = self.threshold / abs_input;
476            let attack_coefficient = 1.0 / self.attack_samples as f32;
477            self.gain_reduction += (target_gain - self.gain_reduction) * attack_coefficient;
478        } else {
479            // Release: increase gain slowly
480            let release_coefficient = 1.0 / self.release_samples as f32;
481            self.gain_reduction += (1.0 - self.gain_reduction) * release_coefficient;
482        }
483
484        input * self.gain_reduction
485    }
486
487    #[allow(dead_code)]
488    fn reset(&mut self) {
489        self.gain_reduction = 1.0;
490    }
491}
492
493/// LTC frame buffer for continuous encoding
494pub struct LtcFrameBuffer {
495    /// Sample rate
496    sample_rate: u32,
497    /// Frame rate
498    frame_rate: FrameRate,
499    /// Amplitude
500    amplitude: f32,
501    /// Buffered samples
502    buffer: Vec<f32>,
503    /// Current timecode
504    current_timecode: Option<Timecode>,
505}
506
507impl LtcFrameBuffer {
508    /// Create a new frame buffer
509    pub fn new(sample_rate: u32, frame_rate: FrameRate, amplitude: f32) -> Self {
510        LtcFrameBuffer {
511            sample_rate,
512            frame_rate,
513            amplitude,
514            buffer: Vec::new(),
515            current_timecode: None,
516        }
517    }
518
519    /// Set the starting timecode
520    pub fn set_timecode(&mut self, timecode: Timecode) {
521        self.current_timecode = Some(timecode);
522    }
523
524    /// Generate samples for the next frame
525    pub fn generate_frame(&mut self) -> Result<Vec<f32>, TimecodeError> {
526        if let Some(ref mut tc) = self.current_timecode {
527            let mut encoder = LtcEncoder::new(self.sample_rate, self.frame_rate, self.amplitude);
528            let samples = encoder.encode_frame(tc)?;
529
530            // Increment timecode for next frame
531            tc.increment()?;
532
533            Ok(samples)
534        } else {
535            Err(TimecodeError::InvalidConfiguration)
536        }
537    }
538
539    /// Fill buffer with samples up to a target duration
540    pub fn fill_buffer(&mut self, target_samples: usize) -> Result<(), TimecodeError> {
541        while self.buffer.len() < target_samples {
542            let frame_samples = self.generate_frame()?;
543            self.buffer.extend_from_slice(&frame_samples);
544        }
545        Ok(())
546    }
547
548    /// Read samples from buffer
549    pub fn read_samples(&mut self, count: usize) -> Vec<f32> {
550        let available = self.buffer.len().min(count);
551        let samples: Vec<f32> = self.buffer.drain(..available).collect();
552        samples
553    }
554
555    /// Get buffer level
556    pub fn buffer_level(&self) -> usize {
557        self.buffer.len()
558    }
559}
560
561/// User bits encoder helpers
562pub struct UserBitsEncoder;
563
564impl UserBitsEncoder {
565    /// Encode ASCII string to user bits (8 characters max)
566    pub fn encode_ascii(text: &str) -> u32 {
567        let bytes = text.as_bytes();
568        let mut user_bits = 0u32;
569
570        for (i, &byte) in bytes.iter().take(4).enumerate() {
571            user_bits |= (byte as u32) << (i * 8);
572        }
573
574        user_bits
575    }
576
577    /// Encode timecode date (MMDDYYYY format, packed BCD)
578    pub fn encode_date(month: u8, day: u8, year: u16) -> u32 {
579        let mut user_bits = 0u32;
580
581        // Month (MM)
582        user_bits |= (month / 10) as u32;
583        user_bits |= ((month % 10) as u32) << 4;
584
585        // Day (DD)
586        user_bits |= ((day / 10) as u32) << 8;
587        user_bits |= ((day % 10) as u32) << 12;
588
589        // Year (YYYY) - last two digits
590        let year_short = (year % 100) as u8;
591        user_bits |= ((year_short / 10) as u32) << 16;
592        user_bits |= ((year_short % 10) as u32) << 20;
593
594        user_bits
595    }
596
597    /// Encode binary data directly
598    pub fn encode_binary(data: u32) -> u32 {
599        data
600    }
601}
602
603/// Signal quality metrics
604pub struct SignalQualityMetrics {
605    /// Peak amplitude
606    pub peak_amplitude: f32,
607    /// RMS amplitude
608    pub rms_amplitude: f32,
609    /// Crest factor (peak/RMS)
610    pub crest_factor: f32,
611    /// DC offset
612    pub dc_offset: f32,
613}
614
615impl SignalQualityMetrics {
616    /// Calculate metrics from samples
617    pub fn from_samples(samples: &[f32]) -> Self {
618        let mut sum = 0.0;
619        let mut sum_squared = 0.0;
620        let mut peak: f32 = 0.0;
621
622        for &sample in samples {
623            sum += sample;
624            sum_squared += sample * sample;
625            peak = peak.max(sample.abs());
626        }
627
628        let dc_offset = sum / samples.len() as f32;
629        let rms = (sum_squared / samples.len() as f32).sqrt();
630        let crest_factor = if rms > 0.0 { peak / rms } else { 0.0 };
631
632        SignalQualityMetrics {
633            peak_amplitude: peak,
634            rms_amplitude: rms,
635            crest_factor,
636            dc_offset,
637        }
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    #[test]
646    fn test_encoder_creation() {
647        let encoder = LtcEncoder::new(48000, FrameRate::Fps25, 0.5);
648        assert_eq!(encoder.amplitude(), 0.5);
649    }
650
651    #[test]
652    fn test_encode_frame() {
653        let mut encoder = LtcEncoder::new(48000, FrameRate::Fps25, 0.5);
654        let timecode = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
655        let samples = encoder
656            .encode_frame(&timecode)
657            .expect("encode should succeed");
658        assert!(!samples.is_empty());
659    }
660
661    #[test]
662    fn test_user_bits_ascii() {
663        let user_bits = UserBitsEncoder::encode_ascii("TEST");
664        assert_ne!(user_bits, 0);
665    }
666
667    #[test]
668    fn test_user_bits_date() {
669        let user_bits = UserBitsEncoder::encode_date(12, 31, 2023);
670        assert_ne!(user_bits, 0);
671    }
672
673    #[test]
674    fn test_even_parity() {
675        let encoder = LtcEncoder::new(48000, FrameRate::Fps25, 0.5);
676        let bits = [true, false, true]; // 2 true bits = even
677        assert!(!encoder.calculate_even_parity(&bits));
678
679        let bits = [true, false, false]; // 1 true bit = odd
680        assert!(encoder.calculate_even_parity(&bits));
681    }
682
683    #[test]
684    fn test_encode_batch_25_frames_count() {
685        // 25 consecutive timecodes at 25fps must produce exactly 25 frame buffers.
686        let timecodes: Vec<Timecode> = (0u8..25)
687            .map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid"))
688            .collect();
689        let frames = LtcEncoder::encode_batch(&timecodes, 48000);
690        assert_eq!(frames.len(), 25, "batch must yield one buffer per timecode");
691    }
692
693    #[test]
694    fn test_encode_batch_frame_length_correct() {
695        // At 48000 Hz and 25fps: 48000/25 = 1920 samples per frame.
696        // Each sample maps to 80 LTC bits → 1920 / 80 = 24 samples per bit.
697        let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
698        let frames = LtcEncoder::encode_batch(&[tc], 48000);
699        let expected_samples = 48000usize / 25; // 1920
700        assert_eq!(
701            frames[0].len(),
702            expected_samples,
703            "frame audio length must equal sample_rate / fps"
704        );
705    }
706
707    #[test]
708    fn test_encode_batch_interleaved_length() {
709        let timecodes: Vec<Timecode> = (0u8..25)
710            .map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid"))
711            .collect();
712        let flat = LtcEncoder::encode_batch_interleaved(&timecodes, 48000);
713        // 25 frames × 1920 samples = 48000 samples (exactly one second)
714        assert_eq!(flat.len(), 48000);
715    }
716
717    #[test]
718    fn test_encode_batch_matches_interleaved() {
719        let timecodes: Vec<Timecode> = (0u8..5)
720            .map(|f| Timecode::new(0, 0, 0, f, FrameRate::Fps25).expect("valid"))
721            .collect();
722        let batched = LtcEncoder::encode_batch(&timecodes, 48000);
723        let flat: Vec<i16> = batched.iter().flatten().copied().collect();
724        let interleaved = LtcEncoder::encode_batch_interleaved(&timecodes, 48000);
725        assert_eq!(
726            flat, interleaved,
727            "batch and interleaved must produce identical output"
728        );
729    }
730}