BogaudioModules

BogaudioModules for VCV Rack
Log | Files | Refs | README | LICENSE

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:
Ares-src/AnalyzerXL-src.svg | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Ares/AnalyzerXL.svg | 0
Msrc/Analyzer.cpp | 626+++----------------------------------------------------------------------------
Msrc/Analyzer.hpp | 42+++---------------------------------------
Asrc/AnalyzerXL.cpp | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AnalyzerXL.hpp | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/UMix.cpp | 2+-
Asrc/analyzer_base.cpp | 464+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/analyzer_base.hpp | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/bogaudio.cpp | 10+++++++---
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);