Anti-alias filter before RF->network downsampling in FM mode

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.
This commit is contained in:
Cort Buffington 2026-08-11 09:57:47 -05:00
parent 894ba76432
commit 4c7e8fbe46
3 changed files with 23 additions and 6 deletions

23
FM.cpp
View File

@ -68,6 +68,9 @@ m_needReverse(false),
m_filterStage1( 724, 1448, 724, 32768, -37895, 21352),//3rd order Cheby Filter 300 to 2700Hz, 0.2dB passband ripple, sampling rate 24kHz
m_filterStage2(32768, 0,-32768, 32768, -50339, 19052),
m_filterStage3(32768, -65536, 32768, 32768, -64075, 31460),
m_dsFilterStage1( 724, 1448, 724, 32768, -37895, 21352),//same design, separate state
m_dsFilterStage2(32768, 0,-32768, 32768, -50339, 19052),
m_dsFilterStage3(32768, -65536, 32768, 32768, -64075, 31460),
m_blanking(),
m_accessMode(1U),
m_linkMode(false),
@ -203,8 +206,10 @@ void CFM::repeaterSamples(bool cos, q15_t* samples, const uint16_t* rssi, uint8_
if (m_duplex) {
if (m_state == FS_RELAYING_RF || m_state == FS_KERCHUNK_RF || m_state == FS_RELAYING_EXT || m_state == FS_KERCHUNK_EXT) {
currentSample = m_blanking.process(currentSample);
if (m_extEnabled && (m_state == FS_RELAYING_RF || m_state == FS_KERCHUNK_RF))
m_downSampler.addSample(currentSample);
if (m_extEnabled && (m_state == FS_RELAYING_RF || m_state == FS_KERCHUNK_RF)) {
q15_t dsSample = m_dsFilterStage3.filter(m_dsFilterStage2.filter(m_dsFilterStage1.filter(currentSample)));
m_downSampler.addSample(dsSample);
}
currentSample *= currentBoost;
} else {
@ -214,9 +219,11 @@ void CFM::repeaterSamples(bool cos, q15_t* samples, const uint16_t* rssi, uint8_
if (m_state == FS_RELAYING_EXT || m_state == FS_KERCHUNK_EXT) {
currentSample *= currentBoost;
} else {
if (m_extEnabled && (m_state == FS_RELAYING_RF || m_state == FS_KERCHUNK_RF))
m_downSampler.addSample(currentSample);
continue;
if (m_extEnabled && (m_state == FS_RELAYING_RF || m_state == FS_KERCHUNK_RF)) {
q15_t dsSample = m_dsFilterStage3.filter(m_dsFilterStage2.filter(m_dsFilterStage1.filter(currentSample)));
m_downSampler.addSample(dsSample);
}
continue;
}
}
@ -332,7 +339,8 @@ void CFM::linkSamples(bool cos, q15_t* samples, uint8_t length)
if (m_rfSignal && m_extEnabled) {
q15_t currentSample = m_blanking.process(currentRFSample);
m_downSampler.addSample(currentSample);
q15_t dsSample = m_dsFilterStage3.filter(m_dsFilterStage2.filter(m_dsFilterStage1.filter(currentSample)));
m_downSampler.addSample(dsSample);
}
if (!m_extSignal)
@ -412,6 +420,9 @@ void CFM::reset()
m_inputExtRB.reset();
m_downSampler.reset();
m_dsFilterStage1.reset();
m_dsFilterStage2.reset();
m_dsFilterStage3.reset();
m_squelch.reset();
m_needReverse = false;

3
FM.h
View File

@ -80,6 +80,9 @@ private:
CFMDirectFormI m_filterStage1;
CFMDirectFormI m_filterStage2;
CFMDirectFormI m_filterStage3;
CFMDirectFormI m_dsFilterStage1; // anti-alias filter for the downsampler, own state
CFMDirectFormI m_dsFilterStage2;
CFMDirectFormI m_dsFilterStage3;
CFMBlanking m_blanking;
uint8_t m_accessMode;
bool m_linkMode;

View File

@ -34,6 +34,7 @@ m_sampleIndex(0U)
void CFMDownSampler::addSample(q15_t sample)
{
sample = __SSAT(sample, 12);//clamp before packing to 12 bits
uint32_t usample = uint32_t(int32_t(sample) + 2048);
//only take one of three samples
switch(m_sampleIndex){
@ -74,6 +75,8 @@ uint16_t CFMDownSampler::getData()
void CFMDownSampler::reset()
{
m_sampleIndex = 0U;
m_samplePack = 0U;
m_ringBuffer.reset();
}
#endif