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: