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:
@@ -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
|
||||
@@ -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
|
||||
Executable
+23
@@ -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"
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user