142 lines
6.2 KiB
C++
142 lines
6.2 KiB
C++
#ifndef PSK_MODULATOR_H
|
|
#define PSK_MODULATOR_H
|
|
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <complex>
|
|
#include <cstdint>
|
|
#include <numeric>
|
|
#include <stdexcept>
|
|
#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;
|
|
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)), srrc_filter(48, _sample_rate, SYMBOL_RATE, ROLLOFF_FACTOR) {
|
|
initializeSymbolMap();
|
|
phase_detector = PhaseDetector(symbolMap);
|
|
}
|
|
|
|
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) {
|
|
if (symbol >= symbolMap.size()) {
|
|
throw std::out_of_range("Invalid symbol value for 8-PSK modulation. Symbol must be between 0 and 7.");
|
|
}
|
|
const std::complex<double> target_symbol = symbolMap[symbol];
|
|
|
|
for (size_t i = 0; i < samples_per_symbol; ++i) {
|
|
baseband_components[symbol_index * samples_per_symbol + i] = target_symbol;
|
|
}
|
|
|
|
symbol_index++;
|
|
}
|
|
|
|
// Filter the I/Q phase components
|
|
std::vector<std::complex<double>> filtered_components = srrc_filter.applyFilter(baseband_components);
|
|
|
|
// Combine the I and Q components
|
|
std::vector<double> passband_signal;
|
|
passband_signal.reserve(baseband_components.size());
|
|
|
|
double carrier_phase = 0.0;
|
|
double carrier_phase_increment = 2 * M_PI * CARRIER_FREQ / sample_rate;
|
|
for (const auto& sample : baseband_components) {
|
|
double carrier_cos = std::cos(carrier_phase);
|
|
double carrier_sin = -std::sin(carrier_phase);
|
|
double passband_value = sample.real() * carrier_cos + sample.imag() * carrier_sin;
|
|
passband_signal.emplace_back(passband_value * 32767.0); // Scale to int16_t
|
|
carrier_phase += carrier_phase_increment;
|
|
if (carrier_phase >= 2 * M_PI)
|
|
carrier_phase -= 2 * M_PI;
|
|
}
|
|
|
|
std::vector<int16_t> final_signal;
|
|
final_signal.reserve(passband_signal.size());
|
|
|
|
for (const auto& sample : passband_signal) {
|
|
int16_t value = static_cast<int16_t>(sample);
|
|
value = std::clamp(value, (int16_t)-32768, (int16_t)32767);
|
|
final_signal.emplace_back(value);
|
|
}
|
|
|
|
return final_signal;
|
|
}
|
|
|
|
std::vector<uint8_t> demodulate(const std::vector<int16_t> passband_signal) {
|
|
// Carrier recovery. initialize the Costas loop.
|
|
CostasLoop costas_loop(CARRIER_FREQ, sample_rate, symbolMap, 5.0);
|
|
|
|
// 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;
|
|
size_t samples_per_symbol = sample_rate / SYMBOL_RATE;
|
|
|
|
for (size_t i = 0; i < baseband_IQ.size(); i += samples_per_symbol) {
|
|
std::complex<double> symbol_avg(0.0, 0.0);
|
|
for (size_t j = 0; j < samples_per_symbol; ++j) {
|
|
symbol_avg += baseband_IQ[i + j];
|
|
}
|
|
symbol_avg /= static_cast<double>(samples_per_symbol);
|
|
|
|
// Detect symbol from averaged signal
|
|
baseband_symbols.emplace_back(phase_detector.getSymbol(symbol_avg));
|
|
}
|
|
|
|
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.
|
|
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) {
|
|
throw std::out_of_range("Sample rate must be above the Nyquist frequency (PSKModulator.h)");
|
|
}
|
|
return rate;
|
|
}
|
|
|
|
inline void initializeSymbolMap() {
|
|
symbolMap = {
|
|
{gain * std::cos(2.0*M_PI*(0.0/8.0)), gain * std::sin(2.0*M_PI*(0.0/8.0))}, // 0 (000) corresponds to I = 1.0, Q = 0.0
|
|
{gain * std::cos(2.0*M_PI*(1.0/8.0)), gain * std::sin(2.0*M_PI*(1.0/8.0))}, // 1 (001) corresponds to I = cos(45), Q = sin(45)
|
|
{gain * std::cos(2.0*M_PI*(2.0/8.0)), gain * std::sin(2.0*M_PI*(2.0/8.0))}, // 2 (010) corresponds to I = 0.0, Q = 1.0
|
|
{gain * std::cos(2.0*M_PI*(3.0/8.0)), gain * std::sin(2.0*M_PI*(3.0/8.0))}, // 3 (011) corresponds to I = cos(135), Q = sin(135)
|
|
{gain * std::cos(2.0*M_PI*(4.0/8.0)), gain * std::sin(2.0*M_PI*(4.0/8.0))}, // 4 (100) corresponds to I = -1.0, Q = 0.0
|
|
{gain * std::cos(2.0*M_PI*(5.0/8.0)), gain * std::sin(2.0*M_PI*(5.0/8.0))}, // 5 (101) corresponds to I = cos(225), Q = sin(225)
|
|
{gain * std::cos(2.0*M_PI*(6.0/8.0)), gain * std::sin(2.0*M_PI*(6.0/8.0))}, // 6 (110) corresponds to I = 0.0, Q = -1.0
|
|
{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)
|
|
};
|
|
}
|
|
};
|
|
|
|
#endif |