#![cfg(feature = "fft-rustfft")]
extern crate alloc;
use alloc::collections::BTreeSet;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::super::decode::{ApHint, DecodeDepth, DecodeResult, DecodeStrictness};
use super::coarse_sync::coarse_sync;
use super::process_candidates::{
RefinedCandidate, fine_refine_pass1, process_candidates_tuned_with_ap_ref, refine_candidates,
};
use super::spectrogram::compute_spectrogram;
use super::types::{AudioSample, DEFAULT_Q_THRESH};
pub(super) const PASS_ID_AUTO_AP: u8 = 18;
const AUTO_AP_HARDERRORS_MAX: u32 = 22;
pub(super) fn extract_caller_callsigns(decodes: &[DecodeResult]) -> Vec<String> {
let mut set: BTreeSet<String> = BTreeSet::new();
for d in decodes {
let Some(text) = crate::msg::wsjt77::unpack77(&d.message77) else {
continue;
};
let mut tokens = text.split_whitespace();
let first = tokens.next();
let second = tokens.next();
let caller = match (first, second) {
(Some("CQ"), Some(c)) => c,
(Some(c), _) => c,
_ => continue,
};
if looks_like_callsign(caller) {
set.insert(caller.to_string());
}
}
set.into_iter().collect()
}
fn looks_like_callsign(s: &str) -> bool {
let len = s.len();
if !(3..=10).contains(&len) {
return false;
}
if s.starts_with('<') || s.ends_with('>') {
return false;
}
if s.contains('/') {
return false;
}
let bytes = s.as_bytes();
let has_digit = bytes.iter().any(|b| b.is_ascii_digit());
let has_alpha = bytes.iter().any(|b| b.is_ascii_alphabetic());
if !(has_alpha && has_digit) {
return false;
}
if s == "RR73" {
return false;
}
true
}
#[allow(clippy::too_many_arguments)]
pub(super) fn run<S>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
max_cand: usize,
existing: &[DecodeResult],
depth: DecodeDepth,
bp_max_iter: u32,
strictness: DecodeStrictness,
) -> Vec<DecodeResult>
where
S: AudioSample,
{
if !matches!(depth, DecodeDepth::BpAllOsd) {
return Vec::new();
}
let callsigns = extract_caller_callsigns(existing);
if callsigns.is_empty() {
return Vec::new();
}
let spec = compute_spectrogram(audio, freq_max);
const AUTO_AP_PASS1_LIMIT: usize = 200;
let cands = coarse_sync(&spec, freq_min, freq_max, sync_min, AUTO_AP_PASS1_LIMIT);
drop(spec);
let cands: alloc::vec::Vec<_> = cands
.into_iter()
.filter(|c| {
!existing
.iter()
.any(|r| (r.freq_hz - c.freq_hz).abs() < 3.0 && (r.dt_sec - c.dt_sec).abs() < 0.5)
})
.collect();
let cands = fine_refine_pass1(audio, cands);
let auto_ap_max_cand = max_cand.saturating_mul(4).max(200);
let refined: Vec<RefinedCandidate> = refine_candidates(audio, cands, auto_ap_max_cand);
let mut remaining: Vec<RefinedCandidate> = refined;
let mut out: Vec<DecodeResult> = Vec::new();
for callsign in &callsigns {
if remaining.is_empty() {
break;
}
let ap = ApHint::new().with_call1(callsign);
let batch_results = process_candidates_tuned_with_ap_ref(
audio,
&remaining,
depth,
DEFAULT_Q_THRESH,
bp_max_iter,
Some(&ap),
strictness,
);
for mut r in batch_results {
if existing.iter().any(|x| x.message77 == r.message77)
|| out.iter().any(|x| x.message77 == r.message77)
{
continue;
}
if r.hard_errors > AUTO_AP_HARDERRORS_MAX {
continue;
}
if let Some(pos) = remaining.iter().position(|c| {
(c.0.freq_hz - r.freq_hz).abs() < 0.1 && (c.0.dt_sec - r.dt_sec).abs() < 0.01
}) {
remaining.remove(pos);
}
r.pass = PASS_ID_AUTO_AP;
out.push(r);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn looks_like_callsign_filter_basics() {
assert!(looks_like_callsign("K1JT"));
assert!(looks_like_callsign("EA3AGB"));
assert!(looks_like_callsign("WA2FZW"));
assert!(!looks_like_callsign("-15"));
assert!(!looks_like_callsign("RR73"));
assert!(!looks_like_callsign("CQ"));
assert!(!looks_like_callsign("<HASH>"));
}
}