From 9fd856744d2c9d4756a282c243e2792c29b92533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Mon, 18 May 2026 16:10:24 +0200 Subject: [PATCH] feat: add synthetic demo signal source Pure-C++ continuous two-channel generator used when no scope is connected: CH1 frequency sweep, CH2 steady tone plus noise. Adds the run_tests.sh runner covering all three pure-C++ units. --- oscope-sphere/src/DemoSignal.cpp | 41 +++++++++++++++++++++++++++++++ oscope-sphere/src/DemoSignal.h | 28 +++++++++++++++++++++ oscope-sphere/tests/run_tests.sh | 23 +++++++++++++++++ oscope-sphere/tests/test_demo.cpp | 41 +++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 oscope-sphere/src/DemoSignal.cpp create mode 100644 oscope-sphere/src/DemoSignal.h create mode 100755 oscope-sphere/tests/run_tests.sh create mode 100644 oscope-sphere/tests/test_demo.cpp diff --git a/oscope-sphere/src/DemoSignal.cpp b/oscope-sphere/src/DemoSignal.cpp new file mode 100644 index 0000000..712b951 --- /dev/null +++ b/oscope-sphere/src/DemoSignal.cpp @@ -0,0 +1,41 @@ +#include "DemoSignal.h" +#include +#include + +namespace oscope { + +namespace { +constexpr double kTwoPi = 6.283185307179586; +} + +DemoSignal::DemoSignal(float sampleRateHz) + : sr_(sampleRateHz > 1.0f ? sampleRateHz : 48000.0f) {} + +float DemoSignal::frand() { + rng_ = rng_ * 1664525u + 1013904223u; + return (static_cast(rng_ >> 8) / 8388608.0f) - 1.0f; // [-1,1) +} + +void DemoSignal::next(std::vector& ch1, std::vector& ch2, + std::size_t n) { + ch1.resize(n); + ch2.resize(n); + for (std::size_t i = 0; i < n; ++i) { + // CH1: sweep 80 Hz .. 2000 Hz, sweep period ~6 s. + sweep_ += 1.0 / sr_; + const double sweepHz = + 80.0 + 960.0 * (1.0 + std::sin(kTwoPi * sweep_ / 6.0)); + phase1_ += kTwoPi * sweepHz / sr_; + ch1[i] = 0.85f * static_cast(std::sin(phase1_)); + + // CH2: steady 440 Hz tone + light noise. + phase2_ += kTwoPi * 440.0 / sr_; + const float v = + 0.7f * static_cast(std::sin(phase2_)) + 0.15f * frand(); + ch2[i] = std::clamp(v, -1.0f, 1.0f); + } + if (phase1_ > kTwoPi * 1e6) phase1_ -= kTwoPi * 1e6; + if (phase2_ > kTwoPi * 1e6) phase2_ -= kTwoPi * 1e6; +} + +} // namespace oscope diff --git a/oscope-sphere/src/DemoSignal.h b/oscope-sphere/src/DemoSignal.h new file mode 100644 index 0000000..470beb2 --- /dev/null +++ b/oscope-sphere/src/DemoSignal.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include + +namespace oscope { + +// Pure-C++ synthetic two-channel signal for when no scope is connected. +// CH1: slow frequency sweep. CH2: steady 440 Hz tone plus light noise. +// Stateful: phase advances across calls so the output stays continuous. +class DemoSignal { +public: + explicit DemoSignal(float sampleRateHz); + + // Fills ch1 and ch2 with n freshly generated samples each. + void next(std::vector& ch1, std::vector& ch2, + std::size_t n); + +private: + float frand(); // cheap LCG noise in [-1, 1) + + float sr_; + double phase1_ = 0.0; + double phase2_ = 0.0; + double sweep_ = 0.0; + uint32_t rng_ = 0x9E3779B9u; +}; + +} // namespace oscope diff --git a/oscope-sphere/tests/run_tests.sh b/oscope-sphere/tests/run_tests.sh new file mode 100755 index 0000000..9946a5b --- /dev/null +++ b/oscope-sphere/tests/run_tests.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Compiles and runs every pure-C++ unit test. No openFrameworks needed. +set -euo pipefail +cd "$(dirname "$0")/.." +CXX="${CXX:-clang++}" +FLAGS=(-std=c++17 -O0 -g -Isrc) + +echo "== analysis ==" +"$CXX" "${FLAGS[@]}" tests/test_analysis.cpp src/FFT.cpp src/AudioAnalyzer.cpp \ + -o /tmp/osph-test-analysis +/tmp/osph-test-analysis + +echo "== spectrogram ==" +"$CXX" "${FLAGS[@]}" tests/test_spectrogram.cpp src/SpectrogramBuffer.cpp \ + -o /tmp/osph-test-spectro +/tmp/osph-test-spectro + +echo "== demo ==" +"$CXX" "${FLAGS[@]}" tests/test_demo.cpp src/DemoSignal.cpp \ + -o /tmp/osph-test-demo +/tmp/osph-test-demo + +echo "ALL TESTS PASSED" diff --git a/oscope-sphere/tests/test_demo.cpp b/oscope-sphere/tests/test_demo.cpp new file mode 100644 index 0000000..4c7c83e --- /dev/null +++ b/oscope-sphere/tests/test_demo.cpp @@ -0,0 +1,41 @@ +#include "check.h" +#include "DemoSignal.h" +#include +#include + +using oscope::DemoSignal; + +// Output has the requested length, stays in [-1,1], and the two +// channels differ. +static void test_shape_and_bounds() { + DemoSignal demo(48000.0f); + std::vector a, b; + demo.next(a, b, 256); + CHECK(a.size() == 256); + CHECK(b.size() == 256); + bool inRange = true, differ = false; + for (std::size_t i = 0; i < a.size(); ++i) { + if (std::fabs(a[i]) > 1.0f || std::fabs(b[i]) > 1.0f) inRange = false; + if (std::fabs(a[i] - b[i]) > 1e-4f) differ = true; + } + CHECK(inRange); + CHECK(differ); +} + +// Phase is continuous across calls: a second block differs from the first. +static void test_phase_advances() { + DemoSignal demo(48000.0f); + std::vector a1, b1, a2, b2; + demo.next(a1, b1, 64); + demo.next(a2, b2, 64); + bool same = true; + for (std::size_t i = 0; i < 64; ++i) + if (std::fabs(a1[i] - a2[i]) > 1e-4f) same = false; + CHECK(!same); +} + +int main() { + test_shape_and_bounds(); + test_phase_advances(); + REPORT(); +}