use crate::core::Block;
use crate::dsp::Rotator;
use crate::modulate::OfdmConfig;
use crate::multicarrier::{CarrierPlan, SymbolFft};
use num_complex::Complex32 as C32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OfdmPreamble {
pub num_repeats: usize,
pub repeat_len: usize,
pub training_symbol: Option<TrainingSymbol>,
}
impl OfdmPreamble {
pub fn new(num_repeats: usize, repeat_len: usize) -> Self {
Self {
num_repeats,
repeat_len,
training_symbol: None,
}
}
pub fn with_training_symbol(mut self, n_fft: usize, cp_len: usize) -> Self {
self.training_symbol = Some(TrainingSymbol { n_fft, cp_len });
self
}
pub fn total_len(&self) -> usize {
self.num_repeats * self.repeat_len + self.training_symbol.map_or(0, |t| t.total_len())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TrainingSymbol {
pub n_fft: usize,
pub cp_len: usize,
}
impl TrainingSymbol {
pub fn total_len(&self) -> usize {
self.n_fft + self.cp_len
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OfdmSyncResult {
pub start_sample: usize,
pub cfo_hz: f32,
pub integer_cfo_bins: i32,
pub score: f32,
}
pub fn generate_ofdm_preamble(preamble: &OfdmPreamble, cfg: &OfdmConfig) -> Vec<C32> {
let n_fft = cfg.carrier_plan.n_fft();
let occupied_half = cfg.carrier_plan.occupied_half_carriers();
let n_data = cfg.carrier_plan.data_carriers().len();
let base = band_limited_repeat_base(preamble.repeat_len, n_fft, occupied_half, n_data)
.unwrap_or_else(|| {
pseudo_random_unit_sequence(preamble.repeat_len, 0x4F46_444D_5052_4531)
});
let mut out = Vec::with_capacity(preamble.total_len());
for _ in 0..preamble.num_repeats {
out.extend_from_slice(&base);
}
if let Some(training) = preamble.training_symbol {
out.extend_from_slice(&generate_training_symbol_time_domain(
training,
&cfg.carrier_plan,
));
}
let g = cfg.gain;
if g != 1.0 {
for s in &mut out {
s.re *= g;
s.im *= g;
}
}
out
}
const SC_PREAMBLE_BOOST: f32 = 2.0;
fn band_limited_repeat_base(
repeat_len: usize,
n_fft: usize,
occupied_half: usize,
n_data: usize,
) -> Option<Vec<C32>> {
if repeat_len == 0 || n_fft == 0 || !n_fft.is_multiple_of(repeat_len) || occupied_half == 0 {
return None;
}
let k = n_fft / repeat_len;
let loaded: Vec<usize> = (1..=occupied_half as i32)
.flat_map(|i| [i, -i])
.filter(|i| (i.unsigned_abs() as usize).is_multiple_of(k))
.map(|i| {
if i >= 0 {
i as usize
} else {
n_fft - i.unsigned_abs() as usize
}
})
.collect();
if loaded.is_empty() {
return None;
}
let values = pseudo_random_unit_sequence(loaded.len(), 0x4F46_444D_5052_4531);
let mut freq = vec![C32::default(); n_fft];
for (&bin, &v) in loaded.iter().zip(values.iter()) {
freq[bin] = v;
}
let mut ifft = crate::multicarrier::IfftBlock::new(n_fft);
let mut time = vec![C32::default(); n_fft];
ifft.process(&freq, &mut time);
time.truncate(repeat_len);
let rms = (time.iter().map(|c| c.norm_sqr()).sum::<f32>() / time.len() as f32).sqrt();
if rms > 0.0 {
let target = SC_PREAMBLE_BOOST * (n_data as f32).sqrt() / n_fft as f32;
let scale = target / rms;
for c in &mut time {
c.re *= scale;
c.im *= scale;
}
}
Some(time)
}
pub(crate) fn training_symbol_freq_pattern(n_fft: usize) -> Vec<C32> {
pseudo_random_unit_sequence(n_fft, 0x4F46_444D_5452_4E31)
}
fn generate_training_symbol_time_domain(training: TrainingSymbol, plan: &CarrierPlan) -> Vec<C32> {
use crate::multicarrier::{CyclicPrefixInsert, IfftBlock};
let mut freq = training_symbol_freq_pattern(training.n_fft);
let occupied = plan.occupied_bins();
if !occupied.is_empty() {
let mut load = vec![false; training.n_fft];
for bin in occupied {
if bin < training.n_fft {
load[bin] = true;
}
}
for (bin, v) in freq.iter_mut().enumerate() {
if !load[bin] {
*v = C32::default();
}
}
}
let mut ifft = IfftBlock::new(training.n_fft);
let mut time = vec![C32::default(); training.n_fft];
ifft.process(&freq, &mut time);
let mut cp_insert = CyclicPrefixInsert::new(training.n_fft, training.cp_len);
let mut out = vec![C32::default(); training.total_len()];
cp_insert.process(&time, &mut out);
out
}
fn pseudo_random_unit_sequence(len: usize, seed: u64) -> Vec<C32> {
let mut state = seed;
let mut next_f32 = || -> f32 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state as f32) / (u64::MAX as f32) - 0.5
};
let scale = std::f32::consts::FRAC_1_SQRT_2;
(0..len)
.map(|_| {
let re = if next_f32() >= 0.0 { scale } else { -scale };
let im = if next_f32() >= 0.0 { scale } else { -scale };
C32::new(re, im)
})
.collect()
}
pub fn ofdm_sync(
iq: &[C32],
fs: f32,
preamble: &OfdmPreamble,
search_start: usize,
search_end: usize,
) -> Vec<OfdmSyncResult> {
let repeat_len = preamble.repeat_len;
let num_repeats = preamble.num_repeats;
if repeat_len == 0 || num_repeats < 2 || fs <= 0.0 {
return Vec::new();
}
let preamble_len = preamble.total_len();
let end = search_end.min(iq.len().saturating_sub(preamble_len));
if search_start >= end {
return Vec::new();
}
let mut all = Vec::with_capacity(end - search_start);
let mut r_peak = 0.0f32;
for d in search_start..end {
let mut p = C32::default();
let mut r = 0.0f32;
for seg in 0..num_repeats - 1 {
let a0 = d + seg * repeat_len;
let b0 = a0 + repeat_len;
let (seg_p, seg_r) = correlate_segment(iq, a0, b0, repeat_len);
p += seg_p;
r += seg_r;
}
if r <= 0.0 {
continue;
}
r_peak = r_peak.max(r);
let score = (p.norm_sqr() / (r * r)).clamp(0.0, 1.0);
let cfo_hz = p.im.atan2(p.re) / (core::f32::consts::TAU * repeat_len as f32 / fs);
all.push((
r,
OfdmSyncResult {
start_sample: d,
cfo_hz,
integer_cfo_bins: 0,
score,
},
));
}
if all.is_empty() || r_peak <= 0.0 {
return Vec::new();
}
let mut ranked: Vec<(f32, OfdmSyncResult)> = all
.into_iter()
.map(|(r, result)| (result.score * (r / r_peak), result))
.collect();
ranked.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let mut results: Vec<OfdmSyncResult> = ranked.into_iter().map(|(_, r)| r).collect();
if let Some(training) = preamble.training_symbol {
let top_n = results.len().min(5);
for result in &mut results[..top_n] {
let training_start = result.start_sample + repeat_len * num_repeats;
result.integer_cfo_bins =
estimate_integer_cfo_bins(iq, fs, training, training_start, result.cfo_hz);
}
}
results
}
pub fn earliest_accepted(
results: Vec<OfdmSyncResult>,
score_threshold: f32,
cluster_len: usize,
) -> Option<OfdmSyncResult> {
let accepted: Vec<OfdmSyncResult> = results
.into_iter()
.filter(|r| r.score >= score_threshold)
.collect();
let earliest = accepted.iter().map(|r| r.start_sample).min()?;
accepted
.into_iter()
.find(|r| r.start_sample - earliest < cluster_len.max(1))
}
fn estimate_integer_cfo_bins(
iq: &[C32],
fs: f32,
training: TrainingSymbol,
training_start: usize,
fractional_cfo_hz: f32,
) -> i32 {
let total_len = training.total_len();
if training_start + total_len > iq.len() {
return 0;
}
let raw = &iq[training_start..training_start + total_len];
let mut corrected = vec![C32::default(); total_len];
let mut rot = Rotator::new(-fractional_cfo_hz, fs);
rot.rotate_block(raw, &mut corrected);
let n_fft = training.n_fft;
let mut symbol_fft = SymbolFft::new(n_fft, training.cp_len);
let freq = match symbol_fft.demod_symbol(&corrected) {
Some(f) => f,
None => return 0,
};
let known = training_symbol_freq_pattern(n_fft);
let max_shift = (n_fft / 2) as i32;
let mut best_shift = 0i32;
let mut best_corr = -1.0f32;
for shift in -max_shift..=max_shift {
let mut corr = C32::default();
for (bin, &k) in known.iter().enumerate() {
let src_bin = (bin as i32 + shift).rem_euclid(n_fft as i32) as usize;
corr += k.conj() * freq[src_bin];
}
let mag = corr.norm_sqr();
if mag > best_corr {
best_corr = mag;
best_shift = shift;
}
}
best_shift
}
#[inline]
fn correlate_segment(iq: &[C32], a0: usize, b0: usize, len: usize) -> (C32, f32) {
let mut p = C32::default();
let mut r = 0.0f32;
let mut i = 0;
let nn = len & !3;
while i < nn {
p += iq[a0 + i].conj() * iq[b0 + i];
r += iq[b0 + i].norm_sqr();
p += iq[a0 + i + 1].conj() * iq[b0 + i + 1];
r += iq[b0 + i + 1].norm_sqr();
p += iq[a0 + i + 2].conj() * iq[b0 + i + 2];
r += iq[b0 + i + 2].norm_sqr();
p += iq[a0 + i + 3].conj() * iq[b0 + i + 3];
r += iq[b0 + i + 3].norm_sqr();
i += 4;
}
while i < len {
p += iq[a0 + i].conj() * iq[b0 + i];
r += iq[b0 + i].norm_sqr();
i += 1;
}
(p, r)
}