Skip to main content

oximedia_timecode/ltc/
mod.rs

1//! Linear Timecode (LTC) reading and writing
2//!
3//! LTC encodes timecode as an audio signal using biphase mark code (BMC).
4//! The audio signal contains 80 bits per frame:
5//! - 64 bits for timecode and user data
6//! - 16 bits for sync word (0x3FFD)
7//!
8//! # Biphase Mark Code
9//! - Bit 0: One transition in the middle of the bit cell
10//! - Bit 1: Two transitions (at the beginning and middle)
11//! - Frequencies: ~1920 Hz (bit 0 at 30fps) to ~2400 Hz (bit 1 at 30fps)
12//!
13//! # Signal Characteristics
14//! - Typically recorded at line level (-10 dBV to +4 dBu)
15//! - Can be read in forward or reverse
16//! - Can be read at varying speeds (0.1x to 10x nominal)
17
18pub mod decoder;
19pub mod encoder;
20
21use crate::{FrameRate, Timecode, TimecodeError, TimecodeReader, TimecodeWriter};
22
23/// LTC reader configuration
24#[derive(Debug, Clone)]
25pub struct LtcReaderConfig {
26    /// Sample rate of the input audio
27    pub sample_rate: u32,
28    /// Expected frame rate
29    pub frame_rate: FrameRate,
30    /// Minimum signal amplitude (0.0 to 1.0)
31    pub min_amplitude: f32,
32    /// Maximum speed variation (1.0 = nominal, 2.0 = 2x speed)
33    pub max_speed: f32,
34}
35
36impl Default for LtcReaderConfig {
37    fn default() -> Self {
38        LtcReaderConfig {
39            sample_rate: 48000,
40            frame_rate: FrameRate::Fps25,
41            min_amplitude: 0.1,
42            max_speed: 2.0,
43        }
44    }
45}
46
47/// LTC reader
48pub struct LtcReader {
49    decoder: decoder::LtcDecoder,
50    frame_rate: FrameRate,
51}
52
53impl LtcReader {
54    /// Create a new LTC reader with configuration
55    pub fn new(config: LtcReaderConfig) -> Self {
56        LtcReader {
57            decoder: decoder::LtcDecoder::new(
58                config.sample_rate,
59                config.frame_rate,
60                config.min_amplitude,
61            ),
62            frame_rate: config.frame_rate,
63        }
64    }
65
66    /// Process audio samples and attempt to decode timecode
67    pub fn process_samples(&mut self, samples: &[f32]) -> Result<Option<Timecode>, TimecodeError> {
68        self.decoder.process_samples(samples)
69    }
70
71    /// Reset the decoder state
72    pub fn reset(&mut self) {
73        self.decoder.reset();
74    }
75
76    /// Get the current sync confidence (0.0 to 1.0)
77    pub fn sync_confidence(&self) -> f32 {
78        self.decoder.sync_confidence()
79    }
80}
81
82impl TimecodeReader for LtcReader {
83    fn read_timecode(&mut self) -> Result<Option<Timecode>, TimecodeError> {
84        // This is a placeholder - in practice, samples would be fed externally
85        Ok(None)
86    }
87
88    fn frame_rate(&self) -> FrameRate {
89        self.frame_rate
90    }
91
92    fn is_synchronized(&self) -> bool {
93        self.decoder.is_synchronized()
94    }
95}
96
97/// LTC writer configuration
98#[derive(Debug, Clone)]
99pub struct LtcWriterConfig {
100    /// Sample rate of the output audio
101    pub sample_rate: u32,
102    /// Frame rate to encode
103    pub frame_rate: FrameRate,
104    /// Output signal amplitude (0.0 to 1.0)
105    pub amplitude: f32,
106}
107
108impl Default for LtcWriterConfig {
109    fn default() -> Self {
110        LtcWriterConfig {
111            sample_rate: 48000,
112            frame_rate: FrameRate::Fps25,
113            amplitude: 0.5,
114        }
115    }
116}
117
118/// LTC writer
119pub struct LtcWriter {
120    encoder: encoder::LtcEncoder,
121    frame_rate: FrameRate,
122}
123
124impl LtcWriter {
125    /// Create a new LTC writer with configuration
126    pub fn new(config: LtcWriterConfig) -> Self {
127        LtcWriter {
128            encoder: encoder::LtcEncoder::new(
129                config.sample_rate,
130                config.frame_rate,
131                config.amplitude,
132            ),
133            frame_rate: config.frame_rate,
134        }
135    }
136
137    /// Encode a timecode frame to audio samples
138    pub fn encode_frame(&mut self, timecode: &Timecode) -> Result<Vec<f32>, TimecodeError> {
139        self.encoder.encode_frame(timecode)
140    }
141
142    /// Reset the encoder state
143    pub fn reset(&mut self) {
144        self.encoder.reset();
145    }
146}
147
148impl TimecodeWriter for LtcWriter {
149    fn write_timecode(&mut self, timecode: &Timecode) -> Result<(), TimecodeError> {
150        let _samples = self.encode_frame(timecode)?;
151        // In a real implementation, samples would be written to an audio output
152        Ok(())
153    }
154
155    fn frame_rate(&self) -> FrameRate {
156        self.frame_rate
157    }
158
159    fn flush(&mut self) -> Result<(), TimecodeError> {
160        Ok(())
161    }
162}
163
164/// LTC bit patterns and constants
165pub(crate) mod constants {
166    /// SMPTE sync word (0x3FFD in binary: 11 1111 1111 1101)
167    pub const SYNC_WORD: u16 = 0x3FFD;
168
169    /// Number of bits per LTC frame
170    pub const BITS_PER_FRAME: usize = 80;
171
172    /// Number of data bits (excluding sync word)
173    pub const DATA_BITS: usize = 64;
174
175    /// Sync word bit length
176    pub const SYNC_BITS: usize = 16;
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_ltc_reader_creation() {
185        let config = LtcReaderConfig::default();
186        let _reader = LtcReader::new(config);
187    }
188
189    #[test]
190    fn test_ltc_writer_creation() {
191        let config = LtcWriterConfig::default();
192        let _writer = LtcWriter::new(config);
193    }
194
195    #[test]
196    fn test_sync_word() {
197        assert_eq!(constants::SYNC_WORD, 0x3FFD);
198        assert_eq!(constants::BITS_PER_FRAME, 80);
199    }
200}