diff --git a/CMakeLists.txt b/CMakeLists.txt index f7006d0..e840fcb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ include_directories(include/modulation) include_directories(include/utils) # Add subdirectories for organization +enable_testing() add_subdirectory(tests) # Set source files diff --git a/include/modulation/FSKDemodulator.h b/include/modulation/FSKDemodulator.h deleted file mode 100644 index 3611003..0000000 --- a/include/modulation/FSKDemodulator.h +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef FSK_DEMODULATOR_H -#define FSK_DEMODULATOR_H - -#include -#include -#include -#include -#include - -#include "BitStreamWriter.h" - -class FSKDemodulatorConfig { - public: - int freq_lo; - int freq_hi; - int sample_rate; - int baud_rate; - std::shared_ptr 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& 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 bit_writer; - - int filter_size; - std::vector filter_lo_i; - std::vector filter_lo_q; - std::vector filter_hi_i; - std::vector filter_hi_q; - std::vector 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(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 */ \ No newline at end of file diff --git a/include/modulation/FSKModulator.h b/include/modulation/FSKModulator.h deleted file mode 100644 index 6ed2648..0000000 --- a/include/modulation/FSKModulator.h +++ /dev/null @@ -1,87 +0,0 @@ -#ifndef FSK_MODULATOR_H -#define FSK_MODULATOR_H - -#include -#include -#include -#include -#include - -#include "BitStreamReader.h" - -class FSKModulatorConfig { - public: - int freq_lo; - int freq_hi; - int sample_rate; - int baud_rate; - std::shared_ptr 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(baud_rate) / sample_rate; - phase = 0.0; - baud_frac = 0.0; - current_bit = 0; - } - - std::vector modulate(unsigned int num_samples) { - std::vector 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(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 bit_reader; - - // state variables - double phase; - double baud_frac; - double baud_incr; - std::array omega; - int current_bit; - }; - -} // namespace milstd - -#endif /* FSK_MODULATOR_H */ \ No newline at end of file diff --git a/include/modulation/PSKDemodulator.h b/include/modulation/PSKDemodulator.h deleted file mode 100644 index e69de29..0000000 diff --git a/include/modulation/PSKModulator.h b/include/modulation/PSKModulator.h index 74c717e..9ce3cf1 100644 --- a/include/modulation/PSKModulator.h +++ b/include/modulation/PSKModulator.h @@ -11,6 +11,9 @@ #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; @@ -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(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(sample_rate / SYMBOL_RATE)), phase_detector(symbolMap), srrc_filter(48, _sample_rate, SYMBOL_RATE, ROLLOFF_FACTOR) { initializeSymbolMap(); } - std::vector modulate(const std::vector& symbols) const { + 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) { @@ -35,23 +39,14 @@ public: const std::complex 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> baseband_components(filtered_I.size()); - for (size_t i = 0; i < filtered_I.size(); i++) { - std::complex baseband_component = {filtered_I[i], filtered_Q[i]}; - baseband_components[i] = baseband_component; - } + std::vector> filtered_components = srrc_filter.applyFilter(baseband_components); // Combine the I and Q components std::vector passband_signal; @@ -81,13 +76,37 @@ public: return final_signal; } + std::vector demodulate(const std::vector passband_signal) { + // Carrier recovery. initialize the Costas loop. + CostasLoop costas_loop(sample_rate, symbolMap); + + // 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; + 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> 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 generateSRRCTaps(size_t num_taps,double sample_rate, double symbol_rate, double rolloff) const { - std::vector freq_response(num_taps, 0.0); - std::vector 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 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 applyFilter(const std::vector& signal, const std::vector& filter_taps) const { - std::vector 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 \ No newline at end of file diff --git a/include/utils/costasloop.h b/include/utils/costasloop.h new file mode 100644 index 0000000..3be7334 --- /dev/null +++ b/include/utils/costasloop.h @@ -0,0 +1,108 @@ +#include +#include +#include +#include + +#include "filters.h" + +class PhaseDetector { +public: + PhaseDetector(const std::vector>& _symbolMap) : symbolMap(_symbolMap) {} + + uint8_t getSymbol(const std::complex& input) { + double phase = std::atan2(input.imag(), input.real()); + return symbolFromPhase(phase); + } + +private: + std::vector> 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>& _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> process(const std::vector& input_signal) { + std::vector> 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 multiplied = input_signal[i] * feedback; + + // Filter signal + std::complex filtered = srrc_filter.filterSample(multiplied); + + // Output best-guess corrected I/Q components + output_signal[i] = filtered; + + // Generate limited components + std::complex 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(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 feedback; + double error_total; + double out_iir_total; + double in_vco_total; + SRRCFilter srrc_filter; + + std::complex limiter(const std::complex& 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(limited_I, limited_Q); + } +}; \ No newline at end of file diff --git a/include/utils/filters.h b/include/utils/filters.h new file mode 100644 index 0000000..96fe140 --- /dev/null +++ b/include/utils/filters.h @@ -0,0 +1,167 @@ +#ifndef FILTERS_H +#define FILTERS_H + +#include +#include +#include +#include +#include + +class TapGenerators { + public: + std::vector generateSRRCTaps(const size_t num_taps, const double sample_rate, const double symbol_rate, const double rolloff) const { + std::vector freq_response(num_taps, 0.0); + std::vector 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 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 generateLowpassTaps(const size_t num_taps, const double cutoff_freq, const double sample_rate) const { + std::vector freq_response(num_taps, 0.0); + std::vector 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 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& _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(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 filterSample(const std::complex& sample) { + buffer[buffer_index] = sample; + std::complex filtered_val = std::complex(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 applyFilter(const std::vector& signal) { + std::vector 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> applyFilter(const std::vector>& signal) { + std::vector> filtered_signal(signal.size(), std::complex(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 filter_taps; + std::vector> 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 */ \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e69de29..6fc0d3d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/tests/FSKModulatorTests.cpp b/tests/FSKModulatorTests.cpp deleted file mode 100644 index 75a7e94..0000000 --- a/tests/FSKModulatorTests.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "gtest/gtest.h" -#include "FSKModulator.h" -#include - -TEST(FSKModulatorTest, SignalLength) { - using namespace milstd; - - // Parameters - FSKModulator modulator(FSKModulator::ShiftType::NarrowShift, 75.0, 8000.0); - - // Input data bits - std::vector dataBits = {1, 0, 1, 1, 0}; - - // Modulate the data - std::vector signal = modulator.modulate(dataBits); - - // Calculate expected number of samples - size_t samplesPerSymbol = static_cast(modulator.getSampleRate() * modulator.getSymbolDuration()); - size_t expectedSamples = dataBits.size() * samplesPerSymbol; - - // Verify signal length - EXPECT_EQ(signal.size(), expectedSamples); -} \ No newline at end of file diff --git a/tests/PSKModulatorTests.cpp b/tests/PSKModulatorTests.cpp new file mode 100644 index 0000000..bcbffe0 --- /dev/null +++ b/tests/PSKModulatorTests.cpp @@ -0,0 +1,41 @@ +#include +#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 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 invalid_symbols = {0, 8, 9}; + + EXPECT_THROW(modulator.modulate(invalid_symbols), std::out_of_range); +} \ No newline at end of file