From 628a25dc240c1762e2a936685ebdca33d7aca5f7 Mon Sep 17 00:00:00 2001 From: RecklessAndFeckless Date: Thu, 10 Oct 2024 01:48:59 -0400 Subject: [PATCH] It's starting to sound right --- include/encoder/ModemController.h | 30 +------- include/modulation/PSKModulator.h | 118 +++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 47 deletions(-) diff --git a/include/encoder/ModemController.h b/include/encoder/ModemController.h index acdfb01..2e070ca 100644 --- a/include/encoder/ModemController.h +++ b/include/encoder/ModemController.h @@ -51,7 +51,7 @@ public: interleaver(baud_rate, interleave_setting, is_frequency_hopping), input_data(std::move(_data)), mgd_decoder(baud_rate, is_frequency_hopping), - modulator(48000) {} + modulator(baud_rate, 48000, 0.5, is_frequency_hopping) {} /** * @brief Transmits the input data by processing it through different phases like FEC encoding, interleaving, symbol formation, scrambling, and modulation. @@ -80,10 +80,7 @@ public: std::vector modulated_signal = modulator.modulate(symbol_stream); - // Step 7: Apply Sqrt Half Cosine Filter with a rolloff factor of 0.3 (30%) - std::vector filtered_signal = SqrtHalfCosine(modulated_signal, 0.3); - - return filtered_signal; + return modulated_signal; } private: @@ -92,6 +89,7 @@ private: bool is_frequency_hopping; ///< Indicates if frequency hopping is used. BitStream input_data; ///< The input data stream. size_t interleave_setting; ///< The interleave setting to be used. + size_t sample_rate; SymbolFormation symbol_formation; ///< Symbol formation instance to form symbols from data. Scrambler scrambler; ///< Scrambler instance for scrambling the data. @@ -128,28 +126,6 @@ private: return eom_data; } - /** - * @brief Applies a square-root raised cosine filter to the input signal. - * @param input_signal The input modulated signal. - * @param rolloff_factor The rolloff factor of the filter. - * @return The filtered signal. - */ - [[nodiscard]] std::vector SqrtHalfCosine(const std::vector& input_signal, double rolloff_factor) const { - std::vector filtered_signal(input_signal.size()); - const double pi = M_PI; - const double normalization_factor = 1.0 / (1.0 + rolloff_factor); - - for (size_t i = 0; i < input_signal.size(); ++i) { - double t = static_cast(i) / input_signal.size(); - double cosine_term = std::cos(pi * t * rolloff_factor); - double filtered_value = input_signal[i] * normalization_factor * (0.5 + 0.5 * cosine_term); - - filtered_signal[i] = clamp(static_cast(filtered_value)); - } - - return filtered_signal; - } - std::vector splitTribitSymbols(const BitStream& input_data) { std::vector return_vector; size_t max_index = input_data.getMaxBitIndex(); diff --git a/include/modulation/PSKModulator.h b/include/modulation/PSKModulator.h index 5299dbd..cbb4e90 100644 --- a/include/modulation/PSKModulator.h +++ b/include/modulation/PSKModulator.h @@ -5,52 +5,130 @@ #include #include #include +#include +#include class PSKModulator { public: - PSKModulator(double sample_rate) : sample_rate(sample_rate), carrier_freq(1800), phase(0.0) { + PSKModulator(double baud_rate, double sample_rate, double energy_per_bit, bool is_frequency_hopping) + : sample_rate(sample_rate), carrier_freq(1800), phase(0.0) { initializeSymbolMap(); + symbol_rate = 2400; // Fixed symbol rate as per specification (2400 symbols per second) + samples_per_symbol = static_cast(sample_rate / symbol_rate); } std::vector modulate(const std::vector& symbols) { - std::vector modulated_signal; + std::vector> modulated_signal; const double phase_increment = 2 * M_PI * carrier_freq / sample_rate; for (auto symbol : symbols) { if (symbol >= symbolMap.size()) { throw std::out_of_range("Invalid symbol value for 8-PSK modulation"); } - double target_phase = symbolMap[symbol]; + std::complex target_symbol = symbolMap[symbol]; for (size_t i = 0; i < samples_per_symbol; ++i) { - double value = std::sin(phase + target_phase); - modulated_signal.push_back(static_cast(value * std::numeric_limits::max())); - phase += phase_increment; - if (phase >= 2 * M_PI) { - phase -= 2 * M_PI; - } + double in_phase = std::cos(phase + target_symbol.real()); + double quadrature = std::sin(phase + target_symbol.imag()); + modulated_signal.emplace_back(in_phase, quadrature); + phase = std::fmod(phase + phase_increment, 2 * M_PI); } } - return modulated_signal; + + // Apply raised-cosine filter + auto filter_taps = sqrtRaisedCosineFilter(201, symbol_rate); // Adjusted number of filter taps to 201 for balance + auto filtered_signal = applyFilter(modulated_signal, filter_taps); + + // Normalize the filtered signal + double max_value = 0.0; + for (const auto& sample : filtered_signal) { + max_value = std::max(max_value, std::abs(sample.real())); + max_value = std::max(max_value, std::abs(sample.imag())); + } + double gain = (max_value > 0) ? (32767.0 / max_value) : 1.0; + + // Combine the I and Q components and apply gain for audio output + std::vector combined_signal; + for (auto& sample : filtered_signal) { + int16_t combined_sample = static_cast(std::clamp(gain * (sample.real() + sample.imag()), -32768.0, 32767.0)); + combined_signal.push_back(combined_sample); + } + + return combined_signal; + } + + std::vector sqrtRaisedCosineFilter(size_t num_taps, double symbol_rate) { + double rolloff = 0.35; // Fixed rolloff factor as per specification + std::vector filter_taps(num_taps); + double norm_factor = 0.0; + double sampling_interval = 1.0 / sample_rate; + double symbol_duration = 1.0 / symbol_rate; + double half_num_taps = static_cast(num_taps - 1) / 2.0; + + for (size_t i = 0; i < num_taps; ++i) { + double t = (i - half_num_taps) * sampling_interval; + if (std::abs(t) < 1e-10) { + filter_taps[i] = 1.0; + } else { + double numerator = std::sin(M_PI * t / symbol_duration * (1.0 - rolloff)) + + 4.0 * rolloff * t / symbol_duration * std::cos(M_PI * t / symbol_duration * (1.0 + rolloff)); + double denominator = M_PI * t * (1.0 - std::pow(4.0 * rolloff * t / symbol_duration, 2)); + filter_taps[i] = numerator / denominator; + } + norm_factor += filter_taps[i] * filter_taps[i]; + } + + norm_factor = std::sqrt(norm_factor); + std::for_each(filter_taps.begin(), filter_taps.end(), [&norm_factor](double &tap) { tap /= norm_factor; }); + return filter_taps; + } + + std::vector> applyFilter(const std::vector>& signal, const std::vector& filter_taps) { + std::vector> filtered_signal(signal.size()); + + 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_i = 0.0; + double filtered_q = 0.0; + + for (size_t j = 0; j < filter_length; ++j) { + if (i >= j) { + filtered_i += filter_taps[j] * signal[i - j].real(); + filtered_q += filter_taps[j] * signal[i - j].imag(); + } else { + // Handle edge case by zero-padding + filtered_i += filter_taps[j] * 0.0; + filtered_q += filter_taps[j] * 0.0; + } + } + + filtered_signal[i] = std::complex(filtered_i, filtered_q); + } + + return filtered_signal; } private: double sample_rate; ///< The sample rate of the system. double carrier_freq; ///< The frequency of the carrier, set to 1800 Hz as per standard. double phase; ///< Current phase of the carrier waveform. - size_t samples_per_symbol = 40; ///< Number of samples per symbol, calculated to match symbol duration with cycle. - std::vector symbolMap; ///< The mapping of tribit symbols to phase shifts. + size_t samples_per_symbol; ///< Number of samples per symbol, calculated to match symbol duration with cycle. + size_t symbol_rate; + std::vector> symbolMap; ///< The mapping of tribit symbols to I/Q components. void initializeSymbolMap() { symbolMap = { - 0.0, // 0 (000) corresponds to 0 degrees - M_PI / 4, // 1 (001) corresponds to 45 degrees - M_PI / 2, // 2 (010) corresponds to 90 degrees - 3 * M_PI / 4, // 3 (011) corresponds to 135 degrees - M_PI, // 4 (100) corresponds to 180 degrees - 5 * M_PI / 4, // 5 (101) corresponds to 225 degrees - 3 * M_PI / 2, // 6 (110) corresponds to 270 degrees - 7 * M_PI / 4 // 7 (111) corresponds to 315 degrees + {1.0, 0.0}, // 0 (000) corresponds to I = 1.0, Q = 0.0 + {std::sqrt(2.0) / 2.0, std::sqrt(2.0) / 2.0}, // 1 (001) corresponds to I = cos(45), Q = sin(45) + {0.0, 1.0}, // 2 (010) corresponds to I = 0.0, Q = 1.0 + {-std::sqrt(2.0) / 2.0, std::sqrt(2.0) / 2.0}, // 3 (011) corresponds to I = cos(135), Q = sin(135) + {-1.0, 0.0}, // 4 (100) corresponds to I = -1.0, Q = 0.0 + {-std::sqrt(2.0) / 2.0, -std::sqrt(2.0) / 2.0}, // 5 (101) corresponds to I = cos(225), Q = sin(225) + {0.0, -1.0}, // 6 (110) corresponds to I = 0.0, Q = -1.0 + {std::sqrt(2.0) / 2.0, -std::sqrt(2.0) / 2.0} // 7 (111) corresponds to I = cos(315), Q = sin(315) }; } };