Skip to content

Commit

Permalink
Implement SignaGenerator block.
Browse files Browse the repository at this point in the history
  • Loading branch information
drslebedev committed Dec 8, 2023
1 parent bdc9485 commit 4c93830
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 4 deletions.
2 changes: 1 addition & 1 deletion blocks/basic/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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 $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/> $<INSTALL_INTERFACE:include/>)

if (ENABLE_TESTING)
Expand Down
127 changes: 127 additions & 0 deletions blocks/basic/include/gnuradio-4.0/basic/SignalGenerator.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#ifndef GNURADIO_SIGNAL_SOURCE_HPP
#define GNURADIO_SIGNAL_SOURCE_HPP

#include <gnuradio-4.0/Block.hpp>
#include <magic_enum.hpp>
#include <magic_enum_utility.hpp>
#include <numbers>

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<Type>();
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<Type>(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<R""(
@brief The SignalGenerator class generates various types of signal waveforms, including sine, cosine, square, constant, saw, and triangle signals.
Users can set parameters such as amplitude, frequency, offset, and phase for the desired waveform.
Note that not all parameters are supported for all signals.
The following signal shapes are supported (A = amplitude, f = frequency, P = phase, O = offset, t = time):
For the Square, Saw and Triangle t corresponds to phase adjusted time t = t + P / (pi2 * f)
* Sine
This waveform represents a smooth periodic oscillation.
formula: s(t) = A * sin(2 * pi * f * t + P) + O
* Cosine
Similar to the sine signal but shifted by pi/2 radians.
formula: s(t) = A * cos(2 * pi * f * t + P) + O
* Square
Represents a signal that alternates between two levels: -amplitude and amplitude.
If timeWithinPeriod < halfPeriod, y(t) = A + O else, y(t) = -A + O
* Constant
A signal that always returns a constant value, irrespective of time.
s(t) = A + O
* Saw (Sawtooth)
This waveform ramps upward and then sharply drops. It's called a sawtooth due to its resemblance to the profile of a saw blade.
s(t) = 2 * A * (t * f - floor(t * f + 0.5)) + O
* Triangle
This waveform linearly increases from -amplitude to amplitude in the first half of its period and then decreases back to -amplitude in the second half, forming a triangle shape.
s(t) = A * (4 * abs(t * f - floor(t * f + 0.75) + 0.25) - 1) + O
)"">;

template<typename T>
requires(std::floating_point<T>)
struct SignalGenerator : public gr::Block<SignalGenerator<T>, BlockingIO<true>, SignalGeneratorDoc> {
PortIn<T> in; // ClockSource input
PortOut<T> out;

Annotated<T, "sample_rate", Visible, Doc<"sample rate">> sample_rate = T(1000.);
Annotated<std::string, "signal_type", Visible, Doc<"see signal_source::Type">> signal_type = "Sin";
Annotated<T, "frequency", Visible> frequency = T(1.);
Annotated<T, "amplitude", Visible> amplitude = T(1.);
Annotated<T, "offset", Visible> offset = T(0.);
Annotated<T, "phase", Visible, Doc<"in rad">> 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>;
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<T>), in, out, sample_rate, signal_type, frequency, amplitude, offset, phase);

#endif // GNURADIO_SIGNAL_SOURCE_HPP
79 changes: 78 additions & 1 deletion blocks/basic/test/qa_sources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
#include <gnuradio-4.0/Scheduler.hpp>
#include <gnuradio-4.0/Tag.hpp>

#include <gnuradio-4.0/algorithm/ImChart.hpp>
#include <gnuradio-4.0/basic/clock_source.hpp>
#include <gnuradio-4.0/basic/SignalGenerator.hpp>
#include <gnuradio-4.0/testing/tag_monitors.hpp>

#if defined(__clang__) && __clang_major__ >= 15
Expand All @@ -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;
Expand Down Expand Up @@ -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<std::string> signals{ "Const", "Sin", "Cos", "Square", "Saw", "Triangle" };

for (const auto &sig : signals) {
SignalGenerator<double> 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<std::string, std ::vector<double>> 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<std::string> signals{ "Const", "Sin", "Cos", "Square", "Saw", "Triangle" };
for (const auto &sig : signals) {
SignalGenerator<double> signalGen{};
std::ignore = signalGen.settings().set({ { "signal_type", sig },
{ "n_samples_max", static_cast<std::uint32_t>(N) },
{ "sample_rate", 8192. },
{ "frequency", 32. },
{ "amplitude", 2. },
{ "offset", 0. },
{ "phase", std::numbers::pi / 4. } });
std::ignore = signalGen.settings().applyStagedParameters();

std::vector<double> 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<double>(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<gr::basic::ClockSource<float>>({ { "sample_rate", sample_rate }, { "n_samples_max", n_samples }, { "name", "ClockSource" } });
auto &signalGen = testGraph.emplaceBlock<SignalGenerator<float>>({ { "sample_rate", sample_rate }, { "name", "SignalGenerator" } });
auto &sink = testGraph.emplaceBlock<TagSink<float, ProcessFunction::USE_PROCESS_ONE>>({ { "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<std::uint32_t>(sink.n_samples_produced))) << "Number of samples does not match";
};
};

int
Expand Down
3 changes: 3 additions & 0 deletions core/include/gnuradio-4.0/Block.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@ struct Block : protected std::tuple<Arguments...> {

auto adjust_for_input_port = [&ps = ports_status]<PortLike Port>(Port &port) {
if constexpr (std::remove_cvref_t<Port>::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());
Expand Down
4 changes: 2 additions & 2 deletions core/include/gnuradio-4.0/Port.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::size_t>::max(); // default: no tags in sight
}

Expand All @@ -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<std::size_t>::max(); // default: no tags in sight
}
const auto tags = port.tagReader().get();
Expand Down

0 comments on commit 4c93830

Please sign in to comment.