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.
This commit is contained in:
L'électron rare
2026-05-18 16:10:24 +02:00
parent 549a6b53a6
commit 9fd856744d
4 changed files with 133 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
#include "DemoSignal.h"
#include <algorithm>
#include <cmath>
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<float>(rng_ >> 8) / 8388608.0f) - 1.0f; // [-1,1)
}
void DemoSignal::next(std::vector<float>& ch1, std::vector<float>& 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<float>(std::sin(phase1_));
// CH2: steady 440 Hz tone + light noise.
phase2_ += kTwoPi * 440.0 / sr_;
const float v =
0.7f * static_cast<float>(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
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <vector>
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<float>& ch1, std::vector<float>& 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
+23
View File
@@ -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"
+41
View File
@@ -0,0 +1,41 @@
#include "check.h"
#include "DemoSignal.h"
#include <cmath>
#include <vector>
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<float> 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<float> 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();
}