#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncSignalType {
BlackBurst,
TriLevel,
SdiEmbedded,
WordClock,
Gps,
}
impl SyncSignalType {
#[must_use]
pub fn description(&self) -> &'static str {
match self {
Self::BlackBurst => "Black Burst (analog composite)",
Self::TriLevel => "Tri-Level Sync (HD reference)",
Self::SdiEmbedded => "SDI Embedded Sync",
Self::WordClock => "Word Clock",
Self::Gps => "GPS PPS",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockState {
NoSignal,
Acquiring,
Locked,
Lost,
}
impl LockState {
#[must_use]
pub fn is_locked(&self) -> bool {
matches!(self, Self::Locked)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PhaseAlignment {
pub offset_degrees: f32,
pub frequency_error_ppm: f32,
pub aligned: bool,
}
impl PhaseAlignment {
#[must_use]
pub fn new(offset_degrees: f32, frequency_error_ppm: f32, tolerance_degrees: f32) -> Self {
let aligned =
offset_degrees.abs() <= tolerance_degrees && frequency_error_ppm.abs() <= 10.0;
Self {
offset_degrees,
frequency_error_ppm,
aligned,
}
}
#[must_use]
pub fn phase_fraction(&self) -> f32 {
self.offset_degrees / 360.0
}
#[must_use]
pub fn offset_microseconds(&self, fps: f32) -> f32 {
if fps <= 0.0 {
return 0.0;
}
let frame_period_us = 1_000_000.0 / fps;
(self.offset_degrees / 360.0) * frame_period_us
}
}
#[derive(Debug, Clone)]
pub struct GenlockGeneratorConfig {
pub signal_type: SyncSignalType,
pub fps_num: u32,
pub fps_den: u32,
pub phase_offset_deg: f32,
pub measure_jitter: bool,
}
impl GenlockGeneratorConfig {
#[must_use]
pub fn hd_default() -> Self {
Self {
signal_type: SyncSignalType::TriLevel,
fps_num: 30000,
fps_den: 1001,
phase_offset_deg: 0.0,
measure_jitter: true,
}
}
#[must_use]
pub fn cinema() -> Self {
Self {
signal_type: SyncSignalType::TriLevel,
fps_num: 24,
fps_den: 1,
phase_offset_deg: 0.0,
measure_jitter: true,
}
}
#[must_use]
pub fn fps(&self) -> f64 {
f64::from(self.fps_num) / f64::from(self.fps_den)
}
#[must_use]
pub fn frame_period(&self) -> Duration {
let nanos = (1_000_000_000.0 / self.fps()) as u64;
Duration::from_nanos(nanos)
}
}
impl Default for GenlockGeneratorConfig {
fn default() -> Self {
Self::hd_default()
}
}
#[allow(dead_code)]
pub struct GenlockGenerator {
config: GenlockGeneratorConfig,
pulse_count: u64,
jitter_ns_accumulated: f64,
}
impl GenlockGenerator {
#[must_use]
pub fn new(config: GenlockGeneratorConfig) -> Self {
Self {
config,
pulse_count: 0,
jitter_ns_accumulated: 0.0,
}
}
pub fn emit_pulse(&mut self, jitter_ns: f64) {
self.pulse_count += 1;
if self.config.measure_jitter {
self.jitter_ns_accumulated += jitter_ns.abs();
}
}
#[must_use]
pub fn pulse_count(&self) -> u64 {
self.pulse_count
}
#[must_use]
pub fn average_jitter_ns(&self) -> f64 {
if !self.config.measure_jitter || self.pulse_count == 0 {
return 0.0;
}
self.jitter_ns_accumulated / self.pulse_count as f64
}
#[must_use]
pub fn config(&self) -> &GenlockGeneratorConfig {
&self.config
}
}
#[allow(dead_code)]
pub struct LockDetector {
expected_period: Duration,
tolerance_ns: u64,
state: LockState,
lock_threshold: u32,
consecutive_in_tolerance: u32,
}
impl LockDetector {
#[must_use]
pub fn new(expected_period: Duration, tolerance_ns: u64, lock_threshold: u32) -> Self {
Self {
expected_period,
tolerance_ns,
state: LockState::NoSignal,
lock_threshold,
consecutive_in_tolerance: 0,
}
}
pub fn process_pulse(&mut self, measured_period: Duration) {
let expected_ns = self.expected_period.as_nanos() as i64;
let measured_ns = measured_period.as_nanos() as i64;
let deviation = (measured_ns - expected_ns).unsigned_abs();
if deviation <= self.tolerance_ns {
self.consecutive_in_tolerance += 1;
if self.consecutive_in_tolerance >= self.lock_threshold {
self.state = LockState::Locked;
} else {
self.state = LockState::Acquiring;
}
} else {
self.consecutive_in_tolerance = 0;
self.state = match self.state {
LockState::Locked => LockState::Lost,
_ => LockState::Acquiring,
};
}
}
#[must_use]
pub fn state(&self) -> LockState {
self.state
}
#[must_use]
pub fn is_locked(&self) -> bool {
self.state.is_locked()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_signal_description() {
assert_eq!(
SyncSignalType::TriLevel.description(),
"Tri-Level Sync (HD reference)"
);
assert_eq!(
SyncSignalType::BlackBurst.description(),
"Black Burst (analog composite)"
);
}
#[test]
fn test_lock_state_is_locked() {
assert!(LockState::Locked.is_locked());
assert!(!LockState::NoSignal.is_locked());
assert!(!LockState::Acquiring.is_locked());
assert!(!LockState::Lost.is_locked());
}
#[test]
fn test_phase_alignment_aligned_within_tolerance() {
let pa = PhaseAlignment::new(1.0, 0.5, 5.0);
assert!(pa.aligned);
}
#[test]
fn test_phase_alignment_not_aligned_offset_exceeds_tolerance() {
let pa = PhaseAlignment::new(10.0, 0.0, 5.0);
assert!(!pa.aligned);
}
#[test]
fn test_phase_alignment_not_aligned_frequency_error() {
let pa = PhaseAlignment::new(0.0, 15.0, 5.0);
assert!(!pa.aligned);
}
#[test]
fn test_phase_alignment_phase_fraction() {
let pa = PhaseAlignment::new(90.0, 0.0, 180.0);
assert!((pa.phase_fraction() - 0.25).abs() < 1e-5);
}
#[test]
fn test_phase_alignment_offset_microseconds_24fps() {
let pa = PhaseAlignment::new(90.0, 0.0, 180.0);
let us = pa.offset_microseconds(24.0);
let expected = (1_000_000.0_f32 / 24.0) * 0.25;
assert!((us - expected).abs() < 1.0);
}
#[test]
fn test_phase_alignment_offset_microseconds_zero_fps() {
let pa = PhaseAlignment::new(90.0, 0.0, 180.0);
assert_eq!(pa.offset_microseconds(0.0), 0.0);
}
#[test]
fn test_genlock_generator_config_fps() {
let config = GenlockGeneratorConfig::hd_default();
assert!((config.fps() - 29.97).abs() < 0.01);
}
#[test]
fn test_genlock_generator_config_cinema_fps() {
let config = GenlockGeneratorConfig::cinema();
assert!((config.fps() - 24.0).abs() < 1e-10);
}
#[test]
fn test_genlock_generator_config_frame_period() {
let config = GenlockGeneratorConfig::cinema();
let period = config.frame_period();
assert!((period.as_secs_f64() - 1.0 / 24.0).abs() < 0.001);
}
#[test]
fn test_genlock_generator_emit_pulse() {
let mut gen = GenlockGenerator::new(GenlockGeneratorConfig::default());
gen.emit_pulse(100.0);
gen.emit_pulse(200.0);
assert_eq!(gen.pulse_count(), 2);
assert!((gen.average_jitter_ns() - 150.0).abs() < 1e-5);
}
#[test]
fn test_genlock_generator_no_jitter_measurement() {
let mut config = GenlockGeneratorConfig::default();
config.measure_jitter = false;
let mut gen = GenlockGenerator::new(config);
gen.emit_pulse(500.0);
assert_eq!(gen.average_jitter_ns(), 0.0);
}
#[test]
fn test_lock_detector_acquires_then_locks() {
let period = Duration::from_nanos(41_666_667); let mut detector = LockDetector::new(period, 10_000, 3);
assert_eq!(detector.state(), LockState::NoSignal);
detector.process_pulse(period);
assert_eq!(detector.state(), LockState::Acquiring);
detector.process_pulse(period);
assert_eq!(detector.state(), LockState::Acquiring);
detector.process_pulse(period);
assert_eq!(detector.state(), LockState::Locked);
assert!(detector.is_locked());
}
#[test]
fn test_lock_detector_loses_lock_on_deviation() {
let period = Duration::from_nanos(41_666_667);
let mut detector = LockDetector::new(period, 10_000, 1);
detector.process_pulse(period);
assert!(detector.is_locked());
detector.process_pulse(Duration::from_nanos(period.as_nanos() as u64 + 100_000));
assert_eq!(detector.state(), LockState::Lost);
}
}