CFMUpSampler zero-stuffs 8kHz to 24kHz with no lowpass after it,
leaving imaging artifacts in the reconstructed ext audio. Filter it
with a second, separately-stated pass of the same 300-2700Hz design
already used for TX shaping, right where it comes out of the upsampler.
The 24kHz->8kHz decimation in CFMDownSampler had no lowpass ahead of
it, aliasing everything above 4kHz back into the audio band. Reuse
the existing 300-2700Hz filter design (separate instance/state) ahead
of all three addSample() call sites in FM.cpp.
Also: reset() wasn't clearing the ring buffer or in-progress sample
pack, so stale audio could bleed into the next transmission. And
addSample() packed samples into 12 bits with no saturation, so an
out-of-range value corrupted the neighbouring sample.
Under [FM] LinkMode=1, linkStateMachine() set m_extSignal false the
instant a single ext-audio sample went missing, with no debounce --
unlike the non-link duplex/simplex paths, which already tolerate a
brief gap via the RELAYING_EXT -> RELAYING_WAIT_EXT / m_ackDelayTimer
pattern before declaring a real loss. Since IO::process() drops PTT
the moment the downstream TX buffer runs dry, that zero-debounce cutoff
meant even a very brief, late-but-recoverable gap from the gateway
(scheduling jitter, a late UDP frame, etc.) could cascade into a full
PTT drop-and-rekey cycle -- far more audible than the underlying gap
itself.
Adds m_extGapTimer, mirroring the existing pattern: on the first missed
sample, start the timer instead of immediately clearing m_extSignal; a
gap that recovers before FM_LINK_EXT_GAP_MS (default 60ms) expires
never affects m_extSignal at all, so the transmitter never notices.
Only a gap that outlasts the timer is treated as a genuine end of
transmission, exactly as before.
linkSamples()'s currentExtSample is now zero-initialized, since
m_extSignal can now stay true for a few samples past an underrun --
silence, not stack garbage, must be what gets played out to the modem
during that window.