use num_complex::Complex32;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use crate::demodulate::{DvbTFrameDemod, DvbTFrameStreamDemod, DvbTSuperFrameDemod};
use crate::fec::PunctureRate;
use crate::modulate::{ConstellationOrder, DvbTFrameMod, DvbTSuperFrameMod, DvbTSuperFrameParams};
use crate::multicarrier::TxLowpass;
use crate::waveform::dvb_t::{
DVB_T_MAX_RX_WINDOW_BACKOFF, DvbTFrameParams, DvbTLinkParams, GuardInterval, NbBandwidth,
};
use crate::waveform::dvb_t_tps::TpsWord;
fn parse_guard(s: &str) -> PyResult<GuardInterval> {
match s {
"1/32" => Ok(GuardInterval::G1_32),
"1/16" => Ok(GuardInterval::G1_16),
"1/8" => Ok(GuardInterval::G1_8),
"1/4" => Ok(GuardInterval::G1_4),
other => Err(PyValueError::new_err(format!(
"unknown guard interval {other:?} (expected 1/32, 1/16, 1/8, 1/4)"
))),
}
}
fn guard_str(g: GuardInterval) -> &'static str {
match g {
GuardInterval::G1_32 => "1/32",
GuardInterval::G1_16 => "1/16",
GuardInterval::G1_8 => "1/8",
GuardInterval::G1_4 => "1/4",
}
}
fn parse_dvb_t_constellation(s: &str) -> PyResult<ConstellationOrder> {
match s {
"qpsk" => Ok(ConstellationOrder::Qpsk),
"qam16" => Ok(ConstellationOrder::Qam16),
"qam64" => Ok(ConstellationOrder::Qam64),
other => Err(PyValueError::new_err(format!(
"unknown DVB-T constellation {other:?} (expected qpsk, qam16, qam64)"
))),
}
}
fn constellation_str(c: ConstellationOrder) -> PyResult<&'static str> {
match c {
ConstellationOrder::Qpsk => Ok("qpsk"),
ConstellationOrder::Qam16 => Ok("qam16"),
ConstellationOrder::Qam64 => Ok("qam64"),
other => Err(PyValueError::new_err(format!(
"{other:?} is not a DVB-T constellation"
))),
}
}
fn parse_rate(s: &str) -> PyResult<PunctureRate> {
match s {
"1/2" => Ok(PunctureRate::R1_2),
"2/3" => Ok(PunctureRate::R2_3),
"3/4" => Ok(PunctureRate::R3_4),
"5/6" => Ok(PunctureRate::R5_6),
"7/8" => Ok(PunctureRate::R7_8),
other => Err(PyValueError::new_err(format!(
"unknown code rate {other:?} (expected 1/2, 2/3, 3/4, 5/6, 7/8)"
))),
}
}
fn rate_str(r: PunctureRate) -> &'static str {
match r {
PunctureRate::R1_2 => "1/2",
PunctureRate::R2_3 => "2/3",
PunctureRate::R3_4 => "3/4",
PunctureRate::R5_6 => "5/6",
PunctureRate::R7_8 => "7/8",
}
}
#[pyclass(name = "DvbTFrameParams", skip_from_py_object)]
#[derive(Clone)]
pub struct PyDvbTFrameParams {
inner: DvbTFrameParams,
}
#[pymethods]
impl PyDvbTFrameParams {
#[new]
#[pyo3(signature = (guard, constellation, code_rate, frame_number = 0, cell_id = 0))]
fn new(
guard: &str,
constellation: &str,
code_rate: &str,
frame_number: u8,
cell_id: u8,
) -> PyResult<Self> {
Ok(Self {
inner: DvbTFrameParams {
link: DvbTLinkParams {
guard: parse_guard(guard)?,
constellation: parse_dvb_t_constellation(constellation)?,
code_rate: parse_rate(code_rate)?,
},
frame_number,
cell_id,
},
})
}
#[getter]
fn guard(&self) -> &'static str {
guard_str(self.inner.guard())
}
#[getter]
fn constellation(&self) -> PyResult<&'static str> {
constellation_str(self.inner.constellation())
}
#[getter]
fn code_rate(&self) -> &'static str {
rate_str(self.inner.code_rate())
}
#[getter]
fn frame_number(&self) -> u8 {
self.inner.frame_number
}
#[getter]
fn cell_id(&self) -> u8 {
self.inner.cell_id
}
fn __repr__(&self) -> PyResult<String> {
Ok(format!(
"DvbTFrameParams(guard={:?}, constellation={:?}, code_rate={:?}, frame_number={}, cell_id={})",
guard_str(self.inner.guard()),
constellation_str(self.inner.constellation())?,
rate_str(self.inner.code_rate()),
self.inner.frame_number,
self.inner.cell_id,
))
}
}
#[pyclass(name = "TpsWord", skip_from_py_object)]
#[derive(Clone)]
pub struct PyTpsWord {
inner: TpsWord,
}
#[pymethods]
impl PyTpsWord {
#[getter]
fn frame_number(&self) -> u8 {
self.inner.frame_number
}
#[getter]
fn constellation(&self) -> PyResult<&'static str> {
constellation_str(self.inner.constellation)
}
#[getter]
fn code_rate(&self) -> &'static str {
rate_str(self.inner.code_rate_hp)
}
#[getter]
fn guard(&self) -> &'static str {
guard_str(self.inner.guard)
}
#[getter]
fn cell_id(&self) -> u8 {
self.inner.cell_id
}
fn __repr__(&self) -> PyResult<String> {
Ok(format!(
"TpsWord(frame_number={}, constellation={:?}, code_rate={:?}, guard={:?}, cell_id={})",
self.inner.frame_number,
constellation_str(self.inner.constellation)?,
rate_str(self.inner.code_rate_hp),
guard_str(self.inner.guard),
self.inner.cell_id,
))
}
}
#[pyclass(name = "DvbTFrame")]
pub struct PyDvbTFrame {
iq: Vec<Complex32>,
#[pyo3(get)]
n_symbols: usize,
#[pyo3(get)]
samples_per_symbol: usize,
}
#[pymethods]
impl PyDvbTFrame {
#[getter]
fn iq<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<Complex32>> {
self.iq.clone().into_pyarray(py)
}
}
#[pyclass(name = "DvbTRxFrame")]
pub struct PyDvbTRxFrame {
payload: Vec<u8>,
#[pyo3(get)]
tps: PyTpsWord,
}
#[pymethods]
impl PyDvbTRxFrame {
#[getter]
fn payload<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
PyBytes::new(py, &self.payload)
}
}
#[pyclass(name = "DvbTFrameMod")]
pub struct PyDvbTFrameMod {
inner: DvbTFrameMod,
}
#[pymethods]
impl PyDvbTFrameMod {
#[new]
fn new(params: &PyDvbTFrameParams) -> Self {
Self {
inner: DvbTFrameMod::new(params.inner),
}
}
fn with_symbol_window(&self, roll_off: usize) -> Self {
Self {
inner: self.inner.clone().with_symbol_window(roll_off),
}
}
#[pyo3(signature = (num_taps, stopband_db = 60.0))]
fn with_tx_lowpass(&self, num_taps: usize, stopband_db: f32) -> Self {
Self {
inner: self
.inner
.clone()
.with_tx_lowpass(DvbTFrameMod::tx_lowpass_for_2k(num_taps, stopband_db)),
}
}
fn modulate(&self, payload: PyReadonlyArray1<'_, u8>) -> PyResult<PyDvbTFrame> {
let frame = self.inner.modulate(payload.as_slice()?);
Ok(PyDvbTFrame {
iq: frame.iq,
n_symbols: frame.n_symbols,
samples_per_symbol: frame.samples_per_symbol,
})
}
}
#[pyclass(name = "DvbTFrameDemod")]
pub struct PyDvbTFrameDemod {
inner: DvbTFrameDemod,
}
#[pymethods]
impl PyDvbTFrameDemod {
#[new]
fn new(params: &PyDvbTFrameParams) -> Self {
Self {
inner: DvbTFrameDemod::new(params.inner),
}
}
fn with_integer_cfo_correction(&self, on: bool) -> Self {
Self {
inner: self.inner.clone().with_integer_cfo_correction(on),
}
}
#[getter]
fn integer_cfo_correction(&self) -> bool {
self.inner.integer_cfo_correction()
}
fn with_rx_window_backoff(&self, backoff: usize) -> Self {
Self {
inner: self.inner.clone().with_rx_window_backoff(backoff),
}
}
#[getter]
fn rx_window_backoff(&self) -> usize {
self.inner.rx_window_backoff()
}
fn decode(
&self,
iq: PyReadonlyArray1<'_, Complex32>,
n_symbols: usize,
payload_len: usize,
) -> PyResult<PyDvbTRxFrame> {
let rx = self
.inner
.decode(iq.as_slice()?, n_symbols, payload_len)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
Ok(PyDvbTRxFrame {
payload: rx.payload,
tps: PyTpsWord { inner: rx.tps },
})
}
}
#[pyfunction]
#[pyo3(name = "nb_bandwidth_fs")]
fn nb_bandwidth_fs(mode: &str) -> PyResult<f32> {
Ok(parse_nb_bandwidth(mode)?.fs())
}
#[pyfunction]
#[pyo3(name = "nb_bandwidth_occupied_hz")]
fn nb_bandwidth_occupied_hz(mode: &str) -> PyResult<f32> {
Ok(parse_nb_bandwidth(mode)?.occupied_hz())
}
fn parse_nb_bandwidth(s: &str) -> PyResult<NbBandwidth> {
match s {
"333khz" | "333k" => Ok(NbBandwidth::Bw333kHz),
"1mhz" | "1m" => Ok(NbBandwidth::Bw1MHz),
"2mhz" | "2m" => Ok(NbBandwidth::Bw2MHz),
other => Err(PyValueError::new_err(format!(
"unknown NB bandwidth {other:?} (expected 333khz, 1mhz, 2mhz)"
))),
}
}
#[pyfunction]
#[pyo3(name = "dvb_t_cp_len")]
fn dvb_t_cp_len(guard: &str) -> PyResult<usize> {
Ok(parse_guard(guard)?.cp_len_2k())
}
#[pyfunction]
#[pyo3(name = "dvb_t_max_rx_window_backoff")]
fn dvb_t_max_rx_window_backoff() -> usize {
DVB_T_MAX_RX_WINDOW_BACKOFF
}
#[pyfunction]
#[pyo3(name = "dvb_t_tx_lowpass_suggested_taps")]
#[pyo3(signature = (stopband_db = 60.0))]
fn dvb_t_tx_lowpass_suggested_taps(stopband_db: f32) -> usize {
TxLowpass::taps_for_null_band(
crate::waveform::dvb_t::DVB_T_N_FFT,
crate::waveform::dvb_t::DVB_T_KMAX / 2,
stopband_db,
)
}
#[pyfunction]
#[pyo3(name = "dvb_t_tx_lowpass_group_delay")]
fn dvb_t_tx_lowpass_group_delay(num_taps: usize) -> usize {
TxLowpass::new(0.25, num_taps, 60.0).group_delay()
}
#[pyfunction]
#[pyo3(name = "dvb_t_tx_lowpass_fits_guard")]
#[pyo3(signature = (guard, num_taps, roll_off, backoff))]
fn dvb_t_tx_lowpass_fits_guard(
guard: &str,
num_taps: usize,
roll_off: usize,
backoff: usize,
) -> PyResult<bool> {
let cp_len = parse_guard(guard)?.cp_len_2k();
Ok(DvbTFrameMod::tx_lowpass_for_2k(num_taps, 60.0).fits_guard(cp_len, roll_off, backoff))
}
#[pyclass(name = "DvbTSuperFrameParams", skip_from_py_object)]
#[derive(Clone)]
pub struct PyDvbTSuperFrameParams {
inner: DvbTSuperFrameParams,
}
#[pymethods]
impl PyDvbTSuperFrameParams {
#[new]
#[pyo3(signature = (guard, constellation, code_rate, cell_id = 0))]
fn new(guard: &str, constellation: &str, code_rate: &str, cell_id: u16) -> PyResult<Self> {
Ok(Self {
inner: DvbTSuperFrameParams {
link: DvbTLinkParams {
guard: parse_guard(guard)?,
constellation: parse_dvb_t_constellation(constellation)?,
code_rate: parse_rate(code_rate)?,
},
cell_id,
},
})
}
#[getter]
fn guard(&self) -> &'static str {
guard_str(self.inner.guard())
}
#[getter]
fn constellation(&self) -> PyResult<&'static str> {
constellation_str(self.inner.constellation())
}
#[getter]
fn code_rate(&self) -> &'static str {
rate_str(self.inner.code_rate())
}
#[getter]
fn cell_id(&self) -> u16 {
self.inner.cell_id
}
}
#[pyclass(name = "DvbTSuperFrame")]
pub struct PyDvbTSuperFrame {
iq: Vec<Complex32>,
#[pyo3(get)]
symbols_per_frame: usize,
#[pyo3(get)]
samples_per_symbol: usize,
frame_payload_lens: [usize; 4],
}
#[pymethods]
impl PyDvbTSuperFrame {
#[getter]
fn iq<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<Complex32>> {
self.iq.clone().into_pyarray(py)
}
#[getter]
fn frame_payload_lens(&self) -> [usize; 4] {
self.frame_payload_lens
}
#[getter]
fn n_symbols(&self) -> usize {
4 * self.symbols_per_frame
}
}
#[pyclass(name = "DvbTRxSuperFrame")]
pub struct PyDvbTRxSuperFrame {
payload: Vec<u8>,
#[pyo3(get)]
cell_id: u16,
}
#[pymethods]
impl PyDvbTRxSuperFrame {
#[getter]
fn payload<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
PyBytes::new(py, &self.payload)
}
}
#[pyclass(name = "DvbTSuperFrameMod")]
pub struct PyDvbTSuperFrameMod {
inner: DvbTSuperFrameMod,
}
#[pymethods]
impl PyDvbTSuperFrameMod {
#[new]
fn new(params: &PyDvbTSuperFrameParams) -> Self {
Self {
inner: DvbTSuperFrameMod::new(params.inner),
}
}
fn with_symbol_window(&self, roll_off: usize) -> Self {
Self {
inner: self.inner.clone().with_symbol_window(roll_off),
}
}
#[pyo3(signature = (num_taps, stopband_db = 60.0))]
fn with_tx_lowpass(&self, num_taps: usize, stopband_db: f32) -> Self {
Self {
inner: self
.inner
.clone()
.with_tx_lowpass(DvbTFrameMod::tx_lowpass_for_2k(num_taps, stopband_db)),
}
}
fn modulate(&self, payload: PyReadonlyArray1<'_, u8>) -> PyResult<PyDvbTSuperFrame> {
let sf = self.inner.modulate(payload.as_slice()?);
Ok(PyDvbTSuperFrame {
iq: sf.iq,
symbols_per_frame: sf.symbols_per_frame,
samples_per_symbol: sf.samples_per_symbol,
frame_payload_lens: sf.frame_payload_lens,
})
}
}
#[pyclass(name = "DvbTSuperFrameDemod")]
pub struct PyDvbTSuperFrameDemod {
inner: DvbTSuperFrameDemod,
}
#[pymethods]
impl PyDvbTSuperFrameDemod {
#[new]
fn new(params: &PyDvbTSuperFrameParams) -> Self {
Self {
inner: DvbTSuperFrameDemod::new(params.inner),
}
}
fn with_integer_cfo_correction(&self, on: bool) -> Self {
Self {
inner: self.inner.clone().with_integer_cfo_correction(on),
}
}
#[getter]
fn integer_cfo_correction(&self) -> bool {
self.inner.integer_cfo_correction()
}
fn with_rx_window_backoff(&self, backoff: usize) -> Self {
Self {
inner: self.inner.clone().with_rx_window_backoff(backoff),
}
}
#[getter]
fn rx_window_backoff(&self) -> usize {
self.inner.rx_window_backoff()
}
fn decode(
&self,
iq: PyReadonlyArray1<'_, Complex32>,
symbols_per_frame: usize,
frame_payload_lens: [usize; 4],
) -> PyResult<PyDvbTRxSuperFrame> {
let rx = self
.inner
.decode(iq.as_slice()?, symbols_per_frame, frame_payload_lens)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
Ok(PyDvbTRxSuperFrame {
payload: rx.payload,
cell_id: rx.cell_id,
})
}
}
#[pyclass(name = "DvbTFrameStreamDemod")]
pub struct PyDvbTFrameStreamDemod {
inner: DvbTFrameStreamDemod,
integer_cfo: bool,
rx_window_backoff: usize,
}
#[pymethods]
impl PyDvbTFrameStreamDemod {
#[new]
#[pyo3(signature = (params, n_symbols, payload_len, integer_cfo_correction = false, rx_window_backoff = 0))]
fn new(
params: &PyDvbTFrameParams,
n_symbols: usize,
payload_len: usize,
integer_cfo_correction: bool,
rx_window_backoff: usize,
) -> Self {
Self {
inner: DvbTFrameStreamDemod::new(params.inner, n_symbols, payload_len)
.with_integer_cfo_correction(integer_cfo_correction)
.with_rx_window_backoff(rx_window_backoff),
integer_cfo: integer_cfo_correction,
rx_window_backoff,
}
}
#[getter]
fn integer_cfo_correction(&self) -> bool {
self.integer_cfo
}
#[getter]
fn rx_window_backoff(&self) -> usize {
self.rx_window_backoff
}
fn feed(&mut self, iq: PyReadonlyArray1<'_, Complex32>) -> PyResult<Vec<PyDvbTRxFrame>> {
Ok(self
.inner
.feed(iq.as_slice()?)
.into_iter()
.filter_map(|r| {
r.ok().map(|f| PyDvbTRxFrame {
payload: f.payload,
tps: PyTpsWord { inner: f.tps },
})
})
.collect())
}
fn feed_with_errors(
&mut self,
iq: PyReadonlyArray1<'_, Complex32>,
) -> PyResult<Vec<(Option<PyDvbTRxFrame>, Option<String>)>> {
Ok(self
.inner
.feed(iq.as_slice()?)
.into_iter()
.map(|r| match r {
Ok(f) => (
Some(PyDvbTRxFrame {
payload: f.payload,
tps: PyTpsWord { inner: f.tps },
}),
None,
),
Err(e) => (None, Some(e.to_string())),
})
.collect())
}
fn flush(&mut self) -> Vec<PyDvbTRxFrame> {
self.inner
.flush()
.into_iter()
.filter_map(|r| {
r.ok().map(|f| PyDvbTRxFrame {
payload: f.payload,
tps: PyTpsWord { inner: f.tps },
})
})
.collect()
}
#[getter]
fn buffered(&self) -> usize {
self.inner.len()
}
fn clear(&mut self) {
self.inner.clear();
}
}
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyDvbTFrameParams>()?;
m.add_class::<PyDvbTFrame>()?;
m.add_class::<PyDvbTRxFrame>()?;
m.add_class::<PyTpsWord>()?;
m.add_class::<PyDvbTFrameMod>()?;
m.add_class::<PyDvbTFrameDemod>()?;
m.add_class::<PyDvbTSuperFrameParams>()?;
m.add_class::<PyDvbTSuperFrame>()?;
m.add_class::<PyDvbTRxSuperFrame>()?;
m.add_class::<PyDvbTSuperFrameMod>()?;
m.add_class::<PyDvbTSuperFrameDemod>()?;
m.add_class::<PyDvbTFrameStreamDemod>()?;
m.add_function(wrap_pyfunction!(nb_bandwidth_fs, m)?)?;
m.add_function(wrap_pyfunction!(nb_bandwidth_occupied_hz, m)?)?;
m.add_function(wrap_pyfunction!(dvb_t_cp_len, m)?)?;
m.add_function(wrap_pyfunction!(dvb_t_max_rx_window_backoff, m)?)?;
m.add_function(wrap_pyfunction!(dvb_t_tx_lowpass_suggested_taps, m)?)?;
m.add_function(wrap_pyfunction!(dvb_t_tx_lowpass_group_delay, m)?)?;
m.add_function(wrap_pyfunction!(dvb_t_tx_lowpass_fits_guard, m)?)?;
Ok(())
}