1#![allow(dead_code)]
9#![allow(clippy::cast_precision_loss)]
10#![allow(clippy::cast_possible_truncation)]
11#![allow(clippy::cast_sign_loss)]
12
13use crate::Timecode;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct LtcSignalParams {
20 pub sample_rate: u32,
22 pub amplitude: f32,
24 pub fps: u8,
26}
27
28impl LtcSignalParams {
29 pub fn default_25fps() -> Self {
31 Self {
32 sample_rate: 48000,
33 amplitude: 0.5,
34 fps: 25,
35 }
36 }
37
38 pub fn default_30fps() -> Self {
40 Self {
41 sample_rate: 48000,
42 amplitude: 0.5,
43 fps: 30,
44 }
45 }
46
47 pub fn samples_per_bit(&self) -> f64 {
51 self.sample_rate as f64 / (self.fps as f64 * 80.0)
52 }
53
54 pub fn samples_per_frame(&self) -> u32 {
56 (self.sample_rate as f64 / self.fps as f64).round() as u32
57 }
58}
59
60#[derive(Debug, Clone)]
66pub struct LtcBitEncoder;
67
68impl LtcBitEncoder {
69 const SYNC_WORD: [u8; 16] = [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1];
71
72 pub fn encode(tc: &Timecode) -> [u8; 80] {
74 let mut word = [0u8; 80];
75
76 let fu = tc.frames % 10;
78 for i in 0..4u8 {
79 word[i as usize] = (fu >> i) & 1;
80 }
81
82 let ft = tc.frames / 10;
84 word[8] = ft & 1;
85 word[9] = (ft >> 1) & 1;
86 word[10] = u8::from(tc.frame_rate.drop_frame);
87
88 let su = tc.seconds % 10;
90 for i in 0..4u8 {
91 word[16 + i as usize] = (su >> i) & 1;
92 }
93
94 let st = tc.seconds / 10;
96 for i in 0..3u8 {
97 word[24 + i as usize] = (st >> i) & 1;
98 }
99
100 let mu = tc.minutes % 10;
102 for i in 0..4u8 {
103 word[32 + i as usize] = (mu >> i) & 1;
104 }
105
106 let mt = tc.minutes / 10;
108 for i in 0..3u8 {
109 word[40 + i as usize] = (mt >> i) & 1;
110 }
111
112 let hu = tc.hours % 10;
114 for i in 0..4u8 {
115 word[48 + i as usize] = (hu >> i) & 1;
116 }
117
118 let ht = tc.hours / 10;
120 for i in 0..2u8 {
121 word[56 + i as usize] = (ht >> i) & 1;
122 }
123
124 word[64..80].copy_from_slice(&Self::SYNC_WORD);
126
127 word
128 }
129
130 pub fn decode(word: &[u8; 80]) -> (u8, u8, u8, u8, bool) {
132 let nibble = |positions: &[usize]| -> u8 {
133 positions
134 .iter()
135 .enumerate()
136 .map(|(shift, &pos)| word[pos] << shift)
137 .sum()
138 };
139
140 let frame_units = nibble(&[0, 1, 2, 3]);
141 let frame_tens = nibble(&[8, 9]) & 0x03;
142 let drop_frame = word[10] != 0;
143
144 let sec_units = nibble(&[16, 17, 18, 19]);
145 let sec_tens = nibble(&[24, 25, 26]) & 0x07;
146
147 let min_units = nibble(&[32, 33, 34, 35]);
148 let min_tens = nibble(&[40, 41, 42]) & 0x07;
149
150 let hr_units = nibble(&[48, 49, 50, 51]);
151 let hr_tens = nibble(&[56, 57]) & 0x03;
152
153 let frames = frame_tens * 10 + frame_units;
154 let seconds = sec_tens * 10 + sec_units;
155 let minutes = min_tens * 10 + min_units;
156 let hours = hr_tens * 10 + hr_units;
157
158 (hours, minutes, seconds, frames, drop_frame)
159 }
160}
161
162#[derive(Debug, Clone)]
186pub struct LtcAudioEncoder {
187 params: LtcSignalParams,
189 polarity: f32,
191}
192
193impl LtcAudioEncoder {
194 pub fn new(params: LtcSignalParams) -> Self {
196 Self {
197 params,
198 polarity: 1.0,
199 }
200 }
201
202 pub fn encode_frame(&mut self, bits: &[u8; 80]) -> Vec<f32> {
206 let spb = self.params.samples_per_bit();
207 let amplitude = self.params.amplitude;
208 let mut samples = Vec::with_capacity(self.params.samples_per_frame() as usize);
209
210 for &bit in bits.iter() {
211 let num_samples = spb.round() as usize;
212 let half = num_samples / 2;
213
214 if bit == 1 {
215 for _ in 0..half {
217 samples.push(self.polarity * amplitude);
218 }
219 self.polarity = -self.polarity;
220 for _ in half..num_samples {
221 samples.push(self.polarity * amplitude);
222 }
223 self.polarity = -self.polarity;
224 } else {
225 for _ in 0..num_samples {
227 samples.push(self.polarity * amplitude);
228 }
229 self.polarity = -self.polarity;
230 }
231 }
232
233 samples
234 }
235
236 pub fn encode_sequence(&mut self, timecodes: &[Timecode]) -> Vec<f32> {
238 let mut all_samples = Vec::new();
239 for tc in timecodes {
240 let bits = LtcBitEncoder::encode(tc);
241 let frame_samples = self.encode_frame(&bits);
242 all_samples.extend_from_slice(&frame_samples);
243 }
244 all_samples
245 }
246
247 pub fn polarity(&self) -> f32 {
249 self.polarity
250 }
251
252 pub fn reset_polarity(&mut self) {
254 self.polarity = 1.0;
255 }
256
257 pub fn params(&self) -> &LtcSignalParams {
259 &self.params
260 }
261}
262
263#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::FrameRateInfo;
269
270 fn make_tc(h: u8, m: u8, s: u8, f: u8, fps: u8, df: bool) -> Timecode {
271 Timecode {
272 hours: h,
273 minutes: m,
274 seconds: s,
275 frames: f,
276 frame_rate: FrameRateInfo {
277 fps,
278 drop_frame: df,
279 },
280 user_bits: 0,
281 }
282 }
283
284 fn default_params() -> LtcSignalParams {
285 LtcSignalParams::default_25fps()
286 }
287
288 #[test]
289 fn test_signal_params_samples_per_bit_25fps() {
290 let p = default_params();
291 let spb = p.samples_per_bit();
292 assert!((spb - 24.0).abs() < 1e-6);
294 }
295
296 #[test]
297 fn test_signal_params_samples_per_frame_25fps() {
298 let p = default_params();
299 assert_eq!(p.samples_per_frame(), 1920); }
301
302 #[test]
303 fn test_signal_params_30fps() {
304 let p = LtcSignalParams::default_30fps();
305 assert_eq!(p.fps, 30);
306 assert_eq!(p.samples_per_frame(), 1600); }
308
309 #[test]
310 fn test_bit_encoder_roundtrip() {
311 let tc = make_tc(12, 34, 56, 7, 25, false);
312 let bits = LtcBitEncoder::encode(&tc);
313 let (h, m, s, f, df) = LtcBitEncoder::decode(&bits);
314 assert_eq!(h, 12);
315 assert_eq!(m, 34);
316 assert_eq!(s, 56);
317 assert_eq!(f, 7);
318 assert!(!df);
319 }
320
321 #[test]
322 fn test_bit_encoder_drop_frame_flag() {
323 let tc = make_tc(0, 0, 0, 2, 30, true);
324 let bits = LtcBitEncoder::encode(&tc);
325 let (_, _, _, _, df) = LtcBitEncoder::decode(&bits);
326 assert!(df);
327 }
328
329 #[test]
330 fn test_bit_encoder_midnight() {
331 let tc = make_tc(0, 0, 0, 0, 25, false);
332 let bits = LtcBitEncoder::encode(&tc);
333 let (h, m, s, f, _) = LtcBitEncoder::decode(&bits);
334 assert_eq!((h, m, s, f), (0, 0, 0, 0));
335 }
336
337 #[test]
338 fn test_bit_encoder_max_values() {
339 let tc = make_tc(23, 59, 59, 24, 25, false);
340 let bits = LtcBitEncoder::encode(&tc);
341 let (h, m, s, f, _) = LtcBitEncoder::decode(&bits);
342 assert_eq!(h, 23);
343 assert_eq!(m, 59);
344 assert_eq!(s, 59);
345 assert_eq!(f, 24);
346 }
347
348 #[test]
349 fn test_bit_encoder_sync_word_present() {
350 let tc = make_tc(0, 0, 0, 0, 25, false);
351 let bits = LtcBitEncoder::encode(&tc);
352 assert_eq!(&bits[64..80], &LtcBitEncoder::SYNC_WORD);
353 }
354
355 #[test]
356 fn test_audio_encoder_output_length() {
357 let params = default_params();
358 let mut enc = LtcAudioEncoder::new(params);
359 let tc = make_tc(0, 0, 0, 0, 25, false);
360 let bits = LtcBitEncoder::encode(&tc);
361 let samples = enc.encode_frame(&bits);
362 assert!(!samples.is_empty());
364 assert!(samples.len() >= 1900 && samples.len() <= 1940);
365 }
366
367 #[test]
368 fn test_audio_encoder_amplitude_bounds() {
369 let params = LtcSignalParams {
370 sample_rate: 48000,
371 amplitude: 0.8,
372 fps: 25,
373 };
374 let mut enc = LtcAudioEncoder::new(params);
375 let tc = make_tc(1, 0, 0, 0, 25, false);
376 let bits = LtcBitEncoder::encode(&tc);
377 let samples = enc.encode_frame(&bits);
378 for &s in &samples {
379 assert!(s.abs() <= 0.8 + 1e-6);
380 }
381 }
382
383 #[test]
384 fn test_audio_encoder_polarity_reset() {
385 let mut enc = LtcAudioEncoder::new(default_params());
386 assert!((enc.polarity() - 1.0).abs() < 1e-6);
387 enc.polarity = -1.0;
388 enc.reset_polarity();
389 assert!((enc.polarity() - 1.0).abs() < 1e-6);
390 }
391
392 #[test]
393 fn test_audio_encoder_sequence() {
394 let mut enc = LtcAudioEncoder::new(default_params());
395 let tcs = vec![
396 make_tc(0, 0, 0, 0, 25, false),
397 make_tc(0, 0, 0, 1, 25, false),
398 ];
399 let samples = enc.encode_sequence(&tcs);
400 assert!(samples.len() >= 3800);
402 }
403
404 #[test]
405 fn test_audio_encoder_params_accessor() {
406 let params = default_params();
407 let enc = LtcAudioEncoder::new(params);
408 assert_eq!(enc.params().fps, 25);
409 assert_eq!(enc.params().sample_rate, 48000);
410 }
411}