From 4c93830e840838ecc0429c06179ebb50cc974656 Mon Sep 17 00:00:00 2001 From: drslebedev Date: Fri, 20 Oct 2023 12:19:46 +0200 Subject: [PATCH] Implement SignaGenerator block. --- blocks/basic/CMakeLists.txt | 2 +- .../gnuradio-4.0/basic/SignalGenerator.hpp | 127 ++++++++++++++++++ blocks/basic/test/qa_sources.cpp | 79 ++++++++++- core/include/gnuradio-4.0/Block.hpp | 3 + core/include/gnuradio-4.0/Port.hpp | 4 +- 5 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 blocks/basic/include/gnuradio-4.0/basic/SignalGenerator.hpp diff --git a/blocks/basic/CMakeLists.txt b/blocks/basic/CMakeLists.txt index 4c408c87..a6e37553 100644 --- a/blocks/basic/CMakeLists.txt +++ b/blocks/basic/CMakeLists.txt @@ -1,5 +1,5 @@ add_library(gr-basic INTERFACE) -target_link_libraries(gr-basic INTERFACE gnuradio-core) +target_link_libraries(gr-basic INTERFACE gnuradio-core gnuradio-algorithm) target_include_directories(gr-basic INTERFACE $ $) if (ENABLE_TESTING) diff --git a/blocks/basic/include/gnuradio-4.0/basic/SignalGenerator.hpp b/blocks/basic/include/gnuradio-4.0/basic/SignalGenerator.hpp new file mode 100644 index 00000000..c0d10bac --- /dev/null +++ b/blocks/basic/include/gnuradio-4.0/basic/SignalGenerator.hpp @@ -0,0 +1,127 @@ +#ifndef GNURADIO_SIGNAL_SOURCE_HPP +#define GNURADIO_SIGNAL_SOURCE_HPP + +#include +#include +#include +#include + +namespace gr::basic { + +using namespace gr; + +namespace signal_source { +enum class Type : int { Const, Sin, Cos, Square, Saw, Triangle }; +using enum Type; +constexpr auto TypeList = magic_enum::enum_values(); +inline static constexpr gr::meta::fixed_string TypeNames = "[Const, Sin, Cos, Square, Saw, Triangle]"; + +constexpr Type +parse(std::string_view name) { + auto signalType = magic_enum::enum_cast(name, magic_enum::case_insensitive); + if (!signalType.has_value()) { + throw std::invalid_argument(fmt::format("unknown signal source type '{}'", name)); + } + return signalType.value(); +} + +} // namespace signal_source + +using SignalGeneratorDoc = Doc; + +template + requires(std::floating_point) +struct SignalGenerator : public gr::Block, BlockingIO, SignalGeneratorDoc> { + PortIn in; // ClockSource input + PortOut out; + + Annotated> sample_rate = T(1000.); + Annotated> signal_type = "Sin"; + Annotated frequency = T(1.); + Annotated amplitude = T(1.); + Annotated offset = T(0.); + Annotated> phase = T(0.); + + T _currentTime = T(0.); + +private: + signal_source::Type _signalType = signal_source::parse(signal_type); + T _timeTick = T(1.) / sample_rate; + +public: + void + settingsChanged(const property_map & /*old_settings*/, const property_map & /*new_settings*/) { + _signalType = signal_source::parse(signal_type); + _timeTick = T(1.) / sample_rate; + } + + [[nodiscard]] constexpr T + processOne(T /*input*/) noexcept { + using enum signal_source::Type; + + constexpr T pi2 = T(2.) * std::numbers::pi_v; + T value{}; + T phaseAdjustedTime = _currentTime + phase / (pi2 * frequency); + + switch (_signalType) { + case Sin: value = amplitude * std::sin(pi2 * frequency * phaseAdjustedTime) + offset; break; + case Cos: value = amplitude * std::cos(pi2 * frequency * phaseAdjustedTime) + offset; break; + case Const: value = amplitude + offset; break; + case Square: { + T halfPeriod = T(0.5) / frequency; + T timeWithinPeriod = std::fmod(phaseAdjustedTime, T(2.) * halfPeriod); + value = (timeWithinPeriod < halfPeriod) ? amplitude + offset : -amplitude + offset; + break; + } + case Saw: value = amplitude * (T(2.) * (phaseAdjustedTime * frequency - std::floor(phaseAdjustedTime * frequency + T(0.5)))) + offset; break; + case Triangle: { + T tmp = phaseAdjustedTime * frequency; + value = amplitude * (T(4.0) * std::abs(tmp - std::floor(tmp + T(0.75)) + T(0.25)) - T(1.0)) + offset; + break; + } + default: value = T(0.); + } + + _currentTime += _timeTick; + + return value; + } +}; + +} // namespace gr::basic + +ENABLE_REFLECTION_FOR_TEMPLATE_FULL((typename T), (gr::basic::SignalGenerator), in, out, sample_rate, signal_type, frequency, amplitude, offset, phase); + +#endif // GNURADIO_SIGNAL_SOURCE_HPP diff --git a/blocks/basic/test/qa_sources.cpp b/blocks/basic/test/qa_sources.cpp index 2b76d6e6..bc3b05af 100644 --- a/blocks/basic/test/qa_sources.cpp +++ b/blocks/basic/test/qa_sources.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include #if defined(__clang__) && __clang_major__ >= 15 @@ -20,7 +22,7 @@ const boost::ut::suite TagTests = [] { using namespace gr::basic; using namespace gr::testing; - "source_test"_test = [] { + "ClockSource test"_test = [] { constexpr bool useIoThreadPool = true; // true: scheduler/graph-provided thread, false: use user-provided call-back or thread constexpr std::uint32_t n_samples = 1900; constexpr float sample_rate = 2000.f; @@ -69,6 +71,81 @@ const boost::ut::suite TagTests = [] { // expect(equal_tag_lists(src.tags, sink1.tags)) << "sink1 (USE_PROCESS_ONE) did not receive the required tags"; // expect(equal_tag_lists(src.tags, sink2.tags)) << "sink2 (USE_PROCESS_BULK) did not receive the required tags"; }; + + "SignalGenerator test"_test = [] { + const std::size_t N = 16; // test points + const double offset = 2.; + std::vector signals{ "Const", "Sin", "Cos", "Square", "Saw", "Triangle" }; + + for (const auto &sig : signals) { + SignalGenerator signalGen{}; + std::ignore = signalGen.settings().set({ { "signal_type", sig }, + { "n_samples_max", std::uint32_t(100) }, + { "sample_rate", 2048. }, + { "frequency", 256. }, + { "amplitude", 1. }, + { "offset", offset }, + { "phase", std::numbers::pi / 4 } }); + std::ignore = signalGen.settings().applyStagedParameters(); + + // expected values corresponds to sample_rate = 1024., frequency = 128., amplitude = 1., offset = 0., phase = pi/4. + std::map> expResults = { + { "Const", { 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1. } }, + { "Sin", { 0.707106, 1., 0.707106, 0., -0.707106, -1., -0.707106, 0., 0.707106, 1., 0.707106, 0., -0.707106, -1., -0.707106, 0. } }, + { "Cos", { 0.707106, 0., -0.707106, -1., -0.7071067, 0., 0.707106, 1., 0.707106, 0., -0.707106, -1., -0.707106, 0., 0.707106, 1. } }, + { "Square", { 1., 1., 1., -1., -1., -1., -1., 1., 1., 1., 1., -1., -1., -1., -1., 1. } }, + { "Saw", { 0.25, 0.5, 0.75, -1., -0.75, -0.5, -0.25, 0., 0.25, 0.5, 0.75, -1., -0.75, -0.5, -0.25, 0. } }, + { "Triangle", { 0.5, 1., 0.5, 0., -0.5, -1., -0.5, 0., 0.5, 1., 0.5, 0., -0.5, -1., -0.5, 0. } } + }; + + for (std::size_t i = 0; i < N; i++) { + const auto val = signalGen.processOne(0); + const auto exp = expResults[sig][i] + offset; + expect(approx(exp, val, 1e-5)) << fmt::format("SignalGenerator for signal: {} and i: {} does not match.", sig, i); + } + } + }; + + "SignalGenerator ImChart test"_test = [] { + const std::size_t N = 512; // test points + std::vector signals{ "Const", "Sin", "Cos", "Square", "Saw", "Triangle" }; + for (const auto &sig : signals) { + SignalGenerator signalGen{}; + std::ignore = signalGen.settings().set({ { "signal_type", sig }, + { "n_samples_max", static_cast(N) }, + { "sample_rate", 8192. }, + { "frequency", 32. }, + { "amplitude", 2. }, + { "offset", 0. }, + { "phase", std::numbers::pi / 4. } }); + std::ignore = signalGen.settings().applyStagedParameters(); + + std::vector xValues(N), yValues(N); + std::iota(xValues.begin(), xValues.end(), 0); + std::ranges::generate(yValues, [&signalGen]() { return signalGen.processOne(0); }); + + fmt::println("Chart {}\n\n", sig); + auto chart = gr::graphs::ImChart<128, 16>({ { 0., static_cast(N) }, { -2.6, 2.6 } }); + chart.draw(xValues, yValues, sig); + chart.draw(); + } + }; + + "SignalGenerator + ClockSource test"_test = [] { + constexpr std::uint32_t n_samples = 200; + constexpr float sample_rate = 1000.f; + Graph testGraph; + auto &clockSrc = testGraph.emplaceBlock>({ { "sample_rate", sample_rate }, { "n_samples_max", n_samples }, { "name", "ClockSource" } }); + auto &signalGen = testGraph.emplaceBlock>({ { "sample_rate", sample_rate }, { "name", "SignalGenerator" } }); + auto &sink = testGraph.emplaceBlock>({ { "name", "TagSink" } }); + + expect(eq(ConnectionResult::SUCCESS, testGraph.connect<"out">(clockSrc).to<"in">(signalGen))); + expect(eq(ConnectionResult::SUCCESS, testGraph.connect<"out">(signalGen).to<"in">(sink))); + + scheduler::Simple sched{ std::move(testGraph) }; + sched.runAndWait(); + expect(eq(n_samples, static_cast(sink.n_samples_produced))) << "Number of samples does not match"; + }; }; int diff --git a/core/include/gnuradio-4.0/Block.hpp b/core/include/gnuradio-4.0/Block.hpp index 5069db29..95a74965 100644 --- a/core/include/gnuradio-4.0/Block.hpp +++ b/core/include/gnuradio-4.0/Block.hpp @@ -405,6 +405,9 @@ struct Block : protected std::tuple { auto adjust_for_input_port = [&ps = ports_status](Port &port) { if constexpr (std::remove_cvref_t::kIsSynch) { + if (!port.isConnected()) { + return; + } ps.has_sync_input_ports = true; ps.in_min_samples = std::max(ps.in_min_samples, port.min_buffer_size()); ps.in_max_samples = std::min(ps.in_max_samples, port.max_buffer_size()); diff --git a/core/include/gnuradio-4.0/Port.hpp b/core/include/gnuradio-4.0/Port.hpp index b4473b1c..e9a988d0 100644 --- a/core/include/gnuradio-4.0/Port.hpp +++ b/core/include/gnuradio-4.0/Port.hpp @@ -867,7 +867,7 @@ publish_tag(PortLike auto &port, const property_map &tag_data, std::size_t tag_o constexpr std::size_t samples_to_next_tag(const PortLike auto &port) { - if (port.tagReader().available() == 0) [[likely]] { + if (!port.isConnected() || port.tagReader().available() == 0) [[likely]] { return std::numeric_limits::max(); // default: no tags in sight } @@ -887,7 +887,7 @@ samples_to_next_tag(const PortLike auto &port) { constexpr std::size_t samples_to_eos_tag(const PortLike auto &port) { - if (port.tagReader().available() == 0) [[likely]] { + if (!port.isConnected() || port.tagReader().available() == 0) [[likely]] { return std::numeric_limits::max(); // default: no tags in sight } const auto tags = port.tagReader().get();