BogaudioModules

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

commit 85d66a357f252daf04e52050199930a503fe70d3
parent 412b236ace3096435a8b5cac122b24f3f8944cea
Author: Matt Demanett <matt@demanett.net>
Date:   Fri, 25 Sep 2020 00:15:36 -0400

Analyzers: on mouse down and hold on the display, freeze the display and show details about the values under the mouse. #141

Diffstat:
MREADME-prerelease.md | 4++++
Msrc/Ranalyzer.cpp | 7++++++-
Msrc/analyzer_base.cpp | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/analyzer_base.hpp | 41+++++++++++++++++++++++------------------
Msrc/dsp/signal.hpp | 3+++
Asrc/utils.cpp | 13+++++++++++++
Msrc/utils.hpp | 3+++
7 files changed, 251 insertions(+), 23 deletions(-)

diff --git a/README-prerelease.md b/README-prerelease.md @@ -944,6 +944,10 @@ Features: - Each channel has a THRU output, which passes the corresponding input through unchanged. - On the context (right-click) menu, the display vertical (amplitude) range can be set to extend down to -120dB (the default is -60dB); alternately it can be set to to display linearly, with the amplitude expressed as a percentage, where 100% is equivalent to 0dB. - By default the frequency axis has a logarithmic plot; this can be set to linear on the context menu. + - When one clicks and holds on the display, the display freezes, and: + - The frequency analysis bin under the mouse pointer is highlighted. + - An overlay box is displayed, with details about the bin number and frequency range, and the level in decibels for each signal at that frequency range. + - Dragging the mouse left and right will update the highlight and overlay. _Polyphony:_ Monophonic, with two exceptions: - If an input is polyphonic, its channels are summed, and the spectra of the summed signal is displayed. diff --git a/src/Ranalyzer.cpp b/src/Ranalyzer.cpp @@ -268,7 +268,9 @@ void Ranalyzer::setWindow(WindowType wt) { struct AnalysisBinsReader : AnalyzerDisplay::BinsReader { - AnalysisBinsReader(Ranalyzer* module) : AnalyzerDisplay::BinsReader(module) {} + AnalyzerBase* _base; + + AnalysisBinsReader(AnalyzerBase* base) : _base(base) {} float at(int i) override { assert(_base->_core._nChannels == 3); @@ -342,6 +344,9 @@ struct RanalyzerWidget : AnalyzerBaseWidget { auto display = new RanalyzerDisplay(module, size, false); display->box.pos = inset; display->box.size = size; + display->channelLabel(0, "Test"); + display->channelLabel(1, "Response"); + display->channelLabel(2, "Analysis"); if (module) { display->setChannelBinsReader(2, new AnalysisBinsReader(module)); module->setChannelDisplayListener(display); diff --git a/src/analyzer_base.cpp b/src/analyzer_base.cpp @@ -1,6 +1,7 @@ #include "analyzer_base.hpp" #include "dsp/signal.hpp" +#include <vector> ChannelAnalyzer::~ChannelAnalyzer() { { @@ -367,10 +368,51 @@ void AnalyzerBaseWidget::addAmplitudePlotContextMenu(Menu* menu, bool linearOpti } +void AnalyzerDisplay::onButton(const event::Button& e) { + if (!(e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == 0)) { + return; + } + e.consume(this); + _freezeMouse = e.pos; + + if (_freezeBufs) { + delete[] _freezeBufs; + } + _freezeBufs = new float[_module->_core._nChannels * _module->_core._outBufferN]; + for (int i = 0; i < _module->_core._nChannels; ++i) { + if (_channelBinsReaders[i]) { + float* dest = _freezeBufs + i * _module->_core._outBufferN; + for (int j = 0; j < _module->_core._outBufferN; ++j) { + *(dest + j) = _channelBinsReaders[i]->at(j); + } + } + else { + float* bins = _module->_core.getBins(i); + std::copy(bins, bins + _module->_core._outBufferN, _freezeBufs + i * _module->_core._outBufferN); + } + } +} + +void AnalyzerDisplay::onDragMove(const event::DragMove& e) { + float zoom = APP->scene->rackScroll->zoomWidget->zoom; + _freezeMouse.x += e.mouseDelta.x / zoom; + _freezeMouse.y += e.mouseDelta.y / zoom; + _freezeDraw = _freezeMouse.x > _insetLeft && _freezeMouse.x < _size.x - _insetRight && _freezeMouse.y > _insetTop && _freezeMouse.y < _size.y - _insetBottom; +} + +void AnalyzerDisplay::onDragEnd(const event::DragEnd& e) { + _freezeMouse = Vec(0, 0); + _freezeDraw = false; + if (_freezeBufs) { + delete[] _freezeBufs; + _freezeBufs = NULL; + } +} + void AnalyzerDisplay::setChannelBinsReader(int channel, BinsReader* br) { assert(_channelBinsReaders); assert(_module); - assert(channel < _module->_core._nChannels); + assert(channel >= 0 && channel < _module->_core._nChannels); if (_channelBinsReaders[channel]) { delete _channelBinsReaders[channel]; } @@ -378,12 +420,18 @@ void AnalyzerDisplay::setChannelBinsReader(int channel, BinsReader* br) { } void AnalyzerDisplay::displayChannel(int channel, bool display) { + assert(channel >= 0 && channel < _module->_core._nChannels); assert(_displayChannel); assert(_module); assert(channel < _module->_core._nChannels); _displayChannel[channel] = display; } +void AnalyzerDisplay::channelLabel(int channel, std::string label) { + assert(channel >= 0 && channel < _module->_core._nChannels); + _channelLabels[channel] = label; +} + void AnalyzerDisplay::draw(const DrawArgs& args) { if (_module) { _module->_core._channelsMutex.lock(); @@ -423,10 +471,18 @@ void AnalyzerDisplay::draw(const DrawArgs& args) { drawYAxis(args, strokeWidth, amplitudePlot); drawXAxis(args, strokeWidth, frequencyPlot, rangeMinHz, rangeMaxHz); if (_module) { + int freezeBinI = 0; + float freezeLowHz = 0.0f; + float freezeHighHz = 0.0f; + if (_freezeDraw) { + freezeValues(rangeMinHz, rangeMaxHz, freezeBinI, freezeLowHz, freezeHighHz); + drawFreezeUnder(args, freezeLowHz, freezeHighHz, rangeMinHz, rangeMaxHz, strokeWidth); + } + for (int i = 0; i < _module->_core._nChannels; ++i) { if (_displayChannel[i]) { if (_module->_core._channels[i]) { - GenericBinsReader br(_module, i); + GenericBinsReader br(_freezeBufs ? _freezeBufs + i * _module->_core._outBufferN : _module->_core.getBins(i)); drawGraph(args, br, _channelColors[i % channelColorsN], strokeWidth, frequencyPlot, rangeMinHz, rangeMaxHz, amplitudePlot); } else if (_channelBinsReaders[i]) { @@ -434,6 +490,10 @@ void AnalyzerDisplay::draw(const DrawArgs& args) { } } } + + if (_freezeDraw) { + drawFreezeOver(args, freezeBinI, _module->_core._size / _module->_core._binAverageN, freezeLowHz, freezeHighHz, strokeWidth); + } } nvgRestore(args.vg); @@ -776,11 +836,146 @@ void AnalyzerDisplay::drawGraph( nvgRestore(args.vg); } -void AnalyzerDisplay::drawText(const DrawArgs& args, const char* s, float x, float y, float rotation, const NVGcolor* color) { +void AnalyzerDisplay::freezeValues(float rangeMinHz, float rangeMaxHz, int& binI, float& lowHz, float& highHz) { + int binsN = _module->_core._size / _module->_core._binAverageN; + float binHz = (0.5f * APP->engine->getSampleRate()) / (float)binsN; + float mouseHz = powf((_freezeMouse.x - _insetLeft) / (float)_graphSize.x, 1.0f / _xAxisLogFactor); + mouseHz *= rangeMaxHz - rangeMinHz; + mouseHz += rangeMinHz; + binI = mouseHz / binHz; + lowHz = binI * binHz; + highHz = (binI + 1) * binHz; +} + +void AnalyzerDisplay::drawFreezeUnder(const DrawArgs& args, float lowHz, float highHz, float rangeMinHz, float rangeMaxHz, float strokeWidth) { + float x1 = _graphSize.x * powf((lowHz - rangeMinHz) / (rangeMaxHz - rangeMinHz), _xAxisLogFactor); + float x2 = _graphSize.x * powf((highHz - rangeMinHz) / (rangeMaxHz - rangeMinHz), _xAxisLogFactor); + if (x2 - x1 < strokeWidth) { + float x = strokeWidth - (x2 - x1); + x /= 2.0f; + x1 -= x; + x2 += x; + } + + nvgSave(args.vg); + nvgScissor(args.vg, _insetLeft, _insetTop, _graphSize.x, _graphSize.y); + nvgBeginPath(args.vg); + nvgRect(args.vg, _insetLeft + x1, _insetTop, x2 - x1, _size.y - _insetBottom); + nvgFillColor(args.vg, nvgRGBA(0xaa, 0xaa, 0xaa, 0xd0)); + nvgFill(args.vg); + nvgRestore(args.vg); +} + +void AnalyzerDisplay::drawFreezeOver(const DrawArgs& args, int binI, int binsN, float lowHz, float highHz, float strokeWidth) { + nvgSave(args.vg); + auto formatHz = [](float hz) -> std::string { + if (hz < 1000.0f) { + return format("%0.2f Hz", hz); + } + return format("%0.3f KHz", hz / 1000.0f); + }; + + std::vector<std::string> labels; + std::vector<std::string> values; + std::vector<const NVGcolor*> colors; + labels.push_back("Bin"); + values.push_back(format("%d of %d", binI + 1, binsN)); + colors.push_back(NULL); + labels.push_back("Bin Low Hz"); + values.push_back(formatHz(lowHz)); + colors.push_back(NULL); + labels.push_back("Bin High Hz"); + values.push_back(formatHz(highHz)); + colors.push_back(NULL); + for (int i = 0; i < _module->_core._nChannels; ++i) { + if (_displayChannel[i] && (_module->_core._channels[i] || _channelBinsReaders[i])) { + if (_channelLabels[i].empty()) { + labels.push_back(format("Channel %d", i + 1)); + } + else { + labels.push_back(_channelLabels[i]); + } + float bv = *(_freezeBufs + i * _module->_core._outBufferN + binI); + values.push_back(format("%0.2f dB", binValueToDb(bv))); + colors.push_back(&_channelColors[i % channelColorsN]); + } + } + assert(labels.size() == values.size()); + + size_t maxLabel = 0; + for (auto& label : labels) { + if (label.size() > maxLabel) { + maxLabel = label.size(); + } + } + + std::vector<std::string> lines; + size_t maxLine = 0; + for (size_t i = 0; i < labels.size(); ++i) { + char spaces[maxLabel + 1]; + int nSpaces = maxLabel - labels[i].size(); + memset(spaces, ' ', nSpaces); + spaces[nSpaces] = '\0'; + std::string line = format("%s%s: %s", spaces, labels[i].c_str(), values[i].c_str()); + lines.push_back(line); + if (line.size() > maxLine) { + maxLine = line.size(); + } + } + + const float charWidth = 8.0f; + const float charHeight = 16.0f; + const float inset = 10.0f; + const float lineSep = 3.0f; + const float mousePad = 15.0f; + const float edgePad = 10.0f; + Vec boxDim( + maxLine * charWidth + 2 * inset, + lines.size() * charHeight + (lines.size() - 1) * lineSep + 2 * inset + ); + Vec boxPos( + _freezeMouse.x + mousePad, + _freezeMouse.y - boxDim.y / 2.0f + ); + if (boxPos.x + boxDim.x > _size.x - _insetRight) { + boxPos.x = _freezeMouse.x - mousePad - boxDim.x; + } + if (_freezeMouse.y - boxDim.y / 2.0f < _insetTop + edgePad) { + boxPos.y = _insetTop + edgePad; + } + if (_freezeMouse.y + boxDim.y / 2.0f > _size.y - _insetBottom - edgePad) { + boxPos.y = _size.y - _insetBottom - edgePad - boxDim.y; + } + + nvgBeginPath(args.vg); + nvgRect(args.vg, boxPos.x, boxPos.y, boxDim.x, boxDim.y); + nvgFillColor(args.vg, nvgRGBA(0x00, 0x00, 0x00, 0xff)); + nvgFill(args.vg); + + nvgStrokeColor(args.vg, _axisColor); // nvgRGBA(0x00, 0xff, 0x00, 0xd0)); + nvgStrokeWidth(args.vg, strokeWidth); + nvgBeginPath(args.vg); + nvgMoveTo(args.vg, boxPos.x, boxPos.y); + nvgLineTo(args.vg, boxPos.x + boxDim.x, boxPos.y); + nvgLineTo(args.vg, boxPos.x + boxDim.x, boxPos.y + boxDim.y); + nvgLineTo(args.vg, boxPos.x, boxPos.y + boxDim.y); + nvgLineTo(args.vg, boxPos.x, boxPos.y); + nvgStroke(args.vg); + + float y = boxPos.y + inset; + for (size_t i = 0; i < labels.size(); ++i) { + drawText(args, lines[i].c_str(), boxPos.x + inset, y + 13.0f, 0.0f, colors[i], 16); + y += charHeight + lineSep; + } + + nvgRestore(args.vg); +} + +void AnalyzerDisplay::drawText(const DrawArgs& args, const char* s, float x, float y, float rotation, const NVGcolor* color, int fontSize) { nvgSave(args.vg); nvgTranslate(args.vg, x, y); nvgRotate(args.vg, rotation); - nvgFontSize(args.vg, 10); + nvgFontSize(args.vg, fontSize); nvgFontFaceId(args.vg, _font->handle); nvgFillColor(args.vg, color ? *color : _textColor); nvgText(args.vg, 0, 0, s, NULL); diff --git a/src/analyzer_base.hpp b/src/analyzer_base.hpp @@ -177,18 +177,15 @@ struct AnalyzerBaseWidget : BGModuleWidget { struct AnalyzerDisplay : TransparentWidget, AnalyzerTypes { struct BinsReader { - AnalyzerBase* _base; - - BinsReader(AnalyzerBase* base) : _base(base) {} + BinsReader() {} virtual ~BinsReader() {} virtual float at(int i) = 0; }; struct GenericBinsReader : BinsReader { - int _channel; - - GenericBinsReader(AnalyzerBase* base, int channel) : BinsReader(base), _channel(channel) {} - float at(int i) override { return _base->_core.getBins(_channel)[i]; } + float* _bins; + GenericBinsReader(float* bins) : _bins(bins) {} + float at(int i) override { return _bins[i]; } }; const int _insetAround = 2; @@ -226,6 +223,10 @@ struct AnalyzerDisplay : TransparentWidget, AnalyzerTypes { float _xAxisLogFactor = baseXAxisLogFactor; BinsReader** _channelBinsReaders = NULL; bool* _displayChannel = NULL; + std::string* _channelLabels = NULL; + Vec _freezeMouse; + bool _freezeDraw = false; + float* _freezeBufs = NULL; AnalyzerDisplay( AnalyzerBase* module, @@ -241,28 +242,29 @@ struct AnalyzerDisplay : TransparentWidget, AnalyzerTypes { if (_module) { _channelBinsReaders = new BinsReader*[_module->_core._nChannels] {}; _displayChannel = new bool[_module->_core._nChannels] {}; + _channelLabels = new std::string[_module->_core._nChannels]; std::fill_n(_displayChannel, _module->_core._nChannels, true); } } ~AnalyzerDisplay() { if (_module) { - if (_channelBinsReaders) { - for (int i = 0; i < _module->_core._nChannels; ++i) { - if (_channelBinsReaders) { - delete _channelBinsReaders[i]; - } + for (int i = 0; i < _module->_core._nChannels; ++i) { + if (_channelBinsReaders) { + delete _channelBinsReaders[i]; } - delete[] _channelBinsReaders; - } - - if (_displayChannel) { - delete[] _displayChannel; } + delete[] _channelBinsReaders; + delete[] _displayChannel; + delete[] _channelLabels; } } + void onButton(const event::Button& e) override; + void onDragMove(const event::DragMove& e) override; + void onDragEnd(const event::DragEnd& e) override; void setChannelBinsReader(int channel, BinsReader* br); void displayChannel(int channel, bool display); + void channelLabel(int channel, std::string label); void draw(const DrawArgs& args) override; void drawBackground(const DrawArgs& args); virtual void drawHeader(const DrawArgs& args); @@ -270,7 +272,10 @@ struct AnalyzerDisplay : TransparentWidget, AnalyzerTypes { void drawXAxis(const DrawArgs& args, float strokeWidth, FrequencyPlot plot, float rangeMinHz, float rangeMaxHz); void drawXAxisLine(const DrawArgs& args, float hz, float rangeMinHz, float rangeMaxHz); void drawGraph(const DrawArgs& args, BinsReader& bins, NVGcolor color, float strokeWidth, FrequencyPlot freqPlot, float rangeMinHz, float rangeMaxHz, AmplitudePlot ampPlot); - void drawText(const DrawArgs& args, const char* s, float x, float y, float rotation = 0.0, const NVGcolor* color = NULL); + void freezeValues(float rangeMinHz, float rangeMaxHz, int& binI, float& lowHz, float& highHz); + void drawFreezeUnder(const DrawArgs& args, float lowHz, float highHz, float rangeMinHz, float rangeMaxHz, float strokeWidth); + void drawFreezeOver(const DrawArgs& args, int binI, int binsN, float lowHz, float highHz, float strokeWidth); + void drawText(const DrawArgs& args, const char* s, float x, float y, float rotation = 0.0, const NVGcolor* color = NULL, int fontSize = 10); int binValueToHeight(float value, AmplitudePlot plot); static float binValueToAmplitude(float value); static float binValueToDb(float value); diff --git a/src/dsp/signal.hpp b/src/dsp/signal.hpp @@ -14,6 +14,9 @@ inline float decibelsToAmplitude(float db) { } inline float amplitudeToDecibels(float amplitude) { + if (amplitude < 0.000001f) { + return -120.0f; + } return 20.0f * log10f(amplitude); } diff --git a/src/utils.cpp b/src/utils.cpp @@ -0,0 +1,13 @@ + +#include "utils.hpp" +#include <cstdarg> + +std::string bogaudio::format(const char* fmt, ...) { + const int n = 1024; + char buf[n]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, n, fmt, args); + va_end(args); + return buf; +} diff --git a/src/utils.hpp b/src/utils.hpp @@ -1,6 +1,7 @@ #pragma once #include <atomic> +#include <string> #include "rack.hpp" @@ -32,4 +33,6 @@ struct SpinLock { } }; +std::string format(const char* fmt, ...); + } // namespace bogaudio