commit 7403972b7cb42df722ddbf2efb418f96239579f5
parent ebb3d63b09b8a654fdf54f74647b0032d4de5b5e
Author: Matt Demanett <matt@demanett.net>
Date: Mon, 26 Nov 2018 23:00:30 -0500
ANALYZER-XL: extra-large, 8-channel spectrum analyzer. #15
Diffstat:
10 files changed, 1055 insertions(+), 649 deletions(-)
diff --git a/res-src/AnalyzerXL-src.svg b/res-src/AnalyzerXL-src.svg
@@ -0,0 +1,51 @@
+<svg
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="630"
+ height="380"
+ viewBox="0 0 630 380"
+>
+ <style>
+ text {
+ fill: #fff;
+ font-family: 'Comfortaa', sans-serif;
+ font-size: 7pt;
+ font-weight: bold;
+ letter-spacing: 2px;
+ }
+ </style>
+
+ <defs>
+ <symbol id="display" viewBox="0 0 600px 380px">
+ <rect cx="0" cy="0" width="600" height="380" fill="#222" />
+ </symbol>
+
+ <symbol id="input" viewBox="0 0 24px 24px">
+ <g transform="translate(12 12)">
+ <circle cx="0" cy="0" r="5" stroke-width="1" stroke="#0f0" fill="#0f0" />
+ <circle cx="0" cy="0" r="10.5" stroke-width="3" stroke="#0f0" fill="none" />
+ </g>
+ </symbol>
+ </defs>
+
+ <rect width="100%" height="100%" fill="#222" />
+ <g transform="translate(0 375) rotate(-90)">
+ <text transform="translate(0 12.5)">ANALYZER-XL</text>
+ <text transform="translate(0 25.5)">BOGAUDIO</text>
+ </g>
+
+ <g transform="translate(3, 13)">
+ <use id="SIGNALA_INPUT" xlink:href="#input" transform="translate(0 0)" />
+ <use id="SIGNALB_INPUT" xlink:href="#input" transform="translate(0 34)" />
+ <use id="SIGNALC_INPUT" xlink:href="#input" transform="translate(0 68)" />
+ <use id="SIGNALD_INPUT" xlink:href="#input" transform="translate(0 102)" />
+ <use id="SIGNALE_INPUT" xlink:href="#input" transform="translate(0 136)" />
+ <use id="SIGNALF_INPUT" xlink:href="#input" transform="translate(0 170)" />
+ <use id="SIGNALG_INPUT" xlink:href="#input" transform="translate(0 204)" />
+ <use id="SIGNALH_INPUT" xlink:href="#input" transform="translate(0 238)" />
+ </g>
+
+ <use id="DISPLAY_WIDGET" xlink:href="#display" transform="translate(30 0)" />
+ <!-- <rect cx="0" cy="0" width="600" height="380" fill="#555" transform="translate(30 0)" /> -->
+</svg>
diff --git a/res/AnalyzerXL.svg b/res/AnalyzerXL.svg
Binary files differ.
diff --git a/src/Analyzer.cpp b/src/Analyzer.cpp
@@ -1,232 +1,20 @@
-#include <thread>
-#include <mutex>
-#include <condition_variable>
-
#include "Analyzer.hpp"
-#include "dsp/signal.hpp"
-
-struct bogaudio::ChannelAnalyzer {
- SpectrumAnalyzer _analyzer;
- int _binsN;
- float* _bins;
- AveragingBuffer<float>* _averagedBins;
- const int _stepBufN;
- float* _stepBuf;
- int _stepBufI = 0;
- const int _workerBufN;
- float* _workerBuf;
- int _workerBufWriteI = 0;
- int _workerBufReadI = 0;
- bool _workerStop = false;
- std::mutex _workerMutex;
- std::condition_variable _workerCV;
- std::thread _worker;
-
- ChannelAnalyzer(
- SpectrumAnalyzer::Size size,
- SpectrumAnalyzer::Overlap overlap,
- SpectrumAnalyzer::WindowType windowType,
- float sampleRate,
- int averageN,
- int binSize
- )
- : _analyzer(size, overlap, windowType, sampleRate, false)
- , _binsN(size / binSize)
- , _bins(averageN == 1 ? new float[_binsN] {} : NULL)
- , _averagedBins(averageN == 1 ? NULL : new AveragingBuffer<float>(_binsN, averageN))
- , _stepBufN(size / overlap)
- , _stepBuf(new float[_stepBufN] {})
- , _workerBufN(size)
- , _workerBuf(new float[_workerBufN] {})
- , _worker(&ChannelAnalyzer::work, this)
- {
- assert(averageN >= 1);
- assert(binSize >= 1);
- }
- virtual ~ChannelAnalyzer() {
- {
- std::lock_guard<std::mutex> lock(_workerMutex);
- _workerStop = true;
- }
- _workerCV.notify_one();
- _worker.join();
- delete[] _workerBuf;
- delete[] _stepBuf;
- if (_bins) {
- delete[] _bins;
- }
- if (_averagedBins) {
- delete _averagedBins;
- }
- }
-
- const float* getBins() {
- if (_bins) {
- return _bins;
- }
- return _averagedBins->getAverages();
- }
-
- float getPeak();
- void step(float sample);
- void work();
-};
-
-float ChannelAnalyzer::getPeak() {
- float max = 0.0;
- float sum = 0.0;
- int maxBin = 0;
- const float* bins = getBins();
- for (int bin = 0; bin < _binsN; ++bin) {
- if (bins[bin] > max) {
- max = bins[bin];
- maxBin = bin;
- }
- sum += bins[bin];
- }
- const int bandsPerBin = _analyzer._size / _binsN;
- const float fWidth = (_analyzer._sampleRate / 2.0f) / (float)(_analyzer._size / bandsPerBin);
- return (maxBin + 0.5f)*fWidth;
-}
-
-void ChannelAnalyzer::step(float sample) {
- _stepBuf[_stepBufI++] = sample;
- if (_stepBufI >= _stepBufN) {
- _stepBufI = 0;
-
- {
- std::lock_guard<std::mutex> lock(_workerMutex);
- for (int i = 0; i < _stepBufN; ++i) {
- _workerBuf[_workerBufWriteI] = _stepBuf[i];
- _workerBufWriteI = (_workerBufWriteI + 1) % _workerBufN;
- if (_workerBufWriteI == _workerBufReadI) {
- _workerBufWriteI = _workerBufReadI = 0;
- break;
- }
- }
- }
- _workerCV.notify_one();
- }
-}
-
-void ChannelAnalyzer::work() {
- bool process = false;
- MAIN: while (true) {
- if (_workerStop) {
- return;
- }
-
- if (process) {
- process = false;
-
- _analyzer.process();
- _analyzer.postProcess();
- if (_bins) {
- _analyzer.getMagnitudes(_bins, _binsN);
- }
- else {
- float* frame = _averagedBins->getInputFrame();
- _analyzer.getMagnitudes(frame, _binsN);
- _averagedBins->commitInputFrame();
- }
- }
-
- while (_workerBufReadI != _workerBufWriteI) {
- float sample = _workerBuf[_workerBufReadI];
- _workerBufReadI = (_workerBufReadI + 1) % _workerBufN;
- if (_analyzer.step(sample)) {
- process = true;
- goto MAIN;
- }
- }
-
- std::unique_lock<std::mutex> lock(_workerMutex);
- while (!(_workerBufReadI != _workerBufWriteI || _workerStop)) {
- _workerCV.wait(lock);
- }
- }
-}
-
void Analyzer::onReset() {
_modulationStep = modulationSteps;
- resetChannels();
+ _core.resetChannels();
}
void Analyzer::onSampleRateChange() {
_modulationStep = modulationSteps;
- resetChannels();
-}
-
-void Analyzer::resetChannels() {
- std::lock_guard<std::mutex> lock(_channelsMutex);
- if (_channelA) {
- delete _channelA;
- _channelA = NULL;
- }
- if (_channelB) {
- delete _channelB;
- _channelB = NULL;
- }
- if (_channelC) {
- delete _channelC;
- _channelC = NULL;
- }
- if (_channelD) {
- delete _channelD;
- _channelD = NULL;
- }
-}
-
-SpectrumAnalyzer::Size Analyzer::size() {
- if (engineGetSampleRate() < 96000.0f) {
- switch (_quality) {
- case QUALITY_ULTRA: {
- return SpectrumAnalyzer::SIZE_8192;
- }
- case QUALITY_HIGH: {
- return SpectrumAnalyzer::SIZE_4096;
- }
- default: {
- return SpectrumAnalyzer::SIZE_1024;
- }
- }
- }
- else {
- switch (_quality) {
- case QUALITY_ULTRA: {
- return SpectrumAnalyzer::SIZE_16384;
- }
- case QUALITY_HIGH: {
- return SpectrumAnalyzer::SIZE_8192;
- }
- default: {
- return SpectrumAnalyzer::SIZE_2048;
- }
- }
- }
-}
-
-SpectrumAnalyzer::WindowType Analyzer::window() {
- switch (_window) {
- case WINDOW_NONE: {
- return SpectrumAnalyzer::WINDOW_NONE;
- }
- case WINDOW_HAMMING: {
- return SpectrumAnalyzer::WINDOW_HAMMING;
- }
- default: {
- return SpectrumAnalyzer::WINDOW_KAISER;
- }
- }
+ _core.resetChannels();
}
void Analyzer::step() {
++_modulationStep;
if (_modulationStep >= modulationSteps) {
_modulationStep = 0;
- bool needResetChannels = false;
float range = params[RANGE2_PARAM].value;
_rangeMinHz = 0.0f;
@@ -243,415 +31,41 @@ void Analyzer::step() {
const float maxTime = 0.5;
float smooth = params[SMOOTH_PARAM].value * maxTime;
- smooth /= size() / (_overlap * engineGetSampleRate());
- int smoothN = std::max(1, (int)roundf(smooth));
- if (_averageN != smoothN) {
- _averageN = smoothN;
- needResetChannels = true;
- }
+ smooth /= _core.size() / (_core._overlap * engineGetSampleRate());
+ int averageN = std::max(1, (int)roundf(smooth));
- Quality quality = QUALITY_GOOD;
+ AnalyzerCore::Quality quality = AnalyzerCore::QUALITY_GOOD;
if (params[QUALITY_PARAM].value > 2.5) {
- quality = QUALITY_ULTRA;
+ quality = AnalyzerCore::QUALITY_ULTRA;
}
else if (params[QUALITY_PARAM].value > 1.5) {
- quality = QUALITY_HIGH;
- }
- if (_quality != quality) {
- _quality = quality;
- needResetChannels = true;
+ quality = AnalyzerCore::QUALITY_HIGH;
}
- Window window = WINDOW_KAISER;
+ AnalyzerCore::Window window = AnalyzerCore::WINDOW_KAISER;
if (params[WINDOW_PARAM].value > 2.5) {
- window = WINDOW_NONE;
+ window = AnalyzerCore::WINDOW_NONE;
}
else if (params[WINDOW_PARAM].value > 1.5) {
- window = WINDOW_HAMMING;
- }
- if (_window != window) {
- _window = window;
- needResetChannels = true;
- }
-
- if (needResetChannels) {
- resetChannels();
+ window = AnalyzerCore::WINDOW_HAMMING;
}
- _running = true; // params[POWER_PARAM].value == 1.0;
+ _core.setParams(averageN, quality, window);
}
- stepChannel(_channelA, _running, inputs[SIGNALA_INPUT], outputs[SIGNALA_OUTPUT]);
- stepChannel(_channelB, _running, inputs[SIGNALB_INPUT], outputs[SIGNALB_OUTPUT]);
- stepChannel(_channelC, _running, inputs[SIGNALC_INPUT], outputs[SIGNALC_OUTPUT]);
- stepChannel(_channelD, _running, inputs[SIGNALD_INPUT], outputs[SIGNALD_OUTPUT]);
-
- lights[QUALITY_ULTRA_LIGHT].value = _running && _quality == QUALITY_ULTRA;
- lights[QUALITY_HIGH_LIGHT].value = _running && _quality == QUALITY_HIGH;
- lights[QUALITY_GOOD_LIGHT].value = _running && _quality == QUALITY_GOOD;
- lights[WINDOW_NONE_LIGHT].value = _running && _window == WINDOW_NONE;
- lights[WINDOW_HAMMING_LIGHT].value = _running && _window == WINDOW_HAMMING;
- lights[WINDOW_KAISER_LIGHT].value = _running && _window == WINDOW_KAISER;
-}
-
-void Analyzer::stepChannel(ChannelAnalyzer*& channelPointer, bool running, Input& input, Output& output) {
- if (running && input.active) {
- if (!channelPointer) {
- std::lock_guard<std::mutex> lock(_channelsMutex);
- channelPointer = new ChannelAnalyzer(
- size(),
- _overlap,
- window(),
- engineGetSampleRate(),
- _averageN,
- _binAverageN
- );
- }
- channelPointer->step(input.value);
- output.value = input.value;
+ for (int i = 0; i < 4; ++i) {
+ _core.stepChannel(i, inputs[SIGNALA_INPUT + i]);
+ outputs[SIGNALA_OUTPUT + i].value = inputs[SIGNALA_INPUT + i].value;
}
- else if (channelPointer) {
- std::lock_guard<std::mutex> lock(_channelsMutex);
- delete channelPointer;
- channelPointer = NULL;
- }
-}
-
-
-struct AnalyzerDisplay : TransparentWidget {
- const int _insetAround = 2;
- const int _insetLeft = _insetAround + 12;
- const int _insetRight = _insetAround + 2;
- const int _insetTop = _insetAround + 13;
- const int _insetBottom = _insetAround + 9;
-
- const float _displayDB = 80.0;
- const float _positiveDisplayDB = 20.0;
- const float baseXAxisLogFactor = 1 / 3.321; // magic number.
-
- const NVGcolor _axisColor = nvgRGBA(0xff, 0xff, 0xff, 0x70);
- const NVGcolor _textColor = nvgRGBA(0xff, 0xff, 0xff, 0xc0);
- const NVGcolor _channelAColor = nvgRGBA(0x00, 0xff, 0x00, 0xd0);
- const NVGcolor _channelBColor = nvgRGBA(0xff, 0x00, 0xff, 0xd0);
- const NVGcolor _channelCColor = nvgRGBA(0xff, 0x80, 0x00, 0xd0);
- const NVGcolor _channelDColor = nvgRGBA(0x00, 0x80, 0xff, 0xd0);
-
- Analyzer* _module;
- const Vec _size;
- const Vec _graphSize;
- std::shared_ptr<Font> _font;
- float _xAxisLogFactor = baseXAxisLogFactor;
-
- AnalyzerDisplay(
- Analyzer* module,
- Vec size
- )
- : _module(module)
- , _size(size)
- , _graphSize(_size.x - _insetLeft - _insetRight, _size.y - _insetTop - _insetBottom)
- , _font(Font::load(assetPlugin(plugin, "res/fonts/inconsolata.ttf")))
- {
- }
-
- void draw(NVGcontext* vg) override;
- void drawBackground(NVGcontext* vg);
- void drawHeader(NVGcontext* vg);
- void drawYAxis(NVGcontext* vg, float strokeWidth);
- void drawXAxis(NVGcontext* vg, float strokeWidth);
- void drawXAxisLine(NVGcontext* vg, float hz);
- void drawGraph(NVGcontext* vg, const float* bins, int binsN, NVGcolor color, float strokeWidth);
- void drawText(NVGcontext* vg, const char* s, float x, float y, float rotation = 0.0, const NVGcolor* color = NULL);
- int binValueToHeight(float value);
-};
-
-void AnalyzerDisplay::draw(NVGcontext* vg) {
- std::lock_guard<std::mutex> lock(_module->_channelsMutex);
-
- drawBackground(vg);
- if (_module->_running) {
- float strokeWidth = std::max(1.0f, 3 - gRackScene->zoomWidget->zoom);
- // _xAxisLogFactor = (_module->_rangeMaxHz - _module->_rangeMinHz) / (0.5f * engineGetSampleRate());
- // _xAxisLogFactor *= 1.0f - baseXAxisLogFactor;
- // _xAxisLogFactor = 1.0f - _xAxisLogFactor;
-
- nvgSave(vg);
- nvgScissor(vg, _insetAround, _insetAround, _size.x - _insetAround, _size.y - _insetAround);
- drawHeader(vg);
- drawYAxis(vg, strokeWidth);
- drawXAxis(vg, strokeWidth);
-
- if (_module->_channelA) {
- drawGraph(vg, _module->_channelA->getBins(), _module->_channelA->_binsN, _channelAColor, strokeWidth);
- }
- if (_module->_channelB) {
- drawGraph(vg, _module->_channelB->getBins(), _module->_channelB->_binsN, _channelBColor, strokeWidth);
- }
- if (_module->_channelC) {
- drawGraph(vg, _module->_channelC->getBins(), _module->_channelC->_binsN, _channelCColor, strokeWidth);
- }
- if (_module->_channelD) {
- drawGraph(vg, _module->_channelD->getBins(), _module->_channelD->_binsN, _channelDColor, strokeWidth);
- }
- nvgRestore(vg);
- }
-}
-
-void AnalyzerDisplay::drawBackground(NVGcontext* vg) {
- nvgSave(vg);
- nvgBeginPath(vg);
- nvgRect(vg, 0, 0, _size.x, _size.y);
- nvgFillColor(vg, nvgRGBA(0x00, 0x00, 0x00, 0xff));
- nvgFill(vg);
- nvgStrokeColor(vg, nvgRGBA(0xc0, 0xc0, 0xc0, 0xff));
- nvgStroke(vg);
- nvgRestore(vg);
-}
-
-void AnalyzerDisplay::drawHeader(NVGcontext* vg) {
- nvgSave(vg);
-
- const int textY = -4;
- const int charPx = 5;
- const int sLen = 100;
- char s[sLen];
- int x = _insetAround + 2;
-
- int n = snprintf(s, sLen, "Peaks (+/-%0.1f):", (engineGetSampleRate() / 2.0f) / (float)(_module->size() / _module->_binAverageN));
- drawText(vg, s, x, _insetTop + textY);
- x += n * charPx - 0;
-
- if (_module->_channelA) {
- snprintf(s, sLen, "A:%7.1f", _module->_channelA->getPeak());
- drawText(vg, s, x, _insetTop + textY, 0.0, &_channelAColor);
- }
- x += 9 * charPx + 3;
-
- if (_module->_channelB) {
- snprintf(s, sLen, "B:%7.1f", _module->_channelB->getPeak());
- drawText(vg, s, x, _insetTop + textY, 0.0, &_channelBColor);
- }
- x += 9 * charPx + 3;
-
- if (_module->_channelC) {
- snprintf(s, sLen, "C:%7.1f", _module->_channelC->getPeak());
- drawText(vg, s, x, _insetTop + textY, 0.0, &_channelCColor);
- }
- x += 9 * charPx + 3;
-
- if (_module->_channelD) {
- snprintf(s, sLen, "D:%7.1f", _module->_channelD->getPeak());
- drawText(vg, s, x, _insetTop + textY, 0.0, &_channelDColor);
- }
-
- nvgRestore(vg);
+ lights[QUALITY_ULTRA_LIGHT].value = _core._quality == AnalyzerCore::QUALITY_ULTRA;
+ lights[QUALITY_HIGH_LIGHT].value = _core._quality == AnalyzerCore::QUALITY_HIGH;
+ lights[QUALITY_GOOD_LIGHT].value = _core._quality == AnalyzerCore::QUALITY_GOOD;
+ lights[WINDOW_NONE_LIGHT].value = _core._window == AnalyzerCore::WINDOW_NONE;
+ lights[WINDOW_HAMMING_LIGHT].value = _core._window == AnalyzerCore::WINDOW_HAMMING;
+ lights[WINDOW_KAISER_LIGHT].value = _core._window == AnalyzerCore::WINDOW_KAISER;
}
-void AnalyzerDisplay::drawYAxis(NVGcontext* vg, float strokeWidth) {
- nvgSave(vg);
- nvgStrokeColor(vg, _axisColor);
- nvgStrokeWidth(vg, strokeWidth);
- const int lineX = _insetLeft - 2;
- const int textX = 9;
- const float textR = -M_PI/2.0;
-
- nvgBeginPath(vg);
- int lineY = _insetTop;
- nvgMoveTo(vg, lineX, lineY);
- nvgLineTo(vg, _size.x - _insetRight, lineY);
- nvgStroke(vg);
-
- nvgBeginPath(vg);
- lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB + 12.0)/_displayDB);
- nvgMoveTo(vg, lineX, lineY);
- nvgLineTo(vg, _size.x - _insetRight, lineY);
- nvgStroke(vg);
- drawText(vg, "12", textX, lineY + 5.0, textR);
-
- nvgBeginPath(vg);
- lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB)/_displayDB);
- nvgMoveTo(vg, lineX, lineY);
- nvgLineTo(vg, _size.x - _insetRight, lineY);
- nvgStrokeWidth(vg, strokeWidth * 1.5);
- nvgStroke(vg);
- nvgStrokeWidth(vg, strokeWidth);
- drawText(vg, "0", textX, lineY + 2.3, textR);
-
- nvgBeginPath(vg);
- lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB - 12.0)/_displayDB);
- nvgMoveTo(vg, lineX, lineY);
- nvgLineTo(vg, _size.x - _insetRight, lineY);
- nvgStroke(vg);
- drawText(vg, "-12", textX, lineY + 10, textR);
-
- nvgBeginPath(vg);
- lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB - 24.0)/_displayDB);
- nvgMoveTo(vg, lineX, lineY);
- nvgLineTo(vg, _size.x - _insetRight, lineY);
- nvgStroke(vg);
- drawText(vg, "-24", textX, lineY + 10, textR);
-
- nvgBeginPath(vg);
- lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB - 48.0)/_displayDB);
- nvgMoveTo(vg, lineX, lineY);
- nvgLineTo(vg, _size.x - _insetRight, lineY);
- nvgStroke(vg);
- drawText(vg, "-48", textX, lineY + 10, textR);
-
- nvgBeginPath(vg);
- lineY = _insetTop + _graphSize.y + 1;
- nvgMoveTo(vg, lineX, lineY);
- nvgLineTo(vg, _size.x - _insetRight, lineY);
- nvgStroke(vg);
-
- nvgBeginPath(vg);
- nvgMoveTo(vg, lineX, _insetTop);
- nvgLineTo(vg, lineX, lineY);
- nvgStroke(vg);
-
- drawText(vg, "dB", textX, _size.y - _insetBottom, textR);
-
- nvgRestore(vg);
-}
-
-void AnalyzerDisplay::drawXAxis(NVGcontext* vg, float strokeWidth) {
- nvgSave(vg);
- nvgStrokeColor(vg, _axisColor);
- nvgStrokeWidth(vg, strokeWidth);
-
- float hz = 100.0f;
- while (hz < _module->_rangeMaxHz && hz < 1001.0) {
- if (hz >= _module->_rangeMinHz) {
- drawXAxisLine(vg, hz);
- }
- hz += 100.0;
- }
- hz = 2000.0;
- while (hz < _module->_rangeMaxHz && hz < 10001.0) {
- if (hz >= _module->_rangeMinHz) {
- drawXAxisLine(vg, hz);
- }
- hz += 1000.0;
- }
- hz = 20000.0;
- while (hz < _module->_rangeMaxHz && hz < 100001.0) {
- if (hz >= _module->_rangeMinHz) {
- drawXAxisLine(vg, hz);
- }
- hz += 10000.0;
- }
-
- drawText(vg, "Hz", _insetLeft, _size.y - 2);
- if (_module->_rangeMinHz <= 100.0f) {
- float x = (100.0 - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
- x = powf(x, _xAxisLogFactor);
- if (x < 1.0) {
- x *= _graphSize.x;
- drawText(vg, "100", _insetLeft + x - 8, _size.y - 2);
- }
- }
- if (_module->_rangeMinHz <= 1000.0f) {
- float x = (1000.0 - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
- x = powf(x, _xAxisLogFactor);
- if (x < 1.0) {
- x *= _graphSize.x;
- drawText(vg, "1k", _insetLeft + x - 4, _size.y - 2);
- }
- }
- if (_module->_rangeMinHz <= 10000.0f) {
- float x = (10000.0 - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
- x = powf(x, _xAxisLogFactor);
- if (x < 1.0) {
- x *= _graphSize.x;
- drawText(vg, "10k", _insetLeft + x - 4, _size.y - 2);
- }
- }
- if (_module->_rangeMinHz > 1000.0f) {
- hz = 20000.0f;
- float lastX = 0.0f;
- while (hz < _module->_rangeMaxHz) {
- if (_module->_rangeMinHz <= hz) {
- float x = (hz - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
- x = powf(x, _xAxisLogFactor);
- if (x > lastX + 0.075f && x < 1.0f) {
- lastX = x;
- x *= _graphSize.x;
- const int sLen = 32;
- char s[sLen];
- snprintf(s, sLen, "%dk", (int)(hz / 1000.0f));
- drawText(vg, s, _insetLeft + x - 7, _size.y - 2);
- }
- }
- hz += 10000.0f;
- }
- }
-
- nvgRestore(vg);
-}
-
-void AnalyzerDisplay::drawXAxisLine(NVGcontext* vg, float hz) {
- float x = (hz - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
- x = powf(x, _xAxisLogFactor);
- if (x < 1.0) {
- x *= _graphSize.x;
- nvgBeginPath(vg);
- nvgMoveTo(vg, _insetLeft + x, _insetTop);
- nvgLineTo(vg, _insetLeft + x, _insetTop + _graphSize.y);
- nvgStroke(vg);
- }
-}
-
-void AnalyzerDisplay::drawGraph(NVGcontext* vg, const float* bins, int binsN, NVGcolor color, float strokeWidth) {
- float range = (_module->_rangeMaxHz - _module->_rangeMinHz) / (0.5f * engineGetSampleRate());
- int pointsN = roundf(range * (_module->size() / 2));
- range = _module->_rangeMinHz / (0.5f * engineGetSampleRate());
- int pointsOffset = roundf(range * (_module->size() / 2));
- nvgSave(vg);
- nvgScissor(vg, _insetLeft, _insetTop, _graphSize.x, _graphSize.y);
- nvgStrokeColor(vg, color);
- nvgStrokeWidth(vg, strokeWidth);
- nvgBeginPath(vg);
- for (int i = 0; i < pointsN; ++i) {
- int height = binValueToHeight(bins[pointsOffset + i]);
- if (i == 0) {
- nvgMoveTo(vg, _insetLeft, _insetTop + (_graphSize.y - height));
- }
- else {
- float x = _graphSize.x * powf(i / (float)pointsN, _xAxisLogFactor);
- nvgLineTo(vg, _insetLeft + x, _insetTop + (_graphSize.y - height));
- }
- }
- nvgStroke(vg);
- nvgRestore(vg);
-}
-
-void AnalyzerDisplay::drawText(NVGcontext* vg, const char* s, float x, float y, float rotation, const NVGcolor* color) {
- nvgSave(vg);
- nvgTranslate(vg, x, y);
- nvgRotate(vg, rotation);
- nvgFontSize(vg, 10);
- nvgFontFaceId(vg, _font->handle);
- nvgFillColor(vg, color ? *color : _textColor);
- nvgText(vg, 0, 0, s, NULL);
- nvgRestore(vg);
-}
-
-int AnalyzerDisplay::binValueToHeight(float value) {
- const float minDB = -(_displayDB - _positiveDisplayDB);
- if (value < 0.00001f) {
- return 0;
- }
- value /= 10.0f; // arbitrarily use 5.0f as reference "maximum" baseline signal (e.g. raw output of an oscillator)...but signals are +/-5, so 10 total.
- value = powf(value, 0.5f); // undoing magnitude scaling of levels back from the FFT?
- value = amplitudeToDecibels(value);
- value = std::max(minDB, value);
- value = std::min(_positiveDisplayDB, value);
- value -= minDB;
- value /= _displayDB;
- return roundf(_graphSize.y * value);
-}
-
-
struct AnalyzerWidget : ModuleWidget {
static constexpr int hp = 20;
diff --git a/src/Analyzer.hpp b/src/Analyzer.hpp
@@ -1,19 +1,13 @@
#pragma once
-#include <mutex>
-
#include "bogaudio.hpp"
-#include "dsp/analyzer.hpp"
-
-using namespace bogaudio::dsp;
+#include "analyzer_base.hpp"
extern Model* modelAnalyzer;
namespace bogaudio {
-struct ChannelAnalyzer;
-
-struct Analyzer : Module {
+struct Analyzer : AnalyzerBase {
enum ParamsIds {
RANGE_PARAM, // no longer used
SMOOTH_PARAM,
@@ -51,36 +45,10 @@ struct Analyzer : Module {
NUM_LIGHTS
};
- enum Quality {
- QUALITY_ULTRA,
- QUALITY_HIGH,
- QUALITY_GOOD
- };
-
- enum Window {
- WINDOW_NONE,
- WINDOW_HAMMING,
- WINDOW_KAISER
- };
-
const int modulationSteps = 100;
int _modulationStep = 0;
- bool _running = false;
- int _averageN;
- ChannelAnalyzer* _channelA = NULL;
- ChannelAnalyzer* _channelB = NULL;
- ChannelAnalyzer* _channelC = NULL;
- ChannelAnalyzer* _channelD = NULL;
- float _rangeMinHz = 0.0;
- float _rangeMaxHz = 0.0;
- float _smooth = 0.0;
- Quality _quality = QUALITY_GOOD;
- Window _window = WINDOW_KAISER;
- const SpectrumAnalyzer::Overlap _overlap = SpectrumAnalyzer::OVERLAP_2;
- const int _binAverageN = 2;
- std::mutex _channelsMutex;
- Analyzer() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
+ Analyzer() : AnalyzerBase(4, NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
onReset();
}
virtual ~Analyzer() {
@@ -89,11 +57,7 @@ struct Analyzer : Module {
void onReset() override;
void onSampleRateChange() override;
- void resetChannels();
- SpectrumAnalyzer::Size size();
- SpectrumAnalyzer::WindowType window();
void step() override;
- void stepChannel(ChannelAnalyzer*& channelPointer, bool running, Input& input, Output& output);
};
} // namespace bogaudio
diff --git a/src/AnalyzerXL.cpp b/src/AnalyzerXL.cpp
@@ -0,0 +1,290 @@
+
+#include "string.h"
+
+#include "AnalyzerXL.hpp"
+
+#define RANGE_KEY "range"
+#define SMOOTH_KEY "smooth"
+#define QUALITY_KEY "quality"
+#define QUALITY_GOOD_KEY "good"
+#define QUALITY_HIGH_KEY "high"
+#define QUALITY_ULTRA_KEY "ultra"
+#define WINDOW_KEY "window"
+#define WINDOW_NONE_KEY "none"
+#define WINDOW_HAMMING_KEY "hamming"
+#define WINDOW_KAISER_KEY "kaiser"
+
+void AnalyzerXL::onReset() {
+ _modulationStep = modulationSteps;
+ _range = 0.0f;
+ _smooth = 0.25f;
+ _quality = AnalyzerCore::QUALITY_GOOD;
+ _window = AnalyzerCore::WINDOW_KAISER;
+ setCoreParams();
+ _core.resetChannels();
+}
+
+void AnalyzerXL::onSampleRateChange() {
+ _modulationStep = modulationSteps;
+ setCoreParams();
+ _core.resetChannels();
+}
+
+void AnalyzerXL::setCoreParams() {
+ _rangeMinHz = 0.0f;
+ _rangeMaxHz = 0.5f * engineGetSampleRate();
+ if (_range < 0.0f) {
+ _rangeMaxHz *= 1.0f + _range;
+ }
+ else if (_range > 0.0f) {
+ _rangeMinHz = _range * _rangeMaxHz;
+ }
+
+ float smooth = _smooth / (_core.size() / (_core._overlap * engineGetSampleRate()));
+ int averageN = std::max(1, (int)roundf(smooth));
+ _core.setParams(averageN, _quality, _window);
+}
+
+json_t* AnalyzerXL::toJson() {
+ json_t* root = json_object();
+ json_object_set_new(root, RANGE_KEY, json_real(_range));
+ json_object_set_new(root, SMOOTH_KEY, json_real(_smooth));
+ switch (_quality) {
+ case AnalyzerCore::QUALITY_GOOD: {
+ json_object_set_new(root, QUALITY_KEY, json_string(QUALITY_GOOD_KEY));
+ break;
+ }
+ case AnalyzerCore::QUALITY_HIGH: {
+ json_object_set_new(root, QUALITY_KEY, json_string(QUALITY_HIGH_KEY));
+ break;
+ }
+ case AnalyzerCore::QUALITY_ULTRA: {
+ json_object_set_new(root, QUALITY_KEY, json_string(QUALITY_ULTRA_KEY));
+ break;
+ }
+ }
+ switch (_window) {
+ case AnalyzerCore::WINDOW_NONE: {
+ json_object_set_new(root, WINDOW_KEY, json_string(WINDOW_NONE_KEY));
+ break;
+ }
+ case AnalyzerCore::WINDOW_HAMMING: {
+ json_object_set_new(root, WINDOW_KEY, json_string(WINDOW_HAMMING_KEY));
+ break;
+ }
+ case AnalyzerCore::WINDOW_KAISER: {
+ json_object_set_new(root, WINDOW_KEY, json_string(WINDOW_KAISER_KEY));
+ break;
+ }
+ }
+ return root;
+}
+
+void AnalyzerXL::fromJson(json_t* root) {
+ json_t* jr = json_object_get(root, RANGE_KEY);
+ if (jr) {
+ _range = clamp(json_real_value(jr), -0.9f, 0.8f);
+ }
+
+ json_t* js = json_object_get(root, SMOOTH_KEY);
+ if (js) {
+ _smooth = clamp(json_real_value(js), 0.0f, 0.5f);
+ }
+
+ json_t* jq = json_object_get(root, QUALITY_KEY);
+ if (jq) {
+ const char *s = json_string_value(jq);
+ if (strcmp(s, QUALITY_GOOD_KEY) == 0) {
+ _quality = AnalyzerCore::QUALITY_GOOD;
+ }
+ else if (strcmp(s, QUALITY_HIGH_KEY) == 0) {
+ _quality = AnalyzerCore::QUALITY_HIGH;
+ }
+ else if (strcmp(s, QUALITY_ULTRA_KEY) == 0) {
+ _quality = AnalyzerCore::QUALITY_ULTRA;
+ }
+ }
+
+ json_t* jw = json_object_get(root, WINDOW_KEY);
+ if (jw) {
+ const char *s = json_string_value(jw);
+ if (strcmp(s, WINDOW_NONE_KEY) == 0) {
+ _window = AnalyzerCore::WINDOW_NONE;
+ }
+ else if (strcmp(s, WINDOW_HAMMING_KEY) == 0) {
+ _window = AnalyzerCore::WINDOW_HAMMING;
+ }
+ else if (strcmp(s, WINDOW_KAISER_KEY) == 0) {
+ _window = AnalyzerCore::WINDOW_KAISER;
+ }
+ }
+}
+
+void AnalyzerXL::step() {
+ ++_modulationStep;
+ if (_modulationStep >= modulationSteps) {
+ _modulationStep = 0;
+ setCoreParams();
+ }
+
+ for (int i = 0; i < 8; ++i) {
+ _core.stepChannel(i, inputs[SIGNALA_INPUT + i]);
+ }
+}
+
+struct RangeMenuItem : MenuItem {
+ AnalyzerXL* _module;
+ const float _range;
+
+ RangeMenuItem(AnalyzerXL* module, const char* label, float range)
+ : _module(module)
+ , _range(range)
+ {
+ this->text = label;
+ }
+
+ void onAction(EventAction &e) override {
+ _module->_range = _range;
+ }
+
+ void step() override {
+ rightText = _module->_range == _range ? "✔" : "";
+ }
+};
+
+struct SmoothMenuItem : MenuItem {
+ AnalyzerXL* _module;
+ const float _smooth;
+
+ SmoothMenuItem(AnalyzerXL* module, const char* label, float smooth)
+ : _module(module)
+ , _smooth(smooth)
+ {
+ this->text = label;
+ }
+
+ void onAction(EventAction &e) override {
+ _module->_smooth = _smooth;
+ }
+
+ void step() override {
+ rightText = _module->_smooth == _smooth ? "✔" : "";
+ }
+};
+
+struct QualityMenuItem : MenuItem {
+ AnalyzerXL* _module;
+ const AnalyzerCore::Quality _quality;
+
+ QualityMenuItem(AnalyzerXL* module, const char* label, AnalyzerCore::Quality quality)
+ : _module(module)
+ , _quality(quality)
+ {
+ this->text = label;
+ }
+
+ void onAction(EventAction &e) override {
+ _module->_quality = _quality;
+ }
+
+ void step() override {
+ rightText = _module->_quality == _quality ? "✔" : "";
+ }
+};
+
+struct WindowMenuItem : MenuItem {
+ AnalyzerXL* _module;
+ const AnalyzerCore::Window _window;
+
+ WindowMenuItem(AnalyzerXL* module, const char* label, AnalyzerCore::Window window)
+ : _module(module)
+ , _window(window)
+ {
+ this->text = label;
+ }
+
+ void onAction(EventAction &e) override {
+ _module->_window = _window;
+ }
+
+ void step() override {
+ rightText = _module->_window == _window ? "✔" : "";
+ }
+};
+
+struct AnalyzerXLWidget : ModuleWidget {
+ static constexpr int hp = 42;
+
+ AnalyzerXLWidget(AnalyzerXL* module) : ModuleWidget(module) {
+ box.size = Vec(RACK_GRID_WIDTH * hp, RACK_GRID_HEIGHT);
+
+ {
+ SVGPanel *panel = new SVGPanel();
+ panel->box.size = box.size;
+ panel->setBackground(SVG::load(assetPlugin(plugin, "res/AnalyzerXL.svg")));
+ addChild(panel);
+ }
+
+ {
+ auto inset = Vec(30, 0);
+ auto size = Vec(box.size.x - inset.x, 380);
+ auto display = new AnalyzerDisplay(module, size);
+ display->box.pos = inset;
+ display->box.size = size;
+ addChild(display);
+ }
+
+ // generated by svg_widgets.rb
+ auto signalaInputPosition = Vec(3.0, 13.0);
+ auto signalbInputPosition = Vec(3.0, 47.0);
+ auto signalcInputPosition = Vec(3.0, 81.0);
+ auto signaldInputPosition = Vec(3.0, 115.0);
+ auto signaleInputPosition = Vec(3.0, 149.0);
+ auto signalfInputPosition = Vec(3.0, 183.0);
+ auto signalgInputPosition = Vec(3.0, 217.0);
+ auto signalhInputPosition = Vec(3.0, 251.0);
+ // end generated by svg_widgets.rb
+
+ addInput(Port::create<Port24>(signalaInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALA_INPUT));
+ addInput(Port::create<Port24>(signalbInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALB_INPUT));
+ addInput(Port::create<Port24>(signalcInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALC_INPUT));
+ addInput(Port::create<Port24>(signaldInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALD_INPUT));
+ addInput(Port::create<Port24>(signaleInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALE_INPUT));
+ addInput(Port::create<Port24>(signalfInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALF_INPUT));
+ addInput(Port::create<Port24>(signalgInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALG_INPUT));
+ addInput(Port::create<Port24>(signalhInputPosition, Port::INPUT, module, AnalyzerXL::SIGNALH_INPUT));
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ AnalyzerXL* a = dynamic_cast<AnalyzerXL*>(module);
+ assert(a);
+
+ menu->addChild(new MenuLabel());
+ menu->addChild(new RangeMenuItem(a, "Range: lower 10%", -0.90f));
+ menu->addChild(new RangeMenuItem(a, "Range: lower 25%", -0.75f));
+ menu->addChild(new RangeMenuItem(a, "Range: lower 50%", -0.5f));
+ menu->addChild(new RangeMenuItem(a, "Range: Full", 0.0f));
+ menu->addChild(new RangeMenuItem(a, "Range: upper 50%", 0.5f));
+ menu->addChild(new RangeMenuItem(a, "Range: upper 25%", 0.75f));
+
+ menu->addChild(new MenuLabel());
+ menu->addChild(new SmoothMenuItem(a, "Smooth: None", 0.0f));
+ menu->addChild(new SmoothMenuItem(a, "Smooth: 10ms", 0.01f));
+ menu->addChild(new SmoothMenuItem(a, "Smooth: 50ms", 0.05f));
+ menu->addChild(new SmoothMenuItem(a, "Smooth: 100ms", 0.1f));
+ menu->addChild(new SmoothMenuItem(a, "Smooth: 250ms", 0.25f));
+ menu->addChild(new SmoothMenuItem(a, "Smooth: 500ms", 0.5f));
+
+ menu->addChild(new MenuLabel());
+ menu->addChild(new QualityMenuItem(a, "Quality: good", AnalyzerCore::QUALITY_GOOD));
+ menu->addChild(new QualityMenuItem(a, "Quality: high", AnalyzerCore::QUALITY_HIGH));
+ menu->addChild(new QualityMenuItem(a, "Quality: ultra", AnalyzerCore::QUALITY_ULTRA));
+
+ menu->addChild(new MenuLabel());
+ menu->addChild(new WindowMenuItem(a, "Window: Kaiser", AnalyzerCore::WINDOW_KAISER));
+ menu->addChild(new WindowMenuItem(a, "Window: Hamming", AnalyzerCore::WINDOW_HAMMING));
+ menu->addChild(new WindowMenuItem(a, "Window: None", AnalyzerCore::WINDOW_NONE));
+ }
+};
+
+Model* modelAnalyzerXL = createModel<AnalyzerXL, AnalyzerXLWidget>("Bogaudio-AnalyzerXL", "Analyzer-XL", "spectrum analyzer", VISUAL_TAG);
diff --git a/src/AnalyzerXL.hpp b/src/AnalyzerXL.hpp
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "bogaudio.hpp"
+#include "analyzer_base.hpp"
+
+extern Model* modelAnalyzerXL;
+
+namespace bogaudio {
+
+struct AnalyzerXL : AnalyzerBase {
+ enum ParamsIds {
+ NUM_PARAMS
+ };
+
+ enum InputsIds {
+ SIGNALA_INPUT,
+ SIGNALB_INPUT,
+ SIGNALC_INPUT,
+ SIGNALD_INPUT,
+ SIGNALE_INPUT,
+ SIGNALF_INPUT,
+ SIGNALG_INPUT,
+ SIGNALH_INPUT,
+ NUM_INPUTS
+ };
+
+ enum OutputsIds {
+ NUM_OUTPUTS
+ };
+
+ enum LightsIds {
+ NUM_LIGHTS
+ };
+
+ const int modulationSteps = 100;
+ int _modulationStep = 0;
+ float _range = 0.0f;
+ float _smooth = 0.25f;
+ AnalyzerCore::Quality _quality = AnalyzerCore::QUALITY_GOOD;
+ AnalyzerCore::Window _window = AnalyzerCore::WINDOW_KAISER;
+
+ AnalyzerXL() : AnalyzerBase(8, NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
+ onReset();
+ }
+
+ void onReset() override;
+ void onSampleRateChange() override;
+ void setCoreParams();
+ json_t* toJson() override;
+ void fromJson(json_t* root) override;
+ void step() override;
+};
+
+} // namespace bogaudio
diff --git a/src/UMix.cpp b/src/UMix.cpp
@@ -110,7 +110,7 @@ struct UMixWidget : ModuleWidget {
}
void appendContextMenu(Menu* menu) override {
- UMix* umix = dynamic_cast<UMix*>(module);
+ UMix* umix = dynamic_cast<UMix*>(module);
assert(umix);
menu->addChild(new MenuLabel());
menu->addChild(new AverageMenuItem(umix, "Average"));
diff --git a/src/analyzer_base.cpp b/src/analyzer_base.cpp
@@ -0,0 +1,464 @@
+
+#include "analyzer_base.hpp"
+#include "dsp/signal.hpp"
+
+ChannelAnalyzer::~ChannelAnalyzer() {
+ {
+ std::lock_guard<std::mutex> lock(_workerMutex);
+ _workerStop = true;
+ }
+ _workerCV.notify_one();
+ _worker.join();
+ delete[] _workerBuf;
+ delete[] _stepBuf;
+ if (_bins) {
+ delete[] _bins;
+ }
+ if (_averagedBins) {
+ delete _averagedBins;
+ }
+}
+
+const float* ChannelAnalyzer::getBins() {
+ if (_bins) {
+ return _bins;
+ }
+ return _averagedBins->getAverages();
+}
+
+float ChannelAnalyzer::getPeak() {
+ float max = 0.0;
+ float sum = 0.0;
+ int maxBin = 0;
+ const float* bins = getBins();
+ for (int bin = 0; bin < _binsN; ++bin) {
+ if (bins[bin] > max) {
+ max = bins[bin];
+ maxBin = bin;
+ }
+ sum += bins[bin];
+ }
+ const int bandsPerBin = _analyzer._size / _binsN;
+ const float fWidth = (_analyzer._sampleRate / 2.0f) / (float)(_analyzer._size / bandsPerBin);
+ return (maxBin + 0.5f)*fWidth;
+}
+
+void ChannelAnalyzer::step(float sample) {
+ _stepBuf[_stepBufI++] = sample;
+ if (_stepBufI >= _stepBufN) {
+ _stepBufI = 0;
+
+ {
+ std::lock_guard<std::mutex> lock(_workerMutex);
+ for (int i = 0; i < _stepBufN; ++i) {
+ _workerBuf[_workerBufWriteI] = _stepBuf[i];
+ _workerBufWriteI = (_workerBufWriteI + 1) % _workerBufN;
+ if (_workerBufWriteI == _workerBufReadI) {
+ _workerBufWriteI = _workerBufReadI = 0;
+ break;
+ }
+ }
+ }
+ _workerCV.notify_one();
+ }
+}
+
+void ChannelAnalyzer::work() {
+ bool process = false;
+ MAIN: while (true) {
+ if (_workerStop) {
+ return;
+ }
+
+ if (process) {
+ process = false;
+
+ _analyzer.process();
+ _analyzer.postProcess();
+ if (_bins) {
+ _analyzer.getMagnitudes(_bins, _binsN);
+ }
+ else {
+ float* frame = _averagedBins->getInputFrame();
+ _analyzer.getMagnitudes(frame, _binsN);
+ _averagedBins->commitInputFrame();
+ }
+ }
+
+ while (_workerBufReadI != _workerBufWriteI) {
+ float sample = _workerBuf[_workerBufReadI];
+ _workerBufReadI = (_workerBufReadI + 1) % _workerBufN;
+ if (_analyzer.step(sample)) {
+ process = true;
+ goto MAIN;
+ }
+ }
+
+ std::unique_lock<std::mutex> lock(_workerMutex);
+ while (!(_workerBufReadI != _workerBufWriteI || _workerStop)) {
+ _workerCV.wait(lock);
+ }
+ }
+}
+
+
+void AnalyzerCore::setParams(int averageN, Quality quality, Window window) {
+ bool reset = false;
+ if (_averageN != averageN) {
+ _averageN = averageN;
+ reset = true;
+ }
+ if (_quality != quality) {
+ _quality = quality;
+ reset = true;
+ }
+ if (_window != window) {
+ _window = window;
+ reset = true;
+ }
+ if (reset) {
+ resetChannels();
+ }
+}
+
+void AnalyzerCore::resetChannels() {
+ std::lock_guard<std::mutex> lock(_channelsMutex);
+ for (int i = 0; i < _nChannels; ++i) {
+ if (_channels[i]) {
+ delete _channels[i];
+ _channels[i] = NULL;
+ }
+ }
+}
+
+SpectrumAnalyzer::Size AnalyzerCore::size() {
+ if (engineGetSampleRate() < 96000.0f) {
+ switch (_quality) {
+ case QUALITY_ULTRA: {
+ return SpectrumAnalyzer::SIZE_8192;
+ }
+ case QUALITY_HIGH: {
+ return SpectrumAnalyzer::SIZE_4096;
+ }
+ default: {
+ return SpectrumAnalyzer::SIZE_1024;
+ }
+ }
+ }
+ else {
+ switch (_quality) {
+ case QUALITY_ULTRA: {
+ return SpectrumAnalyzer::SIZE_16384;
+ }
+ case QUALITY_HIGH: {
+ return SpectrumAnalyzer::SIZE_8192;
+ }
+ default: {
+ return SpectrumAnalyzer::SIZE_2048;
+ }
+ }
+ }
+}
+
+SpectrumAnalyzer::WindowType AnalyzerCore::window() {
+ switch (_window) {
+ case WINDOW_NONE: {
+ return SpectrumAnalyzer::WINDOW_NONE;
+ }
+ case WINDOW_HAMMING: {
+ return SpectrumAnalyzer::WINDOW_HAMMING;
+ }
+ default: {
+ return SpectrumAnalyzer::WINDOW_KAISER;
+ }
+ }
+}
+
+void AnalyzerCore::stepChannel(int channelIndex, Input& input) {
+ assert(channelIndex >= 0);
+ assert(channelIndex < _nChannels);
+
+ if (input.active) {
+ if (!_channels[channelIndex]) {
+ std::lock_guard<std::mutex> lock(_channelsMutex);
+ _channels[channelIndex] = new ChannelAnalyzer(
+ size(),
+ _overlap,
+ window(),
+ engineGetSampleRate(),
+ _averageN,
+ _binAverageN
+ );
+ }
+ _channels[channelIndex]->step(input.value);
+ }
+ else if (_channels[channelIndex]) {
+ std::lock_guard<std::mutex> lock(_channelsMutex);
+ delete _channels[channelIndex];
+ _channels[channelIndex] = NULL;
+ }
+}
+
+
+void AnalyzerDisplay::draw(NVGcontext* vg) {
+ std::lock_guard<std::mutex> lock(_module->_core._channelsMutex);
+
+ drawBackground(vg);
+ float strokeWidth = std::max(1.0f, 3 - gRackScene->zoomWidget->zoom);
+ // _xAxisLogFactor = (_module->_rangeMaxHz - _module->_rangeMinHz) / (0.5f * engineGetSampleRate());
+ // _xAxisLogFactor *= 1.0f - baseXAxisLogFactor;
+ // _xAxisLogFactor = 1.0f - _xAxisLogFactor;
+
+ nvgSave(vg);
+ nvgScissor(vg, _insetAround, _insetAround, _size.x - _insetAround, _size.y - _insetAround);
+ drawHeader(vg);
+ drawYAxis(vg, strokeWidth);
+ drawXAxis(vg, strokeWidth);
+ for (int i = 0; i < _module->_core._nChannels; ++i) {
+ ChannelAnalyzer* channel = _module->_core._channels[i];
+ if (channel) {
+ drawGraph(vg, channel->getBins(), channel->_binsN, _channelColors[i % channelColorsN], strokeWidth);
+ }
+ }
+ nvgRestore(vg);
+}
+
+void AnalyzerDisplay::drawBackground(NVGcontext* vg) {
+ nvgSave(vg);
+ nvgBeginPath(vg);
+ nvgRect(vg, 0, 0, _size.x, _size.y);
+ nvgFillColor(vg, nvgRGBA(0x00, 0x00, 0x00, 0xff));
+ nvgFill(vg);
+ nvgStrokeColor(vg, nvgRGBA(0xc0, 0xc0, 0xc0, 0xff));
+ nvgStroke(vg);
+ nvgRestore(vg);
+}
+
+void AnalyzerDisplay::drawHeader(NVGcontext* vg) {
+ nvgSave(vg);
+
+ const int textY = -4;
+ const int charPx = 5;
+ const int sLen = 100;
+ char s[sLen];
+ int x = _insetAround + 2;
+
+ int n = snprintf(s, sLen, "Peaks (+/-%0.1f):", (engineGetSampleRate() / 2.0f) / (float)(_module->_core.size() / _module->_core._binAverageN));
+ drawText(vg, s, x, _insetTop + textY);
+ x += n * charPx - 0;
+
+ for (int i = 0; i < _module->_core._nChannels; ++i) {
+ ChannelAnalyzer* channel = _module->_core._channels[i];
+ if (channel) {
+ snprintf(s, sLen, "%c:%7.1f", 'A' + i, channel->getPeak());
+ drawText(vg, s, x, _insetTop + textY, 0.0, &_channelColors[i % channelColorsN]);
+ }
+ x += 9 * charPx + 3;
+ }
+
+ nvgRestore(vg);
+}
+
+void AnalyzerDisplay::drawYAxis(NVGcontext* vg, float strokeWidth) {
+ nvgSave(vg);
+ nvgStrokeColor(vg, _axisColor);
+ nvgStrokeWidth(vg, strokeWidth);
+ const int lineX = _insetLeft - 2;
+ const int textX = 9;
+ const float textR = -M_PI/2.0;
+
+ nvgBeginPath(vg);
+ int lineY = _insetTop;
+ nvgMoveTo(vg, lineX, lineY);
+ nvgLineTo(vg, _size.x - _insetRight, lineY);
+ nvgStroke(vg);
+
+ nvgBeginPath(vg);
+ lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB + 12.0)/_displayDB);
+ nvgMoveTo(vg, lineX, lineY);
+ nvgLineTo(vg, _size.x - _insetRight, lineY);
+ nvgStroke(vg);
+ drawText(vg, "12", textX, lineY + 5.0, textR);
+
+ nvgBeginPath(vg);
+ lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB)/_displayDB);
+ nvgMoveTo(vg, lineX, lineY);
+ nvgLineTo(vg, _size.x - _insetRight, lineY);
+ nvgStrokeWidth(vg, strokeWidth * 1.5);
+ nvgStroke(vg);
+ nvgStrokeWidth(vg, strokeWidth);
+ drawText(vg, "0", textX, lineY + 2.3, textR);
+
+ nvgBeginPath(vg);
+ lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB - 12.0)/_displayDB);
+ nvgMoveTo(vg, lineX, lineY);
+ nvgLineTo(vg, _size.x - _insetRight, lineY);
+ nvgStroke(vg);
+ drawText(vg, "-12", textX, lineY + 10, textR);
+
+ nvgBeginPath(vg);
+ lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB - 24.0)/_displayDB);
+ nvgMoveTo(vg, lineX, lineY);
+ nvgLineTo(vg, _size.x - _insetRight, lineY);
+ nvgStroke(vg);
+ drawText(vg, "-24", textX, lineY + 10, textR);
+
+ nvgBeginPath(vg);
+ lineY = _insetTop + (_graphSize.y - _graphSize.y*(_displayDB - _positiveDisplayDB - 48.0)/_displayDB);
+ nvgMoveTo(vg, lineX, lineY);
+ nvgLineTo(vg, _size.x - _insetRight, lineY);
+ nvgStroke(vg);
+ drawText(vg, "-48", textX, lineY + 10, textR);
+
+ nvgBeginPath(vg);
+ lineY = _insetTop + _graphSize.y + 1;
+ nvgMoveTo(vg, lineX, lineY);
+ nvgLineTo(vg, _size.x - _insetRight, lineY);
+ nvgStroke(vg);
+
+ nvgBeginPath(vg);
+ nvgMoveTo(vg, lineX, _insetTop);
+ nvgLineTo(vg, lineX, lineY);
+ nvgStroke(vg);
+
+ drawText(vg, "dB", textX, _size.y - _insetBottom, textR);
+
+ nvgRestore(vg);
+}
+
+void AnalyzerDisplay::drawXAxis(NVGcontext* vg, float strokeWidth) {
+ nvgSave(vg);
+ nvgStrokeColor(vg, _axisColor);
+ nvgStrokeWidth(vg, strokeWidth);
+
+ float hz = 100.0f;
+ while (hz < _module->_rangeMaxHz && hz < 1001.0) {
+ if (hz >= _module->_rangeMinHz) {
+ drawXAxisLine(vg, hz);
+ }
+ hz += 100.0;
+ }
+ hz = 2000.0;
+ while (hz < _module->_rangeMaxHz && hz < 10001.0) {
+ if (hz >= _module->_rangeMinHz) {
+ drawXAxisLine(vg, hz);
+ }
+ hz += 1000.0;
+ }
+ hz = 20000.0;
+ while (hz < _module->_rangeMaxHz && hz < 100001.0) {
+ if (hz >= _module->_rangeMinHz) {
+ drawXAxisLine(vg, hz);
+ }
+ hz += 10000.0;
+ }
+
+ drawText(vg, "Hz", _insetLeft, _size.y - 2);
+ if (_module->_rangeMinHz <= 100.0f) {
+ float x = (100.0 - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
+ x = powf(x, _xAxisLogFactor);
+ if (x < 1.0) {
+ x *= _graphSize.x;
+ drawText(vg, "100", _insetLeft + x - 8, _size.y - 2);
+ }
+ }
+ if (_module->_rangeMinHz <= 1000.0f) {
+ float x = (1000.0 - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
+ x = powf(x, _xAxisLogFactor);
+ if (x < 1.0) {
+ x *= _graphSize.x;
+ drawText(vg, "1k", _insetLeft + x - 4, _size.y - 2);
+ }
+ }
+ if (_module->_rangeMinHz <= 10000.0f) {
+ float x = (10000.0 - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
+ x = powf(x, _xAxisLogFactor);
+ if (x < 1.0) {
+ x *= _graphSize.x;
+ drawText(vg, "10k", _insetLeft + x - 4, _size.y - 2);
+ }
+ }
+ if (_module->_rangeMinHz > 1000.0f) {
+ hz = 20000.0f;
+ float lastX = 0.0f;
+ while (hz < _module->_rangeMaxHz) {
+ if (_module->_rangeMinHz <= hz) {
+ float x = (hz - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
+ x = powf(x, _xAxisLogFactor);
+ if (x > lastX + 0.075f && x < 1.0f) {
+ lastX = x;
+ x *= _graphSize.x;
+ const int sLen = 32;
+ char s[sLen];
+ snprintf(s, sLen, "%dk", (int)(hz / 1000.0f));
+ drawText(vg, s, _insetLeft + x - 7, _size.y - 2);
+ }
+ }
+ hz += 10000.0f;
+ }
+ }
+
+ nvgRestore(vg);
+}
+
+void AnalyzerDisplay::drawXAxisLine(NVGcontext* vg, float hz) {
+ float x = (hz - _module->_rangeMinHz) / (_module->_rangeMaxHz - _module->_rangeMinHz);
+ x = powf(x, _xAxisLogFactor);
+ if (x < 1.0) {
+ x *= _graphSize.x;
+ nvgBeginPath(vg);
+ nvgMoveTo(vg, _insetLeft + x, _insetTop);
+ nvgLineTo(vg, _insetLeft + x, _insetTop + _graphSize.y);
+ nvgStroke(vg);
+ }
+}
+
+void AnalyzerDisplay::drawGraph(NVGcontext* vg, const float* bins, int binsN, NVGcolor color, float strokeWidth) {
+ float range = (_module->_rangeMaxHz - _module->_rangeMinHz) / (0.5f * engineGetSampleRate());
+ int pointsN = roundf(range * (_module->_core.size() / 2));
+ range = _module->_rangeMinHz / (0.5f * engineGetSampleRate());
+ int pointsOffset = roundf(range * (_module->_core.size() / 2));
+ nvgSave(vg);
+ nvgScissor(vg, _insetLeft, _insetTop, _graphSize.x, _graphSize.y);
+ nvgStrokeColor(vg, color);
+ nvgStrokeWidth(vg, strokeWidth);
+ nvgBeginPath(vg);
+ for (int i = 0; i < pointsN; ++i) {
+ int height = binValueToHeight(bins[pointsOffset + i]);
+ if (i == 0) {
+ nvgMoveTo(vg, _insetLeft, _insetTop + (_graphSize.y - height));
+ }
+ else {
+ float x = _graphSize.x * powf(i / (float)pointsN, _xAxisLogFactor);
+ nvgLineTo(vg, _insetLeft + x, _insetTop + (_graphSize.y - height));
+ }
+ }
+ nvgStroke(vg);
+ nvgRestore(vg);
+}
+
+void AnalyzerDisplay::drawText(NVGcontext* vg, const char* s, float x, float y, float rotation, const NVGcolor* color) {
+ nvgSave(vg);
+ nvgTranslate(vg, x, y);
+ nvgRotate(vg, rotation);
+ nvgFontSize(vg, 10);
+ nvgFontFaceId(vg, _font->handle);
+ nvgFillColor(vg, color ? *color : _textColor);
+ nvgText(vg, 0, 0, s, NULL);
+ nvgRestore(vg);
+}
+
+int AnalyzerDisplay::binValueToHeight(float value) {
+ const float minDB = -(_displayDB - _positiveDisplayDB);
+ if (value < 0.00001f) {
+ return 0;
+ }
+ value /= 10.0f; // arbitrarily use 5.0f as reference "maximum" baseline signal (e.g. raw output of an oscillator)...but signals are +/-5, so 10 total.
+ value = powf(value, 0.5f); // undoing magnitude scaling of levels back from the FFT?
+ value = amplitudeToDecibels(value);
+ value = std::max(minDB, value);
+ value = std::min(_positiveDisplayDB, value);
+ value -= minDB;
+ value /= _displayDB;
+ return roundf(_graphSize.y * value);
+}
diff --git a/src/analyzer_base.hpp b/src/analyzer_base.hpp
@@ -0,0 +1,165 @@
+
+#pragma once
+
+#include <thread>
+#include <mutex>
+#include <condition_variable>
+
+#include "bogaudio.hpp"
+#include "dsp/analyzer.hpp"
+
+using namespace bogaudio::dsp;
+
+namespace bogaudio {
+
+struct ChannelAnalyzer {
+ SpectrumAnalyzer _analyzer;
+ int _binsN;
+ float* _bins;
+ AveragingBuffer<float>* _averagedBins;
+ const int _stepBufN;
+ float* _stepBuf;
+ int _stepBufI = 0;
+ const int _workerBufN;
+ float* _workerBuf;
+ int _workerBufWriteI = 0;
+ int _workerBufReadI = 0;
+ bool _workerStop = false;
+ std::mutex _workerMutex;
+ std::condition_variable _workerCV;
+ std::thread _worker;
+
+ ChannelAnalyzer(
+ SpectrumAnalyzer::Size size,
+ SpectrumAnalyzer::Overlap overlap,
+ SpectrumAnalyzer::WindowType windowType,
+ float sampleRate,
+ int averageN,
+ int binSize
+ )
+ : _analyzer(size, overlap, windowType, sampleRate, false)
+ , _binsN(size / binSize)
+ , _bins(averageN == 1 ? new float[_binsN] {} : NULL)
+ , _averagedBins(averageN == 1 ? NULL : new AveragingBuffer<float>(_binsN, averageN))
+ , _stepBufN(size / overlap)
+ , _stepBuf(new float[_stepBufN] {})
+ , _workerBufN(size)
+ , _workerBuf(new float[_workerBufN] {})
+ , _worker(&ChannelAnalyzer::work, this)
+ {
+ assert(averageN >= 1);
+ assert(binSize >= 1);
+ }
+ virtual ~ChannelAnalyzer();
+
+ const float* getBins();
+ float getPeak();
+ void step(float sample);
+ void work();
+};
+
+struct AnalyzerCore {
+ enum Quality {
+ QUALITY_ULTRA,
+ QUALITY_HIGH,
+ QUALITY_GOOD
+ };
+
+ enum Window {
+ WINDOW_NONE,
+ WINDOW_HAMMING,
+ WINDOW_KAISER
+ };
+
+ int _nChannels;
+ ChannelAnalyzer** _channels;
+ int _averageN = 1;
+ Quality _quality = QUALITY_GOOD;
+ Window _window = WINDOW_KAISER;
+ const SpectrumAnalyzer::Overlap _overlap = SpectrumAnalyzer::OVERLAP_2;
+ const int _binAverageN = 2;
+ std::mutex _channelsMutex;
+
+ AnalyzerCore(int nChannels)
+ : _nChannels(nChannels)
+ , _channels(new ChannelAnalyzer*[_nChannels] {})
+ {}
+ virtual ~AnalyzerCore() {
+ resetChannels();
+ delete[] _channels;
+ }
+
+ void setParams(int averageN, Quality quality, Window window);
+ void resetChannels();
+ SpectrumAnalyzer::Size size();
+ SpectrumAnalyzer::WindowType window();
+ void stepChannel(int channelIndex, Input& input);
+};
+
+struct AnalyzerBase : Module {
+ float _rangeMinHz = 0.0;
+ float _rangeMaxHz = 0.0;
+ AnalyzerCore _core;
+
+ AnalyzerBase(int nChannels, int np, int ni, int no, int nl)
+ : Module(np, ni, no, nl)
+ , _core(nChannels)
+ {}
+};
+
+struct AnalyzerDisplay : TransparentWidget {
+ const int _insetAround = 2;
+ const int _insetLeft = _insetAround + 12;
+ const int _insetRight = _insetAround + 2;
+ const int _insetTop = _insetAround + 13;
+ const int _insetBottom = _insetAround + 9;
+
+ const float _displayDB = 80.0;
+ const float _positiveDisplayDB = 20.0;
+
+ const float baseXAxisLogFactor = 1 / 3.321; // magic number.
+
+ const NVGcolor _axisColor = nvgRGBA(0xff, 0xff, 0xff, 0x70);
+ const NVGcolor _textColor = nvgRGBA(0xff, 0xff, 0xff, 0xc0);
+ static constexpr int channelColorsN = 8;
+ const NVGcolor _channelColors[channelColorsN] = {
+ nvgRGBA(0x00, 0xff, 0x00, 0xd0),
+ nvgRGBA(0xff, 0x00, 0xff, 0xd0),
+ nvgRGBA(0xff, 0x80, 0x00, 0xd0),
+ nvgRGBA(0x00, 0x80, 0xff, 0xd0),
+
+ nvgRGBA(0xff, 0x00, 0x00, 0xd0),
+ nvgRGBA(0xff, 0xff, 0x00, 0xd0),
+ nvgRGBA(0x00, 0xff, 0xff, 0xd0),
+ nvgRGBA(0xff, 0x80, 0x80, 0xd0)
+ };
+
+ AnalyzerBase* _module;
+ const Vec _size;
+ const Vec _graphSize;
+ std::shared_ptr<Font> _font;
+ float _xAxisLogFactor = baseXAxisLogFactor;
+
+ AnalyzerDisplay(
+ AnalyzerBase* module,
+ Vec size
+ )
+ : _module(module)
+ , _size(size)
+ , _graphSize(_size.x - _insetLeft - _insetRight, _size.y - _insetTop - _insetBottom)
+ , _font(Font::load(assetPlugin(plugin, "res/fonts/inconsolata.ttf")))
+ {
+ }
+
+ void draw(NVGcontext* vg) override;
+ void drawBackground(NVGcontext* vg);
+ void drawHeader(NVGcontext* vg);
+ void drawYAxis(NVGcontext* vg, float strokeWidth);
+ void drawXAxis(NVGcontext* vg, float strokeWidth);
+ void drawXAxisLine(NVGcontext* vg, float hz);
+ void drawGraph(NVGcontext* vg, const float* bins, int binsN, NVGcolor color, float strokeWidth);
+ void drawText(NVGcontext* vg, const char* s, float x, float y, float rotation = 0.0, const NVGcolor* color = NULL);
+ int binValueToHeight(float value);
+};
+
+} // namespace bogaudio
diff --git a/src/bogaudio.cpp b/src/bogaudio.cpp
@@ -6,6 +6,7 @@
#include "Additator.hpp"
#include "AMRM.hpp"
#include "Analyzer.hpp"
+#include "AnalyzerXL.hpp"
#include "Blank3.hpp"
#include "Blank6.hpp"
#include "Bool.hpp"
@@ -86,9 +87,9 @@ void init(rack::Plugin *p) {
p->addModel(modelMix4);
p->addModel(modelMix8);
p->addModel(modelVCM);
- #ifdef EXPERIMENTAL
- p->addModel(modelMatrix88);
- #endif
+#ifdef EXPERIMENTAL
+ p->addModel(modelMatrix88);
+#endif
p->addModel(modelUMix);
p->addModel(modelMute8);
p->addModel(modelPan);
@@ -103,6 +104,9 @@ void init(rack::Plugin *p) {
p->addModel(modelNsgt);
p->addModel(modelAnalyzer);
+#ifdef EXPERIMENTAL
+ p->addModel(modelAnalyzerXL);
+#endif
p->addModel(modelVU);
p->addModel(modelDetune);