diff --git a/blocks/CMakeLists.txt b/blocks/CMakeLists.txt index 1070f919b..99f45234d 100644 --- a/blocks/CMakeLists.txt +++ b/blocks/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(http) add_subdirectory(math) add_subdirectory(soapy) add_subdirectory(testing) +add_subdirectory(analog) # shared library that contains all registered blocks add_subdirectory(libs) diff --git a/blocks/analog/CMakeLists.txt b/blocks/analog/CMakeLists.txt new file mode 100644 index 000000000..5e52249a5 --- /dev/null +++ b/blocks/analog/CMakeLists.txt @@ -0,0 +1,28 @@ +set(GrAnalogBlocks_HDRS + include/gnuradio-4.0/analog/Agc.hpp + include/gnuradio-4.0/analog/Agc2.hpp + include/gnuradio-4.0/analog/AmDemod.hpp + include/gnuradio-4.0/analog/FmDet.hpp + include/gnuradio-4.0/analog/FrequencyMod.hpp + include/gnuradio-4.0/analog/PhaseModulator.hpp + include/gnuradio-4.0/analog/QuadratureDemod.hpp) + +set(GrAnalogBlocks_LIBS gr-analog) + +add_library(gr-analog INTERFACE ${GrAnalogBlocks_HDRS}) +target_link_libraries(gr-analog INTERFACE gnuradio-core gnuradio-algorithm) +target_include_directories(gr-analog INTERFACE $ + $) + +gr_add_block_library( + GrAnalogBlocks + MAKE_SHARED_LIBRARY + MAKE_STATIC_LIBRARY + HEADERS + ${GrAnalogBlocks_HDRS} + LINK_LIBRARIES + ${GrAnalogBlocks_LIBS}) + +if(TARGET GrAnalogBlocksShared AND ENABLE_TESTING) + add_subdirectory(test) +endif() diff --git a/blocks/analog/include/gnuradio-4.0/analog/Agc.hpp b/blocks/analog/include/gnuradio-4.0/analog/Agc.hpp new file mode 100644 index 000000000..ab7e308d0 --- /dev/null +++ b/blocks/analog/include/gnuradio-4.0/analog/Agc.hpp @@ -0,0 +1,57 @@ +#ifndef INCLUDED_ANALOG_AGC_HPP +#define INCLUDED_ANALOG_AGC_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace gr::blocks::analog { + +template> +struct Agc : public Block> +{ + PortIn in; + PortOut out; + + Annotated rate = 1.0e-4f; + Annotated ref = 1.0f; + Annotated gain = 1.0f; + Annotated gmax = 0.0f; // 0 ⇒ unlimited + + GR_MAKE_REFLECTABLE(Agc, in, out, rate, ref, gain, gmax); + + template + work::Status processBulk(const InSpan& xs, OutSpan& ys) + { + const std::size_t n = std::min(xs.size(), ys.size()); + float g = gain; + const float r = rate, R = ref, M = gmax; + + for (std::size_t i = 0; i < n; ++i) { + const auto x = xs[i]; + const auto y = static_cast(x * g); // apply current gain + + const float amp = std::abs(y); // magnitude of *output* + g += (R - amp) * r; // adapt afterwards + if (M > 0.f && g > M) g = M; + + ys[i] = y; + } + gain = g; + ys.publish(n); + return work::Status::OK; + } +}; + +using AgcCC = Agc, false>; +using AgcFF = Agc; + +GR_REGISTER_BLOCK("gr::blocks::analog::AgcCC", gr::blocks::analog::AgcCC) +GR_REGISTER_BLOCK("gr::blocks::analog::AgcFF", gr::blocks::analog::AgcFF) + +} // namespace gr::blocks::analog +#endif /* INCLUDED_ANALOG_AGC_HPP */ diff --git a/blocks/analog/include/gnuradio-4.0/analog/Agc2.hpp b/blocks/analog/include/gnuradio-4.0/analog/Agc2.hpp new file mode 100644 index 000000000..cb887ded9 --- /dev/null +++ b/blocks/analog/include/gnuradio-4.0/analog/Agc2.hpp @@ -0,0 +1,67 @@ +#ifndef INCLUDED_ANALOG_AGC2_HPP +#define INCLUDED_ANALOG_AGC2_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gr::blocks::analog { + +template> +struct Agc2 : public Block> +{ + PortIn in; + PortOut out; + + Annotated attack_rate = 1.0e-1f; + Annotated decay_rate = 1.0e-2f; + Annotated ref = 1.0f; + Annotated gain = 1.0f; + Annotated gmax = 0.0f; // 0 ⇒ unlimited + + GR_MAKE_REFLECTABLE(Agc2, in, out, + attack_rate, decay_rate, + ref, gain, gmax); + + template + work::Status processBulk(const InSpan& xs, OutSpan& ys) + { + const std::size_t N = std::min(xs.size(), ys.size()); + float g = gain; + const float R = ref, + A = attack_rate, + D = decay_rate, + M = gmax; + + for (std::size_t i = 0; i < N; ++i) { + const auto x = xs[i]; + const auto y = static_cast(x * g); // apply current gain + + const float amp = std::abs(y); // magnitude of output + const float rate = (std::abs(amp - R) > g) ? A : D; // attack vs decay + g -= (amp - R) * rate; + + if (g < 1.0e-5f) g = 1.0e-5f; // avoid blow‑ups + if (M > 0.f && g > M) g = M; + + ys[i] = y; + } + gain = g; + ys.publish(N); + return work::Status::OK; + } +}; + +using Agc2CC = Agc2, false>; +using Agc2FF = Agc2; + +GR_REGISTER_BLOCK("gr::blocks::analog::Agc2CC", gr::blocks::analog::Agc2CC) +GR_REGISTER_BLOCK("gr::blocks::analog::Agc2FF", gr::blocks::analog::Agc2FF) + +} // namespace gr::blocks::analog +#endif /* INCLUDED_ANALOG_AGC2_HPP */ diff --git a/blocks/analog/include/gnuradio-4.0/analog/AmDemod.hpp b/blocks/analog/include/gnuradio-4.0/analog/AmDemod.hpp new file mode 100644 index 000000000..0d29e12c1 --- /dev/null +++ b/blocks/analog/include/gnuradio-4.0/analog/AmDemod.hpp @@ -0,0 +1,86 @@ +#ifndef INCLUDED_ANALOG_AM_DEMOD_HPP +#define INCLUDED_ANALOG_AM_DEMOD_HPP + +#include +#include +#include + +#include +#include + +namespace gr::blocks::analog +{ +struct AmDemod : gr::Block // fixed‑rate block (1→1) +{ + using Description = Doc< + R""(@brief Envelope AM demodulator (complex → float))"">; + + PortIn> in; + PortOut out; + + Annotated> + chan_rate{48'000.f}; + Annotated> + audio_decim{8}; + Annotated> + audio_pass{4'000.f}; + Annotated> + audio_stop{5'500.f}; + + GR_MAKE_REFLECTABLE( + AmDemod, in, out, chan_rate, audio_decim, audio_pass, audio_stop); + + void set_chan_rate (float fs) { chan_rate = fs; _recalc(); } + void set_audio_decim(int d ) { audio_decim = std::max(1, d); _recalc(); } + void set_audio_pass (float fp){ audio_pass = fp; _recalc(); } + + explicit AmDemod(property_map) { _recalc(); } + AmDemod(float fs, int d, float fp, float fsb = 0.f) + : chan_rate(fs), audio_decim(std::max(1, d)), + audio_pass(fp), audio_stop(fsb) + { _recalc(); } + + void settingsChanged(const property_map&, const property_map&) + { _recalc(); } + + template + [[nodiscard]] work::Status + processBulk(const InSpan& xs, OutSpan& ys) + { + std::size_t produced = 0; + + for (auto x : xs) { + const float env = std::abs(x); + _y = env + _alpha * (_y - env); + + if (produced == ys.size()) { + ys.publish(produced); + return work::Status::INSUFFICIENT_OUTPUT_ITEMS; + } + + ys[produced++] = _y; // 1 : 1 output + } + + ys.publish(produced); + return work::Status::OK; + } + +private: + float _alpha{1.f}; // IIR coefficient + float _y{0.f}; // filter state + + void _recalc() + { + /* one‑pole IIR coefficient (designed at the INPUT rate) */ + const float dt = 1.0f / chan_rate; + _alpha = std::exp(-2.f * std::numbers::pi_v + * audio_pass * dt); + } +}; + +GR_REGISTER_BLOCK("gr::blocks::analog::AmDemod", + gr::blocks::analog::AmDemod) + +} // namespace gr::blocks::analog +#endif /* INCLUDED_ANALOG_AM_DEMOD_HPP */ diff --git a/blocks/analog/include/gnuradio-4.0/analog/FmDet.hpp b/blocks/analog/include/gnuradio-4.0/analog/FmDet.hpp new file mode 100644 index 000000000..a79891ff3 --- /dev/null +++ b/blocks/analog/include/gnuradio-4.0/analog/FmDet.hpp @@ -0,0 +1,68 @@ +#ifndef GNURADIO_ANALOG_FMDET_HPP +#define GNURADIO_ANALOG_FMDET_HPP + +#include +#include +#include +#include + +namespace gr::blocks::analog { + +template struct FmDet; + +template<> +struct FmDet> : Block>> +{ + using Description = Doc<"IQ slope detector (complex → float)">; + + PortIn> in; + PortOut out; + + Annotated samplerate = 1.0f; + Annotated f_low = -1.0f; + Annotated f_high = 1.0f; + Annotated scl = 1.0f; + GR_MAKE_REFLECTABLE(FmDet,in,out,samplerate,f_low,f_high,scl); + + std::complex _prev = {1.0f, 0.0f}; // initial phase = 0 + float _bias = 0.0f; + + void recompute_bias() + { + const float hi = f_high, lo = f_low; + _bias = (hi != lo) ? 0.5f * scl * (hi + lo) / (hi - lo) : 0.0f; + } + + void set_scale(float s) { scl = s; recompute_bias(); } + float scale() const { return scl; } + + void set_freq_range(float lo,float hi) { f_low = lo; f_high = hi; recompute_bias(); } + float freq_low() const { return f_low; } + float freq_high() const { return f_high; } + float freq() const { return 0.0f; } // legacy stub + float bias() const { return _bias; } + + void start() { _prev = {1.0f,0.0f}; recompute_bias(); } + + work::Status processOne(const std::complex& x, float& y) + { + const std::complex prod = x * std::conj(_prev); + _prev = x; + y = scl * std::arg(prod) - _bias; + return work::Status::OK; + } + + template + work::Status processBulk(const InSpan& xs, OutSpan& ys) + { + const std::size_t n = std::min(xs.size(), ys.size()); + for(std::size_t i = 0; i < n; ++i) processOne(xs[i], ys[i]); + ys.publish(n); + return work::Status::OK; + } +}; + +GR_REGISTER_BLOCK("gr::blocks::analog::FmDet",gr::blocks::analog::FmDet,([T]),[ std::complex ]) + +} // namespace gr::blocks::analog +#endif /* GNURADIO_ANALOG_FMDET_HPP */ diff --git a/blocks/analog/include/gnuradio-4.0/analog/FrequencyMod.hpp b/blocks/analog/include/gnuradio-4.0/analog/FrequencyMod.hpp new file mode 100644 index 000000000..d6208bf07 --- /dev/null +++ b/blocks/analog/include/gnuradio-4.0/analog/FrequencyMod.hpp @@ -0,0 +1,70 @@ +#ifndef GNURADIO_ANALOG_FREQUENCYMOD_HPP +#define GNURADIO_ANALOG_FREQUENCYMOD_HPP + +#include +#include + +#include +#include + +namespace gr::blocks::analog { + +template +struct FrequencyMod; + +template<> +struct FrequencyMod : Block> +{ + using Description = Doc<"Frequency‑modulator (float → complex)">; + + PortIn in; + PortOut> out; + + Annotated> + sensitivity = std::numbers::pi_v / 4.0f; // π/4 rad/sample + + GR_MAKE_REFLECTABLE(FrequencyMod, in, out, sensitivity); + + float _phase = 0.0f; + void start() { _phase = 0.0f; } + + inline std::complex to_polar(float ph) const + { + return { std::cos(ph), std::sin(ph) }; + } + + work::Status processOne(float x, std::complex& y) + { + _phase += x * sensitivity; + /* keep phase in [-π, π] to avoid float overflow */ + constexpr float two_pi = 2.0f * std::numbers::pi_v; + if (_phase > std::numbers::pi_v) _phase -= two_pi; + if (_phase < -std::numbers::pi_v) _phase += two_pi; + + y = to_polar(_phase); + return work::Status::OK; + } + + template + work::Status processBulk(const InSpan& xs, OutSpan& ys) + { + if (xs.empty()) + return work::Status::DONE; + + const std::size_t n = std::min(xs.size(), ys.size()); + for (std::size_t i = 0; i < n; ++i) + processOne(xs[i], ys[i]); + + ys.publish(n); + return work::Status::OK; + } +}; + +GR_REGISTER_BLOCK("gr::blocks::analog::FrequencyMod",gr::blocks::analog::FrequencyMod,([T]),[ float ]) + +} // namespace gr::blocks::analog +#endif /* GNURADIO_ANALOG_FREQUENCYMOD_HPP */ diff --git a/blocks/analog/include/gnuradio-4.0/analog/PhaseModulator.hpp b/blocks/analog/include/gnuradio-4.0/analog/PhaseModulator.hpp new file mode 100644 index 000000000..e0771f30f --- /dev/null +++ b/blocks/analog/include/gnuradio-4.0/analog/PhaseModulator.hpp @@ -0,0 +1,51 @@ +#ifndef INCLUDED_ANALOG_PHASE_MODULATOR_HPP +#define INCLUDED_ANALOG_PHASE_MODULATOR_HPP + +#include +#include + +#include +#include + +namespace gr::blocks::analog { + +struct PhaseModulator + : gr::Block> // 1 : 1 +{ + using Description = Doc; + + PortIn in; + PortOut> out; + + Annotated> sensitivity{1.0f}; + + GR_MAKE_REFLECTABLE(PhaseModulator, in, out, sensitivity); + + explicit PhaseModulator(gr::property_map) {} + + void set_sensitivity(float s) { sensitivity = s; } + + void settingsChanged(const property_map&, const property_map&) + { /* nothing else to recompute */ } + + template + [[nodiscard]] work::Status + processBulk(const InSpan& xs, OutSpan& ys) + { + const std::size_t n = std::min(xs.size(), ys.size()); + for (std::size_t i = 0; i < n; ++i) { + const float phi = sensitivity * xs[i]; + ys[i] = { std::cos(phi), std::sin(phi) }; + } + ys.publish(n); + return work::Status::OK; + } +}; + +GR_REGISTER_BLOCK("gr::blocks::analog::PhaseModulator", + gr::blocks::analog::PhaseModulator) + +} // namespace gr::blocks::analog +#endif /* INCLUDED_ANALOG_PHASE_MODULATOR_HPP */ diff --git a/blocks/analog/include/gnuradio-4.0/analog/QuadratureDemod.hpp b/blocks/analog/include/gnuradio-4.0/analog/QuadratureDemod.hpp new file mode 100644 index 000000000..8c954ead2 --- /dev/null +++ b/blocks/analog/include/gnuradio-4.0/analog/QuadratureDemod.hpp @@ -0,0 +1,59 @@ +#ifndef GNURADIO_ANALOG_QUADRATUREDEMOD_HPP +#define GNURADIO_ANALOG_QUADRATUREDEMOD_HPP +#include +#include +#include +#include + +namespace gr::blocks::analog { + +template struct QuadratureDemod; + +template<> +struct QuadratureDemod> + : Block>> +{ + using Description = + Doc<"Quadrature FM demodulator (complex → float)">; + + PortIn> in; + PortOut out; + + Annotated> + gain = 1.0f; + + GR_MAKE_REFLECTABLE(QuadratureDemod,in,out,gain); + + std::complex _prev{1.0f,0.0f}; + bool _have_prev{false}; + + void start() { _have_prev = false; _prev = {1.0f,0.0f}; } + + work::Status processOne(std::complex x, float& y) + { + if(!_have_prev){ // first sample ⇒ y = 0 + y = 0.0f; + _prev = x; + _have_prev = true; + } else { + y = gain * std::arg(x * std::conj(_prev)); + _prev = x; + } + return work::Status::OK; + } + + template + work::Status processBulk(const InSpan& xs, OutSpan& ys) + { + const std::size_t n = std::min(xs.size(), ys.size()); + for(std::size_t i=0;i ]) + +} // namespace gr::blocks::analog +#endif /* GNURADIO_ANALOG_QUADRATUREDEMOD_HPP */ diff --git a/blocks/analog/test/CMakeLists.txt b/blocks/analog/test/CMakeLists.txt new file mode 100644 index 000000000..b32c069c0 --- /dev/null +++ b/blocks/analog/test/CMakeLists.txt @@ -0,0 +1,27 @@ +add_ut_test(qa_Agc) +target_link_libraries(qa_Agc PRIVATE gr-analog) +target_compile_options(qa_Agc PRIVATE -Wno-conversion -Wno-error=conversion) + +add_ut_test(qa_Agc2) +target_link_libraries(qa_Agc2 PRIVATE gr-analog) +target_compile_options(qa_Agc2 PRIVATE -Wno-conversion -Wno-error=conversion) + +add_ut_test(qa_AmDemod) +target_link_libraries(qa_AmDemod PRIVATE gr-analog) +target_compile_options(qa_AmDemod PRIVATE -Wno-conversion -Wno-error=conversion) + +add_ut_test(qa_FmDet) +target_link_libraries(qa_FmDet PRIVATE gr-analog) +target_compile_options(qa_FmDet PRIVATE -Wno-conversion -Wno-error=conversion) + +add_ut_test(qa_FrequencyMod) +target_link_libraries(qa_FrequencyMod PRIVATE gr-analog) +target_compile_options(qa_FrequencyMod PRIVATE -Wno-conversion -Wno-error=conversion) + +add_ut_test(qa_PhaseModulator) +target_link_libraries(qa_PhaseModulator PRIVATE gr-analog) +target_compile_options(qa_PhaseModulator PRIVATE -Wno-conversion -Wno-error=conversion) + +add_ut_test(qa_QuadratureDemod) +target_link_libraries(qa_QuadratureDemod PRIVATE gr-analog) +target_compile_options(qa_QuadratureDemod PRIVATE -Wno-conversion -Wno-error=conversion) \ No newline at end of file diff --git a/blocks/analog/test/qa_Agc.cpp b/blocks/analog/test/qa_Agc.cpp new file mode 100644 index 000000000..20127fa90 --- /dev/null +++ b/blocks/analog/test/qa_Agc.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace gr; +using namespace gr::testing; +using namespace gr::blocks::analog; +using namespace boost::ut; + +// Explicit type aliases for CI compatibility +using AgcCC = gr::blocks::analog::Agc, false>; +using AgcFF = gr::blocks::analog::Agc; + +template +std::vector run(const std::vector& drive, + float rate = 2.0e-2f) +{ + Graph g; + + auto& src = g.emplaceBlock>(); + src.values = drive; + + auto& agc = g.emplaceBlock(); + agc.rate = rate; + + auto& sink = + g.emplaceBlock>(); + + [[maybe_unused]] auto c1 = g.connect<"out">(src).template to<"in">(agc); + [[maybe_unused]] auto c2 = g.connect<"out">(agc).template to<"in">(sink); + + scheduler::Simple sch{ std::move(g) }; + expect(bool{ sch.runAndWait() }); + + return sink._samples; +} + +suite agc = [] { + + "AgcCC tracks magnitude"_test = [] { + constexpr std::size_t N = 2'048; + constexpr std::size_t skip = 1'512; // leave 536 samples for eval + + std::vector> drive; + drive.reserve(N); + for (std::size_t n = 0; n < N; ++n) { + const float phi = 2.f * std::numbers::pi_v * + static_cast(n) / static_cast(N); + drive.emplace_back(30.f * std::cos(phi), 30.f * std::sin(phi)); + } + + auto y = run, AgcCC>(drive); + + float err = 0.f; + for (std::size_t i = skip; i < y.size(); ++i) + err += std::fabs(std::abs(y[i]) - 1.f); + err /= static_cast(y.size() - skip); // mean |error| + + expect(err < 0.05f); + }; + + "AgcFF tracks magnitude"_test = [] { + constexpr std::size_t N = 2'048; + constexpr std::size_t skip = 1'512; + + std::vector drive; + drive.reserve(N); + for (std::size_t n = 0; n < N; ++n) { + const float phi = 2.f * std::numbers::pi_v * + static_cast(n) / static_cast(N); + drive.emplace_back(50.f * std::sin(phi)); + } + + auto y = run(drive); + + float err = 0.f; + for (std::size_t i = skip; i < y.size(); ++i) + err += std::fabs(std::fabs(y[i]) - 1.f); + err /= static_cast(y.size() - skip); + + expect(err < 0.05f); + }; +}; + +int main() {} // Boost.UT auto-runs suites diff --git a/blocks/analog/test/qa_Agc2.cpp b/blocks/analog/test/qa_Agc2.cpp new file mode 100644 index 000000000..c0bb26a10 --- /dev/null +++ b/blocks/analog/test/qa_Agc2.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace gr; +using namespace gr::testing; +using namespace gr::blocks::analog; +using namespace boost::ut; + +// Explicit type aliases for CI compatibility +using Agc2CC = gr::blocks::analog::Agc2, false>; +using Agc2FF = gr::blocks::analog::Agc2; + +template +std::vector run(const std::vector& drive, + float a_rate = 1.0e-1f, + float d_rate = 1.0e-2f) +{ + Graph g; + + auto& src = g.emplaceBlock>(); + src.values = drive; + + auto& agc2 = g.emplaceBlock(); + agc2.attack_rate = a_rate; + agc2.decay_rate = d_rate; + + auto& sink = + g.emplaceBlock>(); + + (void)g.connect<"out">(src).template to<"in">(agc2); + (void)g.connect<"out">(agc2).template to<"in">(sink); + + scheduler::Simple sch{ std::move(g) }; + expect(bool{ sch.runAndWait() }); + + return sink._samples; +} + +suite agc2 = [] { + + "Agc2CC converges"_test = [] { + constexpr std::size_t N = 3072; + constexpr std::size_t skip = 2048; + + std::vector> drive; + drive.reserve(N); + for (std::size_t n = 0; n < N; ++n) { + const float phi = 2.f * std::numbers::pi_v * + static_cast(n) / static_cast(N); + drive.emplace_back(40.f * std::sin(phi), + 40.f * std::cos(phi)); + } + + auto y = run, Agc2CC>(drive); + + float err = 0.f; + for (std::size_t i = skip; i < y.size(); ++i) + err += std::fabs(std::abs(y[i]) - 1.f); + err /= static_cast(y.size() - skip); + + expect(err < 0.05f); + }; + + "Agc2FF converges"_test = [] { + constexpr std::size_t N = 3072; + constexpr std::size_t skip = 2048; + + std::vector drive; + drive.reserve(N); + for (std::size_t n = 0; n < N; ++n) { + const float phi = 2.f * std::numbers::pi_v * + static_cast(n) / static_cast(N); + drive.emplace_back(60.f * std::cos(phi)); + } + + auto y = run(drive); + + float err = 0.f; + for (std::size_t i = skip; i < y.size(); ++i) + err += std::fabs(std::fabs(y[i]) - 1.f); + err /= static_cast(y.size() - skip); + + expect(err < 0.05f); + }; +}; + +int main() {} // Boost.UT auto‑runs suites diff --git a/blocks/analog/test/qa_AmDemod.cpp b/blocks/analog/test/qa_AmDemod.cpp new file mode 100644 index 000000000..e90d7275e --- /dev/null +++ b/blocks/analog/test/qa_AmDemod.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace gr; +using namespace gr::testing; +using namespace gr::blocks::analog; +using namespace boost::ut; + +template +std::vector run(const std::vector& drive, + float fs, int decim, float f_pass) +{ + Graph g; + + auto& src = g.emplaceBlock>(); + src.values = drive; + + auto& dem = g.emplaceBlock(); + dem.set_chan_rate(fs); + dem.set_audio_decim(decim); + dem.set_audio_pass(f_pass); + + auto& sink = + g.emplaceBlock>(); + + [[maybe_unused]] auto c1 = + g.connect<"out">(src).template to<"in">(dem); + [[maybe_unused]] auto c2 = + g.connect<"out">(dem).template to<"in">(sink); + + scheduler::Simple sch{std::move(g)}; + expect(bool{sch.runAndWait()}); + + return sink._samples; +} + +suite am_demod = [] { + "constant_envelope"_test = [] { + constexpr float fs = 48'000.f; + constexpr int dec = 8; + constexpr float f_lp = 4'000.f; + constexpr std::size_t N = 4 * 48'000; // 4 s + + std::vector> drive; + drive.reserve(N); + for (std::size_t n = 0; n < N; ++n) { + const float phi = 2.f * std::numbers::pi_v * + 1'000.f * static_cast(n) / fs; + drive.emplace_back(0.7f * std::cos(phi), + 0.7f * std::sin(phi)); + } + + auto y = run, float>(drive, fs, dec, f_lp); + + const std::size_t skip = 200; // allow IIR to settle + float mean = 0.f; + for (std::size_t i = skip; i < y.size(); ++i) + mean += y[i]; + mean /= static_cast(y.size() - skip); + + expect(std::abs(mean - 0.7f) < 0.005f); // ≤ 0.5 % error + }; +}; + +int main() {} // Boost.UT auto‑runs suites diff --git a/blocks/analog/test/qa_FmDet.cpp b/blocks/analog/test/qa_FmDet.cpp new file mode 100644 index 000000000..011495801 --- /dev/null +++ b/blocks/analog/test/qa_FmDet.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include // to build an end‑to‑end loop + +using namespace gr; +using namespace gr::testing; +using namespace gr::blocks::analog; +using namespace boost::ut; + +template +std::vector run_pipeline(const std::vector& drive, + property_map det_cfg = {}) +{ + Graph g; + auto& src = g.emplaceBlock>( + {{"values", drive}, {"n_samples_max", drive.size()}}); + auto& det = g.emplaceBlock>>(std::move(det_cfg)); + auto& sink = g.emplaceBlock>(); + + expect(eq(g.connect<"out">(src ).template to<"in">(det ), ConnectionResult::SUCCESS)); + expect(eq(g.connect<"out">(det ).template to<"in">(sink), ConnectionResult::SUCCESS)); + + scheduler::Simple sch{ std::move(g) }; + expect(bool{ sch.runAndWait() }); + return sink._samples; +} + +static constexpr auto feq = [](float a, float b, float tol = 1e-6f) { + return std::fabs(a - b) <= tol; +}; + +suite fm_det = [] { + + "scale / freq‑range setters"_test = [] { + FmDet> det; + + det.set_freq_range(1.0f, 2.0f); + expect(feq(det.freq_low (), 1.0f)); + expect(feq(det.freq_high(), 2.0f)); + + det.set_scale(4.0f); + expect(feq(det.scale(), 4.0f)); + + /* bias = ½·scl·(hi+lo)/(hi‑lo) = ½·4·3 / 1 = 6 */ + expect(feq(det.bias(), 6.0f)); + }; + + "end‑to‑end FM → slope‑det"_test = [] { + constexpr float f0 = 0.125f; // Hz (matches sens so y≈1) + constexpr float fs [[maybe_unused]] = 1.0f; // kept for clarity + constexpr float sens = std::numbers::pi_v / 4.0f; + constexpr float gain = 1.0f / sens; + constexpr size_t N = 100; + + std::vector> drive; + drive.reserve(N); + for (size_t i = 0; i < N; ++i) { + float phase = 2.0f * std::numbers::pi_v * f0 * + static_cast(i); + drive.emplace_back(std::cos(phase), std::sin(phase)); + } + + auto y = run_pipeline, float>( + drive, {{"scl", gain}}); // detector scale + + /* first sample undefined (prev‑sample = 0) – ignore */ + expect(y.size() == N); + for (size_t i = 1; i < N; ++i) + expect(feq(y[i], 1.0f, 5e-2f)) << "k=" << i << " got " << y[i]; + }; +}; + +int main() { } // Boost.UT launches suites before entering main diff --git a/blocks/analog/test/qa_FrequencyMod.cpp b/blocks/analog/test/qa_FrequencyMod.cpp new file mode 100644 index 000000000..102e391dd --- /dev/null +++ b/blocks/analog/test/qa_FrequencyMod.cpp @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace gr; +using namespace gr::testing; +using namespace gr::blocks::analog; +using namespace boost::ut; + +suite freqmod_tests = [] { + "frequency modulator"_test = [] { + constexpr float k = std::numbers::pi_v/4.0f; // sensitivity + const float src_data[] = {0.25f,0.5f,0.25f,-0.25f,-0.5f,-0.25f}; + + auto sincos = [](float ph){ return std::complex(std::cos(ph),std::sin(ph)); }; + std::vector> ref; + { float acc = 0.0f; + for(float x: src_data){ acc += k*x; ref.push_back(sincos(acc)); } } + + Graph g; + auto& src = g.emplaceBlock>(property_map{{"values",std::vector(std::begin(src_data),std::end(src_data))}, + {"n_samples_max",std::size(src_data)}}); + auto& mod = g.emplaceBlock>(property_map{{"sensitivity",k}}); + auto& sink = g.emplaceBlock,ProcessFunction::USE_PROCESS_BULK>>(); + + expect(eq(g.connect<"out">(src).to<"in">(mod),ConnectionResult::SUCCESS)); + expect(eq(g.connect<"out">(mod).to<"in">(sink),ConnectionResult::SUCCESS)); + + scheduler::Simple sch{std::move(g)}; + expect(bool{sch.runAndWait()}); + + expect(sink._samples.size() == ref.size()); + for(std::size_t i=0;i(src).template to<"in">(mod); + [[maybe_unused]] auto _c2 = + g.connect<"out">(mod).template to<"in">(sink); + + scheduler::Simple sch{ std::move(g) }; + expect(bool{ sch.runAndWait() }); + + return sink._samples; +} + +suite phase_mod = [] { + "basic"_test = [] { + constexpr float sens = std::numbers::pi_v / 4.f; + + const std::vector drive = { 0.25f, 0.5f, 0.25f, + -0.25f, -0.5f, -0.25f }; + + std::vector> ref; + ref.reserve(drive.size()); + for (auto v : drive) { + const float phi = sens * v; + ref.emplace_back(std::cos(phi), std::sin(phi)); + } + + auto y = run(drive, sens); + + expect(y.size() >= ref.size()); + + constexpr float tol = 1e-5f; + for (std::size_t i = 0; i < ref.size(); ++i) { + expect(std::abs(y[i].real() - ref[i].real()) < tol); + expect(std::abs(y[i].imag() - ref[i].imag()) < tol); + } + }; +}; + +int main() {} // Boost.UT auto‑runs suites diff --git a/blocks/analog/test/qa_QuadratureDemod.cpp b/blocks/analog/test/qa_QuadratureDemod.cpp new file mode 100644 index 000000000..66d3dc482 --- /dev/null +++ b/blocks/analog/test/qa_QuadratureDemod.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +using namespace gr; +using namespace gr::testing; +using namespace gr::blocks::analog; +using namespace boost::ut; + +template +std::vector run_vector(const InVecT& in, property_map cfg = {}) +{ + Graph g; + auto& src = g.emplaceBlock>( + { {"values", in}, {"n_samples_max", in.size()} }); + auto& blk = g.emplaceBlock(std::move(cfg)); + auto& sink = g.emplaceBlock>(); + + expect(eq(g.connect<"out">(src).template to<"in">(blk), ConnectionResult::SUCCESS)); + expect(eq(g.connect<"out">(blk).template to<"in">(sink), ConnectionResult::SUCCESS)); + + scheduler::Simple sch{ std::move(g) }; + expect(bool{ sch.runAndWait() }); + return sink._samples; +} + +static constexpr auto fnear = [](float a, float b, float tol = 1e-5f){ + return std::fabs(a-b) <= tol; +}; + +suite quad_demod = [] { + + "frequency mod ↔ quadrature demod"_test = [] { + constexpr float fs = 8000.0f; + constexpr float f = 1000.0f; + constexpr float sensitivity = std::numbers::pi_v/4.0f; + constexpr float gain = 1.0f / sensitivity; + constexpr size_t N = 200; + + std::vector drive; + drive.reserve(N); + for(size_t i=0;i*f* + (static_cast(i)/fs))); + + Graph g; + auto& src = g.emplaceBlock>( + {{"values", drive},{"n_samples_max",N}}); + auto& fm = g.emplaceBlock>( + {{"sensitivity", sensitivity}}); + auto& qd = g.emplaceBlock>>( + {{"gain", gain}}); + auto& sink = g.emplaceBlock>(); + + expect(eq(g.connect<"out">(src).template to<"in">(fm), ConnectionResult::SUCCESS)); + expect(eq(g.connect<"out">(fm ).template to<"in">(qd), ConnectionResult::SUCCESS)); + expect(eq(g.connect<"out">(qd ).template to<"in">(sink), ConnectionResult::SUCCESS)); + + scheduler::Simple sch{ std::move(g) }; + expect(bool{ sch.runAndWait() }); + + auto ref = drive; + if(!ref.empty()) + ref.front() = 0.0f; + + const auto& y = sink._samples; + + expect(y.size() == N); + for(size_t i=0;i