A pair of allpass cascades whose outputs stand a quarter cycle apart across nearly the whole band — the analytic-signal splitter an SSB shifter needs. More...
#include <frequency_shifter_ssb.hpp>
Classes | |
| struct | Outputs |
Public Member Functions | |
| void | reset () |
| Clears state. | |
| Outputs | process (SampleType x) |
Static Public Member Functions | |
| static constexpr double | band_low_hz (double sample_rate) |
| Lower edge of the design band in Hz at a given rate. | |
| static constexpr double | band_high_hz (double sample_rate) |
| Upper edge of the design band in Hz at a given rate. | |
Static Public Attributes | |
| static constexpr std::size_t | kSections = 4 |
| Sections per branch. | |
| static constexpr std::array< double, kSections > | kInPhasePoles |
| Pole magnitudes of the in-phase (undelayed) branch. | |
| static constexpr std::array< double, kSections > | kQuadraturePoles |
| Pole magnitudes of the quadrature (one-sample-delayed) branch. | |
| static constexpr double | kBandEdgeNormalized = 4.6e-4 |
| The lower edge of the design band, as a fraction of the sample rate. | |
| static constexpr double | kImageRejectDb = 40.0 |
| The rejection floor the shipped table holds across the design band. | |
A pair of allpass cascades whose outputs stand a quarter cycle apart across nearly the whole band — the analytic-signal splitter an SSB shifter needs.
Neither output is the input. Both branches are allpass, so both preserve the input's magnitude spectrum exactly and move only its phase; what carries information is the DIFFERENCE between them. A single filter cannot produce that, which is why the structure is a pair.
Each section is second-order with one coefficient:
This is not the section form a polyphase half-band decimator uses, and the difference is one sign, so it is worth stating before someone "corrects" it. The half-band form is (c + z⁻²)/(1 + c·z⁻²), with poles at ±j√c. Fed the SAME coefficient table, that form produces a branch pair whose phase difference is 0 below fs/4 and π above it — a half-band filter, not a quadrature pair. The two forms are related by z → jz, which is the half-band-to-Hilbert rotation, and that identity is precisely why one published table serves both uses. Ship the wrong sign and the module still runs, still sounds like something, and rejects no image at all.
Both branches are cascades of kSections such sections and the QUADRATURE branch carries an additional z⁻¹. Of the four ways to assign (delayed, undelayed) × (table A, table B) to (I, Q), only two give a phase difference that is constant across the band rather than one that drifts with ω; of those two, this one leaves the IN-PHASE branch undelayed, so the module's impulse response is non-zero at sample 0 and latency_samples() is honestly 0 (the other choice puts a bulk sample of delay on I and would have to report 1).
Stability is structural, not a property of the fitted values: |z| = √c < 1 for any 0 < c < 1, so any coefficient search can be re-run without ever risking an unstable filter. What a coefficient search CAN get wrong is the image rejection, which is why that — and not the digits — is what the acceptance suite asserts.
RT contract: every member allocates nothing, takes no locks, and is safe per sample on the audio thread.
|
inlinestaticconstexpr |
Upper edge of the design band in Hz at a given rate.
|
inlinestaticconstexpr |
Lower edge of the design band in Hz at a given rate.
Computed from the shipped constant, never restated.
|
inline |
|
inline |
Clears state.
Cannot clear coefficients — there are none to clear; they are compile-time constants read directly by process().
|
staticconstexpr |
The lower edge of the design band, as a fraction of the sample rate.
A fixed allpass table is a normalised-frequency design, so this is the only form in which the edge is a constant at all (see the file doc block). Measured: kImageRejectDb is met on [kBandEdgeNormalized·fs, (0.5 − kBandEdgeNormalized)·fs], which is 20.3 Hz .. 22.08 kHz at 44.1 kHz and 22.1 Hz .. 23.98 kHz at 48 kHz. The 40 dB crossover itself is at 4.34e-4; this sits a little above it so the claim carries margin rather than sitting on the knife edge.
|
staticconstexpr |
The rejection floor the shipped table holds across the design band.
[design parameter] default 40 dB, range 30 .. 60 dB — the range is the span a regenerated table at kSections 3 .. 6 can reach; 40 dB is what four sections deliver.
|
staticconstexpr |
Pole magnitudes of the in-phase (undelayed) branch.
[design parameter] — a calibration table, validated by the acceptance suite's measured image rejection rather than by inspecting digits. Reached from the published minimax design cited in the file doc block; the section coefficient is the SQUARE of each entry.
|
staticconstexpr |
Pole magnitudes of the quadrature (one-sample-delayed) branch.
[design parameter] — same provenance and same validation as above.
|
staticconstexpr |
Sections per branch.
[design parameter] default 4, range 3 .. 6. Four is Niemitalo's published operating point and measures ≈ 44 dB of broadband rejection here; the range exists so the table can be regenerated deeper without a structural change if a future spec wants a tighter floor.