acoustic_ofdm/
config.rs

1// Copyright (c) 2026 Elias S. G. Carotti
2
3/// Constellation mapping used on OFDM data carriers.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum Modulation {
6    /// Binary phase-shift keying, one bit per symbol.
7    Bpsk,
8    /// Quadrature phase-shift keying, two bits per symbol.
9    Qpsk,
10}
11
12/// Bitfield describing which equalizer stages are enabled.
13///
14/// Rationale:
15/// The equalizer is now a pipeline of optional stages rather than a single
16/// monolithic mode. This bitfield lets the configuration express combinations
17/// such as:
18/// - training baseline + pilot phase
19/// - training baseline + pilot phase + pilot amplitude
20/// - pilot-only equalization
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct EqualizerFeatures(u32);
23
24impl Default for EqualizerFeatures {
25    fn default() -> Self {
26        Self::NONE
27    }
28}
29
30impl EqualizerFeatures {
31    /// No optional equalizer stages.
32    pub const NONE: Self = Self(0);
33    /// Use the training symbol as the baseline channel model.
34    pub const TRAINING_BASELINE: Self = Self(1 << 0);
35    /// Apply a pilot-derived residual phase fit on each data symbol.
36    pub const PILOT_PHASE: Self = Self(1 << 1);
37    /// Apply a pilot-derived residual amplitude fit on each data symbol.
38    pub const PILOT_AMPLITUDE: Self = Self(1 << 2);
39    /// Weight pilot observations by reliability when fitting corrections.
40    pub const WEIGHTED_PILOTS: Self = Self(1 << 3);
41    /// Use a noise-aware MMSE-style inverse instead of the legacy heuristic one.
42    pub const NOISE_AWARE_MMSE: Self = Self(1 << 4);
43    /// Use temporal least-squares tracking on pilot-derived phase-line parameters.
44    pub const TEMPORAL_LS: Self = Self(1 << 5);
45    /// Interpolate pilot residuals and denoise them in the delay domain.
46    pub const PILOT_IFFT_DENOISE: Self = Self(1 << 6);
47    /// Fuse denoised pilot residual curves over time with phase-aligned EMA.
48    pub const TEMPORAL_RESIDUAL_EMA: Self = Self(1 << 7);
49
50    /// Returns whether all requested feature bits are enabled.
51    pub fn contains(self, other: Self) -> bool {
52        (self.0 & other.0) == other.0
53    }
54}
55
56impl std::ops::BitOr for EqualizerFeatures {
57    type Output = Self;
58
59    fn bitor(self, rhs: Self) -> Self::Output {
60        Self(self.0 | rhs.0)
61    }
62}
63
64impl std::ops::BitOrAssign for EqualizerFeatures {
65    fn bitor_assign(&mut self, rhs: Self) {
66        self.0 |= rhs.0;
67    }
68}
69
70/// Equalizer subsystem configuration.
71#[derive(Clone, Copy, Debug, PartialEq)]
72pub struct EqualizerConfig {
73    /// Enabled equalizer stages.
74    pub features: EqualizerFeatures,
75    /// Number of recent symbols used by temporal least-squares tracking.
76    pub temporal_window: usize,
77}
78
79impl EqualizerConfig {
80    /// Starts a builder for an equalizer configuration.
81    pub fn builder() -> EqualizerBuilder {
82        EqualizerBuilder::default()
83    }
84}
85
86impl Default for EqualizerConfig {
87    fn default() -> Self {
88        EqualizerConfig::builder()
89            .training_baseline()
90            .pilot_phase()
91            .pilot_amplitude()
92            .weighted_pilots()
93            .build()
94    }
95}
96
97/// Builder for [`EqualizerConfig`].
98///
99/// Rationale:
100/// The equalizer now consists of composable stages. The builder keeps call
101/// sites readable while still compiling down to simple feature checks.
102#[derive(Clone, Copy, Debug, Default)]
103pub struct EqualizerBuilder {
104    features: EqualizerFeatures,
105    temporal_window: usize,
106}
107
108impl EqualizerBuilder {
109    /// Enable training-symbol baseline equalization.
110    pub fn training_baseline(mut self) -> Self {
111        self.features |= EqualizerFeatures::TRAINING_BASELINE;
112        self
113    }
114
115    /// Enable pilot-derived residual phase correction.
116    pub fn pilot_phase(mut self) -> Self {
117        self.features |= EqualizerFeatures::PILOT_PHASE;
118        self
119    }
120
121    /// Enable pilot-derived residual amplitude correction.
122    pub fn pilot_amplitude(mut self) -> Self {
123        self.features |= EqualizerFeatures::PILOT_AMPLITUDE;
124        self
125    }
126
127    /// Enable reliability-weighted pilot fitting.
128    pub fn weighted_pilots(mut self) -> Self {
129        self.features |= EqualizerFeatures::WEIGHTED_PILOTS;
130        self
131    }
132
133    /// Enable noise-aware MMSE regularization in the baseline equalizer.
134    pub fn noise_aware_mmse(mut self) -> Self {
135        self.features |= EqualizerFeatures::NOISE_AWARE_MMSE;
136        self
137    }
138
139    /// Enable temporal least-squares tracking over the last `window` symbols.
140    pub fn temporal_ls(mut self, window: usize) -> Self {
141        self.features |= EqualizerFeatures::TEMPORAL_LS;
142        self.temporal_window = window.max(1);
143        self
144    }
145
146    /// Enable pilot-residual interpolation followed by delay-domain denoising.
147    pub fn pilot_ifft_denoise(mut self) -> Self {
148        self.features |= EqualizerFeatures::PILOT_IFFT_DENOISE;
149        self
150    }
151
152    /// Enable temporal EMA fusion of residual curves over the last `window` symbols.
153    pub fn temporal_residual_ema(mut self, window: usize) -> Self {
154        self.features |= EqualizerFeatures::TEMPORAL_RESIDUAL_EMA;
155        self.temporal_window = window.max(1);
156        self
157    }
158
159    /// Finalize the equalizer configuration.
160    pub fn build(self) -> EqualizerConfig {
161        EqualizerConfig {
162            features: self.features,
163            temporal_window: self.temporal_window.max(1),
164        }
165    }
166}
167
168/// Forward-error-correction scheme applied to packet bits.
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum FecMode {
171    /// No channel coding.
172    None,
173    /// Hamming(7,4) block coding on the serialized packet bitstream.
174    Hamming74,
175}
176
177/// Passband implementation used to reach the speaker/microphone path.
178///
179/// Rationale:
180/// `Legacy` keeps the modem at the audio rate, while `Iq` uses a separate
181/// complex-baseband rate with explicit resampling and IQ conversion.
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub enum PassbandMode {
184    /// Single-rate legacy path with the modem running directly at the audio rate.
185    Legacy,
186    /// IQ path with separate baseband/audio rates and explicit resampling.
187    Iq,
188}
189
190impl Modulation {
191    /// Returns the number of bits carried by one constellation symbol.
192    ///
193    /// Parameters:
194    /// - `self`: modulation variant.
195    /// Returns:
196    /// - `usize`: bits per symbol (`1` for BPSK, `2` for QPSK).
197    pub fn bits_per_symbol(self) -> usize {
198        match self {
199            Self::Bpsk => 1,
200            Self::Qpsk => 2,
201        }
202    }
203
204    /// Returns the packet header modulation identifier.
205    ///
206    /// Parameters:
207    /// - `self`: modulation variant.
208    /// Returns:
209    /// - `u8`: modulation ID used in packet headers.
210    pub fn mod_id(self) -> u8 {
211        match self {
212            Self::Bpsk => 1,
213            Self::Qpsk => 2,
214        }
215    }
216}
217
218/// Wake-up / preamble family transmitted before the OFDM body.
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum WakePreamble {
221    /// Single tone wake preamble.
222    Tone,
223    /// Linear chirp wake preamble.
224    Chirp,
225    /// PN-sequence wake preamble.
226    Pn,
227    /// Gold-like wake preamble.
228    Gold,
229}
230
231impl WakePreamble {
232    /// Parses a wake preamble mode from CLI/config text.
233    ///
234    /// Parameters:
235    /// - `s`: mode string.
236    /// Returns:
237    /// - `Option<WakePreamble>`: parsed mode when recognized.
238    pub fn parse(s: &str) -> Option<Self> {
239        match s.to_ascii_lowercase().as_str() {
240            "tone" => Some(Self::Tone),
241            "chirp" => Some(Self::Chirp),
242            "pn" => Some(Self::Pn),
243            "gold" => Some(Self::Gold),
244            _ => None,
245        }
246    }
247
248    /// Returns the canonical mode name.
249    ///
250    /// Parameters:
251    /// - `self`: wake preamble variant.
252    /// Returns:
253    /// - `&'static str`: printable mode name.
254    pub fn as_str(self) -> &'static str {
255        match self {
256            Self::Tone => "tone",
257            Self::Chirp => "chirp",
258            Self::Pn => "pn",
259            Self::Gold => "gold",
260        }
261    }
262}
263
264#[derive(Clone, Debug)]
265pub struct OfdmConfig {
266    /// Audio-side sample rate in hertz.
267    pub fs: f32,
268    /// Complex-baseband sample rate in hertz before IQ up/downsampling.
269    pub fs_baseband: f32,
270    /// Passband carrier frequency in hertz.
271    pub fc: f32,
272    /// Additional payload/body gain applied before final packet shaping.
273    pub payload_gain: f32,
274    /// IFFT/FFT size for OFDM symbols.
275    pub nfft: usize,
276    /// Cyclic-prefix length in samples at the active baseband rate.
277    pub ncp: usize,
278    /// Optional explicit baseband frequency origin override.
279    pub base_freq_hz: Option<f32>,
280    /// Active FFT-bin indices used by the modem.
281    pub used_bins: Vec<usize>,
282    /// Active FFT-bin indices reserved for pilots.
283    pub pilot_bins: Vec<usize>,
284    /// Optional legacy pilot-count override kept for compatibility.
285    pub num_pilots: Option<usize>,
286    /// Optional legacy pilot enable flag kept for compatibility.
287    pub use_pilots: Option<bool>,
288    /// Optional periodic retraining interval in data symbols.
289    pub retrain_interval_data_symbols: Option<usize>,
290    /// Whether to append a terminal training symbol at the end of the packet.
291    pub terminal_training_symbol: bool,
292    /// Data modulation used on the active data carriers.
293    pub modulation: Modulation,
294    /// Equalizer subsystem configuration.
295    pub equalizer: EqualizerConfig,
296    /// Forward-error-correction mode applied to packet bits.
297    pub fec_mode: FecMode,
298    /// Passband conversion path: direct legacy path or IQ path.
299    pub passband_mode: PassbandMode,
300    /// Wake-preamble duration in milliseconds.
301    pub wake_ms: f32,
302    /// Tone wake frequency in hertz when tone wake-up is enabled.
303    pub wake_freq: f32,
304    /// Silence/guard interval inserted after the wake preamble, in milliseconds.
305    pub wake_guard_ms: f32,
306    /// Wake-preamble family used before the OFDM body.
307    pub wake_preamble: WakePreamble,
308    /// Start frequency of the chirp wake preamble in hertz.
309    pub sync_chirp_f0: f32,
310    /// End frequency of the chirp wake preamble in hertz.
311    pub sync_chirp_f1: f32,
312    /// Half-length of the repeated-half sync sequence, in baseband samples.
313    pub sync_half_len: usize,
314    /// Maximum application payload bytes per packet fragment before FEC.
315    pub packet_payload_bytes: usize,
316    /// Session identifier written into packet headers.
317    pub session_id: u16,
318}
319
320impl Default for OfdmConfig {
321    /// Builds the default OFDM modem configuration.
322    ///
323    /// Parameters:
324    /// - none.
325    /// Returns:
326    /// - `OfdmConfig`: default configuration values.
327    fn default() -> Self {
328        Self {
329            fs: 44_100.0,
330            fs_baseband: 44_100.0,
331            fc: 7_500.0,
332            payload_gain: 2.0,
333            nfft: 2048,
334            ncp: 1024,
335            base_freq_hz: None,
336            used_bins: vec![
337                24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
338            ],
339            pilot_bins: vec![24, 26, 28, 31, 33, 35, 38],
340            num_pilots: None,
341            use_pilots: Some(true),
342            retrain_interval_data_symbols: None,
343            terminal_training_symbol: false,
344            modulation: Modulation::Bpsk,
345            equalizer: EqualizerConfig::default(),
346            fec_mode: FecMode::None,
347            passband_mode: PassbandMode::Legacy,
348            wake_ms: 80.0,
349            wake_freq: 5_500.0,
350            wake_guard_ms: 20.0,
351            wake_preamble: WakePreamble::Tone,
352            sync_chirp_f0: 4_500.0,
353            sync_chirp_f1: 6_500.0,
354            sync_half_len: 2048,
355            packet_payload_bytes: 24,
356            session_id: 1234,
357        }
358    }
359}
360
361// vim: set ts=4 sw=4 et: