1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// Copyright (c) 2026 G & R Associates LLC
// SPDX-License-Identifier: MIT OR Apache-2.0
// src/multicarrier/tx_lowpass.rs
use crate;
use Complex32 as C32;
/// TX baseband low-pass (spectral-mask) filter for an assembled multicarrier
/// stream: the one out-of-band lever that is **not** bounded by the symbol
/// windowing ceiling.
///
/// Symbol windowing ([`SymbolWindow`](super::SymbolWindow)) attacks the skirt
/// indirectly, by softening the symbol-boundary discontinuity; its taper is
/// capped at `cp_len/2`, which caps the payoff (~11 dB measured). This filter
/// attacks the skirt directly in the frequency domain — a low-pass across the
/// **composite** stream, spanning symbol boundaries — so its attenuation stacks
/// on top of windowing instead of sharing the same budget.
///
/// # What the receiver sees
///
/// The filter is applied with [`FirLowpassIq::filter_aligned`], which is
/// same-length and group-delay-compensated, so stream length and symbol
/// boundaries are unchanged and a fixed-`sps` strided receiver is unaffected.
/// What the receiver then sees is a **linear channel it already inverts**: for
/// an FFT window at back-off `b`, each windowed sample combines symbol samples
/// spanning `±d` around the window, where `d` is the filter's group delay. When
/// that reach stays inside the symbol (and clear of any symbol-window taper),
/// the cyclic prefix makes the combination an exact *circular* convolution, so
/// the FFT sees each subcarrier scaled by a single complex `H[k]` — which the
/// pilot/training channel estimate divides out like any other channel. Nothing
/// about the demodulator's decoding needs to change.
///
/// # Guard budget — the filter needs the back-off too
///
/// That argument holds only while the filter's reach and any taper both fall in
/// the guard samples the receiver discards:
///
/// ```text
/// roll_off + group_delay ≤ min(cp_len − backoff, backoff)
/// ```
///
/// maximized, as for windowing, at `backoff = cp_len/2`. See
/// [`fits_guard`](Self::fits_guard). Note what this means at `backoff = 0`: the
/// slack is zero, so **a group-delay-compensated filter is not transparent
/// against a receiver pinned at the cyclic-prefix boundary** — centring the
/// response makes half of it a pre-echo the standard window has no room for.
/// The RX FFT-window back-off is therefore the shared enabler for *both* TX
/// shaping levers, not a windowing-only requirement: set `backoff ≈ cp_len/2`
/// and spend the resulting slack on `roll_off + group_delay`.
///
/// Overrunning the budget does not corrupt the decode abruptly — it leaks a
/// little inter-symbol energy the equalizer cannot invert — but it is the
/// budget to design against, and it is why a **long guard** (DVB-T G1/4 gives
/// `cp_len = 512`) buys a much sharper mask than a short one.
///
/// A bare, *unequalized* demod sees the filter's in-band tilt uncorrected —
/// the same caveat that applies to the RX window back-off.
///
/// # It needs somewhere to filter
///
/// A mask can only attenuate bandwidth the signal does not occupy. A COFDM plan
/// that fills every bin out to Nyquist leaves no null band for a transition, so
/// pair this with an edge-carrier guard
/// ([`CarrierPlan::with_contiguous_data`](super::CarrierPlan::with_contiguous_data)):
/// the guard makes the room, the mask uses it. DVB-T comes with the room built
/// in (1705 of 2048 bins are active).
///
/// # Acquisition budget (preamble-bearing waveforms)
///
/// The filter is applied to the whole assembled burst, preamble included — a
/// real transmitter band-limits everything it emits, and filtering only part of
/// a burst would reintroduce the spectral step. A Schmidl & Cox preamble is
/// `num_repeats` copies of a `repeat_len` segment, and repetition survives
/// filtering wherever the taps see only repeated samples: exactly for outputs
/// at least `group_delay` from each end of the repeated region. So the second
/// sizing rule, alongside the guard budget, is **`group_delay ≪ repeat_len`**.
/// Preamble-less waveforms (DVB-T, which acquires from the cyclic prefix) are
/// bound only by the guard budget.