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    /// Return the most recently decoded timecode.
84    ///
85    /// Audio samples must be submitted via [`LtcReader::process_samples`]
86    /// before this method can return `Some(timecode)`.  Calling
87    /// `read_timecode` without first feeding samples will always return
88    /// `Ok(None)` because no frames have been decoded yet.
89    ///
90    /// This design keeps the `TimecodeReader` trait pull-based: callers that
91    /// own the sample source feed chunks via `process_samples`, then poll
92    /// `read_timecode` to retrieve completed frames.
93    fn read_timecode(&mut self) -> Result<Option<Timecode>, TimecodeError> {
94        Ok(self.decoder.last_decoded_timecode())
95    }
96
97    fn frame_rate(&self) -> FrameRate {
98        self.frame_rate
99    }
100
101    fn is_synchronized(&self) -> bool {
102        self.decoder.is_synchronized()
103    }
104}
105
106/// LTC writer configuration
107#[derive(Debug, Clone)]
108pub struct LtcWriterConfig {
109    /// Sample rate of the output audio
110    pub sample_rate: u32,
111    /// Frame rate to encode
112    pub frame_rate: FrameRate,
113    /// Output signal amplitude (0.0 to 1.0)
114    pub amplitude: f32,
115}
116
117impl Default for LtcWriterConfig {
118    fn default() -> Self {
119        LtcWriterConfig {
120            sample_rate: 48000,
121            frame_rate: FrameRate::Fps25,
122            amplitude: 0.5,
123        }
124    }
125}
126
127/// LTC writer
128pub struct LtcWriter {
129    encoder: encoder::LtcEncoder,
130    frame_rate: FrameRate,
131}
132
133impl LtcWriter {
134    /// Create a new LTC writer with configuration
135    pub fn new(config: LtcWriterConfig) -> Self {
136        LtcWriter {
137            encoder: encoder::LtcEncoder::new(
138                config.sample_rate,
139                config.frame_rate,
140                config.amplitude,
141            ),
142            frame_rate: config.frame_rate,
143        }
144    }
145
146    /// Encode a timecode frame to audio samples
147    pub fn encode_frame(&mut self, timecode: &Timecode) -> Result<Vec<f32>, TimecodeError> {
148        self.encoder.encode_frame(timecode)
149    }
150
151    /// Reset the encoder state
152    pub fn reset(&mut self) {
153        self.encoder.reset();
154    }
155}
156
157impl TimecodeWriter for LtcWriter {
158    fn write_timecode(&mut self, timecode: &Timecode) -> Result<(), TimecodeError> {
159        let _samples = self.encode_frame(timecode)?;
160        // In a real implementation, samples would be written to an audio output
161        Ok(())
162    }
163
164    fn frame_rate(&self) -> FrameRate {
165        self.frame_rate
166    }
167
168    fn flush(&mut self) -> Result<(), TimecodeError> {
169        Ok(())
170    }
171}
172
173/// LTC bit patterns and constants
174pub(crate) mod constants {
175    /// SMPTE sync word (0x3FFD in binary: 11 1111 1111 1101)
176    pub const SYNC_WORD: u16 = 0x3FFD;
177
178    /// Number of bits per LTC frame
179    pub const BITS_PER_FRAME: usize = 80;
180
181    /// Number of data bits (excluding sync word)
182    pub const DATA_BITS: usize = 64;
183
184    /// Sync word bit length
185    pub const SYNC_BITS: usize = 16;
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_ltc_reader_creation() {
194        let config = LtcReaderConfig::default();
195        let _reader = LtcReader::new(config);
196    }
197
198    #[test]
199    fn test_ltc_writer_creation() {
200        let config = LtcWriterConfig::default();
201        let _writer = LtcWriter::new(config);
202    }
203
204    #[test]
205    fn test_sync_word() {
206        assert_eq!(constants::SYNC_WORD, 0x3FFD);
207        assert_eq!(constants::BITS_PER_FRAME, 80);
208    }
209}