First draft demodulator, refactored out the filters, started adding tests
This commit is contained in:
parent
60d6f6db7d
commit
010e479f89
@ -11,6 +11,7 @@ include_directories(include/modulation)
|
|||||||
include_directories(include/utils)
|
include_directories(include/utils)
|
||||||
|
|
||||||
# Add subdirectories for organization
|
# Add subdirectories for organization
|
||||||
|
enable_testing()
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
|
|
||||||
# Set source files
|
# Set source files
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
#ifndef FSK_DEMODULATOR_H
|
|
||||||
#define FSK_DEMODULATOR_H
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "BitStreamWriter.h"
|
|
||||||
|
|
||||||
class FSKDemodulatorConfig {
|
|
||||||
public:
|
|
||||||
int freq_lo;
|
|
||||||
int freq_hi;
|
|
||||||
int sample_rate;
|
|
||||||
int baud_rate;
|
|
||||||
std::shared_ptr<BitStreamWriter> bitstreamwriter;
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace milstd {
|
|
||||||
class FSKDemodulator {
|
|
||||||
public:
|
|
||||||
FSKDemodulator(const FSKDemodulatorConfig& s) : freq_lo(s.freq_lo), freq_hi(s.freq_hi), sample_rate(s.sample_rate), baud_rate(s.baud_rate), bit_writer(s.bitstreamwriter) {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void demodulate(const std::vector<int16_t>& samples) {
|
|
||||||
size_t nb = samples.size();
|
|
||||||
|
|
||||||
for (size_t i = 0; i < nb; i++) {
|
|
||||||
filter_buf[buf_ptr++] = samples[i];
|
|
||||||
|
|
||||||
if (buf_ptr == filter_buf.size()) {
|
|
||||||
std::copy(filter_buf.begin() + filter_buf.size() - filter_size, filter_buf.end(), filter_buf.begin());
|
|
||||||
buf_ptr = filter_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
int corr;
|
|
||||||
int sum = 0;
|
|
||||||
|
|
||||||
corr = dotProduct(&filter_buf[buf_ptr - filter_size], filter_hi_i.data(), filter_size);
|
|
||||||
sum += corr * corr;
|
|
||||||
|
|
||||||
corr = dotProduct(&filter_buf[buf_ptr - filter_size], filter_hi_q.data(), filter_size);
|
|
||||||
sum += corr * corr;
|
|
||||||
|
|
||||||
corr = dotProduct(&filter_buf[buf_ptr - filter_size], filter_lo_i.data(), filter_size);
|
|
||||||
sum -= corr * corr;
|
|
||||||
|
|
||||||
corr = dotProduct(&filter_buf[buf_ptr - filter_size], filter_lo_q.data(), filter_size);
|
|
||||||
sum -= corr * corr;
|
|
||||||
|
|
||||||
int new_sample = (sum > 0) ? 1 : 0;
|
|
||||||
|
|
||||||
if (last_sample != new_sample) {
|
|
||||||
last_sample = new_sample;
|
|
||||||
if (baud_pll < 0.5)
|
|
||||||
baud_pll += baud_pll_adj;
|
|
||||||
else
|
|
||||||
baud_pll -= baud_pll_adj;
|
|
||||||
}
|
|
||||||
|
|
||||||
baud_pll += baud_incr;
|
|
||||||
|
|
||||||
if (baud_pll >= 1.0) {
|
|
||||||
baud_pll -= 1.0;
|
|
||||||
bit_writer->putBit(last_sample);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
int freq_lo;
|
|
||||||
int freq_hi;
|
|
||||||
int sample_rate;
|
|
||||||
int baud_rate;
|
|
||||||
std::shared_ptr<BitStreamWriter> bit_writer;
|
|
||||||
|
|
||||||
int filter_size;
|
|
||||||
std::vector<double> filter_lo_i;
|
|
||||||
std::vector<double> filter_lo_q;
|
|
||||||
std::vector<double> filter_hi_i;
|
|
||||||
std::vector<double> filter_hi_q;
|
|
||||||
std::vector<double> filter_buf;
|
|
||||||
size_t buf_ptr;
|
|
||||||
|
|
||||||
double baud_incr;
|
|
||||||
double baud_pll;
|
|
||||||
double baud_pll_adj;
|
|
||||||
int last_sample;
|
|
||||||
|
|
||||||
void initialize() {
|
|
||||||
baud_incr = static_cast<double>(baud_rate) / sample_rate;
|
|
||||||
baud_pll = 0.0;
|
|
||||||
baud_pll_adj = baud_incr / 4;
|
|
||||||
|
|
||||||
filter_size = sample_rate / baud_rate;
|
|
||||||
|
|
||||||
filter_buf.resize(filter_size * 2, 0.0);
|
|
||||||
buf_ptr = filter_size;
|
|
||||||
|
|
||||||
last_sample = 0;
|
|
||||||
|
|
||||||
filter_lo_i.resize(filter_size);
|
|
||||||
filter_lo_q.resize(filter_size);
|
|
||||||
filter_hi_i.resize(filter_size);
|
|
||||||
filter_hi_q.resize(filter_size);
|
|
||||||
|
|
||||||
for (int i = 0; i < filter_size; i++) {
|
|
||||||
double phase_lo = 2.0 * M_PI * freq_lo * i / sample_rate;
|
|
||||||
filter_lo_i[i] = std::cos(phase_lo);
|
|
||||||
filter_lo_q[i] = std::sin(phase_lo);
|
|
||||||
|
|
||||||
double phase_hi = 2.0 * M_PI * freq_hi * i / sample_rate;
|
|
||||||
filter_hi_i[i] = std::cos(phase_hi);
|
|
||||||
filter_hi_q[i] = std::sin(phase_hi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double dotProduct(const double* x, const double* y, size_t size) {
|
|
||||||
double sum = 0.0;
|
|
||||||
for (size_t i = 0; i < size; i++) {
|
|
||||||
sum += x[i] * y[i];
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace milstd
|
|
||||||
|
|
||||||
#endif /* FSK_DEMODULATOR_H */
|
|
@ -1,87 +0,0 @@
|
|||||||
#ifndef FSK_MODULATOR_H
|
|
||||||
#define FSK_MODULATOR_H
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "BitStreamReader.h"
|
|
||||||
|
|
||||||
class FSKModulatorConfig {
|
|
||||||
public:
|
|
||||||
int freq_lo;
|
|
||||||
int freq_hi;
|
|
||||||
int sample_rate;
|
|
||||||
int baud_rate;
|
|
||||||
std::shared_ptr<BitStreamReader> bitstreamreader;
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace milstd {
|
|
||||||
class FSKModulator {
|
|
||||||
public:
|
|
||||||
FSKModulator(const FSKModulatorConfig& s) : freq_lo(s.freq_lo), freq_hi(s.freq_hi), sample_rate(s.sample_rate), baud_rate(s.baud_rate), bit_reader(s.bitstreamreader) {
|
|
||||||
omega[0] = (2.0 * M_PI * freq_lo) / sample_rate;
|
|
||||||
omega[1] = (2.0 * M_PI * freq_hi) / sample_rate;
|
|
||||||
baud_incr = static_cast<double>(baud_rate) / sample_rate;
|
|
||||||
phase = 0.0;
|
|
||||||
baud_frac = 0.0;
|
|
||||||
current_bit = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<int16_t> modulate(unsigned int num_samples) {
|
|
||||||
std::vector<int16_t> samples;
|
|
||||||
samples.reserve(num_samples);
|
|
||||||
|
|
||||||
int bit = current_bit;
|
|
||||||
|
|
||||||
for (unsigned int i = 0; i < num_samples; i++) {
|
|
||||||
baud_frac += baud_incr;
|
|
||||||
if (baud_frac >= 1.0) {
|
|
||||||
baud_frac -= 1.0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bit = bit_reader->getNextBit();
|
|
||||||
}
|
|
||||||
catch(const std::out_of_range&)
|
|
||||||
{
|
|
||||||
bit = 0;
|
|
||||||
}
|
|
||||||
if (bit != 0 && bit != 1)
|
|
||||||
bit = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
double sample = std::cos(phase);
|
|
||||||
int16_t sample_int = static_cast<int16_t>(sample * 32767);
|
|
||||||
samples.push_back(sample_int);
|
|
||||||
|
|
||||||
phase += omega[bit];
|
|
||||||
if (phase >= 2.0 * M_PI) {
|
|
||||||
phase -= 2.0 * M_PI;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
current_bit = bit;
|
|
||||||
|
|
||||||
return samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
// parameters
|
|
||||||
int freq_lo, freq_hi;
|
|
||||||
int sample_rate;
|
|
||||||
int baud_rate;
|
|
||||||
std::shared_ptr<BitStreamReader> bit_reader;
|
|
||||||
|
|
||||||
// state variables
|
|
||||||
double phase;
|
|
||||||
double baud_frac;
|
|
||||||
double baud_incr;
|
|
||||||
std::array<double, 2> omega;
|
|
||||||
int current_bit;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace milstd
|
|
||||||
|
|
||||||
#endif /* FSK_MODULATOR_H */
|
|
@ -11,6 +11,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <fftw3.h>
|
#include <fftw3.h>
|
||||||
|
|
||||||
|
#include "costasloop.h"
|
||||||
|
#include "filters.h"
|
||||||
|
|
||||||
static constexpr double CARRIER_FREQ = 1800.0;
|
static constexpr double CARRIER_FREQ = 1800.0;
|
||||||
static constexpr size_t SYMBOL_RATE = 2400;
|
static constexpr size_t SYMBOL_RATE = 2400;
|
||||||
static constexpr double ROLLOFF_FACTOR = 0.35;
|
static constexpr double ROLLOFF_FACTOR = 0.35;
|
||||||
@ -19,13 +22,14 @@ static constexpr double SCALE_FACTOR = 32767.0;
|
|||||||
class PSKModulator {
|
class PSKModulator {
|
||||||
public:
|
public:
|
||||||
PSKModulator(const double _sample_rate, const bool _is_frequency_hopping, const size_t _num_taps)
|
PSKModulator(const double _sample_rate, const bool _is_frequency_hopping, const size_t _num_taps)
|
||||||
: sample_rate(validateSampleRate(_sample_rate)), gain(1.0/sqrt(2.0)), is_frequency_hopping(_is_frequency_hopping), samples_per_symbol(static_cast<size_t>(sample_rate / SYMBOL_RATE)), num_taps(_num_taps) {
|
: sample_rate(validateSampleRate(_sample_rate)), gain(1.0/sqrt(2.0)), is_frequency_hopping(_is_frequency_hopping), samples_per_symbol(static_cast<size_t>(sample_rate / SYMBOL_RATE)), phase_detector(symbolMap), srrc_filter(48, _sample_rate, SYMBOL_RATE, ROLLOFF_FACTOR) {
|
||||||
initializeSymbolMap();
|
initializeSymbolMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<int16_t> modulate(const std::vector<uint8_t>& symbols) const {
|
std::vector<int16_t> modulate(const std::vector<uint8_t>& symbols) {
|
||||||
std::vector<double> baseband_I(symbols.size() * samples_per_symbol);
|
std::vector<double> baseband_I(symbols.size() * samples_per_symbol);
|
||||||
std::vector<double> baseband_Q(symbols.size() * samples_per_symbol);
|
std::vector<double> baseband_Q(symbols.size() * samples_per_symbol);
|
||||||
|
std::vector<std::complex<double>> baseband_components(symbols.size() * samples_per_symbol);
|
||||||
size_t symbol_index = 0;
|
size_t symbol_index = 0;
|
||||||
|
|
||||||
for (const auto& symbol : symbols) {
|
for (const auto& symbol : symbols) {
|
||||||
@ -35,23 +39,14 @@ public:
|
|||||||
const std::complex<double> target_symbol = symbolMap[symbol];
|
const std::complex<double> target_symbol = symbolMap[symbol];
|
||||||
|
|
||||||
for (size_t i = 0; i < samples_per_symbol; ++i) {
|
for (size_t i = 0; i < samples_per_symbol; ++i) {
|
||||||
baseband_I[symbol_index * samples_per_symbol + i] = target_symbol.real();
|
baseband_components[symbol_index * samples_per_symbol + i] = target_symbol;
|
||||||
baseband_Q[symbol_index * samples_per_symbol + i] = target_symbol.imag();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
symbol_index++;
|
symbol_index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter the I/Q phase components
|
// Filter the I/Q phase components
|
||||||
auto filter_taps = generateSRRCTaps(num_taps, sample_rate, SYMBOL_RATE, ROLLOFF_FACTOR);
|
std::vector<std::complex<double>> filtered_components = srrc_filter.applyFilter(baseband_components);
|
||||||
auto filtered_I = applyFilter(baseband_I, filter_taps);
|
|
||||||
auto filtered_Q = applyFilter(baseband_Q, filter_taps);
|
|
||||||
|
|
||||||
std::vector<std::complex<double>> baseband_components(filtered_I.size());
|
|
||||||
for (size_t i = 0; i < filtered_I.size(); i++) {
|
|
||||||
std::complex<double> baseband_component = {filtered_I[i], filtered_Q[i]};
|
|
||||||
baseband_components[i] = baseband_component;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine the I and Q components
|
// Combine the I and Q components
|
||||||
std::vector<double> passband_signal;
|
std::vector<double> passband_signal;
|
||||||
@ -81,14 +76,38 @@ public:
|
|||||||
return final_signal;
|
return final_signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> demodulate(const std::vector<int16_t> passband_signal) {
|
||||||
|
// Carrier recovery. initialize the Costas loop.
|
||||||
|
CostasLoop costas_loop(sample_rate, symbolMap);
|
||||||
|
|
||||||
|
// Convert passband signal to doubles.
|
||||||
|
std::vector<double> normalized_passband(passband_signal.size());
|
||||||
|
for (size_t i = 0; i < passband_signal.size(); i++) {
|
||||||
|
normalized_passband[i] = passband_signal[i] / 32767.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downmix passband to baseband
|
||||||
|
std::vector<std::complex<double>> baseband_IQ = costas_loop.process(normalized_passband);
|
||||||
|
|
||||||
|
// Phase detection and symbol formation
|
||||||
|
std::vector<uint8_t> baseband_symbols;
|
||||||
|
for (size_t i = 0; i < baseband_IQ.size(); i++) {
|
||||||
|
baseband_symbols.emplace_back(phase_detector.getSymbol(baseband_IQ[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseband_symbols;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const double sample_rate; ///< The sample rate of the system.
|
const double sample_rate; ///< The sample rate of the system.
|
||||||
const double gain; ///< The gain of the modulated signal.
|
const double gain; ///< The gain of the modulated signal.
|
||||||
size_t samples_per_symbol; ///< Number of samples per symbol, calculated to match symbol duration with cycle.
|
size_t samples_per_symbol; ///< Number of samples per symbol, calculated to match symbol duration with cycle.
|
||||||
const size_t num_taps;
|
PhaseDetector phase_detector;
|
||||||
|
SRRCFilter srrc_filter;
|
||||||
std::vector<std::complex<double>> symbolMap; ///< The mapping of tribit symbols to I/Q components.
|
std::vector<std::complex<double>> symbolMap; ///< The mapping of tribit symbols to I/Q components.
|
||||||
const bool is_frequency_hopping; ///< Whether to use frequency hopping methods. Not implemented (yet?)
|
const bool is_frequency_hopping; ///< Whether to use frequency hopping methods. Not implemented (yet?)
|
||||||
|
|
||||||
|
|
||||||
static inline double validateSampleRate(const double rate) {
|
static inline double validateSampleRate(const double rate) {
|
||||||
if (rate <= 2 * CARRIER_FREQ) {
|
if (rate <= 2 * CARRIER_FREQ) {
|
||||||
throw std::out_of_range("Sample rate must be above the Nyquist frequency (PSKModulator.h)");
|
throw std::out_of_range("Sample rate must be above the Nyquist frequency (PSKModulator.h)");
|
||||||
@ -108,73 +127,6 @@ private:
|
|||||||
{gain * std::cos(2.0*M_PI*(7.0/8.0)), gain * std::sin(2.0*M_PI*(7.0/8.0))} // 7 (111) corresponds to I = cos(315), Q = sin(315)
|
{gain * std::cos(2.0*M_PI*(7.0/8.0)), gain * std::sin(2.0*M_PI*(7.0/8.0))} // 7 (111) corresponds to I = cos(315), Q = sin(315)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<double> generateSRRCTaps(size_t num_taps,double sample_rate, double symbol_rate, double rolloff) const {
|
|
||||||
std::vector<double> freq_response(num_taps, 0.0);
|
|
||||||
std::vector<double> taps(num_taps);
|
|
||||||
|
|
||||||
double fn = symbol_rate / 2.0;
|
|
||||||
double f_step = sample_rate / num_taps;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < num_taps / 2; i++) {
|
|
||||||
double f = i * f_step;
|
|
||||||
|
|
||||||
if (f <= fn * (1 - rolloff)) {
|
|
||||||
freq_response[i] = 1.0;
|
|
||||||
} else if (f <= fn * (1 + rolloff)) {
|
|
||||||
freq_response[i] = 0.5 * (1 - std::sin(M_PI * (f - fn * (1 - rolloff)) / (2 * rolloff * fn)));
|
|
||||||
} else {
|
|
||||||
freq_response[i] = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (size_t i = num_taps / 2; i < num_taps; i++) {
|
|
||||||
freq_response[i] = freq_response[num_taps - i - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
fftw_complex* freq_domain = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * num_taps);
|
|
||||||
for (size_t i = 0; i < num_taps; i++) {
|
|
||||||
freq_domain[i][0] = freq_response[i];
|
|
||||||
freq_domain[i][1] = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<double> time_domain_taps(num_taps, 0.0);
|
|
||||||
|
|
||||||
fftw_plan plan = fftw_plan_dft_c2r_1d(num_taps, freq_domain, time_domain_taps.data(), FFTW_ESTIMATE);
|
|
||||||
fftw_execute(plan);
|
|
||||||
fftw_destroy_plan(plan);
|
|
||||||
fftw_free(freq_domain);
|
|
||||||
|
|
||||||
double norm_factor = std::sqrt(std::accumulate(time_domain_taps.begin(), time_domain_taps.end(), 0.0, [](double sum, double val) { return sum + val * val; }));
|
|
||||||
for (auto& tap : time_domain_taps) {
|
|
||||||
tap /= norm_factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return time_domain_taps;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<double> applyFilter(const std::vector<double>& signal, const std::vector<double>& filter_taps) const {
|
|
||||||
std::vector<double> filtered_signal(signal.size(), 0.0);
|
|
||||||
|
|
||||||
size_t filter_length = filter_taps.size();
|
|
||||||
size_t half_filter_length = filter_length / 2;
|
|
||||||
|
|
||||||
// Convolve the signal with the filter taps
|
|
||||||
for (size_t i = 0; i < signal.size(); ++i) {
|
|
||||||
double filtered_val = 0.0;
|
|
||||||
|
|
||||||
for (size_t j = 0; j < filter_length; ++j) {
|
|
||||||
size_t signal_index = i + j - half_filter_length;
|
|
||||||
if (signal_index < signal.size()) {
|
|
||||||
filtered_val += filter_taps[j] * signal[signal_index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered_signal[i] = filtered_val;
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered_signal;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
108
include/utils/costasloop.h
Normal file
108
include/utils/costasloop.h
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#include <complex>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "filters.h"
|
||||||
|
|
||||||
|
class PhaseDetector {
|
||||||
|
public:
|
||||||
|
PhaseDetector(const std::vector<std::complex<double>>& _symbolMap) : symbolMap(_symbolMap) {}
|
||||||
|
|
||||||
|
uint8_t getSymbol(const std::complex<double>& input) {
|
||||||
|
double phase = std::atan2(input.imag(), input.real());
|
||||||
|
return symbolFromPhase(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::complex<double>> symbolMap;
|
||||||
|
|
||||||
|
uint8_t symbolFromPhase(const double phase) {
|
||||||
|
// Calculate the closest symbol based on phase difference
|
||||||
|
double min_distance = 2 * M_PI; // Maximum possible phase difference
|
||||||
|
uint8_t closest_symbol = 0;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < symbolMap.size(); ++i) {
|
||||||
|
double symbol_phase = std::atan2(symbolMap[i].imag(), symbolMap[i].real());
|
||||||
|
double distance = std::abs(symbol_phase - phase);
|
||||||
|
|
||||||
|
if (distance < min_distance) {
|
||||||
|
min_distance = distance;
|
||||||
|
closest_symbol = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest_symbol;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CostasLoop {
|
||||||
|
public:
|
||||||
|
CostasLoop(const double _sample_rate, const std::vector<std::complex<double>>& _symbolMap)
|
||||||
|
: sample_rate(_sample_rate), k_factor(-5 / _sample_rate),
|
||||||
|
prev_in_iir(0), prev_out_iir(0), prev_in_vco(0), feedback(1.0, 0.0),
|
||||||
|
error_total(0), out_iir_total(0), in_vco_total(0),
|
||||||
|
srrc_filter(SRRCFilter(48, _sample_rate, 2400, 0.35)) {}
|
||||||
|
|
||||||
|
std::vector<std::complex<double>> process(const std::vector<double>& input_signal) {
|
||||||
|
std::vector<std::complex<double>> output_signal(input_signal.size());
|
||||||
|
double current_phase = 0.0;
|
||||||
|
|
||||||
|
error_total = 0;
|
||||||
|
out_iir_total = 0;
|
||||||
|
in_vco_total = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < input_signal.size(); ++i) {
|
||||||
|
// Multiply input by feedback signal
|
||||||
|
std::complex<double> multiplied = input_signal[i] * feedback;
|
||||||
|
|
||||||
|
// Filter signal
|
||||||
|
std::complex<double> filtered = srrc_filter.filterSample(multiplied);
|
||||||
|
|
||||||
|
// Output best-guess corrected I/Q components
|
||||||
|
output_signal[i] = filtered;
|
||||||
|
|
||||||
|
// Generate limited components
|
||||||
|
std::complex<double> limited = limiter(filtered);
|
||||||
|
|
||||||
|
// IIR Filter
|
||||||
|
double in_iir = std::asin(std::clamp(multiplied.imag() * limited.real() - multiplied.real() * limited.imag(), -1.0, 1.0));
|
||||||
|
error_total += in_iir;
|
||||||
|
|
||||||
|
double out_iir = 1.0001 * in_iir - prev_in_iir + prev_out_iir;
|
||||||
|
prev_in_iir = in_iir;
|
||||||
|
prev_out_iir = out_iir;
|
||||||
|
out_iir_total += out_iir;
|
||||||
|
|
||||||
|
// VCO Block
|
||||||
|
double in_vco = out_iir + prev_in_vco;
|
||||||
|
in_vco_total += in_vco;
|
||||||
|
prev_in_vco = in_vco;
|
||||||
|
|
||||||
|
// Generate feedback signal for next iteration
|
||||||
|
double feedback_real = std::cos(k_factor * in_vco);
|
||||||
|
double feedback_imag = -std::sin(k_factor * in_vco);
|
||||||
|
feedback = std::complex<double>(feedback_real, feedback_imag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output_signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
double sample_rate;
|
||||||
|
double k_factor;
|
||||||
|
double prev_in_iir;
|
||||||
|
double prev_out_iir;
|
||||||
|
double prev_in_vco;
|
||||||
|
std::complex<double> feedback;
|
||||||
|
double error_total;
|
||||||
|
double out_iir_total;
|
||||||
|
double in_vco_total;
|
||||||
|
SRRCFilter srrc_filter;
|
||||||
|
|
||||||
|
std::complex<double> limiter(const std::complex<double>& sample) const {
|
||||||
|
double limited_I = std::clamp(sample.real(), -1.0, 1.0);
|
||||||
|
double limited_Q = std::clamp(sample.imag(), -1.0, 1.0);
|
||||||
|
return std::complex<double>(limited_I, limited_Q);
|
||||||
|
}
|
||||||
|
};
|
167
include/utils/filters.h
Normal file
167
include/utils/filters.h
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
#ifndef FILTERS_H
|
||||||
|
#define FILTERS_H
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fftw3.h>
|
||||||
|
#include <numeric>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class TapGenerators {
|
||||||
|
public:
|
||||||
|
std::vector<double> generateSRRCTaps(const size_t num_taps, const double sample_rate, const double symbol_rate, const double rolloff) const {
|
||||||
|
std::vector<double> freq_response(num_taps, 0.0);
|
||||||
|
std::vector<double> taps(num_taps);
|
||||||
|
|
||||||
|
double fn = symbol_rate / 2.0;
|
||||||
|
double f_step = sample_rate / num_taps;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < num_taps / 2; i++) {
|
||||||
|
double f = i * f_step;
|
||||||
|
|
||||||
|
if (f <= fn * (1 - rolloff)) {
|
||||||
|
freq_response[i] = 1.0;
|
||||||
|
} else if (f <= fn * (1 + rolloff)) {
|
||||||
|
freq_response[i] = 0.5 * (1 - std::sin(M_PI * (f - fn * (1 - rolloff)) / (2 * rolloff * fn)));
|
||||||
|
} else {
|
||||||
|
freq_response[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = num_taps / 2; i < num_taps; i++) {
|
||||||
|
freq_response[i] = freq_response[num_taps - i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
fftw_complex* freq_domain = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * num_taps);
|
||||||
|
for (size_t i = 0; i < num_taps; i++) {
|
||||||
|
freq_domain[i][0] = freq_response[i];
|
||||||
|
freq_domain[i][1] = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> time_domain_taps(num_taps, 0.0);
|
||||||
|
|
||||||
|
fftw_plan plan = fftw_plan_dft_c2r_1d(num_taps, freq_domain, time_domain_taps.data(), FFTW_ESTIMATE);
|
||||||
|
fftw_execute(plan);
|
||||||
|
fftw_destroy_plan(plan);
|
||||||
|
fftw_free(freq_domain);
|
||||||
|
|
||||||
|
double norm_factor = std::sqrt(std::accumulate(time_domain_taps.begin(), time_domain_taps.end(), 0.0, [](double sum, double val) { return sum + val * val; }));
|
||||||
|
for (auto& tap : time_domain_taps) {
|
||||||
|
tap /= norm_factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return time_domain_taps;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> generateLowpassTaps(const size_t num_taps, const double cutoff_freq, const double sample_rate) const {
|
||||||
|
std::vector<double> freq_response(num_taps, 0.0);
|
||||||
|
std::vector<double> taps(num_taps);
|
||||||
|
|
||||||
|
double fn = cutoff_freq / 2.0;
|
||||||
|
double f_step = sample_rate / num_taps;
|
||||||
|
|
||||||
|
// Define frequency response
|
||||||
|
for (size_t i = 0; i < num_taps / 2; i++) {
|
||||||
|
double f = i * f_step;
|
||||||
|
if (f <= fn) {
|
||||||
|
freq_response[i] = 1.0; // Passband
|
||||||
|
} else {
|
||||||
|
freq_response[i] = 0.0; // Stopband
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror the second half of the response for symmetry
|
||||||
|
for (size_t i = num_taps / 2; i < num_taps; i++) {
|
||||||
|
freq_response[i] = freq_response[num_taps - i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform inverse FFT to get time-domain taps
|
||||||
|
fftw_complex* freq_domain = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * num_taps);
|
||||||
|
for (size_t i = 0; i < num_taps; i++) {
|
||||||
|
freq_domain[i][0] = freq_response[i];
|
||||||
|
freq_domain[i][1] = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> time_domain_taps(num_taps, 0.0);
|
||||||
|
fftw_plan plan = fftw_plan_dft_c2r_1d(num_taps, freq_domain, time_domain_taps.data(), FFTW_ESTIMATE);
|
||||||
|
fftw_execute(plan);
|
||||||
|
fftw_destroy_plan(plan);
|
||||||
|
fftw_free(freq_domain);
|
||||||
|
|
||||||
|
// Normalize filter taps
|
||||||
|
double norm_factor = std::sqrt(std::accumulate(time_domain_taps.begin(), time_domain_taps.end(), 0.0, [](double sum, double val) { return sum + val * val; }));
|
||||||
|
for (auto& tap : time_domain_taps) {
|
||||||
|
tap /= norm_factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return time_domain_taps;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Filter {
|
||||||
|
public:
|
||||||
|
Filter(const std::vector<double>& _filter_taps) : filter_taps(_filter_taps), buffer(_filter_taps.size(), 0.0), buffer_index(0) {}
|
||||||
|
|
||||||
|
double filterSample(const double sample) {
|
||||||
|
buffer[buffer_index] = std::complex<double>(sample,0.0);
|
||||||
|
double filtered_val = 0.0;
|
||||||
|
for (size_t j = 0; j < filter_taps.size(); j++) {
|
||||||
|
size_t signal_index = (buffer_index + j) % filter_taps.size();
|
||||||
|
filtered_val += filter_taps[j] * buffer[signal_index].real();
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_index = (buffer_index + 1) % filter_taps.size();
|
||||||
|
return filtered_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::complex<double> filterSample(const std::complex<double>& sample) {
|
||||||
|
buffer[buffer_index] = sample;
|
||||||
|
std::complex<double> filtered_val = std::complex<double>(0.0, 0.0);
|
||||||
|
for (size_t j = 0; j < filter_taps.size(); j++) {
|
||||||
|
size_t signal_index = (buffer_index + j) % filter_taps.size();
|
||||||
|
filtered_val += filter_taps[j] * buffer[signal_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_index = (buffer_index + 1) % filter_taps.size();
|
||||||
|
return filtered_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> applyFilter(const std::vector<double>& signal) {
|
||||||
|
std::vector<double> filtered_signal(signal.size(), 0.0);
|
||||||
|
|
||||||
|
// Convolve the signal with the filter taps
|
||||||
|
for (size_t i = 0; i < signal.size(); ++i) {
|
||||||
|
filtered_signal[i] = filterSample(signal[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered_signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::complex<double>> applyFilter(const std::vector<std::complex<double>>& signal) {
|
||||||
|
std::vector<std::complex<double>> filtered_signal(signal.size(), std::complex<double>(0.0, 0.0));
|
||||||
|
|
||||||
|
// Convolve the signal with the filter taps
|
||||||
|
for (size_t i = 0; i < signal.size(); ++i) {
|
||||||
|
filtered_signal[i] = filterSample(signal[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered_signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<double> filter_taps;
|
||||||
|
std::vector<std::complex<double>> buffer;
|
||||||
|
size_t buffer_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SRRCFilter : public Filter {
|
||||||
|
public:
|
||||||
|
SRRCFilter(const size_t num_taps, const double sample_rate, const double symbol_rate, const double rolloff) : Filter(TapGenerators().generateSRRCTaps(num_taps, sample_rate, symbol_rate, rolloff)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class LowPassFilter : public Filter {
|
||||||
|
public:
|
||||||
|
LowPassFilter(const size_t num_taps, const double cutoff_freq, const double sample_rate) : Filter(TapGenerators().generateLowpassTaps(num_taps, cutoff_freq, sample_rate)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* FILTERS_H */
|
@ -0,0 +1,19 @@
|
|||||||
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||||
|
|
||||||
|
# Find the installed gtest package
|
||||||
|
find_package(GTest REQUIRED)
|
||||||
|
find_package(SndFile REQUIRED)
|
||||||
|
find_package(FFTW3 REQUIRED)
|
||||||
|
|
||||||
|
# Add test executable
|
||||||
|
add_executable(PSKModulatorTest PSKModulatorTests.cpp)
|
||||||
|
|
||||||
|
# Link the test executable with the GTest libraries
|
||||||
|
target_link_libraries(PSKModulatorTest GTest::GTest GTest::Main FFTW3::fftw3 SndFile::sndfile)
|
||||||
|
|
||||||
|
# Enable C++17 standard
|
||||||
|
set_target_properties(PSKModulatorTest PROPERTIES CXX_STANDARD 17)
|
||||||
|
|
||||||
|
# Add test cases
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(PSKModulatorTest)
|
@ -1,23 +0,0 @@
|
|||||||
#include "gtest/gtest.h"
|
|
||||||
#include "FSKModulator.h"
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
TEST(FSKModulatorTest, SignalLength) {
|
|
||||||
using namespace milstd;
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
FSKModulator modulator(FSKModulator::ShiftType::NarrowShift, 75.0, 8000.0);
|
|
||||||
|
|
||||||
// Input data bits
|
|
||||||
std::vector<uint8_t> dataBits = {1, 0, 1, 1, 0};
|
|
||||||
|
|
||||||
// Modulate the data
|
|
||||||
std::vector<double> signal = modulator.modulate(dataBits);
|
|
||||||
|
|
||||||
// Calculate expected number of samples
|
|
||||||
size_t samplesPerSymbol = static_cast<size_t>(modulator.getSampleRate() * modulator.getSymbolDuration());
|
|
||||||
size_t expectedSamples = dataBits.size() * samplesPerSymbol;
|
|
||||||
|
|
||||||
// Verify signal length
|
|
||||||
EXPECT_EQ(signal.size(), expectedSamples);
|
|
||||||
}
|
|
41
tests/PSKModulatorTests.cpp
Normal file
41
tests/PSKModulatorTests.cpp
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "modulation/PSKModulator.h"
|
||||||
|
|
||||||
|
// Fixture for PSK Modulator tests
|
||||||
|
class PSKModulatorTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
double sample_rate = 48000;
|
||||||
|
size_t num_taps = 48;
|
||||||
|
bool is_frequency_hopping = false;
|
||||||
|
PSKModulator modulator{sample_rate, is_frequency_hopping, num_taps};
|
||||||
|
|
||||||
|
std::vector<uint8_t> symbols = {0, 3, 5, 7};
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(PSKModulatorTest, ModulationOutputLength) {
|
||||||
|
auto signal = modulator.modulate(symbols);
|
||||||
|
|
||||||
|
size_t expected_length = symbols.size() * (sample_rate / SYMBOL_RATE);
|
||||||
|
ASSERT_EQ(signal.size(), expected_length);
|
||||||
|
|
||||||
|
for (auto& sample : signal) {
|
||||||
|
EXPECT_GE(sample, -32768);
|
||||||
|
EXPECT_LE(sample, 32767);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PSKModulatorTest, DemodulationOutput) {
|
||||||
|
auto passband_signal = modulator.modulate(symbols);
|
||||||
|
auto decoded_symbols = modulator.demodulate(passband_signal);
|
||||||
|
|
||||||
|
ASSERT_EQ(symbols.size(), decoded_symbols.size());
|
||||||
|
for (size_t i = 0; i < symbols.size(); i++) {
|
||||||
|
EXPECT_EQ(symbols[i], decoded_symbols[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PSKModulatorTest, InvalidSymbolInput) {
|
||||||
|
std::vector<uint8_t> invalid_symbols = {0, 8, 9};
|
||||||
|
|
||||||
|
EXPECT_THROW(modulator.modulate(invalid_symbols), std::out_of_range);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user