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)
|
||||
|
||||
# Add subdirectories for organization
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
|
||||
# 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 <fftw3.h>
|
||||
|
||||
#include "costasloop.h"
|
||||
#include "filters.h"
|
||||
|
||||
static constexpr double CARRIER_FREQ = 1800.0;
|
||||
static constexpr size_t SYMBOL_RATE = 2400;
|
||||
static constexpr double ROLLOFF_FACTOR = 0.35;
|
||||
@ -19,13 +22,14 @@ static constexpr double SCALE_FACTOR = 32767.0;
|
||||
class PSKModulator {
|
||||
public:
|
||||
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();
|
||||
}
|
||||
|
||||
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_Q(symbols.size() * samples_per_symbol);
|
||||
std::vector<std::complex<double>> baseband_components(symbols.size() * samples_per_symbol);
|
||||
size_t symbol_index = 0;
|
||||
|
||||
for (const auto& symbol : symbols) {
|
||||
@ -35,23 +39,14 @@ public:
|
||||
const std::complex<double> target_symbol = symbolMap[symbol];
|
||||
|
||||
for (size_t i = 0; i < samples_per_symbol; ++i) {
|
||||
baseband_I[symbol_index * samples_per_symbol + i] = target_symbol.real();
|
||||
baseband_Q[symbol_index * samples_per_symbol + i] = target_symbol.imag();
|
||||
baseband_components[symbol_index * samples_per_symbol + i] = target_symbol;
|
||||
}
|
||||
|
||||
symbol_index++;
|
||||
}
|
||||
|
||||
// Filter the I/Q phase components
|
||||
auto filter_taps = generateSRRCTaps(num_taps, sample_rate, SYMBOL_RATE, ROLLOFF_FACTOR);
|
||||
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;
|
||||
}
|
||||
std::vector<std::complex<double>> filtered_components = srrc_filter.applyFilter(baseband_components);
|
||||
|
||||
// Combine the I and Q components
|
||||
std::vector<double> passband_signal;
|
||||
@ -81,13 +76,37 @@ public:
|
||||
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:
|
||||
const double sample_rate; ///< The sample rate of the system.
|
||||
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.
|
||||
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.
|
||||
const bool is_frequency_hopping; ///< Whether to use frequency hopping methods. Not implemented (yet?)
|
||||
|
||||
|
||||
static inline double validateSampleRate(const double rate) {
|
||||
if (rate <= 2 * CARRIER_FREQ) {
|
||||
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
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
|
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…
Reference in New Issue
Block a user