mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-15 15:50:59 +02:00
rewrite multiband splitter to native
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user