use super::dvb_t_probe::DvbTRxProbe;
use super::ofdm::{EqualizerMethod, OfdmEqualizer};
use super::ofdm_frame::{bit_error_rate, decode_chain};
use crate::core::Block;
use crate::dsp::Rotator;
use crate::fec::{CrcKind, DecodeRule, InterleaverKind, ScramblerKind, ScramblerPos};
use crate::modulate::ofdm_frame::{CodecCache, block_plan, encode_chain_stages, inner_encode};
use crate::multicarrier::SymbolFft;
use crate::sync::{dvb_t_gi_sync, dvb_t_integer_cfo};
use crate::waveform::dvb_t::{
DVB_T_DATA_CARRIERS, DVB_T_FRAME_OUTER, DVB_T_FRAME_OUTER_IL, DVB_T_N_FFT, DvbTFrameParams,
ScatteredPilotExtractor, dvb_t_frame_fill_with, dvb_t_map_symbol, dvb_t_soft_llr,
tps_carrier_bins,
};
use crate::waveform::dvb_t_tps::{TPS_SYMBOLS_PER_FRAME, TpsDecoder, TpsWord};
use crate::waveform::dvb_t_ts::{TS_PACKET_LEN, ts_depacketize, ts_energy_disperse};
use num_complex::Complex32 as C32;
const INTEGER_CFO_ACCUM_SYMBOLS: usize = 8;
const INTEGER_CFO_MAX_BINS: i32 = 32;
struct IntegerCfo {
corrected: Option<Vec<C32>>,
bins: Option<i32>,
}
impl IntegerCfo {
const NOT_MEASURED: Self = Self {
corrected: None,
bins: None,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct DvbTRxDiagnostics {
pub cfo_hz: Option<f32>,
pub sync_score: Option<f32>,
pub timing_offset_samples: Option<usize>,
pub integer_cfo_bins: Option<i32>,
pub evm_db: Option<f32>,
pub channel_ber: Option<f32>,
pub inner_ber: Option<f32>,
pub outer_fec_ok: Option<bool>,
pub rs_corrected_bytes: Option<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DvbTRxFrame {
pub payload: Vec<u8>,
pub tps: TpsWord,
pub diagnostics: DvbTRxDiagnostics,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum DvbTRxError {
#[error("guard-interval acquisition failed (buffer too short or no CP lock)")]
Acquisition,
#[error("too few samples for the expected frame")]
Incomplete,
#[error("TPS word failed to decode (BCH uncorrectable)")]
TpsDecode,
#[error("payload FEC/CRC decode failed")]
PayloadDecode,
}
#[derive(Debug, Clone)]
pub struct DvbTFrameDemod {
params: DvbTFrameParams,
integer_cfo: bool,
rx_window_backoff: usize,
measure_errors: bool,
}
impl DvbTFrameDemod {
pub fn new(params: DvbTFrameParams) -> Self {
Self {
params,
integer_cfo: false,
rx_window_backoff: 0,
measure_errors: false,
}
}
pub fn with_integer_cfo_correction(mut self, on: bool) -> Self {
self.integer_cfo = on;
self
}
pub fn with_rx_window_backoff(mut self, backoff: usize) -> Self {
self.rx_window_backoff = backoff;
self
}
pub fn with_error_rates(mut self, on: bool) -> Self {
self.measure_errors = on;
self
}
pub fn params(&self) -> DvbTFrameParams {
self.params
}
pub fn error_rates(&self) -> bool {
self.measure_errors
}
pub fn integer_cfo_correction(&self) -> bool {
self.integer_cfo
}
pub fn rx_window_backoff(&self) -> usize {
self.rx_window_backoff
}
fn integer_cfo_correct(&self, iq: &[C32], n_fft: usize, cp_len: usize, fs: f32) -> IntegerCfo {
if !self.integer_cfo {
return IntegerCfo::NOT_MEASURED;
}
let sps = n_fft + cp_len;
let Some(acq) = dvb_t_gi_sync(iq, n_fft, cp_len, fs, sps) else {
return IntegerCfo::NOT_MEASURED;
};
let mut symbol_fft = SymbolFft::new(n_fft, cp_len);
let mut accum = vec![C32::default(); n_fft];
for s in 0..INTEGER_CFO_ACCUM_SYMBOLS {
let off = acq.start_sample + s * sps;
if off + sps > iq.len() {
break;
}
let Some(freq) = symbol_fft.demod_symbol(&iq[off..]) else {
break;
};
for (a, &x) in accum.iter_mut().zip(freq.iter()) {
*a += C32::new(x.norm_sqr(), 0.0);
}
}
let Some(k) = dvb_t_integer_cfo(&accum, n_fft, INTEGER_CFO_MAX_BINS).map(|e| e.bins) else {
return IntegerCfo::NOT_MEASURED;
};
if k == 0 {
return IntegerCfo {
corrected: None,
bins: Some(0),
};
}
let mut corrected = vec![C32::default(); iq.len()];
Rotator::new(-(k as f32) * fs / n_fft as f32, fs).rotate_block(iq, &mut corrected);
IntegerCfo {
corrected: Some(corrected),
bins: Some(k),
}
}
pub fn decode(
&self,
iq: &[C32],
n_symbols: usize,
payload_len: usize,
) -> Result<DvbTRxFrame, DvbTRxError> {
self.decode_inner(iq, n_symbols, payload_len, None)
}
pub(crate) fn decode_probed(
&self,
iq: &[C32],
n_symbols: usize,
payload_len: usize,
probe: &mut DvbTRxProbe,
) -> Result<DvbTRxFrame, DvbTRxError> {
let mark = probe.mark();
let out = self.decode_inner(iq, n_symbols, payload_len, Some(probe));
if out.is_err() && !probe.committed_since(mark) {
probe.rollback(mark);
}
out
}
fn decode_inner(
&self,
iq: &[C32],
n_symbols: usize,
payload_len: usize,
mut probe: Option<&mut DvbTRxProbe>,
) -> Result<DvbTRxFrame, DvbTRxError> {
let params = self.params;
let cache = CodecCache::new();
let base = params
.config()
.with_rx_window_backoff(self.rx_window_backoff);
let n_fft = DVB_T_N_FFT;
let cp_len = base.carrier_plan.cp_len();
let sps = n_fft + cp_len;
let vbits = params.constellation().bits_per_symbol();
let int_cfo = self.integer_cfo_correct(iq, n_fft, cp_len, base.fs);
let iq: &[C32] = int_cfo.corrected.as_deref().unwrap_or(iq);
let acq = dvb_t_gi_sync(iq, n_fft, cp_len, base.fs, sps).ok_or(DvbTRxError::Acquisition)?;
let start = acq.start_sample;
if iq.len() < start + n_symbols * sps {
return Err(DvbTRxError::Incomplete);
}
let subcarrier_spacing = base.fs / n_fft as f32;
let mut diagnostics = DvbTRxDiagnostics {
cfo_hz: Some(acq.cfo_hz + int_cfo.bins.unwrap_or(0) as f32 * subcarrier_spacing),
sync_score: Some(acq.score),
timing_offset_samples: Some(start),
integer_cfo_bins: int_cfo.bins,
..Default::default()
};
let mut extractor = ScatteredPilotExtractor::new(params.guard());
let mut eq = OfdmEqualizer::new(&base, EqualizerMethod::PerSymbolPilotInterp);
let mut symbol_fft =
SymbolFft::new(n_fft, cp_len).with_window_backoff(base.rx_window_backoff);
let mut tps_dec = TpsDecoder::new();
let tps_bins = tps_carrier_bins();
let mut equalized = vec![C32::default(); n_fft];
let mut data_syms = vec![C32::default(); DVB_T_DATA_CARRIERS];
let bits_per_sym = DVB_T_DATA_CARRIERS * vbits;
let mut llrs = vec![0.0f32; n_symbols * bits_per_sym];
let (mut evm_err, mut evm_ref) = (0.0f64, 0.0f64);
let mut hard_bits = [0u8; 6];
let measure_errors = self.measure_errors;
let sym_start = probe.as_deref().map_or(0, |p| p.symbols.len());
let mut tps_word: Option<TpsWord> = None;
for s in 0..n_symbols {
let off = start + s * sps;
let freq = match symbol_fft.demod_symbol(&iq[off..]) {
Some(f) => f,
None => return Err(DvbTRxError::Incomplete),
};
let cells: Vec<C32> = tps_bins.iter().map(|&b| freq[b]).collect();
tps_dec.feed_symbol(&cells);
if (s + 1) % TPS_SYMBOLS_PER_FRAME == 0 && tps_word.is_none() {
tps_word = tps_dec.word();
tps_dec.reset();
}
let pilots = extractor.current_pilot_bins().to_vec();
let data_bins = extractor.data_bins().to_vec();
eq.set_pilot_bins(&pilots, &data_bins);
eq.process(freq, &mut equalized);
extractor.extract_symbol(&equalized, &mut data_syms);
if let Some(p) = probe.as_deref_mut() {
p.symbols.extend_from_slice(&data_syms);
}
let sym_llrs = &mut llrs[s * bits_per_sym..(s + 1) * bits_per_sym];
if measure_errors {
for (c, &sym) in data_syms.iter().enumerate() {
let l = dvb_t_soft_llr(sym, vbits).expect("DVB-T order");
sym_llrs[c * vbits..(c + 1) * vbits].copy_from_slice(&l);
let hard = &mut hard_bits[..vbits];
for (h, &x) in hard.iter_mut().zip(l.iter()) {
*h = u8::from(x <= 0.0);
}
let ideal = dvb_t_map_symbol(hard).expect("DVB-T order");
let e = sym - ideal;
evm_err += (e.re * e.re + e.im * e.im) as f64;
evm_ref += (ideal.re * ideal.re + ideal.im * ideal.im) as f64;
}
} else {
for (c, &sym) in data_syms.iter().enumerate() {
let l = dvb_t_soft_llr(sym, vbits).expect("DVB-T order");
sym_llrs[c * vbits..(c + 1) * vbits].copy_from_slice(&l);
}
}
}
if evm_ref > 0.0 {
diagnostics.evm_db = Some((10.0 * (evm_err / evm_ref).log10()) as f32);
}
let tps = tps_word.ok_or(DvbTRxError::TpsDecode)?;
let payload_packets = payload_len.div_ceil(TS_PACKET_LEN - 1).max(1);
let payload_ts_bytes = payload_packets * TS_PACKET_LEN;
let want_truth = self.measure_errors || probe.is_some();
let n_ts_packets = if want_truth {
dvb_t_frame_fill_with(params, payload_packets, n_symbols, &cache).n_ts_packets
} else {
payload_packets
};
let ts_bytes_len = n_ts_packets * TS_PACKET_LEN;
let plan = block_plan(
ts_bytes_len,
CrcKind::None,
DVB_T_FRAME_OUTER,
params.inner(),
DVB_T_FRAME_OUTER_IL,
InterleaverKind::None,
&cache,
);
let fail = |probe: &mut Option<&mut DvbTRxProbe>| {
if let Some(p) = probe.as_deref_mut() {
p.push_undecoded(sym_start, params.constellation(), tps);
}
DvbTRxError::PayloadDecode
};
let outcome = match decode_chain(
&llrs,
&plan,
CrcKind::None,
DVB_T_FRAME_OUTER,
params.inner(),
DVB_T_FRAME_OUTER_IL,
InterleaverKind::None,
ScramblerKind::None,
ScramblerPos::BeforeOuterFec,
0,
&cache,
DecodeRule::SumProduct,
) {
Ok(o) if o.is_valid() => o,
_ => return Err(fail(&mut probe)),
};
diagnostics.outer_fec_ok = Some(outcome.outer_ok);
diagnostics.rs_corrected_bytes = outcome.outer_corrected_bytes;
let mut ts = outcome.bytes;
if ts.len() < ts_bytes_len {
return Err(fail(&mut probe));
}
let stages = want_truth.then(|| {
encode_chain_stages(
&ts[..ts_bytes_len],
CrcKind::None,
DVB_T_FRAME_OUTER,
params.inner(),
DVB_T_FRAME_OUTER_IL,
InterleaverKind::None,
ScramblerKind::None,
ScramblerPos::BeforeOuterFec,
0,
&cache,
)
});
debug_assert!(
!want_truth || plan.coded_bits <= llrs.len(),
"frame fill must keep the coded stream within the data carriers"
);
let coded_bits = plan.coded_bits;
if let Some(s) = stages.as_ref().filter(|_| self.measure_errors) {
let n = coded_bits.min(s.coded.len());
if n > 0 {
let errs = llrs[..n]
.iter()
.zip(s.coded[..n].iter())
.filter(|&(&l, &c)| u8::from(l <= 0.0) != c)
.count();
diagnostics.channel_ber = Some(errs as f32 / n as f32);
}
diagnostics.inner_ber = bit_error_rate(&outcome.inner_out_bits, &s.outer_il_bits);
}
if let (Some(p), Some(s)) = (&mut probe, stages.as_ref()) {
p.hard.clear();
p.hard
.extend(llrs[..coded_bits].iter().map(|&l| u8::from(l <= 0.0)));
let re = inner_encode(params.inner(), &outcome.inner_out_bits, &cache);
p.estimate.clear();
p.estimate
.extend_from_slice(&re[..coded_bits.min(re.len())]);
p.push_decoded(
sym_start,
params.constellation(),
tps,
&s.coded[..coded_bits.min(s.coded.len())],
);
}
ts.truncate(payload_ts_bytes);
ts_energy_disperse(&mut ts);
let payload = ts_depacketize(&ts).ok_or(DvbTRxError::PayloadDecode)?;
let payload = payload
.get(..payload_len)
.map(|s| s.to_vec())
.unwrap_or(payload);
Ok(DvbTRxFrame {
payload,
tps,
diagnostics,
})
}
}