rewrite multiband splitter to native

This commit is contained in:
Boof2015
2026-05-19 19:02:44 -04:00
parent 3c9413acd4
commit d9cf263856
16 changed files with 1064 additions and 38 deletions
+14
View File
@@ -88,6 +88,20 @@ void BiquadFilter::setLowpass(float frequency, float sampleRate, float Q) {
a2_ = (1.0f - alpha) / a0;
}
void BiquadFilter::setHighpass(float frequency, float sampleRate, float Q) {
float omega = 2.0f * M_PI * frequency / sampleRate;
float sinOmega = sinf(omega);
float cosOmega = cosf(omega);
float alpha = sinOmega / (2.0f * Q);
float a0 = 1.0f + alpha;
b0_ = (1.0f + cosOmega) / 2.0f / a0;
b1_ = -(1.0f + cosOmega) / a0;
b2_ = (1.0f + cosOmega) / 2.0f / a0;
a1_ = -2.0f * cosOmega / a0;
a2_ = (1.0f - alpha) / a0;
}
void BiquadFilter::setBandpass(float frequency, float sampleRate, float Q) {
float omega = 2.0f * M_PI * frequency / sampleRate;
float sinOmega = sinf(omega);
+1
View File
@@ -32,6 +32,7 @@ class BiquadFilter {
public:
BiquadFilter();
void setLowpass(float frequency, float sampleRate, float Q = 0.707f);
void setHighpass(float frequency, float sampleRate, float Q = 0.707f);
void setBandpass(float frequency, float sampleRate, float Q = 2.0f);
void setHighShelf(float frequency, float sampleRate, float gainDB, float Q = 0.707f);
float process(float input);
+104
View File
@@ -9,6 +9,7 @@
#include "spectrum.h"
#include "spectrogram.h"
#include "vectorscope.h"
#include "waveform.h"
#include "vumeter.h"
#include "lufsmeter.h"
@@ -17,6 +18,7 @@ static Visualizer::Oscilloscope oscilloscope;
static Visualizer::Spectrum spectrum(2048);
static Visualizer::SpectrogramAnalyzer spectrogramAnalyzer;
static Visualizer::Vectorscope vectorscope;
static Visualizer::WaveformMultibandAnalyzer waveform;
static Visualizer::VUMeterAnalyzer vuMeter;
static Visualizer::LUFSMeterAnalyzer lufsMeter;
@@ -350,6 +352,19 @@ Napi::Value VectorscopePushSamples(const Napi::CallbackInfo& info) {
return env.Undefined();
}
Napi::Value VectorscopePushMultibandSamples(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsTypedArray()) {
Napi::TypeError::New(env, "Expected two Float32Arrays (left, right)").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Float32Array leftData = info[0].As<Napi::Float32Array>();
Napi::Float32Array rightData = info[1].As<Napi::Float32Array>();
size_t length = std::min(leftData.ElementLength(), rightData.ElementLength());
vectorscope.pushMultibandSamples(leftData.Data(), rightData.Data(), length);
return env.Undefined();
}
Napi::Value VectorscopeGetPoints(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsNumber()) {
@@ -376,6 +391,31 @@ Napi::Value VectorscopeGetPoints(const Napi::CallbackInfo& info) {
return result;
}
Napi::Value VectorscopeGetMultibandPoints(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsNumber()) {
Napi::TypeError::New(env, "Expected max points count").ThrowAsJavaScriptException();
return env.Null();
}
const size_t maxPoints = static_cast<size_t>(info[0].As<Napi::Number>().Uint32Value());
Napi::Float32Array data = Napi::Float32Array::New(env, maxPoints * Visualizer::MULTIBAND_POINT_STRIDE);
const size_t actual = vectorscope.getMultibandPoints(data.Data(), maxPoints);
Napi::Object result = Napi::Object::New(env);
if (actual < maxPoints) {
Napi::Float32Array trimmed = Napi::Float32Array::New(env, actual * Visualizer::MULTIBAND_POINT_STRIDE);
if (actual > 0) {
memcpy(trimmed.Data(), data.Data(), actual * Visualizer::MULTIBAND_POINT_STRIDE * sizeof(float));
}
result.Set("data", trimmed);
} else {
result.Set("data", data);
}
result.Set("count", Napi::Number::New(env, static_cast<double>(actual)));
return result;
}
Napi::Value VectorscopeFillPoints(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsTypedArray()) {
@@ -431,6 +471,60 @@ Napi::Value VectorscopeReset(const Napi::CallbackInfo& info) {
return info.Env().Undefined();
}
// ============== Waveform ==============
Napi::Value WaveformConfigure(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Expected sample rate and samples per column").ThrowAsJavaScriptException();
return env.Null();
}
const float sampleRate = info[0].As<Napi::Number>().FloatValue();
const size_t samplesPerColumn = static_cast<size_t>(info[1].As<Napi::Number>().Uint32Value());
waveform.configure(sampleRate, samplesPerColumn);
return env.Undefined();
}
Napi::Value WaveformProcessMono(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsTypedArray()) {
Napi::TypeError::New(env, "Expected Float32Array").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Float32Array samples = info[0].As<Napi::Float32Array>();
const auto& summaries = waveform.processMono(samples.Data(), samples.ElementLength());
Napi::Float32Array result = Napi::Float32Array::New(env, summaries.size());
if (!summaries.empty()) {
memcpy(result.Data(), summaries.data(), summaries.size() * sizeof(float));
}
return result;
}
Napi::Value WaveformProcessStereo(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsTypedArray()) {
Napi::TypeError::New(env, "Expected two Float32Arrays (left, right)").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Float32Array leftData = info[0].As<Napi::Float32Array>();
Napi::Float32Array rightData = info[1].As<Napi::Float32Array>();
const size_t length = std::min(leftData.ElementLength(), rightData.ElementLength());
const auto& summaries = waveform.processStereo(leftData.Data(), rightData.Data(), length);
Napi::Float32Array result = Napi::Float32Array::New(env, summaries.size());
if (!summaries.empty()) {
memcpy(result.Data(), summaries.data(), summaries.size() * sizeof(float));
}
return result;
}
Napi::Value WaveformReset(const Napi::CallbackInfo& info) {
waveform.reset();
return info.Env().Undefined();
}
// ============== VU Meter ==============
Napi::Value VUMeterSetSampleRate(const Napi::CallbackInfo& info) {
@@ -570,14 +664,24 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Object vecExports = Napi::Object::New(env);
vecExports.Set("setSampleRate", Napi::Function::New(env, VectorscopeSetSampleRate));
vecExports.Set("pushSamples", Napi::Function::New(env, VectorscopePushSamples));
vecExports.Set("pushMultibandSamples", Napi::Function::New(env, VectorscopePushMultibandSamples));
vecExports.Set("fillPoints", Napi::Function::New(env, VectorscopeFillPoints));
vecExports.Set("getPoints", Napi::Function::New(env, VectorscopeGetPoints));
vecExports.Set("getMultibandPoints", Napi::Function::New(env, VectorscopeGetMultibandPoints));
vecExports.Set("setBufferSize", Napi::Function::New(env, VectorscopeSetBufferSize));
vecExports.Set("getBufferSize", Napi::Function::New(env, VectorscopeGetBufferSize));
vecExports.Set("process", Napi::Function::New(env, VectorscopeProcess));
vecExports.Set("reset", Napi::Function::New(env, VectorscopeReset));
exports.Set("vectorscope", vecExports);
// Waveform
Napi::Object waveformExports = Napi::Object::New(env);
waveformExports.Set("configure", Napi::Function::New(env, WaveformConfigure));
waveformExports.Set("processMono", Napi::Function::New(env, WaveformProcessMono));
waveformExports.Set("processStereo", Napi::Function::New(env, WaveformProcessStereo));
waveformExports.Set("reset", Napi::Function::New(env, WaveformReset));
exports.Set("waveform", waveformExports);
// VU Meter
Napi::Object vuExports = Napi::Object::New(env);
vuExports.Set("setSampleRate", Napi::Function::New(env, VUMeterSetSampleRate));
+63
View File
@@ -0,0 +1,63 @@
#include "multiband.h"
#include <algorithm>
#include <cmath>
namespace Visualizer {
namespace {
constexpr float MULTIBAND_FILTER_Q = 1.41421356237f;
}
MultibandSplitter::MultibandSplitter()
: configuredSampleRate_(0.0f) {
configure(48000.0f);
}
void MultibandSplitter::configure(float sampleRate) {
const float nextSampleRate = std::max(1.0f, sampleRate);
if (nextSampleRate == configuredSampleRate_) {
return;
}
configuredSampleRate_ = nextSampleRate;
lowLpL_.setLowpass(MULTIBAND_LOW_MID_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
lowLpR_.setLowpass(MULTIBAND_LOW_MID_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
midHpL_.setHighpass(MULTIBAND_LOW_MID_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
midHpR_.setHighpass(MULTIBAND_LOW_MID_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
midLpL_.setLowpass(MULTIBAND_MID_HIGH_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
midLpR_.setLowpass(MULTIBAND_MID_HIGH_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
highHpL_.setHighpass(MULTIBAND_MID_HIGH_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
highHpR_.setHighpass(MULTIBAND_MID_HIGH_CROSSOVER, configuredSampleRate_, MULTIBAND_FILTER_Q);
reset();
}
MultibandSample MultibandSplitter::process(float left, float right) {
const float midTmpL = midHpL_.process(left);
const float midTmpR = midHpR_.process(right);
return {
lowLpL_.process(left),
lowLpR_.process(right),
midLpL_.process(midTmpL),
midLpR_.process(midTmpR),
highHpL_.process(left),
highHpR_.process(right),
};
}
void MultibandSplitter::reset() {
lowLpL_.reset();
lowLpR_.reset();
midHpL_.reset();
midHpR_.reset();
midLpL_.reset();
midLpR_.reset();
highHpL_.reset();
highHpR_.reset();
}
} // namespace Visualizer
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "dsp_utils.h"
#include <cstddef>
namespace Visualizer {
constexpr float MULTIBAND_LOW_MID_CROSSOVER = 250.0f;
constexpr float MULTIBAND_MID_HIGH_CROSSOVER = 2500.0f;
constexpr size_t MULTIBAND_POINT_STRIDE = 6;
struct MultibandSample {
float lowL;
float lowR;
float midL;
float midR;
float highL;
float highR;
};
class MultibandSplitter {
public:
MultibandSplitter();
void configure(float sampleRate);
MultibandSample process(float left, float right);
void reset();
private:
float configuredSampleRate_;
DSP::BiquadFilter lowLpL_;
DSP::BiquadFilter lowLpR_;
DSP::BiquadFilter midHpL_;
DSP::BiquadFilter midHpR_;
DSP::BiquadFilter midLpL_;
DSP::BiquadFilter midLpR_;
DSP::BiquadFilter highHpL_;
DSP::BiquadFilter highHpR_;
};
} // namespace Visualizer
+59 -1
View File
@@ -8,10 +8,18 @@ Vectorscope::Vectorscope()
: sampleRate_(48000.0f)
, bufferSize_(1024)
, writePos_(0)
, validSamples_(0) {
, validSamples_(0)
, multibandWritePos_(0)
, multibandValidSamples_(0) {
leftBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
rightBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
lowLeftBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
lowRightBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
midLeftBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
midRightBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
highLeftBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
highRightBuffer_.resize(VECTORSCOPE_BUFFER_SIZE, 0.0f);
points_.reserve(1024);
// Cascaded lowpass at 8kHz, Butterworth (Q=0.707)
@@ -21,6 +29,7 @@ Vectorscope::Vectorscope()
leftLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f);
rightLowpass1_.setLowpass(8000.0f, sampleRate_, 0.707f);
rightLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f);
multibandSplitter_.configure(sampleRate_);
}
void Vectorscope::setSampleRate(float sampleRate) {
@@ -30,6 +39,7 @@ void Vectorscope::setSampleRate(float sampleRate) {
leftLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f);
rightLowpass1_.setLowpass(8000.0f, sampleRate_, 0.707f);
rightLowpass2_.setLowpass(8000.0f, sampleRate_, 0.707f);
multibandSplitter_.configure(sampleRate_);
}
void Vectorscope::setBufferSize(size_t size) {
@@ -60,6 +70,28 @@ void Vectorscope::pushSamples(
}
}
void Vectorscope::pushMultibandSamples(
const float* leftChannel,
const float* rightChannel,
size_t length
) {
for (size_t i = 0; i < length; i++) {
const MultibandSample bands = multibandSplitter_.process(leftChannel[i], rightChannel[i]);
lowLeftBuffer_[multibandWritePos_] = bands.lowL;
lowRightBuffer_[multibandWritePos_] = bands.lowR;
midLeftBuffer_[multibandWritePos_] = bands.midL;
midRightBuffer_[multibandWritePos_] = bands.midR;
highLeftBuffer_[multibandWritePos_] = bands.highL;
highRightBuffer_[multibandWritePos_] = bands.highR;
multibandWritePos_ = (multibandWritePos_ + 1) % VECTORSCOPE_BUFFER_SIZE;
if (multibandValidSamples_ < VECTORSCOPE_BUFFER_SIZE) {
multibandValidSamples_++;
}
}
}
size_t Vectorscope::getPoints(float* xOut, float* yOut, size_t maxPoints) const {
size_t count = std::min(maxPoints, validSamples_);
@@ -73,6 +105,23 @@ size_t Vectorscope::getPoints(float* xOut, float* yOut, size_t maxPoints) const
return count;
}
size_t Vectorscope::getMultibandPoints(float* output, size_t maxPoints) const {
const size_t count = std::min(maxPoints, multibandValidSamples_);
for (size_t i = 0; i < count; i++) {
const size_t idx = (multibandWritePos_ + VECTORSCOPE_BUFFER_SIZE - count + i) % VECTORSCOPE_BUFFER_SIZE;
const size_t base = i * MULTIBAND_POINT_STRIDE;
output[base] = lowLeftBuffer_[idx];
output[base + 1] = lowRightBuffer_[idx];
output[base + 2] = midLeftBuffer_[idx];
output[base + 3] = midRightBuffer_[idx];
output[base + 4] = highLeftBuffer_[idx];
output[base + 5] = highRightBuffer_[idx];
}
return count;
}
// Legacy process method (routes through new pipeline)
const std::vector<VectorscopePoint>& Vectorscope::process(
const float* leftChannel,
@@ -98,12 +147,21 @@ const std::vector<VectorscopePoint>& Vectorscope::process(
void Vectorscope::reset() {
writePos_ = 0;
validSamples_ = 0;
multibandWritePos_ = 0;
multibandValidSamples_ = 0;
std::fill(leftBuffer_.begin(), leftBuffer_.end(), 0.0f);
std::fill(rightBuffer_.begin(), rightBuffer_.end(), 0.0f);
std::fill(lowLeftBuffer_.begin(), lowLeftBuffer_.end(), 0.0f);
std::fill(lowRightBuffer_.begin(), lowRightBuffer_.end(), 0.0f);
std::fill(midLeftBuffer_.begin(), midLeftBuffer_.end(), 0.0f);
std::fill(midRightBuffer_.begin(), midRightBuffer_.end(), 0.0f);
std::fill(highLeftBuffer_.begin(), highLeftBuffer_.end(), 0.0f);
std::fill(highRightBuffer_.begin(), highRightBuffer_.end(), 0.0f);
leftLowpass1_.reset();
leftLowpass2_.reset();
rightLowpass1_.reset();
rightLowpass2_.reset();
multibandSplitter_.reset();
points_.clear();
}
+12
View File
@@ -1,6 +1,7 @@
#pragma once
#include "dsp_utils.h"
#include "multiband.h"
#include <vector>
#include <cstddef>
@@ -25,10 +26,12 @@ public:
// Push stereo samples into circular buffer (called per worklet chunk)
void pushSamples(const float* leftChannel, const float* rightChannel, size_t length);
void pushMultibandSamples(const float* leftChannel, const float* rightChannel, size_t length);
// Get the most recent N points for rendering (from circular buffer)
// Returns count of valid points written to output arrays
size_t getPoints(float* xOut, float* yOut, size_t maxPoints) const;
size_t getMultibandPoints(float* output, size_t maxPoints) const;
// Get number of valid samples in buffer
size_t getValidSamples() const { return validSamples_; }
@@ -52,12 +55,21 @@ private:
// Circular buffers for filtered L/R
std::vector<float> leftBuffer_;
std::vector<float> rightBuffer_;
std::vector<float> lowLeftBuffer_;
std::vector<float> lowRightBuffer_;
std::vector<float> midLeftBuffer_;
std::vector<float> midRightBuffer_;
std::vector<float> highLeftBuffer_;
std::vector<float> highRightBuffer_;
// Cascaded lowpass filters (4th order Butterworth at 8kHz per channel)
DSP::BiquadFilter leftLowpass1_;
DSP::BiquadFilter leftLowpass2_;
DSP::BiquadFilter rightLowpass1_;
DSP::BiquadFilter rightLowpass2_;
MultibandSplitter multibandSplitter_;
size_t multibandWritePos_;
size_t multibandValidSamples_;
// Legacy
std::vector<VectorscopePoint> points_;
+152
View File
@@ -0,0 +1,152 @@
#include "waveform.h"
#include <algorithm>
#include <cmath>
namespace Visualizer {
WaveformMultibandAnalyzer::WaveformMultibandAnalyzer()
: sampleRate_(48000.0f)
, samplesPerColumn_(1)
, columnPos_(0)
, leftMin_(0.0f)
, leftMax_(0.0f)
, rightMin_(0.0f)
, rightMax_(0.0f)
, leftLowSum_(0.0f)
, leftMidSum_(0.0f)
, leftHighSum_(0.0f)
, rightLowSum_(0.0f)
, rightMidSum_(0.0f)
, rightHighSum_(0.0f) {
splitter_.configure(sampleRate_);
}
void WaveformMultibandAnalyzer::configure(float sampleRate, size_t samplesPerColumn) {
const float nextSampleRate = std::max(1.0f, sampleRate);
const size_t nextSamplesPerColumn = std::max<size_t>(1, samplesPerColumn);
if (nextSampleRate == sampleRate_ && nextSamplesPerColumn == samplesPerColumn_) {
return;
}
sampleRate_ = nextSampleRate;
samplesPerColumn_ = nextSamplesPerColumn;
splitter_.configure(sampleRate_);
reset();
}
const std::vector<float>& WaveformMultibandAnalyzer::processMono(const float* samples, size_t length) {
summaries_.clear();
for (size_t i = 0; i < length; i++) {
const float sample = samples[i];
const MultibandSample bands = splitter_.process(sample, sample);
accumulateLeft(sample, bands);
columnPos_++;
if (columnPos_ >= samplesPerColumn_) {
flushMonoColumn();
resetColumn();
}
}
return summaries_;
}
const std::vector<float>& WaveformMultibandAnalyzer::processStereo(
const float* left,
const float* right,
size_t length
) {
summaries_.clear();
for (size_t i = 0; i < length; i++) {
const MultibandSample bands = splitter_.process(left[i], right[i]);
accumulateLeft(left[i], bands);
accumulateRight(right[i], bands);
columnPos_++;
if (columnPos_ >= samplesPerColumn_) {
flushStereoColumn();
resetColumn();
}
}
return summaries_;
}
void WaveformMultibandAnalyzer::reset() {
splitter_.reset();
summaries_.clear();
resetColumn();
}
void WaveformMultibandAnalyzer::resetColumn() {
columnPos_ = 0;
leftMin_ = 0.0f;
leftMax_ = 0.0f;
rightMin_ = 0.0f;
rightMax_ = 0.0f;
leftLowSum_ = 0.0f;
leftMidSum_ = 0.0f;
leftHighSum_ = 0.0f;
rightLowSum_ = 0.0f;
rightMidSum_ = 0.0f;
rightHighSum_ = 0.0f;
}
void WaveformMultibandAnalyzer::accumulateLeft(float sample, const MultibandSample& bands) {
if (columnPos_ == 0) {
leftMin_ = sample;
leftMax_ = sample;
} else {
leftMin_ = std::min(leftMin_, sample);
leftMax_ = std::max(leftMax_, sample);
}
leftLowSum_ += bands.lowL * bands.lowL;
leftMidSum_ += bands.midL * bands.midL;
leftHighSum_ += bands.highL * bands.highL;
}
void WaveformMultibandAnalyzer::accumulateRight(float sample, const MultibandSample& bands) {
if (columnPos_ == 0) {
rightMin_ = sample;
rightMax_ = sample;
} else {
rightMin_ = std::min(rightMin_, sample);
rightMax_ = std::max(rightMax_, sample);
}
rightLowSum_ += bands.lowR * bands.lowR;
rightMidSum_ += bands.midR * bands.midR;
rightHighSum_ += bands.highR * bands.highR;
}
void WaveformMultibandAnalyzer::flushMonoColumn() {
summaries_.push_back(leftMin_);
summaries_.push_back(leftMax_);
summaries_.push_back(rms(leftLowSum_));
summaries_.push_back(rms(leftMidSum_));
summaries_.push_back(rms(leftHighSum_));
}
void WaveformMultibandAnalyzer::flushStereoColumn() {
summaries_.push_back(leftMin_);
summaries_.push_back(leftMax_);
summaries_.push_back(rms(leftLowSum_));
summaries_.push_back(rms(leftMidSum_));
summaries_.push_back(rms(leftHighSum_));
summaries_.push_back(rightMin_);
summaries_.push_back(rightMax_);
summaries_.push_back(rms(rightLowSum_));
summaries_.push_back(rms(rightMidSum_));
summaries_.push_back(rms(rightHighSum_));
}
float WaveformMultibandAnalyzer::rms(float sum) const {
const size_t count = std::max<size_t>(1, columnPos_);
return std::sqrt(sum / static_cast<float>(count));
}
} // namespace Visualizer
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "multiband.h"
#include <cstddef>
#include <vector>
namespace Visualizer {
constexpr size_t WAVEFORM_MONO_SUMMARY_STRIDE = 5;
constexpr size_t WAVEFORM_STEREO_SUMMARY_STRIDE = 10;
class WaveformMultibandAnalyzer {
public:
WaveformMultibandAnalyzer();
void configure(float sampleRate, size_t samplesPerColumn);
const std::vector<float>& processMono(const float* samples, size_t length);
const std::vector<float>& processStereo(const float* left, const float* right, size_t length);
void reset();
private:
void resetColumn();
void accumulateLeft(float sample, const MultibandSample& bands);
void accumulateRight(float sample, const MultibandSample& bands);
void flushMonoColumn();
void flushStereoColumn();
float rms(float sum) const;
MultibandSplitter splitter_;
float sampleRate_;
size_t samplesPerColumn_;
size_t columnPos_;
float leftMin_;
float leftMax_;
float rightMin_;
float rightMax_;
float leftLowSum_;
float leftMidSum_;
float leftHighSum_;
float rightLowSum_;
float rightMidSum_;
float rightHighSum_;
std::vector<float> summaries_;
};
} // namespace Visualizer