1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-02-13 21:23:41 -05:00

FIR RRC filter

This commit is contained in:
f4exb 2026-02-10 23:25:06 +01:00
parent 26455261ee
commit 649312d7ba
13 changed files with 432 additions and 23 deletions

View File

@ -153,6 +153,7 @@ set(sdrbase_SOURCES
dsp/filerecord.cpp
dsp/filerecordinterface.cpp
dsp/firfilter.cpp
dsp/firfilterrrc.cpp
dsp/fmpreemphasis.cpp
dsp/freqlockcomplex.cpp
dsp/interpolator.cpp
@ -383,6 +384,7 @@ set(sdrbase_HEADERS
dsp/filerecord.h
dsp/filerecordinterface.h
dsp/firfilter.h
dsp/firfilterrrc.h
dsp/fmpreemphasis.h
dsp/freqlockcomplex.h
dsp/gfft.h

View File

@ -78,10 +78,9 @@ void FFTFilterRRC::create(float symbolRate, float rolloff)
m_rolloff = 1.0f;
}
// Initialize filter to zero
// Create filter directly in frequency domain
std::fill(m_filter, m_filter + m_fftLen, Complex(0.0f, 0.0f));
// Compute frequency-domain RRC response for each FFT bin
for (int i = 0; i < m_fftLen; i++) {
m_filter[i] = computeRRCResponse(m_symbolRate, m_rolloff, i, m_fftLen);
}
@ -121,23 +120,23 @@ FFTFilterRRC::Complex FFTFilterRRC::computeRRCResponse(
// Absolute frequency
float absFreq = std::abs(freq);
// Symbol time (inverse of symbol rate)
float T = 1.0f / symbolRate;
// Compute frequency boundaries
float f1 = (1.0f - rolloff) / (2.0f * T); // Start of transition band
float f2 = (1.0f + rolloff) / (2.0f * T); // End of transition band
// For RRC: passband is Rs/2, where Rs is symbol rate
// Transition band: Rs/2 * (1-beta) to Rs/2 * (1+beta)
float f1 = symbolRate * (1.0f - rolloff) / 2.0f; // Start of transition band
float f2 = symbolRate * (1.0f + rolloff) / 2.0f; // End of transition band
Complex response;
if (absFreq <= f1) {
// Passband: constant response
response = Complex(std::sqrt(T), 0.0f);
response = Complex(1.0f, 0.0f);
} else if (absFreq > f1 && absFreq <= f2) {
// Transition band: raised cosine roll-off
// H(f) = sqrt(T) * 0.5 * [1 + cos(pi*T/beta * (|f| - (1-beta)/(2T)))]
float arg = (M_PI * T / rolloff) * (absFreq - f1);
float amplitude = std::sqrt(T) * 0.5f * (1.0f + std::cos(arg));
// H(f) = 0.5 * [1 + cos(pi/beta * (|f|/Rs - (1-beta)/2))]
float normalizedFreq = absFreq / symbolRate; // Normalize by symbol rate
float arg = (M_PI / rolloff) * (normalizedFreq - (1.0f - rolloff) / 2.0f);
float amplitude = 0.5f * (1.0f + std::cos(arg));
response = Complex(amplitude, 0.0f);
} else {
// Stopband: zero response

View File

@ -68,9 +68,11 @@ public:
*
* The filter bandwidth extends from -symbolRate*(1+rolloff)/2 to
* +symbolRate*(1+rolloff)/2 in normalized frequency (0 to 0.5 = Nyquist).
*
* Creates mathematically correct RRC frequency response for use in
* digital communications applications.
*/
void create(float symbolRate, float rolloff);
/**
* @brief Process one complex input sample through the filter
*

View File

@ -0,0 +1,192 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2019, 2023 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2026 SDRangel contributors //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "dsp/firfilterrrc.h"
#include <algorithm>
#include <cmath>
FIRFilterRRC::FIRFilterRRC() :
m_ptr(0),
m_symbolRate(0.0f),
m_rolloff(0.0f),
m_samplesPerSymbol(0)
{
}
void FIRFilterRRC::create(float symbolRate, float rolloff, int symbolSpan, Normalization normalization)
{
m_symbolRate = symbolRate;
m_rolloff = rolloff;
// Clamp rolloff to valid range [0, 1]
if (m_rolloff < 0.0f) {
m_rolloff = 0.0f;
} else if (m_rolloff > 1.0f) {
m_rolloff = 1.0f;
}
// Calculate samples per symbol
m_samplesPerSymbol = static_cast<int>(std::round(1.0f / symbolRate));
// Calculate number of taps (always odd for symmetry)
int numTaps = symbolSpan * m_samplesPerSymbol + 1;
if ((numTaps & 1) == 0) {
numTaps++; // Ensure odd number of taps
}
// Allocate tap storage (only half + center due to symmetry)
int halfTaps = numTaps / 2 + 1;
m_taps.resize(halfTaps);
// Calculate filter taps
int center = numTaps / 2;
for (int i = 0; i < halfTaps; i++)
{
// Time offset from center in symbol periods
float t = static_cast<float>(i - center) / static_cast<float>(m_samplesPerSymbol);
m_taps[i] = computeRRCTap(t, m_rolloff);
}
// Apply normalization
if (normalization == Normalization::Energy)
{
// Normalize energy: sqrt(sum of squares) = 1
float sum = 0.0f;
for (int i = 0; i < halfTaps - 1; i++) {
sum += m_taps[i] * m_taps[i] * 2.0f; // Two symmetric taps
}
sum += m_taps[halfTaps - 1] * m_taps[halfTaps - 1]; // Center tap
float scale = std::sqrt(sum);
if (scale > 0.0f) {
for (int i = 0; i < halfTaps; i++) {
m_taps[i] /= scale;
}
}
}
else if (normalization == Normalization::Amplitude)
{
// Normalize amplitude: max output for bipolar sequence ≈ 1
float maxGain = 0.0f;
for (int offset = 0; offset < m_samplesPerSymbol; offset++)
{
float gain = 0.0f;
for (int i = offset; i < halfTaps - 1; i += m_samplesPerSymbol) {
gain += std::abs(m_taps[i]) * 2.0f; // Both sides
}
// Add center tap if aligned
if ((center - offset) % m_samplesPerSymbol == 0) {
gain += std::abs(m_taps[halfTaps - 1]);
}
if (gain > maxGain) {
maxGain = gain;
}
}
if (maxGain > 0.0f) {
for (int i = 0; i < halfTaps; i++) {
m_taps[i] /= maxGain;
}
}
}
else if (normalization == Normalization::Gain)
{
// Normalize for unity gain: sum of taps = 1
float sum = 0.0f;
for (int i = 0; i < halfTaps - 1; i++) {
sum += m_taps[i] * 2.0f; // Two symmetric taps
}
sum += m_taps[halfTaps - 1]; // Center tap
if (sum > 0.0f) {
for (int i = 0; i < halfTaps; i++) {
m_taps[i] /= sum;
}
}
}
// Initialize sample buffer
m_samples.resize(numTaps);
std::fill(m_samples.begin(), m_samples.end(), Complex(0.0f, 0.0f));
m_ptr = 0;
}
float FIRFilterRRC::computeRRCTap(float t, float rolloff) const
{
constexpr float Ts = 1.0f; // Symbol period (normalized)
const float beta = rolloff;
// Handle special case: t = 0
if (t == 0.0f) {
// h(0) = (1/T) * (1 + beta*(4/π - 1))
return (1.0f / Ts) * (1.0f + beta * (4.0f / M_PI - 1.0f));
}
// Check for zeros of denominator: 1 - (4*β*t/T)² = 0
// This occurs at t = ±T/(4*β)
const float denomCheck = 4.0f * beta * t / Ts;
if (std::abs(std::abs(denomCheck) - 1.0f) < 1e-6f && beta > 0.0f)
{
// h(±T/4β) = (β/(T*√2)) * [(1+2/π)*sin(π/4β) + (1-2/π)*cos(π/4β)]
const float arg = M_PI / (4.0f * beta);
return (beta / (Ts * std::sqrt(2.0f))) *
((1.0f + 2.0f / M_PI) * std::sin(arg) +
(1.0f - 2.0f / M_PI) * std::cos(arg));
}
// General case
const float numerator = std::sin(M_PI * t / Ts * (1.0f - beta)) +
4.0f * beta * t / Ts * std::cos(M_PI * t / Ts * (1.0f + beta));
const float denominator = M_PI * t / Ts * (1.0f - denomCheck * denomCheck);
return numerator / (denominator * Ts);
}
FIRFilterRRC::Complex FIRFilterRRC::filter(const Complex& input)
{
const int numTaps = static_cast<int>(m_samples.size());
const int halfTaps = static_cast<int>(m_taps.size()) - 1;
// Store input sample in circular buffer
m_samples[m_ptr] = input;
// Perform convolution using symmetry
Complex acc(0.0f, 0.0f);
int a = m_ptr;
int b = (a == numTaps - 1) ? 0 : a + 1;
// Process symmetric pairs
for (int i = 0; i < halfTaps; i++)
{
acc += (m_samples[a] + m_samples[b]) * m_taps[i];
a = (a == 0) ? numTaps - 1 : a - 1;
b = (b == numTaps - 1) ? 0 : b + 1;
}
// Add center tap
acc += m_samples[a] * m_taps[halfTaps];
// Advance circular buffer pointer
m_ptr = (m_ptr == static_cast<size_t>(numTaps) - 1) ? 0 : m_ptr + 1;
return acc;
}

153
sdrbase/dsp/firfilterrrc.h Normal file
View File

@ -0,0 +1,153 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2019, 2023 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2026 SDRangel contributors //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_DSP_FIRFILTERRRC_H
#define INCLUDE_DSP_FIRFILTERRRC_H
#include <complex>
#include <vector>
#include "export.h"
/**
* @brief FIR root raised cosine filter for complex signals
*
* Implements a time-domain FIR root raised cosine (RRC) filter using direct
* convolution. The RRC filter is commonly used for pulse shaping in digital
* communications to minimize intersymbol interference (ISI) while satisfying
* the Nyquist criterion.
*
* This filter offers sample-by-sample processing with low latency, making it
* suitable for real-time applications. For longer filters or higher throughput,
* consider using FFTFilterRRC which uses frequency-domain processing.
*/
class SDRBASE_API FIRFilterRRC
{
public:
using Complex = std::complex<float>;
/**
* @brief Normalization modes for filter taps
*/
enum class Normalization {
Energy, //!< Impulse response energy normalized (TX+RX = raised cosine peak = 1) - for matched filter pairs
Amplitude, //!< Bipolar symbol sequence output amplitude normalized to ±1 - for pulse amplitude preservation
Gain //!< Unity gain (0 dB) - tap coefficients sum to 1 - for continuous wave input
};
/**
* @brief Construct FIR root raised cosine filter
*/
FIRFilterRRC();
/**
* @brief Destructor
*/
~FIRFilterRRC() = default;
/**
* @brief Create root raised cosine filter taps
*
* @param symbolRate Symbol rate (normalized to sample rate, 0 to 0.5)
* @param rolloff Roll-off factor (beta) - typically 0.2 to 0.5
* 0 = rectangular, 1 = full cosine roll-off
* @param symbolSpan Number of symbols over which filter is spread
* Typical values: 6-12 (more = better filtering, more delay)
* @param normalization How to normalize the filter taps
*
* Total number of taps = symbolSpan * samplesPerSymbol + 1 (always odd).
* For symbolRate=0.05 (20 samples/symbol), symbolSpan=8 gives 161 taps.
*/
void create(float symbolRate, float rolloff, int symbolSpan = 8,
Normalization normalization = Normalization::Energy);
/**
* @brief Process one complex input sample through the filter
*
* Direct convolution with stored tap coefficients. Processes samples
* one at a time with minimal latency.
*
* @param input Complex input sample
* @return Filtered complex output sample
*/
Complex filter(const Complex& input);
/**
* @brief Get current symbol rate
* @return Symbol rate (normalized)
*/
float getSymbolRate() const { return m_symbolRate; }
/**
* @brief Get current rolloff factor
* @return Rolloff factor (beta)
*/
float getRolloff() const { return m_rolloff; }
/**
* @brief Get number of taps in filter
* @return Number of filter taps
*/
int getNumTaps() const { return static_cast<int>(m_taps.size()); }
/**
* @brief Get samples per symbol
* @return Samples per symbol (1/symbolRate)
*/
int getSamplesPerSymbol() const { return m_samplesPerSymbol; }
/**
* @brief Get filter delay in samples
* @return Group delay (approximately numTaps/2)
*/
int getDelay() const { return getNumTaps() / 2; }
/**
* @brief Get reference to filter tap coefficients
*
* The taps are stored as half + center due to symmetry. The full impulse
* response can be reconstructed by mirroring the taps around the center.
*
* @return Reference to vector of filter tap coefficients (half + center)
*/
const std::vector<float>& getTaps() const { return m_taps; }
private:
/**
* @brief Compute time-domain RRC tap value
*
* Implements the mathematical RRC impulse response:
* h(t) = [sin(π*t/T*(1-β)) + 4*β*t/T*cos(π*t/T*(1+β))] / [π*t/T*(1-(4*β*t/T)²)]
*
* @param t Time index (in symbol periods)
* @param rolloff Roll-off factor (beta)
* @return Tap value at time t
*/
float computeRRCTap(float t, float rolloff) const;
std::vector<float> m_taps; //!< Filter tap coefficients (symmetric, stored as half + center)
std::vector<Complex> m_samples; //!< Circular buffer for input samples
size_t m_ptr; //!< Current position in circular buffer
float m_symbolRate; //!< Symbol rate (normalized)
float m_rolloff; //!< Rolloff factor (beta)
int m_samplesPerSymbol; //!< Samples per symbol (1/symbolRate)
};
#endif // INCLUDE_DSP_FIRFILTERRRC_H

View File

@ -12,6 +12,7 @@ set(sdrbench_SOURCES
test_callsign.cpp
test_ft8protocols.cpp
test_fftrrc.cpp
test_firrrc.cpp
)
set(sdrbench_HEADERS

View File

@ -75,6 +75,8 @@ void MainBench::run()
testFT8Protocols(m_parser.getArgsStr());
} else if (m_parser.getTestType() == ParserBench::TestFFTRRCFilter) {
testFFTRRCFilter();
} else if (m_parser.getTestType() == ParserBench::TestFIRRRCFilter) {
testFIRRRCFilter();
} else {
qDebug() << "MainBench::run: unknown test type: " << m_parser.getTestType();
}

View File

@ -58,6 +58,7 @@ private:
void testDecimateFF();
void testGolay2312();
void testFFTRRCFilter();
void testFIRRRCFilter();
void testFT8(const QString& wavFile, const QString& argsStr); //!< use with sdrbench/samples/ft8/230105_091630.wav in -f option
void testFT8Protocols(const QString& argsStr);
void testCallsign(const QString& argsStr);

View File

@ -24,7 +24,7 @@
ParserBench::ParserBench() :
m_testOption(QStringList() << "t" << "test",
"Test type: decimateii, decimatefi, decimateff, decimateif, decimateinfii, decimatesupii, ambe, golay2312, ft8, ft8protocols, callsign, fftrrcfilter.",
"Test type: decimateii, decimatefi, decimateff, decimateif, decimateinfii, decimatesupii, ambe, golay2312, ft8, ft8protocols, callsign, fftrrcfilter, firrrcfilter.",
"test",
"decimateii"),
m_nbSamplesOption(QStringList() << "n" << "nb-samples",
@ -153,6 +153,8 @@ ParserBench::TestType ParserBench::getTestType() const
return TestFT8Protocols;
} else if (m_testStr == "fftrrcfilter") {
return TestFFTRRCFilter;
} else if (m_testStr == "firrrcfilter") {
return TestFIRRRCFilter;
} else {
return TestDecimatorsII;
}

View File

@ -41,7 +41,8 @@ public:
TestFT8,
TestCallsign,
TestFT8Protocols,
TestFFTRRCFilter
TestFFTRRCFilter,
TestFIRRRCFilter
} TestType;
ParserBench();

View File

@ -28,22 +28,26 @@ void MainBench::testFFTRRCFilter()
filter.create(0.05f, 0.35f); // 2400 baud, 0.35 rolloff
qDebug() << "MainBench::testFFTRRCFilter: filter created";
FILE *fd_filter = fopen("test_fftrrc_filter.txt", "w");
FILE *fd_filter = fopen("test_rrc_filter.txt", "w");
for (int i = 0; i < RRC_FFT_SIZE; i++) {
fprintf(fd_filter, "%f\n", std::abs(filter.getFilter()[i]));
}
qDebug() << "MainBench::testFFTRRCFilter: filter coefficients written to test_fftrrc_filter.txt";
qDebug() << "MainBench::testFFTRRCFilter: filter coefficients written to test_rrc_filter.txt";
fclose(fd_filter);
qDebug() << "MainBench::testFFTRRCFilter: running filter";
FILE *fd = fopen("test_fftrrc.txt", "w");
FILE *fd = fopen("test_rrc.txt", "w");
int outLen = 0;
for (int i = 0; i < 1024; i++)
for (int i = 0; i < 5000; i++)
{
Real phi = i * (48000.0 / 1200.0) * (2*3.141);
Real x = sin(phi);
int ss = 48000 / 2400; // Samples per symbol at 2400 baud
int d = i / ss; // Symbol index at 2400 baud
Real s = (d % 2 == 0) ? 1.0f : -1.0f; // BPSK symbol sequence
Real x = (i % ss == 0 ? s : 0.0f); // Pulsed signal at 2400 Hz
// Real phi = i * (1200.0 / 48000.0) * (2*3.141);
// Real x = sin(phi) > 0.0 ? 1.0f : -1.0f; // Simulate a BPSK signal at 1200 baud
filter.process(Complex(x, 0.0f), &rrcFilterOut);
outLen++;

50
sdrbench/test_firrrc.cpp Normal file
View File

@ -0,0 +1,50 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "mainbench.h"
#include "dsp/firfilterrrc.h"
#include <QDebug>
void MainBench::testFIRRRCFilter()
{
qDebug() << "MainBench::testFIRRRCFilter";
FIRFilterRRC filter;
filter.create(0.05f, 0.35f, 8, FIRFilterRRC::Normalization::Gain); // 2400 baud, 0.35 rolloff
qDebug() << "MainBench::testFIRRRCFilter: filter created";
FILE *fd_filter = fopen("test_rrc_filter.txt", "w");
const std::vector<float>& taps = filter.getTaps();
for (auto tap : taps) {
fprintf(fd_filter, "%f\n", std::abs(tap));
}
qDebug() << "MainBench::testFIRRRCFilter: filter coefficients written to test_rrc_filter.txt";
fclose(fd_filter);
qDebug() << "MainBench::testFIRRRCFilter: running filter";
FILE *fd = fopen("test_rrc.txt", "w");
for (int i = 0; i < 1000; i++)
{
Real phi = i * (1200.0 / 48000.0) * (2*3.141);
Real x = sin(phi) > 0.0 ? 1.0f : -1.0f; // Simulate a BPSK signal at 1200 baud
Complex rrc = filter.filter(Complex(x, 0.0f));
fprintf(fd, "%f\n", rrc.real());
}
qDebug() << "MainBench::testFIRRRCFilter: output samples written to test_firrrc.txt";
fclose(fd);
}

View File

@ -2,13 +2,13 @@
import matplotlib.pyplot as plt
import numpy as np
with open('../build/test_fftrrc_filter.txt', 'r') as f:
with open('../build/test_rrc_filter.txt', 'r') as f:
filter_data = f.read()
filter_out = [float(x) for x in filter_data.splitlines()]
xf = np.array(filter_out)
with open('../build/test_fftrrc.txt', 'r') as f:
with open('../build/test_rrc.txt', 'r') as f:
data = f.read()
out = [float(x) for x in data.splitlines()]
@ -24,7 +24,7 @@ ax2.set_xlabel('Index')
ax2.set_ylabel('Amplitude')
ax2.grid(True)
ax1.set_title('FFTRRC Filter Output (Real Part)')
ax1.set_title('RRC Filter Output (Real Part)')
ax1.plot(x)
# ax1.scatter(marks, x[marks],
# color='red', marker='.', s=100, zorder=5, edgecolors='black')