feat: add rolling spectrogram column buffer
Pure-C++ store that log-resamples both channels' FFT magnitudes into scrolling columns, CH1 north and CH2 south, high frequencies toward the poles. No GL dependency, fully unit-tested.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
#include "SpectrogramBuffer.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace oscope {
|
||||
|
||||
SpectrogramBuffer::SpectrogramBuffer(int width, int height)
|
||||
: width_(std::max(1, width)),
|
||||
height_(std::max(2, height - (height % 2))) {
|
||||
data_.assign(static_cast<std::size_t>(width_) * height_, 0.0f);
|
||||
}
|
||||
|
||||
float SpectrogramBuffer::logResample(const std::vector<float>& mag,
|
||||
float frac01) {
|
||||
if (mag.size() < 2) return 0.0f;
|
||||
const float lo = 1.0f;
|
||||
const float hi = static_cast<float>(mag.size() - 1);
|
||||
const float f = std::clamp(frac01, 0.0f, 1.0f);
|
||||
const float bin = lo * std::pow(hi / lo, f);
|
||||
const int i0 = static_cast<int>(bin);
|
||||
const int i1 = std::min(i0 + 1, static_cast<int>(mag.size()) - 1);
|
||||
const float t = bin - static_cast<float>(i0);
|
||||
return mag[i0] * (1.0f - t) + mag[i1] * t;
|
||||
}
|
||||
|
||||
float SpectrogramBuffer::norm01(float magnitude) {
|
||||
const float db = 20.0f * std::log10(std::max(magnitude, 1e-6f));
|
||||
return std::clamp((db + 60.0f) / 60.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void SpectrogramBuffer::pushColumn(const std::vector<float>& magCh1,
|
||||
const std::vector<float>& magCh2) {
|
||||
const int col = writeIndex_;
|
||||
const int half = height_ / 2;
|
||||
|
||||
// CH1 -> northern rows [half, height_): row half = equator (low freq),
|
||||
// row height_-1 = north pole (high freq).
|
||||
for (int r = half; r < height_; ++r) {
|
||||
const float frac = static_cast<float>(r - half) /
|
||||
static_cast<float>(half - 1);
|
||||
data_[static_cast<std::size_t>(r) * width_ + col] =
|
||||
norm01(logResample(magCh1, frac));
|
||||
}
|
||||
// CH2 -> southern rows [0, half): row half-1 = equator (low freq),
|
||||
// row 0 = south pole (high freq).
|
||||
for (int r = 0; r < half; ++r) {
|
||||
const float frac = static_cast<float>(half - 1 - r) /
|
||||
static_cast<float>(half - 1);
|
||||
data_[static_cast<std::size_t>(r) * width_ + col] =
|
||||
norm01(logResample(magCh2, frac));
|
||||
}
|
||||
writeIndex_ = (writeIndex_ + 1) % width_;
|
||||
}
|
||||
|
||||
} // namespace oscope
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
|
||||
namespace oscope {
|
||||
|
||||
// Pure-C++ rolling spectrogram column store. No GL dependency.
|
||||
// Layout: row-major, data()[row * width + col].
|
||||
// Height is forced even. Rows [H/2, H) hold CH1 (equator -> north pole),
|
||||
// rows [0, H/2) hold CH2 (equator -> south pole). Frequency is log-scaled
|
||||
// along the rows; magnitudes are normalised to [0,1] via a -60..0 dB map.
|
||||
class SpectrogramBuffer {
|
||||
public:
|
||||
SpectrogramBuffer(int width, int height);
|
||||
|
||||
// magCh1 / magCh2: raw FFT magnitudes. Log-resampled into one column
|
||||
// at the current write index, which then advances modulo width.
|
||||
void pushColumn(const std::vector<float>& magCh1,
|
||||
const std::vector<float>& magCh2);
|
||||
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
int writeIndex() const { return writeIndex_; }
|
||||
const std::vector<float>& data() const { return data_; }
|
||||
|
||||
private:
|
||||
static float logResample(const std::vector<float>& mag, float frac01);
|
||||
static float norm01(float magnitude);
|
||||
|
||||
int width_;
|
||||
int height_;
|
||||
int writeIndex_ = 0;
|
||||
std::vector<float> data_;
|
||||
};
|
||||
|
||||
} // namespace oscope
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "check.h"
|
||||
#include "SpectrogramBuffer.h"
|
||||
#include <vector>
|
||||
|
||||
using oscope::SpectrogramBuffer;
|
||||
|
||||
// writeIndex advances modulo width.
|
||||
static void test_write_index_wraps() {
|
||||
SpectrogramBuffer buf(8, 4);
|
||||
std::vector<float> z(1024, 0.0f);
|
||||
for (int i = 0; i < 10; ++i) buf.pushColumn(z, z);
|
||||
CHECK(buf.writeIndex() == 2); // 10 % 8
|
||||
CHECK(static_cast<int>(buf.data().size()) == 8 * 4);
|
||||
}
|
||||
|
||||
// High-frequency energy must land near the poles for both channels.
|
||||
static void test_high_freq_maps_to_poles() {
|
||||
SpectrogramBuffer buf(4, 8); // H=8 -> CH1 rows 4..7, CH2 rows 0..3
|
||||
std::vector<float> hi(1024, 0.0f);
|
||||
hi[1023] = 100.0f; // energy at the top FFT bin
|
||||
buf.pushColumn(hi, hi); // both channels: high-frequency content
|
||||
const std::vector<float>& d = buf.data();
|
||||
const int W = buf.width();
|
||||
CHECK(d[7 * W + 0] > d[4 * W + 0]); // CH1: north pole > equator
|
||||
CHECK(d[0 * W + 0] > d[3 * W + 0]); // CH2: south pole > equator
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_write_index_wraps();
|
||||
test_high_freq_maps_to_poles();
|
||||
REPORT();
|
||||
}
|
||||
Reference in New Issue
Block a user