lesavka/client/src/sync_probe/signature.rs

189 lines
4.1 KiB
Rust

//! Shared A/V signature palette for synthetic sync probes.
//!
//! The analyzer and generator both need the same small codebook. Keeping the
//! colors and tones here prevents a subtle class of transport-test regressions:
//! a client could emit a valid-looking pulse that the downstream analyzer no
//! longer recognizes as the same event identity.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ProbeColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
pub(crate) const MAX_EVENT_CODE: u32 = 16;
const PROBE_AUDIO_TONE_FREQUENCIES_HZ: [f64; MAX_EVENT_CODE as usize] = [
620.0, 780.0, 940.0, 1120.0, 1320.0, 1540.0, 1780.0, 2040.0, 2320.0, 2620.0, 2960.0, 3340.0,
3760.0, 4220.0, 4740.0, 5320.0,
];
const PROBE_COLOR_PALETTE: [(u32, ProbeColor); MAX_EVENT_CODE as usize] = [
(
1,
ProbeColor {
r: 255,
g: 45,
b: 45,
},
),
(
2,
ProbeColor {
r: 0,
g: 230,
b: 118,
},
),
(
3,
ProbeColor {
r: 41,
g: 121,
b: 255,
},
),
(
4,
ProbeColor {
r: 255,
g: 179,
b: 0,
},
),
(
5,
ProbeColor {
r: 216,
g: 27,
b: 96,
},
),
(
6,
ProbeColor {
r: 0,
g: 188,
b: 212,
},
),
(
7,
ProbeColor {
r: 205,
g: 220,
b: 57,
},
),
(
8,
ProbeColor {
r: 126,
g: 87,
b: 194,
},
),
(
9,
ProbeColor {
r: 255,
g: 112,
b: 67,
},
),
(
10,
ProbeColor {
r: 38,
g: 166,
b: 154,
},
),
(
11,
ProbeColor {
r: 255,
g: 64,
b: 129,
},
),
(
12,
ProbeColor {
r: 92,
g: 107,
b: 192,
},
),
(
13,
ProbeColor {
r: 255,
g: 235,
b: 59,
},
),
(
14,
ProbeColor {
r: 105,
g: 240,
b: 174,
},
),
(
15,
ProbeColor {
r: 171,
g: 71,
b: 188,
},
),
(
16,
ProbeColor {
r: 3,
g: 169,
b: 244,
},
),
];
/// Return the audio frequency assigned to a probe event code.
///
/// Inputs: one-based event code.
/// Outputs: tone frequency in hertz, or `None` for unsupported codes.
/// Why: coded client-origin probes must preserve event identity after network
/// transport, not just pulse cadence.
pub(crate) fn probe_audio_frequency_for_event_code(code: u32) -> Option<f64> {
PROBE_AUDIO_TONE_FREQUENCIES_HZ
.get(code.checked_sub(1)? as usize)
.copied()
}
/// Return the video color assigned to a probe event code.
///
/// Inputs: one-based event code.
/// Outputs: saturated RGB color, or `None` for unsupported codes.
/// Why: final RCT captures can drop early frames; color identity lets the
/// analyzer rejoin observed flashes with the exact client-generated event.
pub(crate) fn probe_color_for_event_code(code: u32) -> Option<ProbeColor> {
PROBE_COLOR_PALETTE
.into_iter()
.find_map(|(palette_code, color)| (palette_code == code).then_some(color))
}
/// Validate that every event code can be rendered and analyzed.
///
/// Inputs: user-supplied code sequence.
/// Outputs: unsupported code if one is present.
/// Why: failing at CLI/config time produces clearer diagnostics than a later
/// analyzer report with missing pulses.
pub(crate) fn first_unsupported_event_code(codes: &[u32]) -> Option<u32> {
codes.iter().copied().find(|code| {
probe_audio_frequency_for_event_code(*code).is_none()
|| probe_color_for_event_code(*code).is_none()
})
}