#ifndef PSK_MODULATOR_H #define PSK_MODULATOR_H #include #include #include #include #include #include #include #include #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(sample_rate / SYMBOL_RATE)), srrc_filter(48, _sample_rate, SYMBOL_RATE, ROLLOFF_FACTOR) { initializeSymbolMap(); phase_detector = PhaseDetector(symbolMap); } std::vector modulate(const std::vector& symbols) { std::vector baseband_I(symbols.size() * samples_per_symbol); std::vector baseband_Q(symbols.size() * samples_per_symbol); std::vector> 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 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> filtered_components = srrc_filter.applyFilter(baseband_components); // Combine the I and Q components std::vector 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 final_signal; final_signal.reserve(passband_signal.size()); for (const auto& sample : passband_signal) { int16_t value = static_cast(sample); value = std::clamp(value, (int16_t)-32768, (int16_t)32767); final_signal.emplace_back(value); } return final_signal; } std::vector demodulate(const std::vector 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 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> baseband_IQ = costas_loop.process(normalized_passband); // Phase detection and symbol formation std::vector 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 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(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> 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