const JUMP_THRESH: i32 = 0x4000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SerializerArmOutcome {
pub left_shift_bits: u8,
pub adjacent_jumps: u32,
pub low_byte_stuck_pct: u8,
pub signal_bin_permille: u16,
pub accepted: bool,
}
#[must_use]
pub fn detect_arm_phase(samples: &[i16], goertzel_coeff: f32) -> SerializerArmOutcome {
let n = samples.len();
if n < 4 {
return SerializerArmOutcome::default();
}
let or_acc: u16 = samples.iter().fold(0u16, |a, &s| a | (s as u16));
let left_shift_bits = if or_acc == 0 {
0
} else {
or_acc.trailing_zeros() as u8
};
let mut adjacent_jumps: u32 = 0;
for w in samples.windows(2) {
if (w[1] as i32 - w[0] as i32).abs() > JUMP_THRESH {
adjacent_jumps += 1;
}
}
let stuck = samples
.iter()
.filter(|&&s| {
let lb = (s as u16) & 0x00FF;
lb == 0x00 || lb == 0xFF
})
.count();
let low_byte_stuck_pct = ((stuck * 100) / n) as u8;
let mut s_prev = 0f32;
let mut s_prev2 = 0f32;
let mut total_power = 0f32;
for &x in samples {
let xf = x as f32;
total_power += xf * xf;
let s = xf + goertzel_coeff * s_prev - s_prev2;
s_prev2 = s_prev;
s_prev = s;
}
let tone_power = s_prev2 * s_prev2 + s_prev * s_prev - goertzel_coeff * s_prev * s_prev2;
let denom = total_power * (n as f32) * 0.5;
let frac = if denom > 0.0 { tone_power / denom } else { 0.0 };
let signal_bin_permille = if frac <= 0.0 {
0
} else if frac >= 1.0 {
1000
} else {
(frac * 1000.0) as u16
};
let accepted = left_shift_bits == 0
&& adjacent_jumps == 0
&& low_byte_stuck_pct < 5
&& signal_bin_permille > 800;
SerializerArmOutcome {
left_shift_bits,
adjacent_jumps,
low_byte_stuck_pct,
signal_bin_permille,
accepted,
}
}
#[cfg(test)]
mod tests {
use super::*;
const COEFF_1K: f32 = 1.982_889_7;
const SINW_1K: f32 = 0.130_526_2;
fn clean_1k(buf: &mut [i16], amp: f32) {
let mut s2 = 0f32; let mut s1 = amp * SINW_1K; buf[0] = 0;
if buf.len() > 1 {
buf[1] = s1 as i16;
}
for slot in buf.iter_mut().skip(2) {
let s = COEFF_1K * s1 - s2;
*slot = s as i16;
s2 = s1;
s1 = s;
}
}
#[test]
fn clean_tone_accepted() {
let mut buf = [0i16; 256];
clean_1k(&mut buf, 16_000.0);
let o = detect_arm_phase(&buf, COEFF_1K);
assert_eq!(o.left_shift_bits, 0, "clean tone is LSB-aligned");
assert_eq!(o.adjacent_jumps, 0, "clean tone has no wraps");
assert!(o.low_byte_stuck_pct < 5, "stuck {}", o.low_byte_stuck_pct);
assert!(
o.signal_bin_permille > 800,
"permille {}",
o.signal_bin_permille
);
assert!(o.accepted, "{o:?}");
}
#[test]
fn left_shift_two_rejected() {
let mut buf = [0i16; 256];
clean_1k(&mut buf, 4_000.0);
for s in buf.iter_mut() {
*s = ((*s as i32) << 2) as i16;
}
let o = detect_arm_phase(&buf, COEFF_1K);
assert_eq!(o.left_shift_bits, 2, "{o:?}");
assert!(!o.accepted, "left-shifted capture must be rejected: {o:?}");
}
#[test]
fn byte_stuck_rejected() {
let mut buf = [0i16; 256];
clean_1k(&mut buf, 16_000.0);
for s in buf.iter_mut() {
*s = (*s as u16 & 0xFF00) as i16; }
let o = detect_arm_phase(&buf, COEFF_1K);
assert!(o.low_byte_stuck_pct >= 50, "stuck {}", o.low_byte_stuck_pct);
assert!(!o.accepted, "byte-stuck capture must be rejected: {o:?}");
}
#[test]
fn wrap_rejected() {
let mut buf = [0i16; 256];
for (i, s) in buf.iter_mut().enumerate() {
*s = if i % 2 == 0 { 30_001 } else { -30_001 };
}
let o = detect_arm_phase(&buf, COEFF_1K);
assert!(o.adjacent_jumps > 100, "jumps {}", o.adjacent_jumps);
assert!(!o.accepted, "wrapped capture must be rejected: {o:?}");
}
#[test]
fn real_2x_lock_vector_rejected() {
const COEFF_500: f32 = 1.995_717_4; let v: [i16; 24] = [
-1504, -5728, -9952, -13920, -17728, -21152, -24256, -26816, -28928, -30752, -31808,
-32416, -32480, -31872, -30816, -29184, 29311, 30991, 32159, 32703, 32767, 32255,
31167, 29567,
];
let o = detect_arm_phase(&v, COEFF_500);
assert!(o.adjacent_jumps >= 1, "must catch the discontinuity: {o:?}");
assert!(
!o.accepted,
"real bit-shifted/clipped capture must be rejected: {o:?}"
);
}
#[test]
fn too_short_returns_default() {
let o = detect_arm_phase(&[1, 2, 3], COEFF_1K);
assert_eq!(o, SerializerArmOutcome::default());
assert!(!o.accepted);
}
}