From 4c7e8fbe46dfc4352c906cdae1decb5d93039285 Mon Sep 17 00:00:00 2001 From: Cort Buffington Date: Tue, 11 Aug 2026 09:57:47 -0500 Subject: [PATCH 1/3] 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. --- FM.cpp | 23 +++++++++++++++++------ FM.h | 3 +++ FMDownSampler.cpp | 3 +++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/FM.cpp b/FM.cpp index efe3e05..9928edd 100644 --- a/FM.cpp +++ b/FM.cpp @@ -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; diff --git a/FM.h b/FM.h index a1a198f..8d5f49b 100644 --- a/FM.h +++ b/FM.h @@ -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; diff --git a/FMDownSampler.cpp b/FMDownSampler.cpp index 47fb120..04b2c96 100644 --- a/FMDownSampler.cpp +++ b/FMDownSampler.cpp @@ -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 From 384c5d559d2398874e085db478d84e102b1201dc Mon Sep 17 00:00:00 2001 From: Cort Buffington Date: Wed, 12 Aug 2026 07:43:23 -0500 Subject: [PATCH 2/3] Reconstruction filter after ext-audio upsampling in FM mode 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. --- FM.cpp | 8 ++++++++ FM.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/FM.cpp b/FM.cpp index 9928edd..d39bb46 100644 --- a/FM.cpp +++ b/FM.cpp @@ -71,6 +71,9 @@ 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_usFilterStage1( 724, 1448, 724, 32768, -37895, 21352),//same design, separate state +m_usFilterStage2(32768, 0,-32768, 32768, -50339, 19052), +m_usFilterStage3(32768, -65536, 32768, 32768, -64075, 31460), m_blanking(), m_accessMode(1U), m_linkMode(false), @@ -125,6 +128,7 @@ void CFM::repeaterSamples(bool cos, q15_t* samples, const uint16_t* rssi, uint8_ q15_t currentExtSample; bool inputExt = m_inputExtRB.getSample(currentExtSample);//always consume the external input data so it does not overflow + currentExtSample = m_usFilterStage3.filter(m_usFilterStage2.filter(m_usFilterStage1.filter(currentExtSample))); inputExt = inputExt && m_extEnabled; switch (m_accessMode) { @@ -270,6 +274,7 @@ void CFM::linkSamples(bool cos, q15_t* samples, uint8_t length) // few samples past that point q15_t currentExtSample = 0; bool inputExt = m_inputExtRB.getSample(currentExtSample);//always consume the external input data so it does not overflow + currentExtSample = m_usFilterStage3.filter(m_usFilterStage2.filter(m_usFilterStage1.filter(currentExtSample))); inputExt = inputExt && m_extEnabled; switch (m_accessMode) { @@ -423,6 +428,9 @@ void CFM::reset() m_dsFilterStage1.reset(); m_dsFilterStage2.reset(); m_dsFilterStage3.reset(); + m_usFilterStage1.reset(); + m_usFilterStage2.reset(); + m_usFilterStage3.reset(); m_squelch.reset(); m_needReverse = false; diff --git a/FM.h b/FM.h index 8d5f49b..29df859 100644 --- a/FM.h +++ b/FM.h @@ -83,6 +83,9 @@ private: CFMDirectFormI m_dsFilterStage1; // anti-alias filter for the downsampler, own state CFMDirectFormI m_dsFilterStage2; CFMDirectFormI m_dsFilterStage3; + CFMDirectFormI m_usFilterStage1; // reconstruction filter for the upsampler, own state + CFMDirectFormI m_usFilterStage2; + CFMDirectFormI m_usFilterStage3; CFMBlanking m_blanking; uint8_t m_accessMode; bool m_linkMode; From d70ab85bae2b67a81e761dc2f0cc1f0ef046b247 Mon Sep 17 00:00:00 2001 From: Cort Buffington Date: Thu, 13 Aug 2026 13:52:06 -0500 Subject: [PATCH 3/3] Copy-construct the ds/us filters from m_filterStage1/2/3 Same coefficients, no need to retype them. CFMDirectFormI's copy ctor already copies coefficients and (zeroed, at this point) state. --- FM.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/FM.cpp b/FM.cpp index d39bb46..57afcf0 100644 --- a/FM.cpp +++ b/FM.cpp @@ -68,12 +68,12 @@ 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_usFilterStage1( 724, 1448, 724, 32768, -37895, 21352),//same design, separate state -m_usFilterStage2(32768, 0,-32768, 32768, -50339, 19052), -m_usFilterStage3(32768, -65536, 32768, 32768, -64075, 31460), +m_dsFilterStage1(m_filterStage1),//same coefficients, separate state +m_dsFilterStage2(m_filterStage2), +m_dsFilterStage3(m_filterStage3), +m_usFilterStage1(m_filterStage1),//same coefficients, separate state +m_usFilterStage2(m_filterStage2), +m_usFilterStage3(m_filterStage3), m_blanking(), m_accessMode(1U), m_linkMode(false),