From 9be31e003fd87f6de8be0c1fd35698553d884629 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 21 Jul 2019 15:40:13 +0200 Subject: [PATCH 01/36] tweak amplitude ranges, update and fix readme.md --- plugins/SpectrumAnalyzer/README.md | 6 +++++- plugins/SpectrumAnalyzer/SaControls.cpp | 6 +++--- plugins/SpectrumAnalyzer/SaControls.h | 14 +++++++------- plugins/SpectrumAnalyzer/SaProcessor.cpp | 12 ++++++------ 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md index 3d3506d6540..926669b357c 100644 --- a/plugins/SpectrumAnalyzer/README.md +++ b/plugins/SpectrumAnalyzer/README.md @@ -6,11 +6,15 @@ This plugin consists of three widgets and back-end code to provide them with req The top-level widget is SaControlDialog. It populates a configuration widget (created dynamically) and instantiates spectrum display widgets. Its main back-end class is SaControls, which holds all configuration values and globally valid constants (e.g. range definitions). -SaSpectrumDisplay and SaWaterfallDisplay show the result of spectrum analysis. Their main back-end class is SaProcessor, which performs FFT analysis on data received from the Analyzer class, which in turn handles the interface with LMMS. +SaSpectrumView and SaWaterfallView show the result of spectrum analysis. Their main back-end class is SaProcessor, which performs FFT analysis on data received from the Analyzer class, which in turn handles the interface with LMMS. ## Changelog + 1.0.3 2019-07-25 + - rename and tweak amplitude ranges based on feedback + 1.0.2 2019-07-12 + - variety of small changes based on code review 1.0.1 2019-06-02 - code style changes - added tool-tips diff --git a/plugins/SpectrumAnalyzer/SaControls.cpp b/plugins/SpectrumAnalyzer/SaControls.cpp index 5691c0ae44a..5ce50fd7205 100644 --- a/plugins/SpectrumAnalyzer/SaControls.cpp +++ b/plugins/SpectrumAnalyzer/SaControls.cpp @@ -62,10 +62,10 @@ SaControls::SaControls(Analyzer *effect) : m_freqRangeModel.setValue(m_freqRangeModel.findText(tr("Full (auto)"))); m_ampRangeModel.addItem(tr("Extended")); - m_ampRangeModel.addItem(tr("Default")); m_ampRangeModel.addItem(tr("Audible")); - m_ampRangeModel.addItem(tr("Noise")); - m_ampRangeModel.setValue(m_ampRangeModel.findText(tr("Default"))); + m_ampRangeModel.addItem(tr("Loud")); + m_ampRangeModel.addItem(tr("Silent")); + m_ampRangeModel.setValue(m_ampRangeModel.findText(tr("Audible"))); // FFT block size labels are generated automatically, based on // FFT_BLOCK_SIZES vector defined in fft_helpers.h diff --git a/plugins/SpectrumAnalyzer/SaControls.h b/plugins/SpectrumAnalyzer/SaControls.h index e0b54e6a2ba..2f54016ba37 100644 --- a/plugins/SpectrumAnalyzer/SaControls.h +++ b/plugins/SpectrumAnalyzer/SaControls.h @@ -59,19 +59,19 @@ const int FRANGE_HIGH_END = 20000; enum AMPLITUDE_RANGES { ARANGE_EXTENDED = 0, - ARANGE_DEFAULT, ARANGE_AUDIBLE, - ARANGE_NOISE + ARANGE_LOUD, + ARANGE_SILENT }; const int ARANGE_EXTENDED_START = -80; const int ARANGE_EXTENDED_END = 20; -const int ARANGE_DEFAULT_START = -30; -const int ARANGE_DEFAULT_END = 0; const int ARANGE_AUDIBLE_START = -50; -const int ARANGE_AUDIBLE_END = 10; -const int ARANGE_NOISE_START = -60; -const int ARANGE_NOISE_END = -20; +const int ARANGE_AUDIBLE_END = 0; +const int ARANGE_LOUD_START = -30; +const int ARANGE_LOUD_END = 0; +const int ARANGE_SILENT_START = -60; +const int ARANGE_SILENT_END = -10; class Analyzer; diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 9261658aa49..18f16cbd8eb 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -499,10 +499,10 @@ float SaProcessor::getAmpRangeMin(bool linear) const switch (m_controls->m_ampRangeModel.value()) { case ARANGE_EXTENDED: return ARANGE_EXTENDED_START; - case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START; - case ARANGE_NOISE: return ARANGE_NOISE_START; + case ARANGE_SILENT: return ARANGE_SILENT_START; + case ARANGE_LOUD: return ARANGE_LOUD_START; default: - case ARANGE_DEFAULT: return ARANGE_DEFAULT_START; + case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START; } } @@ -512,10 +512,10 @@ float SaProcessor::getAmpRangeMax() const switch (m_controls->m_ampRangeModel.value()) { case ARANGE_EXTENDED: return ARANGE_EXTENDED_END; - case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END; - case ARANGE_NOISE: return ARANGE_NOISE_END; + case ARANGE_SILENT: return ARANGE_SILENT_END; + case ARANGE_LOUD: return ARANGE_LOUD_END; default: - case ARANGE_DEFAULT: return ARANGE_DEFAULT_END; + case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END; } } From 4e2f1687b9021f664d7f7e08702c5d5d5e511b2e Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 15 Aug 2019 08:35:01 +0200 Subject: [PATCH 02/36] Add and implement advanced settings --- plugins/SpectrumAnalyzer/SaControls.cpp | 31 ++- plugins/SpectrumAnalyzer/SaControls.h | 12 + plugins/SpectrumAnalyzer/SaControlsDialog.cpp | 125 ++++++++- plugins/SpectrumAnalyzer/SaProcessor.cpp | 46 +++- plugins/SpectrumAnalyzer/SaProcessor.h | 10 +- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 13 +- plugins/SpectrumAnalyzer/SaSpectrumView.h | 3 - plugins/SpectrumAnalyzer/advanced_off.svg | 243 ++++++++++++++++++ plugins/SpectrumAnalyzer/advanced_on.svg | 224 ++++++++++++++++ plugins/SpectrumAnalyzer/advanced_src.svg | 238 +++++++++++++++++ 10 files changed, 915 insertions(+), 30 deletions(-) create mode 100644 plugins/SpectrumAnalyzer/advanced_off.svg create mode 100644 plugins/SpectrumAnalyzer/advanced_on.svg create mode 100644 plugins/SpectrumAnalyzer/advanced_src.svg diff --git a/plugins/SpectrumAnalyzer/SaControls.cpp b/plugins/SpectrumAnalyzer/SaControls.cpp index 5ce50fd7205..ed0955366f2 100644 --- a/plugins/SpectrumAnalyzer/SaControls.cpp +++ b/plugins/SpectrumAnalyzer/SaControls.cpp @@ -50,7 +50,17 @@ SaControls::SaControls(Analyzer *effect) : m_freqRangeModel(this, tr("Frequency range")), m_ampRangeModel(this, tr("Amplitude range")), m_blockSizeModel(this, tr("FFT block size")), - m_windowModel(this, tr("FFT window type")) + m_windowModel(this, tr("FFT window type")), + + // Advanced settings knobs + m_envelopeResolutionModel(0.25f, 0.1f, 3.0f, 0.05f, this, tr("Peak envelope resolution")), + m_spectrumResolutionModel(1.5f, 0.1f, 3.0f, 0.05f, this, tr("Spectrum display resolution")), + m_peakDecayFactorModel(0.992f, 0.95f, 0.999f, 0.001f, this, tr("Peak decay multiplier")), + m_averagingWeightModel(0.15f, 0.01f, 0.5f, 0.01f, this, tr("Averaging weight")), + m_waterfallHeightModel(250.0f, 50.0f, 1000.0f, 50.0f, this, tr("Waterfall history size")), + m_waterfallGammaModel(0.30f, 0.10f, 1.00f, 0.05f, this, tr("Waterfall gamma correction")), + m_windowOverlapModel(1.0f, 1.0f, 4.0f, 1.0f, this, tr("FFT window overlap")), + m_zeroPaddingModel(2.0f, 0.0f, 4.0f, 1.0f, this, tr("FFT zero padding")) { // Frequency and amplitude ranges; order must match // FREQUENCY_RANGES and AMPLITUDE_RANGES defined in SaControls.h @@ -126,6 +136,15 @@ void SaControls::loadSettings(const QDomElement &_this) m_ampRangeModel.loadSettings(_this, "RangeY"); m_blockSizeModel.loadSettings(_this, "BlockSize"); m_windowModel.loadSettings(_this, "WindowType"); + + m_envelopeResolutionModel.loadSettings(_this, "EnvelopeRes"); + m_spectrumResolutionModel.loadSettings(_this, "SpectrumRes"); + m_peakDecayFactorModel.loadSettings(_this, "PeakDecayFactor"); + m_averagingWeightModel.loadSettings(_this, "AverageWeight"); + m_waterfallHeightModel.loadSettings(_this, "WaterfallHeight"); + m_waterfallGammaModel.loadSettings(_this, "WaterfallGamma"); + m_windowOverlapModel.loadSettings(_this, "WindowOverlap"); + m_zeroPaddingModel.loadSettings(_this, "ZeroPadding"); } @@ -141,4 +160,14 @@ void SaControls::saveSettings(QDomDocument &doc, QDomElement &parent) m_ampRangeModel.saveSettings(doc, parent, "RangeY"); m_blockSizeModel.saveSettings(doc, parent, "BlockSize"); m_windowModel.saveSettings(doc, parent, "WindowType"); + + m_envelopeResolutionModel.saveSettings(doc, parent, "EnvelopeRes"); + m_spectrumResolutionModel.saveSettings(doc, parent, "SpectrumRes"); + m_peakDecayFactorModel.saveSettings(doc, parent, "PeakDecayFactor"); + m_averagingWeightModel.saveSettings(doc, parent, "AverageWeight"); + m_waterfallHeightModel.saveSettings(doc, parent, "WaterfallHeight"); + m_waterfallGammaModel.saveSettings(doc, parent, "WaterfallGamma"); + m_windowOverlapModel.saveSettings(doc, parent, "WindowOverlap"); + m_zeroPaddingModel.saveSettings(doc, parent, "ZeroPadding"); + } diff --git a/plugins/SpectrumAnalyzer/SaControls.h b/plugins/SpectrumAnalyzer/SaControls.h index 2f54016ba37..3357ba436ed 100644 --- a/plugins/SpectrumAnalyzer/SaControls.h +++ b/plugins/SpectrumAnalyzer/SaControls.h @@ -95,6 +95,7 @@ class SaControls : public EffectControls private: Analyzer *m_effect; + // basic settings BoolModel m_pauseModel; BoolModel m_refFreezeModel; @@ -111,6 +112,17 @@ class SaControls : public EffectControls ComboBoxModel m_blockSizeModel; ComboBoxModel m_windowModel; + // advanced settings + FloatModel m_envelopeResolutionModel; + FloatModel m_spectrumResolutionModel; + FloatModel m_peakDecayFactorModel; + FloatModel m_averagingWeightModel; + FloatModel m_waterfallHeightModel; + FloatModel m_waterfallGammaModel; + FloatModel m_windowOverlapModel; + FloatModel m_zeroPaddingModel; + + // colors (hard-coded, values must add up to specific numbers) QColor m_colorL; QColor m_colorR; QColor m_colorMono; diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp index 4ba307a4def..85f379c1fed 100644 --- a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -34,6 +34,7 @@ #include "ComboBoxModel.h" #include "embed.h" #include "Engine.h" +#include "Knob.h" #include "LedCheckbox.h" #include "PixmapButton.h" #include "SaControls.h" @@ -53,13 +54,24 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) master_layout->setContentsMargins(2, 6, 2, 8); setLayout(master_layout); - // QSplitter top: configuration section + // Display splitter top: controls section + QWidget *controls_widget = new QWidget; + QHBoxLayout *controls_layout = new QHBoxLayout; + controls_layout->setContentsMargins(0, 0, 0, 0); + controls_widget->setLayout(controls_layout); + controls_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + controls_widget->setMaximumHeight(m_configHeight); + display_splitter->addWidget(controls_widget); + + + // Basic configuration QWidget *config_widget = new QWidget; QGridLayout *config_layout = new QGridLayout; config_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); config_widget->setMaximumHeight(m_configHeight); config_widget->setLayout(config_layout); - display_splitter->addWidget(config_widget); + controls_layout->addWidget(config_widget); + controls_layout->setStretchFactor(config_widget, 10); // Pre-compute target pixmap size based on monitor DPI. // Using setDevicePixelRatio() on pixmap allows the SVG image to be razor @@ -67,6 +79,8 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) // enlarged. No idea how to make Qt do it in a more reasonable way. QSize iconSize = QSize(22.0 * devicePixelRatio(), 22.0 * devicePixelRatio()); QSize buttonSize = 1.2 * iconSize; + QSize advButtonSize = QSize((m_configHeight * devicePixelRatio()) / 3, m_configHeight * devicePixelRatio()); + // pause and freeze buttons PixmapButton *pauseButton = new PixmapButton(this, tr("Pause")); @@ -79,7 +93,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) pauseButton->setInactiveGraphic(*pauseOffPixmap); pauseButton->setCheckable(true); pauseButton->setModel(&controls->m_pauseModel); - config_layout->addWidget(pauseButton, 0, 0, 2, 1); + config_layout->addWidget(pauseButton, 0, 0, 2, 1, Qt::AlignHCenter); PixmapButton *refFreezeButton = new PixmapButton(this, tr("Reference freeze")); refFreezeButton->setToolTip(tr("Freeze current input as a reference / disable falloff in peak-hold mode.")); @@ -91,7 +105,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) refFreezeButton->setInactiveGraphic(*freezeOffPixmap); refFreezeButton->setCheckable(true); refFreezeButton->setModel(&controls->m_refFreezeModel); - config_layout->addWidget(refFreezeButton, 2, 0, 2, 1); + config_layout->addWidget(refFreezeButton, 2, 0, 2, 1, Qt::AlignHCenter); // misc configuration switches LedCheckBox *waterfallButton = new LedCheckBox(tr("Waterfall"), this); @@ -194,6 +208,109 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) processor->rebuildWindow(); connect(&controls->m_windowModel, &ComboBoxModel::dataChanged, [=] {processor->rebuildWindow();}); + // set stretch factors so that combo boxes expand first + config_layout->setColumnStretch(3, 2); + config_layout->setColumnStretch(5, 3); + + + // Advanced configuration + QWidget *advanced_widget = new QWidget; + QGridLayout *advanced_layout = new QGridLayout; + advanced_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + advanced_widget->setMaximumHeight(m_configHeight); + advanced_widget->setLayout(advanced_layout); + advanced_widget->hide(); + controls_layout->addWidget(advanced_widget); + controls_layout->setStretchFactor(advanced_widget, 10); + + // Peak envelope resolution + Knob *envelopeResolutionKnob = new Knob(knobSmall_17, this); + envelopeResolutionKnob->setModel(&controls->m_envelopeResolutionModel); + envelopeResolutionKnob->setLabel(tr("Envelope res.")); + envelopeResolutionKnob->setHintText(tr("Draw at most"), tr(" envelope points per pixel")); + advanced_layout->addWidget(envelopeResolutionKnob, 0, 0, 1, 1, Qt::AlignCenter); + + // Spectrum graph resolution + Knob *spectrumResolutionKnob = new Knob(knobSmall_17, this); + spectrumResolutionKnob->setModel(&controls->m_spectrumResolutionModel); + spectrumResolutionKnob->setLabel(tr("Spectrum res.")); + spectrumResolutionKnob->setHintText(tr("Draw at most"), tr(" spectrum points per pixel")); + advanced_layout->addWidget(spectrumResolutionKnob, 1, 0, 1, 1, Qt::AlignCenter); + + // Peak falloff speed + Knob *peakDecayFactorKnob = new Knob(knobSmall_17, this); + peakDecayFactorKnob->setModel(&controls->m_peakDecayFactorModel); + peakDecayFactorKnob->setLabel(tr("Falloff factor")); + peakDecayFactorKnob->setHintText(tr("Multiply buffered value by"), ""); + advanced_layout->addWidget(peakDecayFactorKnob, 0, 1, 1, 1, Qt::AlignCenter); + + // Averaging weight + Knob *averagingWeightKnob = new Knob(knobSmall_17, this); + averagingWeightKnob->setModel(&controls->m_averagingWeightModel); + averagingWeightKnob->setLabel(tr("Averaging weight")); + averagingWeightKnob->setHintText(tr("New sample contributes"), ""); + advanced_layout->addWidget(averagingWeightKnob, 1, 1, 1, 1, Qt::AlignCenter); + + // Waterfall history size + Knob *waterfallHeightKnob = new Knob(knobSmall_17, this); + waterfallHeightKnob->setModel(&controls->m_waterfallHeightModel); + waterfallHeightKnob->setLabel(tr("Waterfall height")); + waterfallHeightKnob->setHintText(tr("Keep"), tr(" lines")); + advanced_layout->addWidget(waterfallHeightKnob, 0, 2, 1, 1, Qt::AlignCenter); + processor->reallocateBuffers(); + connect(&controls->m_waterfallHeightModel, &FloatModel::dataChanged, [=] {processor->reallocateBuffers();}); + + // Waterfall gamma correction + Knob *waterfallGammaKnob = new Knob(knobSmall_17, this); + waterfallGammaKnob->setModel(&controls->m_waterfallGammaModel); + waterfallGammaKnob->setLabel(tr("Waterfall gamma")); + waterfallGammaKnob->setHintText(tr("Gamma value:"), ""); + advanced_layout->addWidget(waterfallGammaKnob, 1, 2, 1, 1, Qt::AlignCenter); + + // FFT window overlap + Knob *windowOverlapKnob = new Knob(knobSmall_17, this); + windowOverlapKnob->setModel(&controls->m_windowOverlapModel); + windowOverlapKnob->setLabel(tr("Window overlap")); + windowOverlapKnob->setHintText(tr("Each sample processed"), tr(" times")); + advanced_layout->addWidget(windowOverlapKnob, 0, 3, 1, 1, Qt::AlignCenter); + + // FFT zero padding + Knob *zeroPaddingKnob = new Knob(knobSmall_17, this); + zeroPaddingKnob->setModel(&controls->m_zeroPaddingModel); + zeroPaddingKnob->setLabel(tr("Zero padding")); + zeroPaddingKnob->setHintText(tr("Processing buffer is"), tr(" steps larger than input block")); + advanced_layout->addWidget(zeroPaddingKnob, 1, 3, 1, 1, Qt::AlignCenter); + processor->reallocateBuffers(); + connect(&controls->m_zeroPaddingModel, &FloatModel::dataChanged, [=] {processor->reallocateBuffers();}); + + + // Advanced settings button + PixmapButton *advancedButton = new PixmapButton(this, tr("Advanced settings")); + advancedButton->setToolTip(tr("Access advanced settings")); + QPixmap *advancedOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_on").scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + QPixmap *advancedOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_off").scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + advancedOnPixmap->setDevicePixelRatio(devicePixelRatio()); + advancedOffPixmap->setDevicePixelRatio(devicePixelRatio()); + advancedButton->setActiveGraphic(*advancedOnPixmap); + advancedButton->setInactiveGraphic(*advancedOffPixmap); + advancedButton->setCheckable(true); + controls_layout->addStretch(0); + controls_layout->addWidget(advancedButton); + + connect(advancedButton, &PixmapButton::toggled, [=](bool checked) + { + if (checked) + { + config_widget->hide(); + advanced_widget->show(); + } + else + { + config_widget->show(); + advanced_widget->hide(); + } + } + ); // QSplitter middle and bottom: spectrum display widgets m_spectrum = new SaSpectrumView(controls, processor, this); diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 18f16cbd8eb..6d4227cf26f 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -47,12 +47,14 @@ SaProcessor::SaProcessor(SaControls *controls) : m_fftWindow.resize(m_inBlockSize, 1.0); precomputeWindow(m_fftWindow.data(), m_inBlockSize, BLACKMAN_HARRIS); - m_bufferL.resize(m_fftBlockSize, 0); - m_bufferR.resize(m_fftBlockSize, 0); + m_bufferL.resize(m_inBlockSize, 0); + m_bufferR.resize(m_inBlockSize, 0); + m_filteredBufferL.resize(m_fftBlockSize, 0); + m_filteredBufferR.resize(m_fftBlockSize, 0); m_spectrumL = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex)); m_spectrumR = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex)); - m_fftPlanL = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferL.data(), m_spectrumL, FFTW_MEASURE); - m_fftPlanR = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferR.data(), m_spectrumR, FFTW_MEASURE); + m_fftPlanL = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_filteredBufferL.data(), m_spectrumL, FFTW_MEASURE); + m_fftPlanR = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_filteredBufferR.data(), m_spectrumR, FFTW_MEASURE); m_absSpectrumL.resize(binCount(), 0); m_absSpectrumR.resize(binCount(), 0); @@ -123,8 +125,8 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) // apply FFT window for (unsigned int i = 0; i < m_inBlockSize; i++) { - m_bufferL[i] = m_bufferL[i] * m_fftWindow[i]; - m_bufferR[i] = m_bufferR[i] * m_fftWindow[i]; + m_filteredBufferL[i] = m_bufferL[i] * m_fftWindow[i]; + m_filteredBufferR[i] = m_bufferR[i] * m_fftWindow[i]; } // lock data shared with SaSpectrumView and SaWaterfallView @@ -239,7 +241,20 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) #endif // clean up before checking for more data from input buffer - m_framesFilledUp = 0; + const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); + if (overlaps == 1) // each sample used only once + { + m_framesFilledUp = 0; + } + else + { + const unsigned int drop = m_inBlockSize / overlaps; + m_bufferL.erase(m_bufferL.begin(), m_bufferL.begin() + drop); + m_bufferR.erase(m_bufferR.begin(), m_bufferR.begin() + drop); + m_bufferL.resize(m_inBlockSize, 0); + m_bufferR.resize(m_inBlockSize, 0); + m_framesFilledUp -= m_inBlockSize / overlaps; + } } } } @@ -251,8 +266,9 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) // Gamma correction is applied to make small values more visible and to make // a linear gradient actually appear roughly linear. The correction should be // around 0.42 to 0.45 for sRGB displays (or lower for bigger visibility boost). -QRgb SaProcessor::makePixel(float left, float right, float gamma_correction) const +QRgb SaProcessor::makePixel(float left, float right) const { + const float gamma_correction = m_controls->m_waterfallGammaModel.value(); if (m_controls->m_stereoModel.value()) { float ampL = pow(left, gamma_correction); @@ -301,6 +317,7 @@ void SaProcessor::reallocateBuffers() { new_in_size = FFT_BLOCK_SIZES.back(); } + m_zeroPadFactor = m_controls->m_zeroPaddingModel.value(); if (new_size_index + m_zeroPadFactor < FFT_BLOCK_SIZES.size()) { new_fft_size = FFT_BLOCK_SIZES[new_size_index + m_zeroPadFactor]; @@ -328,12 +345,14 @@ void SaProcessor::reallocateBuffers() // allocate new space, create new plan and resize containers m_fftWindow.resize(new_in_size, 1.0); precomputeWindow(m_fftWindow.data(), new_in_size, (FFT_WINDOWS) m_controls->m_windowModel.value()); - m_bufferL.resize(new_fft_size, 0); - m_bufferR.resize(new_fft_size, 0); + m_bufferL.resize(new_in_size, 0); + m_bufferR.resize(new_in_size, 0); + m_filteredBufferL.resize(new_fft_size, 0); + m_filteredBufferR.resize(new_fft_size, 0); m_spectrumL = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex)); m_spectrumR = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex)); - m_fftPlanL = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferL.data(), m_spectrumL, FFTW_MEASURE); - m_fftPlanR = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferR.data(), m_spectrumR, FFTW_MEASURE); + m_fftPlanL = fftwf_plan_dft_r2c_1d(new_fft_size, m_filteredBufferL.data(), m_spectrumL, FFTW_MEASURE); + m_fftPlanR = fftwf_plan_dft_r2c_1d(new_fft_size, m_filteredBufferR.data(), m_spectrumR, FFTW_MEASURE); if (m_fftPlanL == NULL || m_fftPlanR == NULL) { @@ -344,6 +363,7 @@ void SaProcessor::reallocateBuffers() m_normSpectrumL.resize(new_bins, 0); m_normSpectrumR.resize(new_bins, 0); + m_waterfallHeight = m_controls->m_waterfallHeightModel.value(); m_history.resize(new_bins * m_waterfallHeight * sizeof qRgb(0,0,0), 0); // done; publish new sizes and clean up @@ -373,6 +393,8 @@ void SaProcessor::clear() m_framesFilledUp = 0; std::fill(m_bufferL.begin(), m_bufferL.end(), 0); std::fill(m_bufferR.begin(), m_bufferR.end(), 0); + std::fill(m_filteredBufferL.begin(), m_filteredBufferL.end(), 0); + std::fill(m_filteredBufferR.begin(), m_filteredBufferR.end(), 0); std::fill(m_absSpectrumL.begin(), m_absSpectrumL.end(), 0); std::fill(m_absSpectrumR.begin(), m_absSpectrumR.end(), 0); std::fill(m_normSpectrumL.begin(), m_normSpectrumL.end(), 0); diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index ae2df16f8c8..547d7aed751 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -80,7 +80,7 @@ class SaProcessor SaControls *m_controls; // currently valid configuration - const unsigned int m_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size + unsigned int m_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size unsigned int m_inBlockSize; //!< size of input (time domain) data block unsigned int m_fftBlockSize; //!< size of padded block for FFT processing unsigned int m_sampleRate; @@ -92,6 +92,8 @@ class SaProcessor std::vector m_bufferL; //!< time domain samples (left) std::vector m_bufferR; //!< time domain samples (right) std::vector m_fftWindow; //!< precomputed window function coefficients + std::vector m_filteredBufferL; //!< time domain samples with window function applied (left) + std::vector m_filteredBufferR; //!< time domain samples with window function applied (right) fftwf_plan m_fftPlanL; fftwf_plan m_fftPlanR; fftwf_complex *m_spectrumL; //!< frequency domain samples (complex) (left) @@ -103,8 +105,8 @@ class SaProcessor // spectrum history for waterfall: new normSpectrum lines are added on top std::vector m_history; - const unsigned int m_waterfallHeight = 200; // Number of stored lines. - // Note: high values may make it harder to see transients. + unsigned int m_waterfallHeight = 250; // Number of stored lines. + // Note: high values may make it harder to see transients. // book keeping bool m_spectrumActive; @@ -113,7 +115,7 @@ class SaProcessor bool m_reallocating; // merge L and R channels and apply gamma correction to make a spectrogram pixel - QRgb makePixel(float left, float right, float gamma_correction = 0.30) const; + QRgb makePixel(float left, float right) const; friend class SaSpectrumView; friend class SaWaterfallView; diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index 746d52cfdc1..fa323a5d63a 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -259,17 +259,17 @@ void SaSpectrumView::refreshPaths() // Use updated display buffers to prepare new paths for QPainter. // This is the second slowest action (first is the subsequent drawing); use // the resolution parameter to balance display quality and performance. - m_pathL = makePath(m_displayBufferL, 1.5); + m_pathL = makePath(m_displayBufferL, m_controls->m_spectrumResolutionModel.value()); if (m_controls->m_stereoModel.value()) { - m_pathR = makePath(m_displayBufferR, 1.5); + m_pathR = makePath(m_displayBufferR, m_controls->m_spectrumResolutionModel.value()); } if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value()) { - m_pathPeakL = makePath(m_peakBufferL, 0.25); + m_pathPeakL = makePath(m_peakBufferL, m_controls->m_envelopeResolutionModel.value()); if (m_controls->m_stereoModel.value()) { - m_pathPeakR = makePath(m_peakBufferR, 0.25); + m_pathPeakR = makePath(m_peakBufferR, m_controls->m_envelopeResolutionModel.value()); } } #ifdef SA_DEBUG @@ -297,7 +297,8 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float { if (m_controls->m_smoothModel.value()) { - displayBuffer[n] = spectrum[n] * m_smoothFactor + displayBuffer[n] * (1 - m_smoothFactor); + const float smoothFactor = m_controls->m_averagingWeightModel.value(); + displayBuffer[n] = spectrum[n] * smoothFactor + displayBuffer[n] * (1 - smoothFactor); } else { @@ -319,7 +320,7 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float } else if (!m_controls->m_refFreezeModel.value()) { - peakBuffer[n] = peakBuffer[n] * m_peakDecayFactor; + peakBuffer[n] = peakBuffer[n] * m_controls->m_peakDecayFactorModel.value(); } } else if (!m_controls->m_refFreezeModel.value() && !m_controls->m_peakHoldModel.value()) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.h b/plugins/SpectrumAnalyzer/SaSpectrumView.h index 0db5852e19d..b2ad4f9f4a8 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.h +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.h @@ -99,9 +99,6 @@ private slots: bool m_freezeRequest; // new reference should be acquired bool m_frozen; // a reference is currently stored in the peakBuffer - const float m_smoothFactor = 0.15; // alpha for exponential smoothing - const float m_peakDecayFactor = 0.992; // multiplier for gradual peak decay - // top level: refresh buffers, make paths and draw the spectrum void drawSpectrum(QPainter &painter); diff --git a/plugins/SpectrumAnalyzer/advanced_off.svg b/plugins/SpectrumAnalyzer/advanced_off.svg new file mode 100644 index 00000000000..6d3ed82b105 --- /dev/null +++ b/plugins/SpectrumAnalyzer/advanced_off.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/advanced_on.svg b/plugins/SpectrumAnalyzer/advanced_on.svg new file mode 100644 index 00000000000..9e6b1ca3fb2 --- /dev/null +++ b/plugins/SpectrumAnalyzer/advanced_on.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/advanced_src.svg b/plugins/SpectrumAnalyzer/advanced_src.svg new file mode 100644 index 00000000000..ae201aad0a8 --- /dev/null +++ b/plugins/SpectrumAnalyzer/advanced_src.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + ADV. + + + From f5eda879ff8043901455d954c1d379e2568cde01 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Fri, 23 Aug 2019 11:33:01 +0200 Subject: [PATCH 03/36] Display waterfall at native resolution --- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 617e80b2c49..4d13f96da25 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -121,15 +121,17 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) if (m_processor->m_waterfallNotEmpty) { QMutexLocker lock(&m_processor->m_dataAccess); - painter.drawImage(displayLeft, displayTop, // top left corner coordinates - QImage(m_processor->m_history.data(), // raw pixel data to display - m_processor->binCount(), // width = number of frequency bins - m_processor->m_waterfallHeight, // height = number of history lines - QImage::Format_RGB32 - ).scaled(displayWidth, // scale to fit view.. - displayBottom, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation)); + QImage temp = QImage(m_processor->m_history.data(), // raw pixel data to display + m_processor->binCount(), // width = number of frequency bins + m_processor->m_waterfallHeight, // height = number of history lines + QImage::Format_RGB32 + ).scaled(displayWidth * devicePixelRatio(), // scale to fit view.. + displayBottom * devicePixelRatio(), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution + painter.drawImage(displayLeft, displayTop, temp); + lock.unlock(); } else From 7d630e170e4c562c174cb92ace5ad42d2ec4d993 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Fri, 23 Aug 2019 17:17:51 +0200 Subject: [PATCH 04/36] Add waterfall cursor, fix time labels and make density change with widget size; improve perf. measurement --- plugins/SpectrumAnalyzer/Analyzer.cpp | 20 +++ plugins/SpectrumAnalyzer/Analyzer.h | 7 + plugins/SpectrumAnalyzer/SaControls.cpp | 4 +- plugins/SpectrumAnalyzer/SaControlsDialog.cpp | 8 + plugins/SpectrumAnalyzer/SaProcessor.cpp | 37 +++- plugins/SpectrumAnalyzer/SaProcessor.h | 7 + plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 41 +++-- plugins/SpectrumAnalyzer/SaSpectrumView.h | 11 +- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 158 ++++++++++++++---- plugins/SpectrumAnalyzer/SaWaterfallView.h | 26 ++- 10 files changed, 254 insertions(+), 65 deletions(-) diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index 9c3fe0814ca..95d2574990e 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -30,6 +30,10 @@ #include "embed.h" #include "plugin_export.h" +#ifdef SA_DEBUG + #include + #include +#endif extern "C" { Plugin::Descriptor PLUGIN_EXPORT analyzer_plugin_descriptor = @@ -59,8 +63,24 @@ Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::K // Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) { + #ifdef SA_DEBUG + unsigned int audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + if (audio_time - m_last_dump_time > 1000000000) + { + std::cout << "Audio thread: " << m_sum_execution / m_dump_count << " ms avg / " + << m_max_execution << " ms peak." << std::endl; + m_last_dump_time = audio_time; + m_sum_execution = m_max_execution = m_dump_count = 0; + } + #endif if (!isEnabled() || !isRunning ()) {return false;} if (m_controls.isViewVisible()) {m_processor.analyse(buffer, frame_count);} + #ifdef SA_DEBUG + audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - audio_time; + m_dump_count++; + m_sum_execution += audio_time / 1000000.0; + if (audio_time / 1000000.0 > m_max_execution) {m_max_execution = audio_time / 1000000.0;} + #endif return isRunning(); } diff --git a/plugins/SpectrumAnalyzer/Analyzer.h b/plugins/SpectrumAnalyzer/Analyzer.h index 157cc1eae20..64a6ef3109f 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.h +++ b/plugins/SpectrumAnalyzer/Analyzer.h @@ -47,6 +47,13 @@ class Analyzer : public Effect private: SaProcessor m_processor; SaControls m_controls; + + #ifdef SA_DEBUG + int m_last_dump_time; + int m_dump_count; + float m_sum_execution; + float m_max_execution; + #endif }; #endif // ANALYZER_H diff --git a/plugins/SpectrumAnalyzer/SaControls.cpp b/plugins/SpectrumAnalyzer/SaControls.cpp index ed0955366f2..5484a06077e 100644 --- a/plugins/SpectrumAnalyzer/SaControls.cpp +++ b/plugins/SpectrumAnalyzer/SaControls.cpp @@ -59,8 +59,8 @@ SaControls::SaControls(Analyzer *effect) : m_averagingWeightModel(0.15f, 0.01f, 0.5f, 0.01f, this, tr("Averaging weight")), m_waterfallHeightModel(250.0f, 50.0f, 1000.0f, 50.0f, this, tr("Waterfall history size")), m_waterfallGammaModel(0.30f, 0.10f, 1.00f, 0.05f, this, tr("Waterfall gamma correction")), - m_windowOverlapModel(1.0f, 1.0f, 4.0f, 1.0f, this, tr("FFT window overlap")), - m_zeroPaddingModel(2.0f, 0.0f, 4.0f, 1.0f, this, tr("FFT zero padding")) + m_windowOverlapModel(1.0f, 1.0f, 3.0f, 1.0f, this, tr("FFT window overlap")), + m_zeroPaddingModel(2.0f, 0.0f, 3.0f, 1.0f, this, tr("FFT zero padding")) { // Frequency and amplitude ranges; order must match // FREQUENCY_RANGES and AMPLITUDE_RANGES defined in SaControls.h diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp index 85f379c1fed..b713dc90c42 100644 --- a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -227,6 +227,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *envelopeResolutionKnob = new Knob(knobSmall_17, this); envelopeResolutionKnob->setModel(&controls->m_envelopeResolutionModel); envelopeResolutionKnob->setLabel(tr("Envelope res.")); + envelopeResolutionKnob->setToolTip(tr("Increase envelope resolution for better details, decrease for better GUI performance.")); envelopeResolutionKnob->setHintText(tr("Draw at most"), tr(" envelope points per pixel")); advanced_layout->addWidget(envelopeResolutionKnob, 0, 0, 1, 1, Qt::AlignCenter); @@ -234,6 +235,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *spectrumResolutionKnob = new Knob(knobSmall_17, this); spectrumResolutionKnob->setModel(&controls->m_spectrumResolutionModel); spectrumResolutionKnob->setLabel(tr("Spectrum res.")); + spectrumResolutionKnob->setToolTip(tr("Increase spectrum resolution for better details, decrease for better GUI performance.")); spectrumResolutionKnob->setHintText(tr("Draw at most"), tr(" spectrum points per pixel")); advanced_layout->addWidget(spectrumResolutionKnob, 1, 0, 1, 1, Qt::AlignCenter); @@ -241,6 +243,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *peakDecayFactorKnob = new Knob(knobSmall_17, this); peakDecayFactorKnob->setModel(&controls->m_peakDecayFactorModel); peakDecayFactorKnob->setLabel(tr("Falloff factor")); + peakDecayFactorKnob->setToolTip(tr("Decrease to make peaks fall faster.")); peakDecayFactorKnob->setHintText(tr("Multiply buffered value by"), ""); advanced_layout->addWidget(peakDecayFactorKnob, 0, 1, 1, 1, Qt::AlignCenter); @@ -248,6 +251,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *averagingWeightKnob = new Knob(knobSmall_17, this); averagingWeightKnob->setModel(&controls->m_averagingWeightModel); averagingWeightKnob->setLabel(tr("Averaging weight")); + averagingWeightKnob->setToolTip(tr("Decrease to make averaging slower and smoother.")); averagingWeightKnob->setHintText(tr("New sample contributes"), ""); advanced_layout->addWidget(averagingWeightKnob, 1, 1, 1, 1, Qt::AlignCenter); @@ -255,6 +259,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *waterfallHeightKnob = new Knob(knobSmall_17, this); waterfallHeightKnob->setModel(&controls->m_waterfallHeightModel); waterfallHeightKnob->setLabel(tr("Waterfall height")); + waterfallHeightKnob->setToolTip(tr("Increase to get slower scrolling, decrease to see fast transitions better.")); waterfallHeightKnob->setHintText(tr("Keep"), tr(" lines")); advanced_layout->addWidget(waterfallHeightKnob, 0, 2, 1, 1, Qt::AlignCenter); processor->reallocateBuffers(); @@ -264,6 +269,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *waterfallGammaKnob = new Knob(knobSmall_17, this); waterfallGammaKnob->setModel(&controls->m_waterfallGammaModel); waterfallGammaKnob->setLabel(tr("Waterfall gamma")); + waterfallGammaKnob->setToolTip(tr("Decrease to see very weak signals, increase to get better contrast.")); waterfallGammaKnob->setHintText(tr("Gamma value:"), ""); advanced_layout->addWidget(waterfallGammaKnob, 1, 2, 1, 1, Qt::AlignCenter); @@ -271,6 +277,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *windowOverlapKnob = new Knob(knobSmall_17, this); windowOverlapKnob->setModel(&controls->m_windowOverlapModel); windowOverlapKnob->setLabel(tr("Window overlap")); + windowOverlapKnob->setToolTip(tr("Increase to prevent missing fast transitions arriving near FFT window edges. Warning: high CPU usage.")); windowOverlapKnob->setHintText(tr("Each sample processed"), tr(" times")); advanced_layout->addWidget(windowOverlapKnob, 0, 3, 1, 1, Qt::AlignCenter); @@ -278,6 +285,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *zeroPaddingKnob = new Knob(knobSmall_17, this); zeroPaddingKnob->setModel(&controls->m_zeroPaddingModel); zeroPaddingKnob->setLabel(tr("Zero padding")); + zeroPaddingKnob->setToolTip(tr("Increase to get smoother-looking spectrum. Warning: high CPU usage.")); zeroPaddingKnob->setHintText(tr("Processing buffer is"), tr(" steps larger than input block")); advanced_layout->addWidget(zeroPaddingKnob, 1, 3, 1, 1, Qt::AlignCenter); processor->reallocateBuffers(); diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 6d4227cf26f..70743c2bf15 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -27,11 +27,15 @@ #include #include -#include #include #include "lmms_math.h" +#ifdef SA_DEBUG + #include + #include + #include +#endif SaProcessor::SaProcessor(SaControls *controls) : m_controls(controls), @@ -84,9 +88,6 @@ SaProcessor::~SaProcessor() // Load a batch of data from LMMS; run FFT analysis if buffer is full enough. void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) { - #ifdef SA_DEBUG - int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - #endif // only take in data if any view is visible and not paused if ((m_spectrumActive || m_waterfallActive) && !m_controls->m_pauseModel.value()) { @@ -119,6 +120,21 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) // skip analysis if buffers are being reallocated. if (m_framesFilledUp < m_inBlockSize || m_reallocating) {return;} + // Print performance analysis once per second if debug is enabled + #ifdef SA_DEBUG + unsigned int fft_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + if (fft_time - m_last_dump_time > 1000000000) + { + std::cout << "FFT analysis: " << std::fixed << std::setprecision(2) + << m_sum_execution / m_dump_count << " ms avg / " + << m_max_execution << " ms peak, executing " + << m_dump_count << " times per second (" + << m_sum_execution / 10.0 << " % CPU usage)." << std::endl; + m_last_dump_time = fft_time; + m_sum_execution = m_max_execution = m_dump_count = 0; + } + #endif + // update sample rate m_sampleRate = Engine::mixer()->processingSampleRate(); @@ -234,11 +250,6 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) } } } - #ifdef SA_DEBUG - // report FFT processing speed - start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time; - std::cout << "Processed " << m_framesFilledUp << " samples in " << start_time / 1000000.0 << " ms" << std::endl; - #endif // clean up before checking for more data from input buffer const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); @@ -255,6 +266,14 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) m_bufferR.resize(m_inBlockSize, 0); m_framesFilledUp -= m_inBlockSize / overlaps; } + + #ifdef SA_DEBUG + // report FFT processing speed + fft_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - fft_time; + m_dump_count++; + m_sum_execution += fft_time / 1000000.0; + if (fft_time / 1000000.0 > m_max_execution) {m_max_execution = fft_time / 1000000.0;} + #endif } } } diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index 547d7aed751..5611e38965a 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -119,6 +119,13 @@ class SaProcessor friend class SaSpectrumView; friend class SaWaterfallView; + + #ifdef SA_DEBUG + unsigned int m_last_dump_time; + unsigned int m_dump_count; + float m_sum_execution; + float m_max_execution; + #endif }; #endif // SAPROCESSOR_H diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index fa323a5d63a..13904c441f7 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -68,7 +68,11 @@ SaSpectrumView::SaSpectrumView(SaControls *controls, SaProcessor *processor, QWi m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); - m_cursor = QPoint(0, 0); + m_cursor = QPointF(0, 0); + + #ifdef SA_DEBUG + m_execution_avg = m_path_avg = m_draw_avg = 0; + #endif } @@ -134,12 +138,18 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) 2.0, 2.0); #ifdef SA_DEBUG - // display what FPS would be achieved if spectrum display ran in a loop + // display performance measurements if enabled total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; + m_execution_avg = 0.95 * m_execution_avg + 0.05 * total_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft, - QString(std::string("Max FPS: " + std::to_string(1000000000.0 / total_time)).c_str())); + painter.drawText(m_displayRight -100, 10, 100, 16, Qt::AlignLeft, + QString(std::string("Exec avg.: " + std::to_string(m_execution_avg).substr(0, 5) + " ms").c_str())); + painter.drawText(m_displayRight -100, 30, 100, 16, Qt::AlignLeft, + QString(std::string("Path avg: " + std::to_string(m_path_avg).substr(0, 5) + " ms").c_str())); + painter.drawText(m_displayRight -100, 50, 100, 16, Qt::AlignLeft, + QString(std::string("Draw avg: " + std::to_string(m_draw_avg).substr(0, 5) + " ms").c_str())); + #endif } @@ -205,11 +215,9 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) } #ifdef SA_DEBUG - // display measurement results - painter.drawText(m_displayRight -100, 90, 100, 16, Qt::AlignLeft, - QString(std::string("Path ms: " + std::to_string(path_time / 1000000.0)).c_str())); - painter.drawText(m_displayRight -100, 110, 100, 16, Qt::AlignLeft, - QString(std::string("Draw ms: " + std::to_string(draw_time / 1000000.0)).c_str())); + // save performance measurement results + m_path_avg = 0.95 * m_path_avg + 0.05 * path_time / 1000000.0; + m_draw_avg = 0.95 * m_draw_avg + 0.05 * draw_time / 1000000.0; #endif } @@ -540,7 +548,7 @@ void SaSpectrumView::drawGrid(QPainter &painter) // Draw cursor and its coordinates if it is within display bounds. void SaSpectrumView::drawCursor(QPainter &painter) { - if( m_cursor.x() >= m_displayLeft + if ( m_cursor.x() >= m_displayLeft && m_cursor.x() <= m_displayRight && m_cursor.y() >= m_displayTop && m_cursor.y() <= m_displayBottom) @@ -552,26 +560,25 @@ void SaSpectrumView::drawCursor(QPainter &painter) // coordinates painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(m_displayRight -60, 5, 100, 16, Qt::AlignLeft, "Cursor"); - QString tmps; + // frequency int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str()); - painter.drawText(m_displayRight -60, 18, 100, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft +8, 8, 100, 16, Qt::AlignLeft, tmps); // amplitude float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); if (m_controls->m_logYModel.value()) { - tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dB").c_str()); + tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dBFS").c_str()); } else { // add 0.0005 to get proper rounding to 3 decimal places tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str()); } - painter.drawText(m_displayRight -60, 30, 100, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft +8, 20, 100, 16, Qt::AlignLeft, tmps); } } @@ -777,12 +784,12 @@ void SaSpectrumView::periodicUpdate() // Handle mouse input: set new cursor position. void SaSpectrumView::mouseMoveEvent(QMouseEvent *event) { - m_cursor = event->pos(); + m_cursor = event->localPos(); } void SaSpectrumView::mousePressEvent(QMouseEvent *event) { - m_cursor = event->pos(); + m_cursor = event->localPos(); } diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.h b/plugins/SpectrumAnalyzer/SaSpectrumView.h index b2ad4f9f4a8..43b8693d36a 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.h +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.h @@ -27,6 +27,8 @@ #ifndef SASPECTRUMVIEW_H #define SASPECTRUMVIEW_H +#include "SaControls.h" + #include #include #include @@ -34,7 +36,6 @@ class QMouseEvent; class QPainter; -class SaControls; class SaProcessor; //! Widget that displays a spectrum curve and frequency / amplitude grid @@ -103,7 +104,7 @@ private slots: void drawSpectrum(QPainter &painter); // current cursor location and a method to draw it - QPoint m_cursor; + QPointF m_cursor; void drawCursor(QPainter &painter); // wrappers for most used SaProcessor conversion helpers @@ -118,6 +119,12 @@ private slots: unsigned int m_displayLeft; unsigned int m_displayRight; unsigned int m_displayWidth; + + #ifdef SA_DEBUG + float m_execution_avg; + float m_path_avg; + float m_draw_avg; + #endif }; #endif // SASPECTRUMVIEW_H diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 4d13f96da25..0d780db9195 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,10 @@ #include "MainWindow.h" #include "SaProcessor.h" +#ifdef SA_DEBUG + #include +#endif + SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, QWidget *_parent) : QWidget(_parent), @@ -48,7 +53,14 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); m_timeTics = makeTimeTics(); - m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate(); + m_oldSecondsPerLine = 0; + m_oldHeight = 0; + + m_cursor = QPointF(0, 0); + + #ifdef SA_DEBUG + m_execution_avg = 0; + #endif } @@ -58,15 +70,16 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q void SaWaterfallView::paintEvent(QPaintEvent *event) { #ifdef SA_DEBUG - int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif - // all drawing done here, local variables are sufficient for the boundary - const int displayTop = 1; - const int displayBottom = height() -2; - const int displayLeft = 26; - const int displayRight = width() -26; - const int displayWidth = displayRight - displayLeft; + // update boundary + m_displayTop = 1; + m_displayBottom = height() -2; + m_displayLeft = 26; + m_displayRight = width() -26; + m_displayWidth = m_displayRight - m_displayLeft; + m_displayHeight = m_displayBottom - m_displayTop; float label_width = 20; float label_height = 16; float margin = 2; @@ -75,10 +88,11 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) painter.setRenderHint(QPainter::Antialiasing, true); // check if time labels need to be rebuilt - if ((float)m_processor->m_inBlockSize / m_processor->getSampleRate() != m_oldTimePerLine) + if (secondsPerLine() != m_oldSecondsPerLine || m_processor->m_waterfallHeight != m_oldHeight) { m_timeTics = makeTimeTics(); - m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate(); + m_oldSecondsPerLine = secondsPerLine(); + m_oldHeight = m_processor->m_waterfallHeight; } // print time labels @@ -86,32 +100,32 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); for (auto & line: m_timeTics) { - pos = timeToYPixel(line.first, displayBottom); + pos = timeToYPixel(line.first, m_displayHeight); // align first and last label to the edge if needed, otherwise center them if (line == m_timeTics.front() && pos < label_height / 2) { - painter.drawText(displayLeft - label_width - margin, displayTop - 1, + painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 1, label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip, QString(line.second.c_str())); - painter.drawText(displayRight + margin, displayTop - 1, + painter.drawText(m_displayRight + margin, m_displayTop - 1, label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, QString(line.second.c_str())); } - else if (line == m_timeTics.back() && pos > displayBottom - label_height + 2) + else if (line == m_timeTics.back() && pos > m_displayBottom - label_height + 2) { - painter.drawText(displayLeft - label_width - margin, displayBottom - label_height, + painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height, label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip, QString(line.second.c_str())); - painter.drawText(displayRight + margin, displayBottom - label_height + 2, + painter.drawText(m_displayRight + margin, m_displayBottom - label_height + 2, label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip, QString(line.second.c_str())); } else { - painter.drawText(displayLeft - label_width - margin, pos - label_height / 2, + painter.drawText(m_displayLeft - label_width - margin, pos - label_height / 2, label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, QString(line.second.c_str())); - painter.drawText(displayRight + margin, pos - label_height / 2, + painter.drawText(m_displayRight + margin, pos - label_height / 2, label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip, QString(line.second.c_str())); } @@ -125,41 +139,64 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) m_processor->binCount(), // width = number of frequency bins m_processor->m_waterfallHeight, // height = number of history lines QImage::Format_RGB32 - ).scaled(displayWidth * devicePixelRatio(), // scale to fit view.. - displayBottom * devicePixelRatio(), + ).scaled(m_displayWidth * devicePixelRatio(), // scale to fit view.. + m_displayHeight * devicePixelRatio(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution - painter.drawImage(displayLeft, displayTop, temp); + painter.drawImage(m_displayLeft, m_displayTop, temp); lock.unlock(); } else { - painter.fillRect(displayLeft, displayTop, displayWidth, displayBottom, QColor(0,0,0)); + painter.fillRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, QColor(0,0,0)); } + // draw cursor (if it is within bounds) + drawCursor(painter); + // always draw the outline painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawRoundedRect(displayLeft, displayTop, displayWidth, displayBottom, 2.0, 2.0); + painter.drawRoundedRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, 2.0, 2.0); #ifdef SA_DEBUG - // display what FPS would be achieved if waterfall ran in a loop - start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time; + draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time; + m_execution_avg = 0.95 * m_execution_avg + 0.05 * draw_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(displayRight -100, 10, 100, 16, Qt::AlignLeft, - QString(std::string("Max FPS: " + std::to_string(1000000000.0 / start_time)).c_str())); + painter.drawText(m_displayRight -100, 10, 100, 16, Qt::AlignLeft, + QString(std::string("Exec avg.: " + std::to_string(m_execution_avg).substr(0, 5) + " ms").c_str())); #endif } +// Helper functions for time conversion +float SaWaterfallView::samplesPerLine() +{ + return (float)m_processor->m_inBlockSize / m_controls->m_windowOverlapModel.value(); +} + +float SaWaterfallView::secondsPerLine() +{ + return samplesPerLine() / m_processor->getSampleRate(); +} + + // Convert time value to Y coordinate for display of given height. float SaWaterfallView::timeToYPixel(float time, int height) { float pixels_per_line = (float)height / m_processor->m_waterfallHeight; - float seconds_per_line = ((float)m_processor->m_inBlockSize / m_processor->getSampleRate()); - return pixels_per_line * time / seconds_per_line; + return pixels_per_line * time / secondsPerLine(); +} + + +// Convert Y coordinate on display of given height back to time value. +float SaWaterfallView::yPixelToTime(float position, int height) +{ + float pixels_per_line = (float)height / m_processor->m_waterfallHeight; + + return (position / pixels_per_line) * secondsPerLine(); } @@ -169,16 +206,21 @@ std::vector> SaWaterfallView::makeTimeTics() std::vector> result; float i; - // upper limit defined by number of lines * time per line - float limit = m_processor->m_waterfallHeight * ((float)m_processor->m_inBlockSize / m_processor->getSampleRate()); + // get time value of the last line + float limit = yPixelToTime(m_displayBottom, m_displayHeight); - // set increment so that about 8 tics are generated - float increment = std::round(10 * limit / 7) / 10; + // set increment to about 30 pixels (but min. 0.1 s) + float increment = std::round(10 * limit / (m_displayHeight / 30)) / 10; + if (increment < 0.1) {increment = 0.1;} // NOTE: labels positions are rounded to match the (rounded) label value for (i = 0; i <= limit; i += increment) { - if (i < 10) + if (i > 99) + { + result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 3)); + } + else if (i < 10) { result.emplace_back(std::round(i * 10) / 10, std::to_string(std::round(i * 10) / 10).substr(0, 3)); } @@ -230,3 +272,51 @@ void SaWaterfallView::updateVisibility() } } + +// Draw cursor and its coordinates if it is within display bounds. +void SaWaterfallView::drawCursor(QPainter &painter) +{ + if ( m_cursor.x() >= m_displayLeft + && m_cursor.x() <= m_displayRight + && m_cursor.y() >= m_displayTop + && m_cursor.y() <= m_displayBottom) + { + // cursor lines + painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); + painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); + + // coordinates + painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + QString tmps; + + // frequency + int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); + tmps = QString(std::string(std::to_string(freq) + " Hz").c_str()); + painter.drawText(m_displayLeft +8, 8, 100, 16, Qt::AlignLeft, tmps); + + // time + float time = yPixelToTime(m_cursor.y(), m_displayBottom); + tmps = QString(std::string(std::string(std::to_string(time)).substr(0, 5) + " s").c_str()); + painter.drawText(m_displayLeft +8, 20, 100, 16, Qt::AlignLeft, tmps); + } +} + + +// Handle mouse input: set new cursor position. +void SaWaterfallView::mouseMoveEvent(QMouseEvent *event) +{ + m_cursor = event->localPos(); +} + +void SaWaterfallView::mousePressEvent(QMouseEvent *event) +{ + m_cursor = event->localPos(); +} + + +// Handle resize event: rebuild time labels +void SaWaterfallView::resizeEvent(QResizeEvent *event) +{ + m_timeTics = makeTimeTics(); +} diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.h b/plugins/SpectrumAnalyzer/SaWaterfallView.h index 0e104c0a168..bd91d6d1641 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.h +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.h @@ -32,6 +32,7 @@ #include "SaControls.h" #include "SaProcessor.h" +class QMouseEvent; // Widget that displays a spectrum waterfall (spectrogram) and time labels. class SaWaterfallView : public QWidget @@ -48,6 +49,9 @@ class SaWaterfallView : public QWidget protected: void paintEvent(QPaintEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; private slots: void periodicUpdate(); @@ -58,9 +62,29 @@ private slots: const EffectControlDialog *m_controlDialog; // Methods and data used to make time labels - float m_oldTimePerLine; + float m_oldSecondsPerLine; + float m_oldHeight; + float samplesPerLine(); + float secondsPerLine(); float timeToYPixel(float time, int height); + float yPixelToTime(float position, int height); std::vector> makeTimeTics(); std::vector> m_timeTics; // 0..n (s) + + // current cursor location and a method to draw it + QPointF m_cursor; + void drawCursor(QPainter &painter); + + // current boundaries for drawing + unsigned int m_displayTop; + unsigned int m_displayBottom; + unsigned int m_displayLeft; + unsigned int m_displayRight; + unsigned int m_displayWidth; + unsigned int m_displayHeight; + + #ifdef SA_DEBUG + float m_execution_avg; + #endif }; #endif // SAWATERFALLVIEW_H From 249d161959783cebc034f0dee289060234e01083 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Fri, 23 Aug 2019 20:09:27 +0200 Subject: [PATCH 05/36] Fix normalization so that full scale sinewave is 0 dBFS; tweak perf. measurement outputs --- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 28 ++++++++------------- plugins/SpectrumAnalyzer/SaSpectrumView.h | 1 + src/core/fft_helpers.cpp | 1 + 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index 13904c441f7..32d87115da4 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -39,7 +39,6 @@ #ifdef SA_DEBUG #include - #include #endif @@ -146,9 +145,11 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) painter.drawText(m_displayRight -100, 10, 100, 16, Qt::AlignLeft, QString(std::string("Exec avg.: " + std::to_string(m_execution_avg).substr(0, 5) + " ms").c_str())); painter.drawText(m_displayRight -100, 30, 100, 16, Qt::AlignLeft, - QString(std::string("Path avg: " + std::to_string(m_path_avg).substr(0, 5) + " ms").c_str())); + QString(std::string("Buff. upd. avg: " + std::to_string(m_refresh_avg).substr(0, 5) + " ms").c_str())); painter.drawText(m_displayRight -100, 50, 100, 16, Qt::AlignLeft, - QString(std::string("Draw avg: " + std::to_string(m_draw_avg).substr(0, 5) + " ms").c_str())); + QString(std::string("Path build avg: " + std::to_string(m_path_avg).substr(0, 5) + " ms").c_str())); + painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft, + QString(std::string("Path draw avg: " + std::to_string(m_draw_avg).substr(0, 5) + " ms").c_str())); #endif } @@ -158,7 +159,7 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) void SaSpectrumView::drawSpectrum(QPainter &painter) { #ifdef SA_DEBUG - int path_time = 0, draw_time = 0; + int draw_time = 0; #endif // draw the graph only if there is any input, averaging residue or peaks @@ -166,14 +167,8 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR)) { lock.unlock(); - #ifdef SA_DEBUG - path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - #endif // update data buffers and reconstruct paths refreshPaths(); - #ifdef SA_DEBUG - path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time; - #endif // draw stored paths #ifdef SA_DEBUG @@ -215,8 +210,7 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) } #ifdef SA_DEBUG - // save performance measurement results - m_path_avg = 0.95 * m_path_avg + 0.05 * path_time / 1000000.0; + // save performance measurement result m_draw_avg = 0.95 * m_draw_avg + 0.05 * draw_time / 1000000.0; #endif } @@ -262,7 +256,7 @@ void SaSpectrumView::refreshPaths() } #ifdef SA_DEBUG - int make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + int path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // Use updated display buffers to prepare new paths for QPainter. // This is the second slowest action (first is the subsequent drawing); use @@ -281,13 +275,13 @@ void SaSpectrumView::refreshPaths() } } #ifdef SA_DEBUG - make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - make_time; + path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time; #endif #ifdef SA_DEBUG - // print measurement results - std::cout << "Buffer update ms: " << std::to_string(refresh_time / 1000000.0) << ", "; - std::cout << "Path-make ms: " << std::to_string(make_time / 1000000.0) << std::endl; + // save performance measurement results + m_refresh_avg = 0.95 * m_refresh_avg + 0.05 * refresh_time / 1000000.0; + m_path_avg = 0.95 * m_path_avg + 0.05 * path_time / 1000000.0; #endif } diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.h b/plugins/SpectrumAnalyzer/SaSpectrumView.h index 43b8693d36a..cc35d3d7d2d 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.h +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.h @@ -122,6 +122,7 @@ private slots: #ifdef SA_DEBUG float m_execution_avg; + float m_refresh_avg; float m_path_avg; float m_draw_avg; #endif diff --git a/src/core/fft_helpers.cpp b/src/core/fft_helpers.cpp index bc7d289e337..2cf54b7f206 100644 --- a/src/core/fft_helpers.cpp +++ b/src/core/fft_helpers.cpp @@ -66,6 +66,7 @@ int normalize(const float *abs_spectrum, float *norm_spectrum, unsigned int bin_ if (abs_spectrum == NULL || norm_spectrum == NULL) {return -1;} if (bin_count == 0 || block_size == 0) {return -1;} + block_size /= 2; for (i = 0; i < bin_count; i++) { norm_spectrum[i] = abs_spectrum[i] / block_size; From 25aa5374eb71625e91195119d466aefae99f547c Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Fri, 23 Aug 2019 23:58:07 +0200 Subject: [PATCH 06/36] Move FFT analysis to a separate thread for better performance and realtime-safe operation --- .gitmodules | 3 + include/lmms_basics.h | 18 + include/lmms_constants.h | 43 ++ plugins/SpectrumAnalyzer/Analyzer.cpp | 35 +- plugins/SpectrumAnalyzer/Analyzer.h | 18 +- plugins/SpectrumAnalyzer/CMakeLists.txt | 6 +- plugins/SpectrumAnalyzer/DataprocLauncher.h | 55 +++ plugins/SpectrumAnalyzer/README.md | 13 +- plugins/SpectrumAnalyzer/SaControls.cpp | 6 +- plugins/SpectrumAnalyzer/SaControls.h | 46 +-- plugins/SpectrumAnalyzer/SaProcessor.cpp | 403 +++++++++++-------- plugins/SpectrumAnalyzer/SaProcessor.h | 25 +- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 9 +- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 27 +- src/3rdparty/CMakeLists.txt | 1 + src/3rdparty/ringbuffer | 1 + 16 files changed, 464 insertions(+), 245 deletions(-) create mode 100644 plugins/SpectrumAnalyzer/DataprocLauncher.h create mode 160000 src/3rdparty/ringbuffer diff --git a/.gitmodules b/.gitmodules index 28d6c5d46de..56b7f3eabb3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,3 +34,6 @@ [submodule "doc/wiki"] path = doc/wiki url = https://github.com/lmms/lmms.wiki.git +[submodule "src/3rdparty/ringbuffer"] + path = src/3rdparty/ringbuffer + url = https://github.com/JohannesLorenz/ringbuffer.git diff --git a/include/lmms_basics.h b/include/lmms_basics.h index cca04e97d8f..840df09d1cf 100644 --- a/include/lmms_basics.h +++ b/include/lmms_basics.h @@ -137,6 +137,24 @@ typedef sample_t surroundSampleFrame[SURROUND_CHANNELS]; typedef sample_t sampleFrameA[DEFAULT_CHANNELS] __attribute__((__aligned__(ALIGN_SIZE))); #endif +// The sampleFrame_copier is required to store samples into the lockless ringbuffer. +// This is because sampleFrame is just a two-element array and therefore does +// not have a copy constructor which the ringbuffer class needs. +class sampleFrame_copier +{ + const sampleFrame* src; +public: + sampleFrame_copier(const sampleFrame* src) : src(src) {} + void operator()(std::size_t src_offset, std::size_t count, sampleFrame* dest) + { + for (std::size_t i = src_offset; i < src_offset + count; i++, dest++) + { + (*dest)[0] = src[i][0]; + (*dest)[1] = src[i][1]; + } + } +}; + #define STRINGIFY(s) STR(s) #define STR(PN) #PN diff --git a/include/lmms_constants.h b/include/lmms_constants.h index befa789dd5c..ae6d3d277b1 100644 --- a/include/lmms_constants.h +++ b/include/lmms_constants.h @@ -49,4 +49,47 @@ const float F_PI_SQR = (float) LD_PI_SQR; const float F_E = (float) LD_E; const float F_E_R = (float) LD_E_R; +// Frequency ranges (in Hz). +// Arbitrary low limit for logarithmic frequency scale; >1 Hz. +const int LOWEST_LOG_FREQ = 10; + +// Full range is defined by LOWEST_LOG_FREQ and current sample rate. +enum FREQUENCY_RANGES +{ + FRANGE_FULL = 0, + FRANGE_AUDIBLE, + FRANGE_BASS, + FRANGE_MIDS, + FRANGE_HIGH +}; + +const int FRANGE_AUDIBLE_START = 20; +const int FRANGE_AUDIBLE_END = 20000; +const int FRANGE_BASS_START = 20; +const int FRANGE_BASS_END = 300; +const int FRANGE_MIDS_START = 200; +const int FRANGE_MIDS_END = 5000; +const int FRANGE_HIGH_START = 4000; +const int FRANGE_HIGH_END = 20000; + +// Amplitude ranges (in dBFS). +// Reference: full scale sine wave (-1.0 to 1.0) is 0 dB. +// Doubling or halving the amplitude produces 3 dB difference. +enum AMPLITUDE_RANGES +{ + ARANGE_EXTENDED = 0, + ARANGE_AUDIBLE, + ARANGE_LOUD, + ARANGE_SILENT +}; + +const int ARANGE_EXTENDED_START = -80; +const int ARANGE_EXTENDED_END = 20; +const int ARANGE_AUDIBLE_START = -50; +const int ARANGE_AUDIBLE_END = 0; +const int ARANGE_LOUD_START = -30; +const int ARANGE_LOUD_END = 0; +const int ARANGE_SILENT_START = -60; +const int ARANGE_SILENT_END = -10; + #endif diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index 95d2574990e..084eade11ef 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -28,6 +28,7 @@ #include "Analyzer.h" #include "embed.h" +#include "lmms_basics.h" #include "plugin_export.h" #ifdef SA_DEBUG @@ -54,33 +55,57 @@ extern "C" { Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::Key *key) : Effect(&analyzer_plugin_descriptor, parent, key), m_processor(&m_controls), - m_controls(this) + m_controls(this), + m_processorThread(m_processor, m_inputBuffer, m_notifier), + // Buffer is sized to cover 4* the current maximum LMMS audio buffer size, + // so that it has some reserve space in case data processor is busy. + m_inputBuffer(4 * m_maxBufferSize) { + m_processorThread.start(); } +Analyzer::~Analyzer() +{ + m_processor.terminate(); + m_notifier.wakeAll(); + m_processorThread.wait(); +} + // Take audio data and pass them to the spectrum processor. -// Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) { + // Measure time spent in audio thread; both average and peak should be well under 1 ms. #ifdef SA_DEBUG unsigned int audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - if (audio_time - m_last_dump_time > 1000000000) + if (audio_time - m_last_dump_time > 5000000000) // print every 5 seconds { - std::cout << "Audio thread: " << m_sum_execution / m_dump_count << " ms avg / " + std::cout << "Analyzer audio thread: " << m_sum_execution / m_dump_count << " ms avg / " << m_max_execution << " ms peak." << std::endl; m_last_dump_time = audio_time; m_sum_execution = m_max_execution = m_dump_count = 0; } #endif + if (!isEnabled() || !isRunning ()) {return false;} - if (m_controls.isViewVisible()) {m_processor.analyse(buffer, frame_count);} + + // Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. + if (m_controls.isViewVisible()) { + // To avoid processing spikes on audio thread, data are stored in + // a lockless ringbuffer and processed in a separate thread. + sampleFrame_copier copier(buffer); + m_inputBuffer.write_func(copier, frame_count); + + // Inform processor to check the buffer (to avoid busy waiting). + m_notifier.wakeAll(); + } #ifdef SA_DEBUG audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - audio_time; m_dump_count++; m_sum_execution += audio_time / 1000000.0; if (audio_time / 1000000.0 > m_max_execution) {m_max_execution = audio_time / 1000000.0;} #endif + return isRunning(); } diff --git a/plugins/SpectrumAnalyzer/Analyzer.h b/plugins/SpectrumAnalyzer/Analyzer.h index 64a6ef3109f..1af82b73529 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.h +++ b/plugins/SpectrumAnalyzer/Analyzer.h @@ -27,17 +27,19 @@ #ifndef ANALYZER_H #define ANALYZER_H +#include "DataprocLauncher.h" #include "Effect.h" #include "SaControls.h" #include "SaProcessor.h" - +#include +#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" //! Top level class; handles LMMS interface and feeds data to the data processor. class Analyzer : public Effect { public: Analyzer(Model *parent, const Descriptor::SubPluginFeatures::Key *key); - virtual ~Analyzer() {}; + virtual ~Analyzer(); bool processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) override; EffectControls *controls() override {return &m_controls;} @@ -48,6 +50,18 @@ class Analyzer : public Effect SaProcessor m_processor; SaControls m_controls; + // Maximum LMMS buffer size (hard coded, the actual constant is hard to get) + const unsigned int m_maxBufferSize = 4096; + + // QThread::create() workaround + // Replace DataprocLauncher by QThread and replace initializer in constructor + // with the following commented line when LMMS CI starts using Qt > 5.9 + //m_processorThread = QThread::create([=]{m_processor.analyse(m_inputBuffer, m_notifier);}); + DataprocLauncher m_processorThread; + + ringbuffer_t m_inputBuffer; + QWaitCondition m_notifier; + #ifdef SA_DEBUG int m_last_dump_time; int m_dump_count; diff --git a/plugins/SpectrumAnalyzer/CMakeLists.txt b/plugins/SpectrumAnalyzer/CMakeLists.txt index 630fbf1be01..9ece0f1dc90 100644 --- a/plugins/SpectrumAnalyzer/CMakeLists.txt +++ b/plugins/SpectrumAnalyzer/CMakeLists.txt @@ -1,5 +1,7 @@ INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) -LINK_LIBRARIES(${FFTW3F_LIBRARIES}) + +LINK_LIBRARIES(${FFTW3F_LIBRARIES} ringbuffer) + BUILD_PLUGIN(analyzer Analyzer.cpp SaProcessor.cpp SaControls.cpp SaControlsDialog.cpp SaSpectrumView.cpp SaWaterfallView.cpp -MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h EMBEDDED_RESOURCES *.svg logo.png) +MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h DataprocLauncher.h EMBEDDED_RESOURCES *.svg logo.png) diff --git a/plugins/SpectrumAnalyzer/DataprocLauncher.h b/plugins/SpectrumAnalyzer/DataprocLauncher.h new file mode 100644 index 00000000000..1f82c726b2a --- /dev/null +++ b/plugins/SpectrumAnalyzer/DataprocLauncher.h @@ -0,0 +1,55 @@ +/* + * DataprocLauncher.h - QThread::create workaround for older Qt version + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef DATAPROCLAUNCHER_H +#define DATAPROCLAUNCHER_H + +#include +#include + +#include "SaProcessor.h" +#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" + +class DataprocLauncher : public QThread +{ +public: + explicit DataprocLauncher(SaProcessor &proc, ringbuffer_t &buffer, QWaitCondition ¬ifier) + : m_processor(&proc), + m_inputBuffer(&buffer), + m_notifier(¬ifier) + { + } + +private: + void run() override + { + m_processor->analyse(*m_inputBuffer, *m_notifier); + } + + SaProcessor *m_processor; + ringbuffer_t *m_inputBuffer; + QWaitCondition *m_notifier; +}; + +#endif // DATAPROCLAUNCHER_H diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md index 926669b357c..917e74fc687 100644 --- a/plugins/SpectrumAnalyzer/README.md +++ b/plugins/SpectrumAnalyzer/README.md @@ -4,13 +4,22 @@ This plugin consists of three widgets and back-end code to provide them with required data. -The top-level widget is SaControlDialog. It populates a configuration widget (created dynamically) and instantiates spectrum display widgets. Its main back-end class is SaControls, which holds all configuration values and globally valid constants (e.g. range definitions). +The top-level widget is SaControlDialog. It populates configuration widgets (created dynamically) and instantiates spectrum display widgets. Its main back-end class is SaControls, which holds all configuration values and globally valid constants (e.g. range definitions). SaSpectrumView and SaWaterfallView show the result of spectrum analysis. Their main back-end class is SaProcessor, which performs FFT analysis on data received from the Analyzer class, which in turn handles the interface with LMMS. ## Changelog - + 1.1.0 2019-08-29 + - advanced config: expose hidden constants to user + - advanced config: add support for FFT window overlapping + - waterfall: display at native resolution on high-DPI screens + - waterfall: add cursor and improve label density + - FFT: fix normalization so that 0 dBFS matches full-scale sinewave + - FFT: decouple data acquisition from processing and display + - FFT: separate lock for reallocation (to avoid some needless waiting) + - moved ranges and other constants to a separate file + - debug: better performance measurements 1.0.3 2019-07-25 - rename and tweak amplitude ranges based on feedback 1.0.2 2019-07-12 diff --git a/plugins/SpectrumAnalyzer/SaControls.cpp b/plugins/SpectrumAnalyzer/SaControls.cpp index 5484a06077e..d9c9f91bc19 100644 --- a/plugins/SpectrumAnalyzer/SaControls.cpp +++ b/plugins/SpectrumAnalyzer/SaControls.cpp @@ -57,10 +57,10 @@ SaControls::SaControls(Analyzer *effect) : m_spectrumResolutionModel(1.5f, 0.1f, 3.0f, 0.05f, this, tr("Spectrum display resolution")), m_peakDecayFactorModel(0.992f, 0.95f, 0.999f, 0.001f, this, tr("Peak decay multiplier")), m_averagingWeightModel(0.15f, 0.01f, 0.5f, 0.01f, this, tr("Averaging weight")), - m_waterfallHeightModel(250.0f, 50.0f, 1000.0f, 50.0f, this, tr("Waterfall history size")), + m_waterfallHeightModel(300.0f, 50.0f, 1000.0f, 50.0f, this, tr("Waterfall history size")), m_waterfallGammaModel(0.30f, 0.10f, 1.00f, 0.05f, this, tr("Waterfall gamma correction")), - m_windowOverlapModel(1.0f, 1.0f, 3.0f, 1.0f, this, tr("FFT window overlap")), - m_zeroPaddingModel(2.0f, 0.0f, 3.0f, 1.0f, this, tr("FFT zero padding")) + m_windowOverlapModel(2.0f, 1.0f, 4.0f, 1.0f, this, tr("FFT window overlap")), + m_zeroPaddingModel(2.0f, 0.0f, 4.0f, 1.0f, this, tr("FFT zero padding")) { // Frequency and amplitude ranges; order must match // FREQUENCY_RANGES and AMPLITUDE_RANGES defined in SaControls.h diff --git a/plugins/SpectrumAnalyzer/SaControls.h b/plugins/SpectrumAnalyzer/SaControls.h index 3357ba436ed..e4cdd8b7827 100644 --- a/plugins/SpectrumAnalyzer/SaControls.h +++ b/plugins/SpectrumAnalyzer/SaControls.h @@ -27,52 +27,10 @@ #include "ComboBoxModel.h" #include "EffectControls.h" +#include "lmms_constants.h" //#define SA_DEBUG 1 // define SA_DEBUG to enable performance measurements -// Frequency ranges (in Hz). -// Full range is defined by LOWEST_LOG_FREQ and current sample rate. -const int LOWEST_LOG_FREQ = 10; // arbitrary low limit for log. scale, >1 - -enum FREQUENCY_RANGES -{ - FRANGE_FULL = 0, - FRANGE_AUDIBLE, - FRANGE_BASS, - FRANGE_MIDS, - FRANGE_HIGH -}; - -const int FRANGE_AUDIBLE_START = 20; -const int FRANGE_AUDIBLE_END = 20000; -const int FRANGE_BASS_START = 20; -const int FRANGE_BASS_END = 300; -const int FRANGE_MIDS_START = 200; -const int FRANGE_MIDS_END = 5000; -const int FRANGE_HIGH_START = 4000; -const int FRANGE_HIGH_END = 20000; - -// Amplitude ranges. -// Reference: sine wave from -1.0 to 1.0 = 0 dB. -// I.e. if master volume is 100 %, positive values signify clipping. -// Doubling or halving the amplitude produces 3 dB difference. -enum AMPLITUDE_RANGES -{ - ARANGE_EXTENDED = 0, - ARANGE_AUDIBLE, - ARANGE_LOUD, - ARANGE_SILENT -}; - -const int ARANGE_EXTENDED_START = -80; -const int ARANGE_EXTENDED_END = 20; -const int ARANGE_AUDIBLE_START = -50; -const int ARANGE_AUDIBLE_END = 0; -const int ARANGE_LOUD_START = -30; -const int ARANGE_LOUD_END = 0; -const int ARANGE_SILENT_START = -60; -const int ARANGE_SILENT_END = -10; - class Analyzer; @@ -90,7 +48,7 @@ class SaControls : public EffectControls void loadSettings (const QDomElement &_this) override; QString nodeName() const override {return "Analyzer";} - int controlCount() override {return 12;} + int controlCount() override {return 20;} private: Analyzer *m_effect; diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 70743c2bf15..4c42c8c8b4f 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -30,6 +30,7 @@ #include #include "lmms_math.h" +#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" #ifdef SA_DEBUG #include @@ -39,6 +40,7 @@ SaProcessor::SaProcessor(SaControls *controls) : m_controls(controls), + m_terminate(false), m_inBlockSize(FFT_BLOCK_SIZES[0]), m_fftBlockSize(FFT_BLOCK_SIZES[0]), m_sampleRate(Engine::mixer()->processingSampleRate()), @@ -65,7 +67,8 @@ SaProcessor::SaProcessor(SaControls *controls) : m_normSpectrumL.resize(binCount(), 0); m_normSpectrumR.resize(binCount(), 0); - m_history.resize(binCount() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); + m_waterfallHeight = 100; // a small safe value + m_history.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); clear(); } @@ -85,197 +88,241 @@ SaProcessor::~SaProcessor() } -// Load a batch of data from LMMS; run FFT analysis if buffer is full enough. -void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) +// Load data from audio thread ringbuffer and run FFT analysis if buffer is full enough. +void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier) { - // only take in data if any view is visible and not paused - if ((m_spectrumActive || m_waterfallActive) && !m_controls->m_pauseModel.value()) - { - const bool stereo = m_controls->m_stereoModel.value(); - fpp_t in_frame = 0; - while (in_frame < frame_count) + ringbuffer_reader_t reader(ring_buffer); + + // Processing thread loop + while (!m_terminate) { + // If there is nothing to read, wait for notification from the writing side. + if (!reader.read_space()) { + QMutex useless_lock; + notifier.wait(&useless_lock); + useless_lock.unlock(); + } + + // skip waterfall render if processing can't keep up with input + bool overload = ring_buffer.write_space() < ring_buffer.maximum_eventual_write_space() / 2; + + auto in_buffer = reader.read_max(ring_buffer.maximum_eventual_write_space() / 4); + std::size_t frame_count = in_buffer.size(); + + // Process received data only if any view is visible and not paused. + // Also, to prevent a momentary GUI freeze under high load (due to lock + // starvation), skip analysis when buffer reallocation is requested. + if ((m_spectrumActive || m_waterfallActive) && !m_controls->m_pauseModel.value() && !m_reallocating) { - // fill sample buffers and check for zero input - bool block_empty = true; - for (; in_frame < frame_count && m_framesFilledUp < m_inBlockSize; in_frame++, m_framesFilledUp++) + const bool stereo = m_controls->m_stereoModel.value(); + fpp_t in_frame = 0; + while (in_frame < frame_count) { - if (stereo) - { - m_bufferL[m_framesFilledUp] = in_buffer[in_frame][0]; - m_bufferR[m_framesFilledUp] = in_buffer[in_frame][1]; - } - else + // Lock data access to prevent reallocation from changing + // buffers and control variables. + QMutexLocker data_lock(&m_dataAccess); + + // Fill sample buffers and check for zero input. + bool block_empty = true; + for (; in_frame < frame_count && m_framesFilledUp < m_inBlockSize; in_frame++, m_framesFilledUp++) { - m_bufferL[m_framesFilledUp] = - m_bufferR[m_framesFilledUp] = (in_buffer[in_frame][0] + in_buffer[in_frame][1]) * 0.5f; + if (stereo) + { + m_bufferL[m_framesFilledUp] = in_buffer[in_frame][0]; + m_bufferR[m_framesFilledUp] = in_buffer[in_frame][1]; + } + else + { + m_bufferL[m_framesFilledUp] = + m_bufferR[m_framesFilledUp] = (in_buffer[in_frame][0] + in_buffer[in_frame][1]) * 0.5f; + } + if (in_buffer[in_frame][0] != 0.f || in_buffer[in_frame][1] != 0.f) + { + block_empty = false; + } } - if (in_buffer[in_frame][0] != 0.f || in_buffer[in_frame][1] != 0.f) + + // Run analysis only if buffers contain enough data. + if (m_framesFilledUp < m_inBlockSize) {break;} + + // Print performance analysis once per 2 seconds if debug is enabled + #ifdef SA_DEBUG + unsigned int total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + if (total_time - m_last_dump_time > 2000000000) + { + std::cout << "FFT analysis: " << std::fixed << std::setprecision(2) + << m_sum_execution / m_dump_count << " ms avg / " + << m_max_execution << " ms peak, executing " + << m_dump_count << " times per second (" + << m_sum_execution / 10.0 << " % CPU usage)." << std::endl; + m_last_dump_time = total_time; + m_sum_execution = m_max_execution = m_dump_count = 0; + } + #endif + + // update sample rate + m_sampleRate = Engine::mixer()->processingSampleRate(); + + // apply FFT window + for (unsigned int i = 0; i < m_inBlockSize; i++) { - block_empty = false; + m_filteredBufferL[i] = m_bufferL[i] * m_fftWindow[i]; + m_filteredBufferR[i] = m_bufferR[i] * m_fftWindow[i]; } - } - - // Run analysis only if buffers contain enough data. - // Also, to prevent audio interruption and a momentary GUI freeze, - // skip analysis if buffers are being reallocated. - if (m_framesFilledUp < m_inBlockSize || m_reallocating) {return;} - - // Print performance analysis once per second if debug is enabled - #ifdef SA_DEBUG - unsigned int fft_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - if (fft_time - m_last_dump_time > 1000000000) + + // Run FFT on left channel, convert the result to absolute magnitude + // spectrum and normalize it. + fftwf_execute(m_fftPlanL); + absspec(m_spectrumL, m_absSpectrumL.data(), binCount()); + normalize(m_absSpectrumL, m_normSpectrumL, m_inBlockSize); + + // repeat analysis for right channel if stereo processing is enabled + if (stereo) { - std::cout << "FFT analysis: " << std::fixed << std::setprecision(2) - << m_sum_execution / m_dump_count << " ms avg / " - << m_max_execution << " ms peak, executing " - << m_dump_count << " times per second (" - << m_sum_execution / 10.0 << " % CPU usage)." << std::endl; - m_last_dump_time = fft_time; - m_sum_execution = m_max_execution = m_dump_count = 0; + fftwf_execute(m_fftPlanR); + absspec(m_spectrumR, m_absSpectrumR.data(), binCount()); + normalize(m_absSpectrumR, m_normSpectrumR, m_inBlockSize); } - #endif - // update sample rate - m_sampleRate = Engine::mixer()->processingSampleRate(); - - // apply FFT window - for (unsigned int i = 0; i < m_inBlockSize; i++) - { - m_filteredBufferL[i] = m_bufferL[i] * m_fftWindow[i]; - m_filteredBufferR[i] = m_bufferR[i] * m_fftWindow[i]; - } - - // lock data shared with SaSpectrumView and SaWaterfallView - QMutexLocker lock(&m_dataAccess); - - // Run FFT on left channel, convert the result to absolute magnitude - // spectrum and normalize it. - fftwf_execute(m_fftPlanL); - absspec(m_spectrumL, m_absSpectrumL.data(), binCount()); - normalize(m_absSpectrumL, m_normSpectrumL, m_inBlockSize); - - // repeat analysis for right channel if stereo processing is enabled - if (stereo) - { - fftwf_execute(m_fftPlanR); - absspec(m_spectrumR, m_absSpectrumR.data(), binCount()); - normalize(m_absSpectrumR, m_normSpectrumR, m_inBlockSize); - } + #ifdef SA_DEBUG + unsigned int analysis_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; + std::cout << "FFT analysis: "<< analysis_time / 1000000.0 << ", "; + #endif - // count empty lines so that empty history does not have to update - if (block_empty && m_waterfallNotEmpty) - { - m_waterfallNotEmpty -= 1; - } - else if (!block_empty) - { - m_waterfallNotEmpty = m_waterfallHeight + 2; - } + // count empty lines so that empty history does not have to update + if (block_empty && m_waterfallNotEmpty) + { + m_waterfallNotEmpty -= 1; + } + else if (!block_empty) + { + m_waterfallNotEmpty = m_waterfallHeight + 2; + } - if (m_waterfallActive && m_waterfallNotEmpty) - { - // move waterfall history one line down and clear the top line - QRgb *pixel = (QRgb *)m_history.data(); - std::copy(pixel, - pixel + binCount() * m_waterfallHeight - binCount(), - pixel + binCount()); - memset(pixel, 0, binCount() * sizeof (QRgb)); - - // add newest result on top - int target; // pixel being constructed - float accL = 0; // accumulators for merging multiple bins - float accR = 0; - - for (unsigned int i = 0; i < binCount(); i++) + if (m_waterfallActive && m_waterfallNotEmpty) { - // Every frequency bin spans a frequency range that must be - // partially or fully mapped to a pixel. Any inconsistency - // may be seen in the spectrogram as dark or white lines -- - // play white noise to confirm your change did not break it. - float band_start = freqToXPixel(binToFreq(i) - binBandwidth() / 2.0, binCount()); - float band_end = freqToXPixel(binToFreq(i + 1) - binBandwidth() / 2.0, binCount()); - if (m_controls->m_logXModel.value()) + #ifdef SA_DEBUG + unsigned int move_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + #endif + // move waterfall history one line down and clear the top line + QRgb *pixel = (QRgb *)m_history.data(); + std::copy(pixel, + pixel + waterfallWidth() * m_waterfallHeight - waterfallWidth(), + pixel + waterfallWidth()); + memset(pixel, 0, waterfallWidth() * sizeof (QRgb)); + #ifdef SA_DEBUG + move_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - move_time; + std::cout << "Waterfall movement: "<< move_time / 1000000.0 << ", "; + unsigned int render_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + #endif + + // add newest result on top + int target; // pixel being constructed + float accL = 0; // accumulators for merging multiple bins + float accR = 0; + + for (unsigned int i = 0; i < waterfallWidth(); i++) { - // Logarithmic scale - if (band_end - band_start > 1.0) + // fill line with red color to indicate lost data if CPU cannot keep up + if (overload) { + pixel[i] = qRgb(42, 0, 0); + continue; + } + + // Every frequency bin spans a frequency range that must be + // partially or fully mapped to a pixel. Any inconsistency + // may be seen in the spectrogram as dark or white lines -- + // play white noise to confirm your change did not break it. + float band_start = freqToXPixel(binToFreq(i) - binBandwidth() / 2.0, waterfallWidth()); + float band_end = freqToXPixel(binToFreq(i + 1) - binBandwidth() / 2.0, waterfallWidth()); + if (m_controls->m_logXModel.value()) { - // band spans multiple pixels: draw all pixels it covers - for (target = (int)band_start; target < (int)band_end; target++) + // Logarithmic scale + if (band_end - band_start > 1.0) { - if (target >= 0 && target < binCount()) + // band spans multiple pixels: draw all pixels it covers + for (target = (int)band_start; target < (int)band_end; target++) { - pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + if (target >= 0 && target < waterfallWidth()) + { + pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + } } - } - // save remaining portion of the band for the following band / pixel - // (in case the next band uses sub-pixel drawing) - accL = (band_end - (int)band_end) * m_normSpectrumL[i]; - accR = (band_end - (int)band_end) * m_normSpectrumR[i]; - } - else - { - // sub-pixel drawing; add contribution of current band - target = (int)band_start; - if ((int)band_start == (int)band_end) - { - // band ends within current target pixel, accumulate - accL += (band_end - band_start) * m_normSpectrumL[i]; - accR += (band_end - band_start) * m_normSpectrumR[i]; + // save remaining portion of the band for the following band / pixel + // (in case the next band uses sub-pixel drawing) + accL = (band_end - (int)band_end) * m_normSpectrumL[i]; + accR = (band_end - (int)band_end) * m_normSpectrumR[i]; } else { - // Band ends in the next pixel -- finalize the current pixel. - // Make sure contribution is split correctly on pixel boundary. - accL += ((int)band_end - band_start) * m_normSpectrumL[i]; - accR += ((int)band_end - band_start) * m_normSpectrumR[i]; - - if (target >= 0 && target < binCount()) {pixel[target] = makePixel(accL, accR);} + // sub-pixel drawing; add contribution of current band + target = (int)band_start; + if ((int)band_start == (int)band_end) + { + // band ends within current target pixel, accumulate + accL += (band_end - band_start) * m_normSpectrumL[i]; + accR += (band_end - band_start) * m_normSpectrumR[i]; + } + else + { + // Band ends in the next pixel -- finalize the current pixel. + // Make sure contribution is split correctly on pixel boundary. + accL += ((int)band_end - band_start) * m_normSpectrumL[i]; + accR += ((int)band_end - band_start) * m_normSpectrumR[i]; - // save remaining portion of the band for the following band / pixel - accL = (band_end - (int)band_end) * m_normSpectrumL[i]; - accR = (band_end - (int)band_end) * m_normSpectrumR[i]; + if (target >= 0 && target < waterfallWidth()) {pixel[target] = makePixel(accL, accR);} + + // save remaining portion of the band for the following band / pixel + accL = (band_end - (int)band_end) * m_normSpectrumL[i]; + accR = (band_end - (int)band_end) * m_normSpectrumR[i]; + } } } - } - else - { - // Linear: always draws one or more pixels per band - for (target = (int)band_start; target < band_end; target++) + else { - if (target >= 0 && target < binCount()) + // Linear: always draws one or more pixels per band + for (target = (int)band_start; target < band_end; target++) { - pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + if (target >= 0 && target < waterfallWidth()) + { + pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + } } } } + #ifdef SA_DEBUG + render_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - render_time; + std::cout << "Waterfall render: "<< render_time / 1000000.0 << ", "; + #endif + } + #ifdef SA_DEBUG + unsigned int cleanup_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + #endif + // clean up before checking for more data from input buffer + const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); + if (overlaps == 1) // each sample used only once + { + m_framesFilledUp = 0; + } + else + { + const unsigned int drop = m_inBlockSize / overlaps; + std::move(m_bufferL.begin() + drop, m_bufferL.end(), m_bufferL.begin()); + std::move(m_bufferR.begin() + drop, m_bufferR.end(), m_bufferR.begin()); + m_framesFilledUp -= drop; } - } - // clean up before checking for more data from input buffer - const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); - if (overlaps == 1) // each sample used only once - { - m_framesFilledUp = 0; - } - else - { - const unsigned int drop = m_inBlockSize / overlaps; - m_bufferL.erase(m_bufferL.begin(), m_bufferL.begin() + drop); - m_bufferR.erase(m_bufferR.begin(), m_bufferR.begin() + drop); - m_bufferL.resize(m_inBlockSize, 0); - m_bufferR.resize(m_inBlockSize, 0); - m_framesFilledUp -= m_inBlockSize / overlaps; - } - - #ifdef SA_DEBUG - // report FFT processing speed - fft_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - fft_time; - m_dump_count++; - m_sum_execution += fft_time / 1000000.0; - if (fft_time / 1000000.0 > m_max_execution) {m_max_execution = fft_time / 1000000.0;} - #endif - } - } + #ifdef SA_DEBUG + cleanup_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - cleanup_time; + std::cout << "Cleanup: "<< cleanup_time / 1000000.0 << std::endl; + // measure overall FFT processing speed + total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; + m_dump_count++; + m_sum_execution += total_time / 1000000.0; + if (total_time / 1000000.0 > m_max_execution) {m_max_execution = total_time / 1000000.0;} + #endif + } // frame filler and processing + } // process if active + } // thread loop end } @@ -348,12 +395,16 @@ void SaProcessor::reallocateBuffers() new_bins = new_fft_size / 2 +1; - // Lock data shared with SaSpectrumView and SaWaterfallView. - // The m_reallocating is here to tell analyse() to avoid asking for the - // lock, since fftw3 can take a while to find the fastest FFT algorithm - // for given machine, which would produce interruption in the audio stream. + // Use m_reallocating to tell analyse() to avoid asking for the lock. This + // is needed because under heavy load the FFT thread requests data lock so + // often that this routine could end up waiting even for several seconds. m_reallocating = true; - QMutexLocker lock(&m_dataAccess); + + // Lock data shared with SaSpectrumView and SaWaterfallView. + // Reallocation lock must be acquired first to avoid deadlock (a view class + // may already have it and request the "stronger" data lock on top of that). + QMutexLocker reloc_lock(&m_reallocationAccess); + QMutexLocker data_lock(&m_dataAccess); // destroy old FFT plan and free the result buffer if (m_fftPlanL != NULL) {fftwf_destroy_plan(m_fftPlanL);} @@ -375,7 +426,9 @@ void SaProcessor::reallocateBuffers() if (m_fftPlanL == NULL || m_fftPlanR == NULL) { - std::cerr << "Failed to create new FFT plan!" << std::endl; + #ifdef SA_DEBUG + std::cerr << "Analyzer: failed to create new FFT plan!" << std::endl; + #endif } m_absSpectrumL.resize(new_bins, 0); m_absSpectrumR.resize(new_bins, 0); @@ -383,14 +436,18 @@ void SaProcessor::reallocateBuffers() m_normSpectrumR.resize(new_bins, 0); m_waterfallHeight = m_controls->m_waterfallHeightModel.value(); - m_history.resize(new_bins * m_waterfallHeight * sizeof qRgb(0,0,0), 0); + m_history.resize((new_bins < m_waterfallMaxWidth ? new_bins : m_waterfallMaxWidth) + * m_waterfallHeight + * sizeof qRgb(0,0,0), 0); // done; publish new sizes and clean up m_inBlockSize = new_in_size; m_fftBlockSize = new_fft_size; - lock.unlock(); + data_lock.unlock(); + reloc_lock.unlock(); m_reallocating = false; + clear(); } @@ -448,6 +505,14 @@ unsigned int SaProcessor::binCount() const } +// FFT transform can easily produce more bins than can be reasonably useful for +// display. Cap the width at 3840: full screen on UHD display should be enough. +unsigned int SaProcessor::waterfallWidth() const +{ + return binCount() < m_waterfallMaxWidth ? binCount() : m_waterfallMaxWidth; +} + + // Return the center frequency of given frequency bin. float SaProcessor::binToFreq(unsigned int bin_index) const { diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index 5611e38965a..00b534fa290 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -29,11 +29,14 @@ #include #include +#include #include #include "fft_helpers.h" #include "SaControls.h" +template +class ringbuffer_t; //! Receives audio data, runs FFT analysis and stores the result. class SaProcessor @@ -42,7 +45,9 @@ class SaProcessor explicit SaProcessor(SaControls *controls); virtual ~SaProcessor(); - void analyse(sampleFrame *in_buffer, const fpp_t frame_count); + // analysis thread and a method to terminate it + void analyse(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier); + void terminate() {m_terminate = true;} // inform processor if any processing is actually required void setSpectrumActive(bool active); @@ -72,13 +77,23 @@ class SaProcessor float getAmpRangeMin(bool linear = false) const; float getAmpRangeMax() const; - // data access lock must be acquired by any friendly class that touches - // the results, mainly to prevent unexpected mid-way reallocation + // Reallocation lock prevents the processor from changing size of its buffers. + // It is used to keep consistent bin-to-frequency mapping while drawing the + // spectrum. The processor is meanwhile free to work on another block. + QMutex m_reallocationAccess; + // Data access lock prevents the processor from changing both size and content + // of its buffers. It is used when any friendly class reads the results directly. + // It causes FFT analysis to be paused, so this lock should be used sparingly. + // If using both locks at the same time, reallocation lock MUST be acquired first. QMutex m_dataAccess; + private: SaControls *m_controls; + // thread communication and control + bool m_terminate; + // currently valid configuration unsigned int m_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size unsigned int m_inBlockSize; //!< size of input (time domain) data block @@ -105,8 +120,10 @@ class SaProcessor // spectrum history for waterfall: new normSpectrum lines are added on top std::vector m_history; - unsigned int m_waterfallHeight = 250; // Number of stored lines. + unsigned int m_waterfallHeight; // Number of stored lines. // Note: high values may make it harder to see transients. + const unsigned int m_waterfallMaxWidth = 3840; + unsigned int waterfallWidth() const; //!< binCount value capped at 3840 (for display) // book keeping bool m_spectrumActive; diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index 32d87115da4..c72b70e65f0 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -220,9 +220,9 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) // and build QPainter paths. void SaSpectrumView::refreshPaths() { - // Lock is required for the entire function, mainly to prevent block size - // changes from causing reallocation of data structures mid-way. - QMutexLocker lock(&m_processor->m_dataAccess); + // Reallocation lock is required for the entire function, to keep display + // buffer size consistent with block size. + QMutexLocker reloc_lock(&m_processor->m_reallocationAccess); // check if bin count changed and reallocate display buffers accordingly if (m_processor->binCount() != m_displayBufferL.size()) @@ -241,9 +241,12 @@ void SaSpectrumView::refreshPaths() #ifdef SA_DEBUG int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif + // The stronger Data lock is needed only for the duration of actual data reading. + QMutexLocker data_lock(&m_processor->m_dataAccess); m_decaySum = 0; updateBuffers(m_processor->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data()); updateBuffers(m_processor->m_normSpectrumR.data(), m_displayBufferR.data(), m_peakBufferR.data()); + data_lock.unlock(); #ifdef SA_DEBUG refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time; #endif diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 0d780db9195..6b8dd69ebc4 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -52,6 +52,13 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); + m_displayTop = 1; + m_displayBottom = height() -2; + m_displayLeft = 26; + m_displayRight = width() -26; + m_displayWidth = m_displayRight - m_displayLeft; + m_displayHeight = m_displayBottom - m_displayTop; + m_timeTics = makeTimeTics(); m_oldSecondsPerLine = 0; m_oldHeight = 0; @@ -70,13 +77,11 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q void SaWaterfallView::paintEvent(QPaintEvent *event) { #ifdef SA_DEBUG - int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + unsigned int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // update boundary - m_displayTop = 1; m_displayBottom = height() -2; - m_displayLeft = 26; m_displayRight = width() -26; m_displayWidth = m_displayRight - m_displayLeft; m_displayHeight = m_displayBottom - m_displayTop; @@ -136,17 +141,16 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) { QMutexLocker lock(&m_processor->m_dataAccess); QImage temp = QImage(m_processor->m_history.data(), // raw pixel data to display - m_processor->binCount(), // width = number of frequency bins + m_processor->waterfallWidth(), // width = number of frequency bins m_processor->m_waterfallHeight, // height = number of history lines - QImage::Format_RGB32 - ).scaled(m_displayWidth * devicePixelRatio(), // scale to fit view.. + QImage::Format_RGB32); + lock.unlock(); + temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution + painter.drawImage(m_displayLeft, m_displayTop, + temp.scaled(m_displayWidth * devicePixelRatio(), m_displayHeight * devicePixelRatio(), Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution - painter.drawImage(m_displayLeft, m_displayTop, temp); - - lock.unlock(); + Qt::SmoothTransformation)); } else { @@ -194,6 +198,7 @@ float SaWaterfallView::timeToYPixel(float time, int height) // Convert Y coordinate on display of given height back to time value. float SaWaterfallView::yPixelToTime(float position, int height) { + if (height == 0) {height = 1;} float pixels_per_line = (float)height / m_processor->m_waterfallHeight; return (position / pixels_per_line) * secondsPerLine(); diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index 473e7702f09..a2047a2f4e4 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -8,5 +8,6 @@ IF(LMMS_BUILD_LINUX AND WANT_VST) add_subdirectory(qt5-x11embed) ENDIF() +ADD_SUBDIRECTORY(ringbuffer) ADD_SUBDIRECTORY(rpmalloc) ADD_SUBDIRECTORY(weakjack) diff --git a/src/3rdparty/ringbuffer b/src/3rdparty/ringbuffer new file mode 160000 index 00000000000..fc0e1f38f74 --- /dev/null +++ b/src/3rdparty/ringbuffer @@ -0,0 +1 @@ +Subproject commit fc0e1f38f740e5d7d11963f52cfcb6445db6e192 From 3707eeb7e668665011fc03a332fde9fb7f6b3c02 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 1 Sep 2019 20:12:00 +0200 Subject: [PATCH 07/36] Performance optimizations and some final touches here and there --- plugins/SpectrumAnalyzer/README.md | 1 + plugins/SpectrumAnalyzer/SaControls.cpp | 9 ++-- plugins/SpectrumAnalyzer/SaControls.h | 13 ++--- plugins/SpectrumAnalyzer/SaControlsDialog.cpp | 2 +- plugins/SpectrumAnalyzer/SaProcessor.cpp | 50 ++++++++----------- plugins/SpectrumAnalyzer/SaProcessor.h | 13 +++-- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 29 ++++++----- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 17 ++++--- 8 files changed, 72 insertions(+), 62 deletions(-) diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md index 917e74fc687..40159846676 100644 --- a/plugins/SpectrumAnalyzer/README.md +++ b/plugins/SpectrumAnalyzer/README.md @@ -20,6 +20,7 @@ SaSpectrumView and SaWaterfallView show the result of spectrum analysis. Their m - FFT: separate lock for reallocation (to avoid some needless waiting) - moved ranges and other constants to a separate file - debug: better performance measurements + - various performance optimizations 1.0.3 2019-07-25 - rename and tweak amplitude ranges based on feedback 1.0.2 2019-07-12 diff --git a/plugins/SpectrumAnalyzer/SaControls.cpp b/plugins/SpectrumAnalyzer/SaControls.cpp index d9c9f91bc19..6be298e27e4 100644 --- a/plugins/SpectrumAnalyzer/SaControls.cpp +++ b/plugins/SpectrumAnalyzer/SaControls.cpp @@ -105,12 +105,15 @@ SaControls::SaControls(Analyzer *effect) : // Colors // Background color is defined by Qt / theme. - // Make sure the sum of colors for L and R channel stays lower or equal - // to 255. Otherwise the Waterfall pixels may overflow back to 0 even when - // the input signal isn't clipping (over 1.0). + // Make sure the sum of colors for L and R channel results into a neutral + // color that has at least one component equal to 255 (i.e. ideally white). + // This means the color overflows to zero exactly when signal reaches + // clipping threshold, indicating the problematic frequency to user. + // Mono waterfall color should have similarly at least one component at 255. m_colorL = QColor(51, 148, 204, 135); m_colorR = QColor(204, 107, 51, 135); m_colorMono = QColor(51, 148, 204, 204); + m_colorMonoW = QColor(64, 185, 255, 255); m_colorBG = QColor(7, 7, 7, 255); // ~20 % gray (after gamma correction) m_colorGrid = QColor(30, 34, 38, 255); // ~40 % gray (slightly cold / blue) m_colorLabels = QColor(192, 202, 212, 255); // ~90 % gray (slightly cold / blue) diff --git a/plugins/SpectrumAnalyzer/SaControls.h b/plugins/SpectrumAnalyzer/SaControls.h index e4cdd8b7827..4673416bc20 100644 --- a/plugins/SpectrumAnalyzer/SaControls.h +++ b/plugins/SpectrumAnalyzer/SaControls.h @@ -81,12 +81,13 @@ class SaControls : public EffectControls FloatModel m_zeroPaddingModel; // colors (hard-coded, values must add up to specific numbers) - QColor m_colorL; - QColor m_colorR; - QColor m_colorMono; - QColor m_colorBG; - QColor m_colorGrid; - QColor m_colorLabels; + QColor m_colorL; //!< color of the left channel + QColor m_colorR; //!< color of the right channel + QColor m_colorMono; //!< mono color for spectrum display + QColor m_colorMonoW; //!< mono color for waterfall display + QColor m_colorBG; //!< spectrum display background color + QColor m_colorGrid; //!< color of grid lines + QColor m_colorLabels; //!< color of axis labels friend class SaControlsDialog; friend class SaSpectrumView; diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp index b713dc90c42..d89cc109315 100644 --- a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -259,7 +259,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) Knob *waterfallHeightKnob = new Knob(knobSmall_17, this); waterfallHeightKnob->setModel(&controls->m_waterfallHeightModel); waterfallHeightKnob->setLabel(tr("Waterfall height")); - waterfallHeightKnob->setToolTip(tr("Increase to get slower scrolling, decrease to see fast transitions better.")); + waterfallHeightKnob->setToolTip(tr("Increase to get slower scrolling, decrease to see fast transitions better. Warning: medium CPU usage.")); waterfallHeightKnob->setHintText(tr("Keep"), tr(" lines")); advanced_layout->addWidget(waterfallHeightKnob, 0, 2, 1, 1, Qt::AlignCenter); processor->reallocateBuffers(); diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 4c42c8c8b4f..f3cf91c33c6 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -68,6 +68,7 @@ SaProcessor::SaProcessor(SaControls *controls) : m_normSpectrumR.resize(binCount(), 0); m_waterfallHeight = 100; // a small safe value + m_history_work.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); m_history.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); clear(); @@ -153,7 +154,7 @@ void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition << m_sum_execution / m_dump_count << " ms avg / " << m_max_execution << " ms peak, executing " << m_dump_count << " times per second (" - << m_sum_execution / 10.0 << " % CPU usage)." << std::endl; + << m_sum_execution / 20.0 << " % CPU usage)." << std::endl; m_last_dump_time = total_time; m_sum_execution = m_max_execution = m_dump_count = 0; } @@ -183,11 +184,6 @@ void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition normalize(m_absSpectrumR, m_normSpectrumR, m_inBlockSize); } - #ifdef SA_DEBUG - unsigned int analysis_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; - std::cout << "FFT analysis: "<< analysis_time / 1000000.0 << ", "; - #endif - // count empty lines so that empty history does not have to update if (block_empty && m_waterfallNotEmpty) { @@ -200,20 +196,12 @@ void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition if (m_waterfallActive && m_waterfallNotEmpty) { - #ifdef SA_DEBUG - unsigned int move_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - #endif // move waterfall history one line down and clear the top line - QRgb *pixel = (QRgb *)m_history.data(); + QRgb *pixel = (QRgb *)m_history_work.data(); std::copy(pixel, pixel + waterfallWidth() * m_waterfallHeight - waterfallWidth(), pixel + waterfallWidth()); memset(pixel, 0, waterfallWidth() * sizeof (QRgb)); - #ifdef SA_DEBUG - move_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - move_time; - std::cout << "Waterfall movement: "<< move_time / 1000000.0 << ", "; - unsigned int render_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - #endif // add newest result on top int target; // pixel being constructed @@ -289,14 +277,16 @@ void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition } } } - #ifdef SA_DEBUG - render_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - render_time; - std::cout << "Waterfall render: "<< render_time / 1000000.0 << ", "; - #endif + + // Copy work buffer to result buffer. Done only if requested, so + // that time isn't wasted on updating faster than display FPS. + // (The copy is about as expensive as the movement.) + if (m_flipRequest) + { + m_history = m_history_work; + m_flipRequest = false; + } } - #ifdef SA_DEBUG - unsigned int cleanup_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); - #endif // clean up before checking for more data from input buffer const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); if (overlaps == 1) // each sample used only once @@ -312,8 +302,6 @@ void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition } #ifdef SA_DEBUG - cleanup_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - cleanup_time; - std::cout << "Cleanup: "<< cleanup_time / 1000000.0 << std::endl; // measure overall FFT processing speed total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; m_dump_count++; @@ -347,9 +335,9 @@ QRgb SaProcessor::makePixel(float left, float right) const { float ampL = pow(left, gamma_correction); // make mono color brighter to compensate for the fact it is not summed - return qRgb(m_controls->m_colorMono.lighter().red() * ampL, - m_controls->m_colorMono.lighter().green() * ampL, - m_controls->m_colorMono.lighter().blue() * ampL); + return qRgb(m_controls->m_colorMonoW.red() * ampL, + m_controls->m_colorMonoW.green() * ampL, + m_controls->m_colorMonoW.blue() * ampL); } } @@ -436,9 +424,12 @@ void SaProcessor::reallocateBuffers() m_normSpectrumR.resize(new_bins, 0); m_waterfallHeight = m_controls->m_waterfallHeightModel.value(); + m_history_work.resize((new_bins < m_waterfallMaxWidth ? new_bins : m_waterfallMaxWidth) + * m_waterfallHeight + * sizeof qRgb(0,0,0), 0); m_history.resize((new_bins < m_waterfallMaxWidth ? new_bins : m_waterfallMaxWidth) - * m_waterfallHeight - * sizeof qRgb(0,0,0), 0); + * m_waterfallHeight + * sizeof qRgb(0,0,0), 0); // done; publish new sizes and clean up m_inBlockSize = new_in_size; @@ -475,6 +466,7 @@ void SaProcessor::clear() std::fill(m_absSpectrumR.begin(), m_absSpectrumR.end(), 0); std::fill(m_normSpectrumL.begin(), m_normSpectrumL.end(), 0); std::fill(m_normSpectrumR.begin(), m_normSpectrumR.end(), 0); + std::fill(m_history_work.begin(), m_history_work.end(), 0); std::fill(m_history.begin(), m_history.end(), 0); } diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index 00b534fa290..fac90107815 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -52,6 +52,7 @@ class SaProcessor // inform processor if any processing is actually required void setSpectrumActive(bool active); void setWaterfallActive(bool active); + void flipRequest() {m_flipRequest = true;} // request refresh of history buffer // configuration is taken from models in SaControls; some changes require // an exlicit update request (reallocation and window rebuild) @@ -79,10 +80,12 @@ class SaProcessor // Reallocation lock prevents the processor from changing size of its buffers. // It is used to keep consistent bin-to-frequency mapping while drawing the - // spectrum. The processor is meanwhile free to work on another block. + // spectrum and to make sure reading side does not find itself out of bounds. + // The processor is meanwhile free to work on another block. QMutex m_reallocationAccess; // Data access lock prevents the processor from changing both size and content - // of its buffers. It is used when any friendly class reads the results directly. + // of its buffers. It is used when writing to a result buffer, or when a friendly + // class reads them and needs guaranteed data consistency. // It causes FFT analysis to be paused, so this lock should be used sparingly. // If using both locks at the same time, reallocation lock MUST be acquired first. QMutex m_dataAccess; @@ -119,8 +122,10 @@ class SaProcessor std::vector m_normSpectrumR; //!< frequency domain samples (normalized) (right) // spectrum history for waterfall: new normSpectrum lines are added on top - std::vector m_history; - unsigned int m_waterfallHeight; // Number of stored lines. + std::vector m_history_work; //!< local history buffer for render + std::vector m_history; //!< public buffer for reading + bool m_flipRequest; //!< update public buffer only when requested + unsigned int m_waterfallHeight; //!< number of stored lines in history buffer // Note: high values may make it harder to see transients. const unsigned int m_waterfallMaxWidth = 3840; unsigned int waterfallWidth() const; //!< binCount value capped at 3840 (for display) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index c72b70e65f0..de9358caae8 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -142,13 +142,13 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) m_execution_avg = 0.95 * m_execution_avg + 0.05 * total_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(m_displayRight -100, 10, 100, 16, Qt::AlignLeft, + painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft, QString(std::string("Exec avg.: " + std::to_string(m_execution_avg).substr(0, 5) + " ms").c_str())); - painter.drawText(m_displayRight -100, 30, 100, 16, Qt::AlignLeft, + painter.drawText(m_displayRight -150, 30, 100, 16, Qt::AlignLeft, QString(std::string("Buff. upd. avg: " + std::to_string(m_refresh_avg).substr(0, 5) + " ms").c_str())); - painter.drawText(m_displayRight -100, 50, 100, 16, Qt::AlignLeft, + painter.drawText(m_displayRight -150, 50, 100, 16, Qt::AlignLeft, QString(std::string("Path build avg: " + std::to_string(m_path_avg).substr(0, 5) + " ms").c_str())); - painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft, + painter.drawText(m_displayRight -150, 70, 100, 16, Qt::AlignLeft, QString(std::string("Path draw avg: " + std::to_string(m_draw_avg).substr(0, 5) + " ms").c_str())); #endif @@ -163,7 +163,7 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) #endif // draw the graph only if there is any input, averaging residue or peaks - QMutexLocker lock(&m_processor->m_dataAccess); + QMutexLocker lock(&m_processor->m_reallocationAccess); if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR)) { lock.unlock(); @@ -241,12 +241,9 @@ void SaSpectrumView::refreshPaths() #ifdef SA_DEBUG int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif - // The stronger Data lock is needed only for the duration of actual data reading. - QMutexLocker data_lock(&m_processor->m_dataAccess); m_decaySum = 0; updateBuffers(m_processor->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data()); updateBuffers(m_processor->m_normSpectrumR.data(), m_displayBufferR.data(), m_peakBufferR.data()); - data_lock.unlock(); #ifdef SA_DEBUG refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time; #endif @@ -291,8 +288,10 @@ void SaSpectrumView::refreshPaths() // Update display buffers: add new data, update average and peaks / reference. // Output the sum of all displayed values -- draw only if it is non-zero. -// NOTE: The calling function is responsible for acquiring SaProcessor data -// access lock! +// NOTE: The calling function is responsible for acquiring SaProcessor +// reallocation access lock! Data access lock is not needed: the final result +// buffer is updated very quickly and the worst case is that one frame will be +// part new, part old. At reasonable frame rate, such difference is invisible.. void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer) { for (int n = 0; n < m_processor->binCount(); n++) @@ -555,14 +554,18 @@ void SaSpectrumView::drawCursor(QPainter &painter) painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); - // coordinates + // coordinates: background painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.fillRect(m_displayLeft +5, 5, 80, 32, QColor(0, 0, 0, 64)); + + // coordinates: text + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); QString tmps; // frequency int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str()); - painter.drawText(m_displayLeft +8, 8, 100, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft +8, 8, 80, 16, Qt::AlignLeft, tmps); // amplitude float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); @@ -575,7 +578,7 @@ void SaSpectrumView::drawCursor(QPainter &painter) // add 0.0005 to get proper rounding to 3 decimal places tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str()); } - painter.drawText(m_displayLeft +8, 20, 100, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft +8, 20, 80, 16, Qt::AlignLeft, tmps); } } diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 6b8dd69ebc4..d4a3b13b7d2 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -139,7 +139,7 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) // draw the spectrogram precomputed in SaProcessor if (m_processor->m_waterfallNotEmpty) { - QMutexLocker lock(&m_processor->m_dataAccess); + QMutexLocker lock(&m_processor->m_reallocationAccess); QImage temp = QImage(m_processor->m_history.data(), // raw pixel data to display m_processor->waterfallWidth(), // width = number of frequency bins m_processor->m_waterfallHeight, // height = number of history lines @@ -151,6 +151,7 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) m_displayHeight * devicePixelRatio(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + m_processor->flipRequest(); } else { @@ -168,7 +169,7 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time; m_execution_avg = 0.95 * m_execution_avg + 0.05 * draw_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(m_displayRight -100, 10, 100, 16, Qt::AlignLeft, + painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft, QString(std::string("Exec avg.: " + std::to_string(m_execution_avg).substr(0, 5) + " ms").c_str())); #endif } @@ -258,7 +259,7 @@ void SaWaterfallView::updateVisibility() { // clear old data before showing the waterfall QMutexLocker lock(&m_processor->m_dataAccess); - std::fill(m_processor->m_history.begin(), m_processor->m_history.end(), 0); + std::fill(m_processor->m_history_work.begin(), m_processor->m_history_work.end(), 0); lock.unlock(); setVisible(true); @@ -291,19 +292,23 @@ void SaWaterfallView::drawCursor(QPainter &painter) painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); - // coordinates + // coordinates: background painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.fillRect(m_displayLeft +5, 5, 80, 32, QColor(0, 0, 0, 64)); + + // coordinates: text + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); QString tmps; // frequency int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); tmps = QString(std::string(std::to_string(freq) + " Hz").c_str()); - painter.drawText(m_displayLeft +8, 8, 100, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft +8, 8, 80, 16, Qt::AlignLeft, tmps); // time float time = yPixelToTime(m_cursor.y(), m_displayBottom); tmps = QString(std::string(std::string(std::to_string(time)).substr(0, 5) + " s").c_str()); - painter.drawText(m_displayLeft +8, 20, 100, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft +8, 20, 80, 16, Qt::AlignLeft, tmps); } } From cb3e701a58a5e42883e9d6172cdee2a8d148d26d Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 1 Sep 2019 22:12:14 +0200 Subject: [PATCH 08/36] Improve cursor coordinates display --- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 19 +++++++++++++++---- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 19 +++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index de9358caae8..c8c5bfd3cea 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -554,9 +554,16 @@ void SaSpectrumView::drawCursor(QPainter &painter) painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); - // coordinates: background + // coordinates: background box + QFontMetrics fontMetrics = painter.fontMetrics(); + unsigned int const box_left = 5; + unsigned int const box_top = 5; + unsigned int const box_margin = 3; + unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 HzdBFS").height() + box_margin); + unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "-99.9 dBFS").width() + 2*box_margin; painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.fillRect(m_displayLeft +5, 5, 80, 32, QColor(0, 0, 0, 64)); + painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top, + box_width, box_height, QColor(0, 0, 0, 64)); // coordinates: text painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); @@ -565,7 +572,9 @@ void SaSpectrumView::drawCursor(QPainter &painter) // frequency int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str()); - painter.drawText(m_displayLeft +8, 8, 80, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_margin, + box_width, box_height / 2, Qt::AlignLeft, tmps); // amplitude float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); @@ -578,7 +587,9 @@ void SaSpectrumView::drawCursor(QPainter &painter) // add 0.0005 to get proper rounding to 3 decimal places tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str()); } - painter.drawText(m_displayLeft +8, 20, 80, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_height / 2, + box_width, box_height / 2, Qt::AlignLeft, tmps); } } diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index d4a3b13b7d2..f9ae1d2daf6 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -292,9 +292,16 @@ void SaWaterfallView::drawCursor(QPainter &painter) painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); - // coordinates: background + // coordinates: background box + QFontMetrics fontMetrics = painter.fontMetrics(); + unsigned int const box_left = 5; + unsigned int const box_top = 5; + unsigned int const box_margin = 3; + unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 Hz").height() + box_margin); + unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "20000 Hz ").width() + 2*box_margin; painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.fillRect(m_displayLeft +5, 5, 80, 32, QColor(0, 0, 0, 64)); + painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top, + box_width, box_height, QColor(0, 0, 0, 64)); // coordinates: text painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); @@ -303,12 +310,16 @@ void SaWaterfallView::drawCursor(QPainter &painter) // frequency int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); tmps = QString(std::string(std::to_string(freq) + " Hz").c_str()); - painter.drawText(m_displayLeft +8, 8, 80, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_margin, + box_width, box_height / 2, Qt::AlignLeft, tmps); // time float time = yPixelToTime(m_cursor.y(), m_displayBottom); tmps = QString(std::string(std::string(std::to_string(time)).substr(0, 5) + " s").c_str()); - painter.drawText(m_displayLeft +8, 20, 80, 16, Qt::AlignLeft, tmps); + painter.drawText(m_displayLeft + box_left + box_margin, + m_displayTop + box_top + box_height / 2, + box_width, box_height / 2, Qt::AlignLeft, tmps); } } From 8d639e5c2dc86b1a758b46a729a0b508d5166c65 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Mon, 16 Sep 2019 22:49:13 +0200 Subject: [PATCH 09/36] Fix missed transient in the first block despite having overlapping enabled --- plugins/SpectrumAnalyzer/SaProcessor.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index f3cf91c33c6..66c0538a539 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -70,8 +70,6 @@ SaProcessor::SaProcessor(SaControls *controls) : m_waterfallHeight = 100; // a small safe value m_history_work.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); m_history.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); - - clear(); } @@ -289,12 +287,16 @@ void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition } // clean up before checking for more data from input buffer const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); - if (overlaps == 1) // each sample used only once + if (overlaps == 1) // Discard buffer, each sample used only once { m_framesFilledUp = 0; } else { + // Drop only a part of the buffer from the beginning, so that new + // data can be added to the end. This means the older samples will + // be analyzed again, but in a different position in the window, + // making short transient signals show up better in the waterfall. const unsigned int drop = m_inBlockSize / overlaps; std::move(m_bufferL.begin() + drop, m_bufferL.end(), m_bufferL.begin()); std::move(m_bufferR.begin() + drop, m_bufferR.end(), m_bufferR.begin()); @@ -456,8 +458,12 @@ void SaProcessor::rebuildWindow() // Note: may take a few milliseconds, do not call in a loop! void SaProcessor::clear() { + const unsigned int overlaps = m_controls->m_windowOverlapModel.value(); QMutexLocker lock(&m_dataAccess); - m_framesFilledUp = 0; + // If there is any window overlap, leave space only for the new samples + // and treat the rest at initialized with zeros. Prevents missing + // transients at the start of the very first block. + m_framesFilledUp = m_inBlockSize - m_inBlockSize / overlaps; std::fill(m_bufferL.begin(), m_bufferL.end(), 0); std::fill(m_bufferR.begin(), m_bufferR.end(), 0); std::fill(m_filteredBufferL.begin(), m_filteredBufferL.end(), 0); From 36feb653d73e2b7ce5d56d0ba1c7f028c9cf7752 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Wed, 9 Oct 2019 00:32:18 +0200 Subject: [PATCH 10/36] workaround for QMouseEvent::localPos() bug --- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 12 ++++++++---- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index c8c5bfd3cea..321a732b2cb 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -551,8 +551,8 @@ void SaSpectrumView::drawCursor(QPainter &painter) { // cursor lines painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); - painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); + painter.drawLine(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom)); + painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y())); // coordinates: background box QFontMetrics fontMetrics = painter.fontMetrics(); @@ -793,14 +793,18 @@ void SaSpectrumView::periodicUpdate() // Handle mouse input: set new cursor position. +// For some reason (a bug?), localPos() only returns integers. As a workaround +// the fractional part is taken from windowPos() (which works correctly). void SaSpectrumView::mouseMoveEvent(QMouseEvent *event) { - m_cursor = event->localPos(); + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); } void SaSpectrumView::mousePressEvent(QMouseEvent *event) { - m_cursor = event->localPos(); + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); } diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index f9ae1d2daf6..bea8fcd7df4 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -289,8 +289,8 @@ void SaWaterfallView::drawCursor(QPainter &painter) { // cursor lines painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); - painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); + painter.drawLine(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom)); + painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y())); // coordinates: background box QFontMetrics fontMetrics = painter.fontMetrics(); @@ -325,14 +325,18 @@ void SaWaterfallView::drawCursor(QPainter &painter) // Handle mouse input: set new cursor position. +// For some reason (a bug?), localPos() only returns integers. As a workaround +// the fractional part is taken from windowPos() (which works correctly). void SaWaterfallView::mouseMoveEvent(QMouseEvent *event) { - m_cursor = event->localPos(); + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); } void SaWaterfallView::mousePressEvent(QMouseEvent *event) { - m_cursor = event->localPos(); + m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()), + event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y())); } From b650838fd96c3c2383b5828ef6bc76ae1b8a2356 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 13 Oct 2019 12:59:03 +0200 Subject: [PATCH 11/36] Update plugins/SpectrumAnalyzer/SaSpectrumView.cpp Co-Authored-By: Johannes Lorenz <1042576+JohannesLorenz@users.noreply.github.com> --- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index 321a732b2cb..84236378410 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -281,7 +281,7 @@ void SaSpectrumView::refreshPaths() #ifdef SA_DEBUG // save performance measurement results m_refresh_avg = 0.95 * m_refresh_avg + 0.05 * refresh_time / 1000000.0; - m_path_avg = 0.95 * m_path_avg + 0.05 * path_time / 1000000.0; + m_path_avg = .95f * m_path_avg + .05f * path_time / 1000000.f; #endif } From 42d74db553eea6005d98366be3a1dc266e11ee41 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 13 Oct 2019 16:16:28 +0200 Subject: [PATCH 12/36] Make SaProcessor unfriendly to view classes --- plugins/SpectrumAnalyzer/SaProcessor.cpp | 15 ++++++++++++++ plugins/SpectrumAnalyzer/SaProcessor.h | 21 +++++++++++++------- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 14 ++++--------- plugins/SpectrumAnalyzer/SaSpectrumView.h | 2 +- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 21 +++++++++----------- 5 files changed, 43 insertions(+), 30 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 66c0538a539..28854283f28 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -476,6 +476,21 @@ void SaProcessor::clear() std::fill(m_history.begin(), m_history.end(), 0); } +// Clear only history work buffer. Used to flush old data when waterfall +// is shown after a period of inactivity. +void SaProcessor::clearHistory() +{ + QMutexLocker lock(&m_dataAccess); + std::fill(m_history_work.begin(), m_history_work.end(), 0); +} + +// Check if result buffers contain any non-zero values +bool SaProcessor::spectrumNotEmpty() +{ + QMutexLocker lock(&m_reallocationAccess); + return notEmpty(m_normSpectrumL) || notEmpty(m_normSpectrumR); +} + // -------------------------------------- // Frequency conversion helpers diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index fac90107815..01d05db4cfb 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -59,8 +59,21 @@ class SaProcessor void reallocateBuffers(); void rebuildWindow(); void clear(); + void clearHistory(); + + const float *getSpectrumL() const {return m_normSpectrumL.data();} + const float *getSpectrumR() const {return m_normSpectrumR.data();} + const uchar *getHistory() const {return m_history.data();} // information about results and unit conversion helpers + const unsigned int &inBlockSize() const {return m_inBlockSize;} + unsigned int binCount() const; //!< size of output (frequency domain) data block + bool spectrumNotEmpty(); //!< check if result buffers contain any non-zero values + + unsigned int waterfallWidth() const; //!< binCount value capped at 3840 (for display) + const unsigned int& waterfallHeight() const {return m_waterfallHeight;} + bool waterfallNotEmpty() const {return m_waterfallNotEmpty;} + float binToFreq(unsigned int bin_index) const; float binBandwidth() const; @@ -103,8 +116,6 @@ class SaProcessor unsigned int m_fftBlockSize; //!< size of padded block for FFT processing unsigned int m_sampleRate; - unsigned int binCount() const; //!< size of output (frequency domain) data block - // data buffers (roughly in the order of processing, from input to output) unsigned int m_framesFilledUp; std::vector m_bufferL; //!< time domain samples (left) @@ -128,20 +139,16 @@ class SaProcessor unsigned int m_waterfallHeight; //!< number of stored lines in history buffer // Note: high values may make it harder to see transients. const unsigned int m_waterfallMaxWidth = 3840; - unsigned int waterfallWidth() const; //!< binCount value capped at 3840 (for display) // book keeping bool m_spectrumActive; bool m_waterfallActive; - unsigned int m_waterfallNotEmpty; + unsigned int m_waterfallNotEmpty; //!< number of lines remaining visible on display bool m_reallocating; // merge L and R channels and apply gamma correction to make a spectrogram pixel QRgb makePixel(float left, float right) const; - friend class SaSpectrumView; - friend class SaWaterfallView; - #ifdef SA_DEBUG unsigned int m_last_dump_time; unsigned int m_dump_count; diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index 84236378410..d68e89821d5 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -163,10 +163,8 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) #endif // draw the graph only if there is any input, averaging residue or peaks - QMutexLocker lock(&m_processor->m_reallocationAccess); - if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR)) + if (m_decaySum > 0 || m_processor->spectrumNotEmpty()) { - lock.unlock(); // update data buffers and reconstruct paths refreshPaths(); @@ -204,10 +202,6 @@ void SaSpectrumView::drawSpectrum(QPainter &painter) draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time; #endif } - else - { - lock.unlock(); - } #ifdef SA_DEBUG // save performance measurement result @@ -242,8 +236,8 @@ void SaSpectrumView::refreshPaths() int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif m_decaySum = 0; - updateBuffers(m_processor->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data()); - updateBuffers(m_processor->m_normSpectrumR.data(), m_displayBufferR.data(), m_peakBufferR.data()); + updateBuffers(m_processor->getSpectrumL(), m_displayBufferL.data(), m_peakBufferL.data()); + updateBuffers(m_processor->getSpectrumR(), m_displayBufferR.data(), m_peakBufferR.data()); #ifdef SA_DEBUG refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time; #endif @@ -292,7 +286,7 @@ void SaSpectrumView::refreshPaths() // reallocation access lock! Data access lock is not needed: the final result // buffer is updated very quickly and the worst case is that one frame will be // part new, part old. At reasonable frame rate, such difference is invisible.. -void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer) +void SaSpectrumView::updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer) { for (int n = 0; n < m_processor->binCount(); n++) { diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.h b/plugins/SpectrumAnalyzer/SaSpectrumView.h index cc35d3d7d2d..b59264d9ce7 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.h +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.h @@ -85,7 +85,7 @@ private slots: std::vector m_displayBufferR; std::vector m_peakBufferL; std::vector m_peakBufferR; - void updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer); + void updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer); // final paths to be drawn by QPainter and methods to build them QPainterPath m_pathL; diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index bea8fcd7df4..460f9c4d694 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -93,11 +93,11 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) painter.setRenderHint(QPainter::Antialiasing, true); // check if time labels need to be rebuilt - if (secondsPerLine() != m_oldSecondsPerLine || m_processor->m_waterfallHeight != m_oldHeight) + if (secondsPerLine() != m_oldSecondsPerLine || m_processor->waterfallHeight() != m_oldHeight) { m_timeTics = makeTimeTics(); m_oldSecondsPerLine = secondsPerLine(); - m_oldHeight = m_processor->m_waterfallHeight; + m_oldHeight = m_processor->waterfallHeight(); } // print time labels @@ -137,12 +137,12 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) } // draw the spectrogram precomputed in SaProcessor - if (m_processor->m_waterfallNotEmpty) + if (m_processor->waterfallNotEmpty()) { QMutexLocker lock(&m_processor->m_reallocationAccess); - QImage temp = QImage(m_processor->m_history.data(), // raw pixel data to display + QImage temp = QImage(m_processor->getHistory(), // raw pixel data to display m_processor->waterfallWidth(), // width = number of frequency bins - m_processor->m_waterfallHeight, // height = number of history lines + m_processor->waterfallHeight(), // height = number of history lines QImage::Format_RGB32); lock.unlock(); temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution @@ -178,7 +178,7 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) // Helper functions for time conversion float SaWaterfallView::samplesPerLine() { - return (float)m_processor->m_inBlockSize / m_controls->m_windowOverlapModel.value(); + return (float)m_processor->inBlockSize() / m_controls->m_windowOverlapModel.value(); } float SaWaterfallView::secondsPerLine() @@ -190,7 +190,7 @@ float SaWaterfallView::secondsPerLine() // Convert time value to Y coordinate for display of given height. float SaWaterfallView::timeToYPixel(float time, int height) { - float pixels_per_line = (float)height / m_processor->m_waterfallHeight; + float pixels_per_line = (float)height / m_processor->waterfallHeight(); return pixels_per_line * time / secondsPerLine(); } @@ -200,7 +200,7 @@ float SaWaterfallView::timeToYPixel(float time, int height) float SaWaterfallView::yPixelToTime(float position, int height) { if (height == 0) {height = 1;} - float pixels_per_line = (float)height / m_processor->m_waterfallHeight; + float pixels_per_line = (float)height / m_processor->waterfallHeight(); return (position / pixels_per_line) * secondsPerLine(); } @@ -258,10 +258,7 @@ void SaWaterfallView::updateVisibility() if (m_controls->m_waterfallModel.value()) { // clear old data before showing the waterfall - QMutexLocker lock(&m_processor->m_dataAccess); - std::fill(m_processor->m_history_work.begin(), m_processor->m_history_work.end(), 0); - lock.unlock(); - + m_processor->clearHistory(); setVisible(true); // increase window size if it is too small From 9dd9ef018c365840940e98de3fcb8316618f7833 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 13 Oct 2019 18:58:35 +0200 Subject: [PATCH 13/36] Update and improve readme file; use consistent "analyzer" spelling --- plugins/SpectrumAnalyzer/Analyzer.cpp | 2 +- plugins/SpectrumAnalyzer/Analyzer.h | 2 +- plugins/SpectrumAnalyzer/DataprocLauncher.h | 2 +- plugins/SpectrumAnalyzer/README.md | 15 +++++++++++++-- plugins/SpectrumAnalyzer/SaProcessor.cpp | 4 ++-- plugins/SpectrumAnalyzer/SaProcessor.h | 2 +- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index 084eade11ef..3fa39fb7fb5 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -43,7 +43,7 @@ extern "C" { "Spectrum Analyzer", QT_TRANSLATE_NOOP("pluginBrowser", "A graphical spectrum analyzer."), "Martin Pavelek ", - 0x0100, + 0x0111, Plugin::Effect, new PluginPixmapLoader("logo"), NULL, diff --git a/plugins/SpectrumAnalyzer/Analyzer.h b/plugins/SpectrumAnalyzer/Analyzer.h index 1af82b73529..0f400c31e97 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.h +++ b/plugins/SpectrumAnalyzer/Analyzer.h @@ -56,7 +56,7 @@ class Analyzer : public Effect // QThread::create() workaround // Replace DataprocLauncher by QThread and replace initializer in constructor // with the following commented line when LMMS CI starts using Qt > 5.9 - //m_processorThread = QThread::create([=]{m_processor.analyse(m_inputBuffer, m_notifier);}); + //m_processorThread = QThread::create([=]{m_processor.analyze(m_inputBuffer, m_notifier);}); DataprocLauncher m_processorThread; ringbuffer_t m_inputBuffer; diff --git a/plugins/SpectrumAnalyzer/DataprocLauncher.h b/plugins/SpectrumAnalyzer/DataprocLauncher.h index 1f82c726b2a..be3e8cae218 100644 --- a/plugins/SpectrumAnalyzer/DataprocLauncher.h +++ b/plugins/SpectrumAnalyzer/DataprocLauncher.h @@ -44,7 +44,7 @@ class DataprocLauncher : public QThread private: void run() override { - m_processor->analyse(*m_inputBuffer, *m_notifier); + m_processor->analyze(*m_inputBuffer, *m_notifier); } SaProcessor *m_processor; diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md index 40159846676..965460f004f 100644 --- a/plugins/SpectrumAnalyzer/README.md +++ b/plugins/SpectrumAnalyzer/README.md @@ -4,12 +4,23 @@ This plugin consists of three widgets and back-end code to provide them with required data. -The top-level widget is SaControlDialog. It populates configuration widgets (created dynamically) and instantiates spectrum display widgets. Its main back-end class is SaControls, which holds all configuration values and globally valid constants (e.g. range definitions). +The top-level widget is `SaControlDialog`. It populates configuration widgets (created dynamically) and instantiates spectrum display widgets. Its main back-end class is `SaControls`, which holds all configuration values. -SaSpectrumView and SaWaterfallView show the result of spectrum analysis. Their main back-end class is SaProcessor, which performs FFT analysis on data received from the Analyzer class, which in turn handles the interface with LMMS. +`SaSpectrumView` and `SaWaterfallView` widgets show the result of spectrum analysis. Their main back-end class is `SaProcessor`, which performs FFT analysis on data received from the `Analyzer` class, which in turn handles the interface with LMMS. + +## Threads + +The Spectrum Analyzer is involved in three different threads: + - **Effect mixer thread**: periodically calls `Analyzer::processAudioBuffer()` to provide the plugin with more data. This thread is real-time sensitive -- any latency spikes can potentially cause interruptions in the audio stream. For this reason, `Analyzer::processAudioBuffer()` must finish as fast as possible and must not call any functions that could cause it to be delayed for unpredictable amount of time. A lock-less ring buffer is used to safely feed data to the FFT analysis thread without risking any latency spikes due to a shared mutex being unavailable at the time of writing. + - **FFT analysis thread**: a standalone thread formed by the `SaProcessor::analyze()` function. Takes in data from the ring buffer, performs FFT analysis and prepares results for display. This thread is not real-time sensitive but excessive locking is discouraged to maintain good performance. + - **GUI thread**: periodically triggers `paintEvent()` of all Qt widgets, including `SaSpectrumView` and `SaWaterfallView`. While it is not as sensitive to latency spikes as the effect mixer thread, the `paintEvent()`s appear to be called sequentially and the execution time of each widget therefore adds to the total time needed to complete one full refresh cycle. This means the maximum frame rate of the Qt GUI will be limited to `1 / total_execution_time`. Good performance of the `paintEvent()` functions should be therefore kept in mind. ## Changelog + 1.1.1 2019-10-13 + - improved interface for accessing SaProcessor provate data + - readme file update + - other small improvements based on reviews 1.1.0 2019-08-29 - advanced config: expose hidden constants to user - advanced config: add support for FFT window overlapping diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 28854283f28..d68deb11aa0 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -88,7 +88,7 @@ SaProcessor::~SaProcessor() // Load data from audio thread ringbuffer and run FFT analysis if buffer is full enough. -void SaProcessor::analyse(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier) +void SaProcessor::analyze(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier) { ringbuffer_reader_t reader(ring_buffer); @@ -385,7 +385,7 @@ void SaProcessor::reallocateBuffers() new_bins = new_fft_size / 2 +1; - // Use m_reallocating to tell analyse() to avoid asking for the lock. This + // Use m_reallocating to tell analyze() to avoid asking for the lock. This // is needed because under heavy load the FFT thread requests data lock so // often that this routine could end up waiting even for several seconds. m_reallocating = true; diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index 01d05db4cfb..e2a80deb69a 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -46,7 +46,7 @@ class SaProcessor virtual ~SaProcessor(); // analysis thread and a method to terminate it - void analyse(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier); + void analyze(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier); void terminate() {m_terminate = true;} // inform processor if any processing is actually required From 6a5089ddc100117860d25d7e92c01f70964c9738 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 13 Oct 2019 22:15:11 +0200 Subject: [PATCH 14/36] Use QString directly where possible --- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 22 ++++++++++---------- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index d68e89821d5..f48054d2b8a 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -142,14 +142,14 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) m_execution_avg = 0.95 * m_execution_avg + 0.05 * total_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); - painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft, - QString(std::string("Exec avg.: " + std::to_string(m_execution_avg).substr(0, 5) + " ms").c_str())); - painter.drawText(m_displayRight -150, 30, 100, 16, Qt::AlignLeft, - QString(std::string("Buff. upd. avg: " + std::to_string(m_refresh_avg).substr(0, 5) + " ms").c_str())); - painter.drawText(m_displayRight -150, 50, 100, 16, Qt::AlignLeft, - QString(std::string("Path build avg: " + std::to_string(m_path_avg).substr(0, 5) + " ms").c_str())); - painter.drawText(m_displayRight -150, 70, 100, 16, Qt::AlignLeft, - QString(std::string("Path draw avg: " + std::to_string(m_draw_avg).substr(0, 5) + " ms").c_str())); + painter.drawText(m_displayRight -150, 10, 130, 16, Qt::AlignLeft, + QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).data()).append(" ms")); + painter.drawText(m_displayRight -150, 30, 130, 16, Qt::AlignLeft, + QString("Buff. upd. avg: ").append(std::to_string(m_refresh_avg).substr(0, 5).data()).append(" ms")); + painter.drawText(m_displayRight -150, 50, 130, 16, Qt::AlignLeft, + QString("Path build avg: ").append(std::to_string(m_path_avg).substr(0, 5).data()).append(" ms")); + painter.drawText(m_displayRight -150, 70, 130, 16, Qt::AlignLeft, + QString("Path draw avg: ").append(std::to_string(m_draw_avg).substr(0, 5).data()).append(" ms")); #endif } @@ -565,7 +565,7 @@ void SaSpectrumView::drawCursor(QPainter &painter) // frequency int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); - tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str()); + tmps = QString("%1 Hz").arg(xFreq); painter.drawText(m_displayLeft + box_left + box_margin, m_displayTop + box_top + box_margin, box_width, box_height / 2, Qt::AlignLeft, tmps); @@ -574,12 +574,12 @@ void SaSpectrumView::drawCursor(QPainter &painter) float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); if (m_controls->m_logYModel.value()) { - tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dBFS").c_str()); + tmps = QString(std::to_string(yAmp).substr(0, 5).data()).append(" dBFS"); } else { // add 0.0005 to get proper rounding to 3 decimal places - tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str()); + tmps = QString(std::to_string(0.0005f + yAmp).substr(0, 5).c_str()); } painter.drawText(m_displayLeft + box_left + box_margin, m_displayTop + box_top + box_height / 2, diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 460f9c4d694..136157495ca 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -170,7 +170,7 @@ void SaWaterfallView::paintEvent(QPaintEvent *event) m_execution_avg = 0.95 * m_execution_avg + 0.05 * draw_time / 1000000.0; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft, - QString(std::string("Exec avg.: " + std::to_string(m_execution_avg).substr(0, 5) + " ms").c_str())); + QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms")); #endif } @@ -306,14 +306,14 @@ void SaWaterfallView::drawCursor(QPainter &painter) // frequency int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); - tmps = QString(std::string(std::to_string(freq) + " Hz").c_str()); + tmps = QString("%1 Hz").arg(freq); painter.drawText(m_displayLeft + box_left + box_margin, m_displayTop + box_top + box_margin, box_width, box_height / 2, Qt::AlignLeft, tmps); // time float time = yPixelToTime(m_cursor.y(), m_displayBottom); - tmps = QString(std::string(std::string(std::to_string(time)).substr(0, 5) + " s").c_str()); + tmps = QString(std::to_string(time).substr(0, 5).c_str()).append(" s"); painter.drawText(m_displayLeft + box_left + box_margin, m_displayTop + box_top + box_height / 2, box_width, box_height / 2, Qt::AlignLeft, tmps); From 693970286cf281e5e3688f028881fc5518848672 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 13 Oct 2019 22:23:10 +0200 Subject: [PATCH 15/36] Fix bug introduced in previous commit --- plugins/SpectrumAnalyzer/SaSpectrumView.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp index f48054d2b8a..13aaeb72418 100644 --- a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -143,13 +143,13 @@ void SaSpectrumView::paintEvent(QPaintEvent *event) painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawText(m_displayRight -150, 10, 130, 16, Qt::AlignLeft, - QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).data()).append(" ms")); + QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms")); painter.drawText(m_displayRight -150, 30, 130, 16, Qt::AlignLeft, - QString("Buff. upd. avg: ").append(std::to_string(m_refresh_avg).substr(0, 5).data()).append(" ms")); + QString("Buff. upd. avg: ").append(std::to_string(m_refresh_avg).substr(0, 5).c_str()).append(" ms")); painter.drawText(m_displayRight -150, 50, 130, 16, Qt::AlignLeft, - QString("Path build avg: ").append(std::to_string(m_path_avg).substr(0, 5).data()).append(" ms")); + QString("Path build avg: ").append(std::to_string(m_path_avg).substr(0, 5).c_str()).append(" ms")); painter.drawText(m_displayRight -150, 70, 130, 16, Qt::AlignLeft, - QString("Path draw avg: ").append(std::to_string(m_draw_avg).substr(0, 5).data()).append(" ms")); + QString("Path draw avg: ").append(std::to_string(m_draw_avg).substr(0, 5).c_str()).append(" ms")); #endif } @@ -574,7 +574,7 @@ void SaSpectrumView::drawCursor(QPainter &painter) float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); if (m_controls->m_logYModel.value()) { - tmps = QString(std::to_string(yAmp).substr(0, 5).data()).append(" dBFS"); + tmps = QString(std::to_string(yAmp).substr(0, 5).c_str()).append(" dBFS"); } else { From 11013cced2f65ac7fb8ea6bc751562911896eb92 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 17 Oct 2019 22:58:20 +0200 Subject: [PATCH 16/36] SaProcessor: make some variables accessed by other classes atomic; make SaControls constant --- plugins/SpectrumAnalyzer/SaProcessor.cpp | 2 +- plugins/SpectrumAnalyzer/SaProcessor.h | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index d68deb11aa0..8501ea00f04 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -38,7 +38,7 @@ #include #endif -SaProcessor::SaProcessor(SaControls *controls) : +SaProcessor::SaProcessor(const SaControls *controls) : m_controls(controls), m_terminate(false), m_inBlockSize(FFT_BLOCK_SIZES[0]), diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index e2a80deb69a..a0c1e98b325 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -27,6 +27,7 @@ #ifndef SAPROCESSOR_H #define SAPROCESSOR_H +#include #include #include #include @@ -42,7 +43,7 @@ class ringbuffer_t; class SaProcessor { public: - explicit SaProcessor(SaControls *controls); + explicit SaProcessor(const SaControls *controls); virtual ~SaProcessor(); // analysis thread and a method to terminate it @@ -66,12 +67,12 @@ class SaProcessor const uchar *getHistory() const {return m_history.data();} // information about results and unit conversion helpers - const unsigned int &inBlockSize() const {return m_inBlockSize;} + unsigned int inBlockSize() const {return m_inBlockSize;} unsigned int binCount() const; //!< size of output (frequency domain) data block bool spectrumNotEmpty(); //!< check if result buffers contain any non-zero values unsigned int waterfallWidth() const; //!< binCount value capped at 3840 (for display) - const unsigned int& waterfallHeight() const {return m_waterfallHeight;} + unsigned int waterfallHeight() const {return m_waterfallHeight;} bool waterfallNotEmpty() const {return m_waterfallNotEmpty;} float binToFreq(unsigned int bin_index) const; @@ -105,14 +106,14 @@ class SaProcessor private: - SaControls *m_controls; + const SaControls *m_controls; // thread communication and control bool m_terminate; // currently valid configuration unsigned int m_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size - unsigned int m_inBlockSize; //!< size of input (time domain) data block + std::atomic m_inBlockSize;//!< size of input (time domain) data block unsigned int m_fftBlockSize; //!< size of padded block for FFT processing unsigned int m_sampleRate; @@ -136,14 +137,14 @@ class SaProcessor std::vector m_history_work; //!< local history buffer for render std::vector m_history; //!< public buffer for reading bool m_flipRequest; //!< update public buffer only when requested - unsigned int m_waterfallHeight; //!< number of stored lines in history buffer + std::atomic m_waterfallHeight; //!< number of stored lines in history buffer // Note: high values may make it harder to see transients. const unsigned int m_waterfallMaxWidth = 3840; // book keeping bool m_spectrumActive; bool m_waterfallActive; - unsigned int m_waterfallNotEmpty; //!< number of lines remaining visible on display + std::atomic m_waterfallNotEmpty; //!< number of lines remaining visible on display bool m_reallocating; // merge L and R channels and apply gamma correction to make a spectrogram pixel From edefc936b4845f6da1bffea4298e020c674364ab Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 24 Oct 2019 16:44:03 +0200 Subject: [PATCH 17/36] test a change required to make analyzer work after make install --- plugins/SpectrumAnalyzer/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/SpectrumAnalyzer/CMakeLists.txt b/plugins/SpectrumAnalyzer/CMakeLists.txt index 9ece0f1dc90..3fcc75066c0 100644 --- a/plugins/SpectrumAnalyzer/CMakeLists.txt +++ b/plugins/SpectrumAnalyzer/CMakeLists.txt @@ -1,6 +1,10 @@ INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) +# This is required so that libanalyzer.so can find libringbuffer.so +set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib") +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + LINK_LIBRARIES(${FFTW3F_LIBRARIES} ringbuffer) BUILD_PLUGIN(analyzer Analyzer.cpp SaProcessor.cpp SaControls.cpp SaControlsDialog.cpp SaSpectrumView.cpp SaWaterfallView.cpp From e3c89d5695e0803bcfc0120a4bed28fe1415507a Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Mon, 28 Oct 2019 21:18:05 +0100 Subject: [PATCH 18/36] Build the ringbuffer libary as part of LMMS core --- include/LocklessRingBuffer.h | 127 ++++++++++++++++++++ include/RingBuffer.h | 2 + include/lmms_basics.h | 18 --- plugins/SpectrumAnalyzer/Analyzer.cpp | 10 +- plugins/SpectrumAnalyzer/Analyzer.h | 7 +- plugins/SpectrumAnalyzer/CMakeLists.txt | 6 +- plugins/SpectrumAnalyzer/DataprocLauncher.h | 13 +- plugins/SpectrumAnalyzer/SaProcessor.cpp | 16 +-- plugins/SpectrumAnalyzer/SaProcessor.h | 5 +- src/3rdparty/CMakeLists.txt | 1 - src/3rdparty/ringbuffer | 2 +- src/CMakeLists.txt | 21 +++- 12 files changed, 170 insertions(+), 58 deletions(-) create mode 100644 include/LocklessRingBuffer.h diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h new file mode 100644 index 00000000000..90da771a6e1 --- /dev/null +++ b/include/LocklessRingBuffer.h @@ -0,0 +1,127 @@ +/* + * LocklessRingBuffer.h - LMMS wrapper for a lockless ringbuffer library + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LOCKLESSRINGBUFFER_H +#define LOCKLESSRINGBUFFER_H + +#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" +#include "lmms_basics.h" +#include + + +//! A convenience layer for a realtime-safe and thread-safe multi-reader ring buffer library. +template +class LocklessRingBuffer +{ + template + friend class LocklessRingBufferReader; +public: + LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + ~LocklessRingBuffer() {}; + + std::size_t write(const sampleFrame *src, size_t cnt) + { + std::size_t written = m_buffer.write(src, cnt); + m_notifier.wakeAll(); // Let all waiting readers know new data are available. + return written; + } + + std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t free() {return m_buffer.write_space();} + void wakeAll() {m_notifier.wakeAll();} + +private: + ringbuffer_t m_buffer; + QWaitCondition m_notifier; +}; + + +// The sampleFrame_copier is required because sampleFrame is just a two-element +// array and therefore does not have a copy constructor needed by std::copy. +class sampleFrame_copier +{ + const sampleFrame* src; +public: + sampleFrame_copier(const sampleFrame* src) : src(src) {} + void operator()(std::size_t src_offset, std::size_t count, sampleFrame* dest) + { + for (std::size_t i = src_offset; i < src_offset + count; i++, dest++) + { + (*dest)[0] = src[i][0]; + (*dest)[1] = src[i][1]; + } + } +}; + + +//! Specialized ring buffer wrapper with write function modified to support sampleFrame. +template <> +class LocklessRingBuffer +{ + template + friend class LocklessRingBufferReader; +public: + LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + ~LocklessRingBuffer() {}; + + std::size_t write(const sampleFrame *src, size_t cnt) + { + sampleFrame_copier copier(src); + std::size_t written = m_buffer.write_func(copier, cnt); + // Let all waiting readers know new data are available. + m_notifier.wakeAll(); + return written; + } + + std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t free() {return m_buffer.write_space();} + void wakeAll() {m_notifier.wakeAll();} + +private: + ringbuffer_t m_buffer; + QWaitCondition m_notifier; +}; + + +//! Wrapper for lockless ringbuffer reader +template +class LocklessRingBufferReader : public ringbuffer_reader_t +{ +public: + LocklessRingBufferReader(LocklessRingBuffer &rb) : + ringbuffer_reader_t(rb.m_buffer), + m_notifier(&rb.m_notifier) {}; + + bool empty() {return !this->read_space();} + + void waitForData() + { + QMutex useless_lock; + m_notifier->wait(&useless_lock); + useless_lock.unlock(); + } +private: + QWaitCondition *m_notifier; +}; +#endif //LOCKLESSRINGBUFFER_H diff --git a/include/RingBuffer.h b/include/RingBuffer.h index c761616bd78..c7e91bd3392 100644 --- a/include/RingBuffer.h +++ b/include/RingBuffer.h @@ -32,6 +32,8 @@ #include "lmms_math.h" #include "MemoryManager.h" +/** \brief A basic LMMS ring buffer for single-thread use. For thread and realtime safe alternative see LocklessRingBuffer. +*/ class LMMS_EXPORT RingBuffer : public QObject { Q_OBJECT diff --git a/include/lmms_basics.h b/include/lmms_basics.h index 840df09d1cf..cca04e97d8f 100644 --- a/include/lmms_basics.h +++ b/include/lmms_basics.h @@ -137,24 +137,6 @@ typedef sample_t surroundSampleFrame[SURROUND_CHANNELS]; typedef sample_t sampleFrameA[DEFAULT_CHANNELS] __attribute__((__aligned__(ALIGN_SIZE))); #endif -// The sampleFrame_copier is required to store samples into the lockless ringbuffer. -// This is because sampleFrame is just a two-element array and therefore does -// not have a copy constructor which the ringbuffer class needs. -class sampleFrame_copier -{ - const sampleFrame* src; -public: - sampleFrame_copier(const sampleFrame* src) : src(src) {} - void operator()(std::size_t src_offset, std::size_t count, sampleFrame* dest) - { - for (std::size_t i = src_offset; i < src_offset + count; i++, dest++) - { - (*dest)[0] = src[i][0]; - (*dest)[1] = src[i][1]; - } - } -}; - #define STRINGIFY(s) STR(s) #define STR(PN) #PN diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index 3fa39fb7fb5..c5f9ce19795 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -56,7 +56,7 @@ Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::K Effect(&analyzer_plugin_descriptor, parent, key), m_processor(&m_controls), m_controls(this), - m_processorThread(m_processor, m_inputBuffer, m_notifier), + m_processorThread(m_processor, m_inputBuffer), // Buffer is sized to cover 4* the current maximum LMMS audio buffer size, // so that it has some reserve space in case data processor is busy. m_inputBuffer(4 * m_maxBufferSize) @@ -68,7 +68,7 @@ Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::K Analyzer::~Analyzer() { m_processor.terminate(); - m_notifier.wakeAll(); + m_inputBuffer.wakeAll(); m_processorThread.wait(); } @@ -93,11 +93,7 @@ bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) if (m_controls.isViewVisible()) { // To avoid processing spikes on audio thread, data are stored in // a lockless ringbuffer and processed in a separate thread. - sampleFrame_copier copier(buffer); - m_inputBuffer.write_func(copier, frame_count); - - // Inform processor to check the buffer (to avoid busy waiting). - m_notifier.wakeAll(); + m_inputBuffer.write(buffer, frame_count); } #ifdef SA_DEBUG audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - audio_time; diff --git a/plugins/SpectrumAnalyzer/Analyzer.h b/plugins/SpectrumAnalyzer/Analyzer.h index 0f400c31e97..1f49fe6e7e7 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.h +++ b/plugins/SpectrumAnalyzer/Analyzer.h @@ -32,7 +32,7 @@ #include "SaControls.h" #include "SaProcessor.h" #include -#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" +#include "LocklessRingBuffer.h" //! Top level class; handles LMMS interface and feeds data to the data processor. class Analyzer : public Effect @@ -56,11 +56,10 @@ class Analyzer : public Effect // QThread::create() workaround // Replace DataprocLauncher by QThread and replace initializer in constructor // with the following commented line when LMMS CI starts using Qt > 5.9 - //m_processorThread = QThread::create([=]{m_processor.analyze(m_inputBuffer, m_notifier);}); + //m_processorThread = QThread::create([=]{m_processor.analyze(m_inputBuffer);}); DataprocLauncher m_processorThread; - ringbuffer_t m_inputBuffer; - QWaitCondition m_notifier; + LocklessRingBuffer m_inputBuffer; #ifdef SA_DEBUG int m_last_dump_time; diff --git a/plugins/SpectrumAnalyzer/CMakeLists.txt b/plugins/SpectrumAnalyzer/CMakeLists.txt index 3fcc75066c0..488495a9e3d 100644 --- a/plugins/SpectrumAnalyzer/CMakeLists.txt +++ b/plugins/SpectrumAnalyzer/CMakeLists.txt @@ -1,11 +1,7 @@ INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) -# This is required so that libanalyzer.so can find libringbuffer.so -set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib") -set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) - -LINK_LIBRARIES(${FFTW3F_LIBRARIES} ringbuffer) +LINK_LIBRARIES(${FFTW3F_LIBRARIES}) BUILD_PLUGIN(analyzer Analyzer.cpp SaProcessor.cpp SaControls.cpp SaControlsDialog.cpp SaSpectrumView.cpp SaWaterfallView.cpp MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h DataprocLauncher.h EMBEDDED_RESOURCES *.svg logo.png) diff --git a/plugins/SpectrumAnalyzer/DataprocLauncher.h b/plugins/SpectrumAnalyzer/DataprocLauncher.h index be3e8cae218..d91e0bedfcc 100644 --- a/plugins/SpectrumAnalyzer/DataprocLauncher.h +++ b/plugins/SpectrumAnalyzer/DataprocLauncher.h @@ -26,30 +26,27 @@ #define DATAPROCLAUNCHER_H #include -#include #include "SaProcessor.h" -#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" +#include "LocklessRingBuffer.h" class DataprocLauncher : public QThread { public: - explicit DataprocLauncher(SaProcessor &proc, ringbuffer_t &buffer, QWaitCondition ¬ifier) + explicit DataprocLauncher(SaProcessor &proc, LocklessRingBuffer &buffer) : m_processor(&proc), - m_inputBuffer(&buffer), - m_notifier(¬ifier) + m_inputBuffer(&buffer) { } private: void run() override { - m_processor->analyze(*m_inputBuffer, *m_notifier); + m_processor->analyze(*m_inputBuffer); } SaProcessor *m_processor; - ringbuffer_t *m_inputBuffer; - QWaitCondition *m_notifier; + LocklessRingBuffer *m_inputBuffer; }; #endif // DATAPROCLAUNCHER_H diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 8501ea00f04..f984dd8ff92 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -30,7 +30,7 @@ #include #include "lmms_math.h" -#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" +#include "LocklessRingBuffer.h" #ifdef SA_DEBUG #include @@ -88,23 +88,19 @@ SaProcessor::~SaProcessor() // Load data from audio thread ringbuffer and run FFT analysis if buffer is full enough. -void SaProcessor::analyze(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier) +void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) { - ringbuffer_reader_t reader(ring_buffer); + LocklessRingBufferReader reader(ring_buffer); // Processing thread loop while (!m_terminate) { // If there is nothing to read, wait for notification from the writing side. - if (!reader.read_space()) { - QMutex useless_lock; - notifier.wait(&useless_lock); - useless_lock.unlock(); - } + if (reader.empty()) {reader.waitForData();} // skip waterfall render if processing can't keep up with input - bool overload = ring_buffer.write_space() < ring_buffer.maximum_eventual_write_space() / 2; + bool overload = ring_buffer.free() < ring_buffer.capacity() / 2; - auto in_buffer = reader.read_max(ring_buffer.maximum_eventual_write_space() / 4); + auto in_buffer = reader.read_max(ring_buffer.capacity() / 4); std::size_t frame_count = in_buffer.size(); // Process received data only if any view is visible and not paused. diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h index a0c1e98b325..0c396b3c031 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.h +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -30,14 +30,13 @@ #include #include #include -#include #include #include "fft_helpers.h" #include "SaControls.h" template -class ringbuffer_t; +class LocklessRingBuffer; //! Receives audio data, runs FFT analysis and stores the result. class SaProcessor @@ -47,7 +46,7 @@ class SaProcessor virtual ~SaProcessor(); // analysis thread and a method to terminate it - void analyze(ringbuffer_t &ring_buffer, QWaitCondition ¬ifier); + void analyze(LocklessRingBuffer &ring_buffer); void terminate() {m_terminate = true;} // inform processor if any processing is actually required diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index a2047a2f4e4..473e7702f09 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -8,6 +8,5 @@ IF(LMMS_BUILD_LINUX AND WANT_VST) add_subdirectory(qt5-x11embed) ENDIF() -ADD_SUBDIRECTORY(ringbuffer) ADD_SUBDIRECTORY(rpmalloc) ADD_SUBDIRECTORY(weakjack) diff --git a/src/3rdparty/ringbuffer b/src/3rdparty/ringbuffer index fc0e1f38f74..5332e6d03c7 160000 --- a/src/3rdparty/ringbuffer +++ b/src/3rdparty/ringbuffer @@ -1 +1 @@ -Subproject commit fc0e1f38f740e5d7d11963f52cfcb6445db6e192 +Subproject commit 5332e6d03c7449a3c343866f654aeb2526a1a7d1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 10ce72ae6c4..5101fdbe3e5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -89,6 +89,25 @@ IF(NOT ("${LAME_INCLUDE_DIRS}" STREQUAL "")) INCLUDE_DIRECTORIES("${LAME_INCLUDE_DIRS}") ENDIF() +# A 3rd party lockless ring buffer library is compiled as part of the core +SET(RINGBUFFER_DIR "${CMAKE_SOURCE_DIR}/src/3rdparty/ringbuffer/") +LIST(APPEND LMMS_SRCS "${RINGBUFFER_DIR}/src/lib/ringbuffer.cpp") +LIST(APPEND LMMS_INCLUDES "${RINGBUFFER_DIR}/include/ringbuffer/ringbuffer.h") +INCLUDE_DIRECTORIES("${RINGBUFFER_DIR}/include/") +# Create a dummy ringbuffer_export.h, since ringbuffer is not compiled as a library +FILE(WRITE ${CMAKE_BINARY_DIR}/src/ringbuffer_export.h "#define RINGBUFFER_EXPORT") +# Enable MLOCK support for ringbuffer if available +INCLUDE(CheckIncludeFiles) +CHECK_INCLUDE_FILES(sys/mman.h HAVE_SYS_MMAN) +IF(HAVE_SYS_MMAN) + SET(USE_MLOCK ON) +ELSE() + SET(USE_MLOCK OFF) +ENDIF() +# Generate ringbuffer configuration headers +CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-config.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-config.h) +CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-version.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-version.h) + # Use libraries in non-standard directories (e.g., another version of Qt) IF(LMMS_BUILD_LINUX) LINK_LIBRARIES(-Wl,--enable-new-dtags) @@ -202,4 +221,4 @@ ELSE() PERMISSIONS OWNER_READ GROUP_READ WORLD_READ) ENDIF() -INSTALL(TARGETS lmms RUNTIME DESTINATION "${BIN_DIR}") \ No newline at end of file +INSTALL(TARGETS lmms RUNTIME DESTINATION "${BIN_DIR}") From 5b1f28c577eb0a4f47e76a0bfab2acc57def955d Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Wed, 30 Oct 2019 18:57:13 +0100 Subject: [PATCH 19/36] Attempted fix of missing ringbuffer.cpp symbols on Win platforms --- src/CMakeLists.txt | 1 - src/core/CMakeLists.txt | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5101fdbe3e5..96bf35d4e8d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -91,7 +91,6 @@ ENDIF() # A 3rd party lockless ring buffer library is compiled as part of the core SET(RINGBUFFER_DIR "${CMAKE_SOURCE_DIR}/src/3rdparty/ringbuffer/") -LIST(APPEND LMMS_SRCS "${RINGBUFFER_DIR}/src/lib/ringbuffer.cpp") LIST(APPEND LMMS_INCLUDES "${RINGBUFFER_DIR}/include/ringbuffer/ringbuffer.h") INCLUDE_DIRECTORIES("${RINGBUFFER_DIR}/include/") # Create a dummy ringbuffer_export.h, since ringbuffer is not compiled as a library diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ba41e089c7a..48d55b500f0 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,5 +1,7 @@ set(LMMS_SRCS ${LMMS_SRCS} + 3rdparty/ringbuffer/src/lib/ringbuffer.cpp + core/AutomatableModel.cpp core/AutomationPattern.cpp core/BandLimitedWave.cpp From 7cf189c5f12ec8ccda5b35a63d738392468b7eaa Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Wed, 30 Oct 2019 21:25:34 +0100 Subject: [PATCH 20/36] Move most ringbuffer cmake setup to 3rdparty/, hijack RINGBUFFER_EXPORT and replace it by LMMS_EXPORT --- include/LocklessRingBuffer.h | 8 ++++---- src/3rdparty/CMakeLists.txt | 19 +++++++++++++++++++ src/CMakeLists.txt | 19 +------------------ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 90da771a6e1..a4bc9257f9e 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -32,7 +32,7 @@ //! A convenience layer for a realtime-safe and thread-safe multi-reader ring buffer library. template -class LocklessRingBuffer +class LMMS_EXPORT LocklessRingBuffer { template friend class LocklessRingBufferReader; @@ -59,7 +59,7 @@ class LocklessRingBuffer // The sampleFrame_copier is required because sampleFrame is just a two-element // array and therefore does not have a copy constructor needed by std::copy. -class sampleFrame_copier +class LMMS_EXPORT sampleFrame_copier { const sampleFrame* src; public: @@ -77,7 +77,7 @@ class sampleFrame_copier //! Specialized ring buffer wrapper with write function modified to support sampleFrame. template <> -class LocklessRingBuffer +class LMMS_EXPORT LocklessRingBuffer { template friend class LocklessRingBufferReader; @@ -106,7 +106,7 @@ class LocklessRingBuffer //! Wrapper for lockless ringbuffer reader template -class LocklessRingBufferReader : public ringbuffer_reader_t +class LMMS_EXPORT LocklessRingBufferReader : public ringbuffer_reader_t { public: LocklessRingBufferReader(LocklessRingBuffer &rb) : diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index 473e7702f09..bdc4a4d8690 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -10,3 +10,22 @@ ENDIF() ADD_SUBDIRECTORY(rpmalloc) ADD_SUBDIRECTORY(weakjack) + +# The lockless ring buffer library is compiled as part of the core +SET(RINGBUFFER_DIR "${CMAKE_SOURCE_DIR}/src/3rdparty/ringbuffer/") +SET(RINGBUFFER_DIR ${RINGBUFFER_DIR} PARENT_SCOPE) +# Create a dummy ringbuffer_export.h, since ringbuffer is not compiled as a library +FILE(WRITE ${CMAKE_BINARY_DIR}/src/ringbuffer_export.h + "#include \"${CMAKE_BINARY_DIR}/src/lmms_export.h\"\n + #define RINGBUFFER_EXPORT LMMS_EXPORT") +# Enable MLOCK support for ringbuffer if available +INCLUDE(CheckIncludeFiles) +CHECK_INCLUDE_FILES(sys/mman.h HAVE_SYS_MMAN) +IF(HAVE_SYS_MMAN) + SET(USE_MLOCK ON) +ELSE() + SET(USE_MLOCK OFF) +ENDIF() +# Generate ringbuffer configuration headers +CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-config.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-config.h) +CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-version.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-version.h) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 96bf35d4e8d..0ce2360536a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ INCLUDE_DIRECTORIES( "${CMAKE_BINARY_DIR}/include" "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/include" + "${RINGBUFFER_DIR}/include" ) IF(WIN32 AND MSVC) @@ -89,24 +90,6 @@ IF(NOT ("${LAME_INCLUDE_DIRS}" STREQUAL "")) INCLUDE_DIRECTORIES("${LAME_INCLUDE_DIRS}") ENDIF() -# A 3rd party lockless ring buffer library is compiled as part of the core -SET(RINGBUFFER_DIR "${CMAKE_SOURCE_DIR}/src/3rdparty/ringbuffer/") -LIST(APPEND LMMS_INCLUDES "${RINGBUFFER_DIR}/include/ringbuffer/ringbuffer.h") -INCLUDE_DIRECTORIES("${RINGBUFFER_DIR}/include/") -# Create a dummy ringbuffer_export.h, since ringbuffer is not compiled as a library -FILE(WRITE ${CMAKE_BINARY_DIR}/src/ringbuffer_export.h "#define RINGBUFFER_EXPORT") -# Enable MLOCK support for ringbuffer if available -INCLUDE(CheckIncludeFiles) -CHECK_INCLUDE_FILES(sys/mman.h HAVE_SYS_MMAN) -IF(HAVE_SYS_MMAN) - SET(USE_MLOCK ON) -ELSE() - SET(USE_MLOCK OFF) -ENDIF() -# Generate ringbuffer configuration headers -CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-config.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-config.h) -CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-version.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-version.h) - # Use libraries in non-standard directories (e.g., another version of Qt) IF(LMMS_BUILD_LINUX) LINK_LIBRARIES(-Wl,--enable-new-dtags) From 11bb2c770cfaa3d482ec42ad712737f17ad77ddc Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 7 Nov 2019 21:45:11 +0100 Subject: [PATCH 21/36] Add LMMS_EXPORT to LocklessRingBuffer methods --- include/LocklessRingBuffer.h | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index a4bc9257f9e..07196c8f492 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -37,19 +37,19 @@ class LMMS_EXPORT LocklessRingBuffer template friend class LocklessRingBufferReader; public: - LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - ~LocklessRingBuffer() {}; + LMMS_EXPORT LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + LMMS_EXPORT ~LocklessRingBuffer() {}; - std::size_t write(const sampleFrame *src, size_t cnt) + std::size_t LMMS_EXPORT write(const sampleFrame *src, size_t cnt) { std::size_t written = m_buffer.write(src, cnt); m_notifier.wakeAll(); // Let all waiting readers know new data are available. return written; } - std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t free() {return m_buffer.write_space();} - void wakeAll() {m_notifier.wakeAll();} + std::size_t LMMS_EXPORT capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t LMMS_EXPORT free() {return m_buffer.write_space();} + void LMMS_EXPORT wakeAll() {m_notifier.wakeAll();} private: ringbuffer_t m_buffer; @@ -82,10 +82,10 @@ class LMMS_EXPORT LocklessRingBuffer template friend class LocklessRingBufferReader; public: - LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - ~LocklessRingBuffer() {}; + LMMS_EXPORT LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + LMMS_EXPORT ~LocklessRingBuffer() {}; - std::size_t write(const sampleFrame *src, size_t cnt) + std::size_t LMMS_EXPORT write(const sampleFrame *src, size_t cnt) { sampleFrame_copier copier(src); std::size_t written = m_buffer.write_func(copier, cnt); @@ -94,9 +94,9 @@ class LMMS_EXPORT LocklessRingBuffer return written; } - std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t free() {return m_buffer.write_space();} - void wakeAll() {m_notifier.wakeAll();} + std::size_t LMMS_EXPORT capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t LMMS_EXPORT free() {return m_buffer.write_space();} + void LMMS_EXPORT wakeAll() {m_notifier.wakeAll();} private: ringbuffer_t m_buffer; @@ -109,13 +109,13 @@ template class LMMS_EXPORT LocklessRingBufferReader : public ringbuffer_reader_t { public: - LocklessRingBufferReader(LocklessRingBuffer &rb) : + LMMS_EXPORT LocklessRingBufferReader(LocklessRingBuffer &rb) : ringbuffer_reader_t(rb.m_buffer), m_notifier(&rb.m_notifier) {}; - bool empty() {return !this->read_space();} + bool LMMS_EXPORT empty() {return !this->read_space();} - void waitForData() + void LMMS_EXPORT waitForData() { QMutex useless_lock; m_notifier->wait(&useless_lock); From 2963fb6301f416fc56b72685f307687a4aa09a18 Mon Sep 17 00:00:00 2001 From: Hyunjin Song Date: Fri, 8 Nov 2019 08:01:35 +0900 Subject: [PATCH 22/36] Revert "Add LMMS_EXPORT to LocklessRingBuffer methods" This reverts commit 11bb2c770cfaa3d482ec42ad712737f17ad77ddc. --- include/LocklessRingBuffer.h | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 07196c8f492..a4bc9257f9e 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -37,19 +37,19 @@ class LMMS_EXPORT LocklessRingBuffer template friend class LocklessRingBufferReader; public: - LMMS_EXPORT LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - LMMS_EXPORT ~LocklessRingBuffer() {}; + LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + ~LocklessRingBuffer() {}; - std::size_t LMMS_EXPORT write(const sampleFrame *src, size_t cnt) + std::size_t write(const sampleFrame *src, size_t cnt) { std::size_t written = m_buffer.write(src, cnt); m_notifier.wakeAll(); // Let all waiting readers know new data are available. return written; } - std::size_t LMMS_EXPORT capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t LMMS_EXPORT free() {return m_buffer.write_space();} - void LMMS_EXPORT wakeAll() {m_notifier.wakeAll();} + std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t free() {return m_buffer.write_space();} + void wakeAll() {m_notifier.wakeAll();} private: ringbuffer_t m_buffer; @@ -82,10 +82,10 @@ class LMMS_EXPORT LocklessRingBuffer template friend class LocklessRingBufferReader; public: - LMMS_EXPORT LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - LMMS_EXPORT ~LocklessRingBuffer() {}; + LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + ~LocklessRingBuffer() {}; - std::size_t LMMS_EXPORT write(const sampleFrame *src, size_t cnt) + std::size_t write(const sampleFrame *src, size_t cnt) { sampleFrame_copier copier(src); std::size_t written = m_buffer.write_func(copier, cnt); @@ -94,9 +94,9 @@ class LMMS_EXPORT LocklessRingBuffer return written; } - std::size_t LMMS_EXPORT capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t LMMS_EXPORT free() {return m_buffer.write_space();} - void LMMS_EXPORT wakeAll() {m_notifier.wakeAll();} + std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t free() {return m_buffer.write_space();} + void wakeAll() {m_notifier.wakeAll();} private: ringbuffer_t m_buffer; @@ -109,13 +109,13 @@ template class LMMS_EXPORT LocklessRingBufferReader : public ringbuffer_reader_t { public: - LMMS_EXPORT LocklessRingBufferReader(LocklessRingBuffer &rb) : + LocklessRingBufferReader(LocklessRingBuffer &rb) : ringbuffer_reader_t(rb.m_buffer), m_notifier(&rb.m_notifier) {}; - bool LMMS_EXPORT empty() {return !this->read_space();} + bool empty() {return !this->read_space();} - void LMMS_EXPORT waitForData() + void waitForData() { QMutex useless_lock; m_notifier->wait(&useless_lock); From 6dd2619f37e6dfce4c01d862d9404f3bf434b5d2 Mon Sep 17 00:00:00 2001 From: Hyunjin Song Date: Fri, 8 Nov 2019 08:02:41 +0900 Subject: [PATCH 23/36] Try to fix an export error --- include/LocklessRingBuffer.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index a4bc9257f9e..3e3d71cbc93 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -25,6 +25,7 @@ #ifndef LOCKLESSRINGBUFFER_H #define LOCKLESSRINGBUFFER_H +#include "lmms_export.h" #include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" #include "lmms_basics.h" #include From 6856b3dbe4ade0901fba520e4609205318763db1 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sat, 9 Nov 2019 17:30:45 +0100 Subject: [PATCH 24/36] Rework LocklessRingBuffer and force export of template instance on MSVC compilers --- include/LocklessRingBuffer.h | 62 +++--------------- src/core/CMakeLists.txt | 1 + src/core/LocklessRingBuffer.cpp | 109 ++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 52 deletions(-) create mode 100644 src/core/LocklessRingBuffer.cpp diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 3e3d71cbc93..e9e78c271a1 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -28,6 +28,7 @@ #include "lmms_export.h" #include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" #include "lmms_basics.h" +#include #include @@ -38,19 +39,13 @@ class LMMS_EXPORT LocklessRingBuffer template friend class LocklessRingBufferReader; public: - LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - ~LocklessRingBuffer() {}; + LocklessRingBuffer(std::size_t sz); + ~LocklessRingBuffer(); - std::size_t write(const sampleFrame *src, size_t cnt) - { - std::size_t written = m_buffer.write(src, cnt); - m_notifier.wakeAll(); // Let all waiting readers know new data are available. - return written; - } - - std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t free() {return m_buffer.write_space();} - void wakeAll() {m_notifier.wakeAll();} + std::size_t write(const T *src, size_t cnt); + std::size_t capacity(); + std::size_t free(); + void wakeAll(); private: ringbuffer_t m_buffer; @@ -76,52 +71,15 @@ class LMMS_EXPORT sampleFrame_copier }; -//! Specialized ring buffer wrapper with write function modified to support sampleFrame. -template <> -class LMMS_EXPORT LocklessRingBuffer -{ - template - friend class LocklessRingBufferReader; -public: - LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - ~LocklessRingBuffer() {}; - - std::size_t write(const sampleFrame *src, size_t cnt) - { - sampleFrame_copier copier(src); - std::size_t written = m_buffer.write_func(copier, cnt); - // Let all waiting readers know new data are available. - m_notifier.wakeAll(); - return written; - } - - std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t free() {return m_buffer.write_space();} - void wakeAll() {m_notifier.wakeAll();} - -private: - ringbuffer_t m_buffer; - QWaitCondition m_notifier; -}; - - //! Wrapper for lockless ringbuffer reader template class LMMS_EXPORT LocklessRingBufferReader : public ringbuffer_reader_t { public: - LocklessRingBufferReader(LocklessRingBuffer &rb) : - ringbuffer_reader_t(rb.m_buffer), - m_notifier(&rb.m_notifier) {}; - - bool empty() {return !this->read_space();} + LocklessRingBufferReader(LocklessRingBuffer &rb); - void waitForData() - { - QMutex useless_lock; - m_notifier->wait(&useless_lock); - useless_lock.unlock(); - } + bool empty(); + void waitForData(); private: QWaitCondition *m_notifier; }; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 48d55b500f0..03c2fec3756 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -33,6 +33,7 @@ set(LMMS_SRCS core/LadspaManager.cpp core/LfoController.cpp core/LocklessAllocator.cpp + core/LocklessRingBuffer.cpp core/MemoryHelper.cpp core/MemoryManager.cpp core/MeterModel.cpp diff --git a/src/core/LocklessRingBuffer.cpp b/src/core/LocklessRingBuffer.cpp new file mode 100644 index 00000000000..713624a9cad --- /dev/null +++ b/src/core/LocklessRingBuffer.cpp @@ -0,0 +1,109 @@ +/* + * LocklessRingBuffer.cpp - LMMS wrapper for a lockless ringbuffer library + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "LocklessRingBuffer.h" + + +// This is required to force MSVC compilers to export symbols for template class methods. +// Any template instances that are not specified here will not work in plugins. +template class LMMS_EXPORT LocklessRingBuffer; +template class LMMS_EXPORT LocklessRingBufferReader; + + +template +LocklessRingBuffer::LocklessRingBuffer(std::size_t sz) : + m_buffer(sz) +{ +} + + +template +LocklessRingBuffer::~LocklessRingBuffer() +{ +} + + +template +std::size_t LocklessRingBuffer::write(const T *src, size_t cnt) +{ + std::size_t written = m_buffer.write(src, cnt); + m_notifier.wakeAll(); // Let all waiting readers know new data are available. + return written; +} + +//! Specialized write function modified to support sampleFrame. +template <> +std::size_t LocklessRingBuffer::write(const sampleFrame *src, size_t cnt) +{ + sampleFrame_copier copier(src); + std::size_t written = m_buffer.write_func(copier, cnt); + // Let all waiting readers know new data are available. + m_notifier.wakeAll(); + return written; +} + + +template +std::size_t LocklessRingBuffer::capacity() +{ + return m_buffer.maximum_eventual_write_space(); +} + + +template +std::size_t LocklessRingBuffer::free() +{ + return m_buffer.write_space(); +} + + +template +void LocklessRingBuffer::wakeAll() +{ + m_notifier.wakeAll(); +} + + +template +LocklessRingBufferReader::LocklessRingBufferReader(LocklessRingBuffer &rb) : + ringbuffer_reader_t(rb.m_buffer), + m_notifier(&rb.m_notifier) +{ +} + + +template +bool LocklessRingBufferReader::empty() +{ + return !this->read_space(); +} + + +template +void LocklessRingBufferReader::waitForData() +{ + QMutex useless_lock; + m_notifier->wait(&useless_lock); + useless_lock.unlock(); +} From 0d56ae89933e2a0662ba4bdf6bb382c1d0405888 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sat, 9 Nov 2019 18:35:35 +0100 Subject: [PATCH 25/36] Move instances to the bottom of file --- src/core/LocklessRingBuffer.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/LocklessRingBuffer.cpp b/src/core/LocklessRingBuffer.cpp index 713624a9cad..0c8d2b691a4 100644 --- a/src/core/LocklessRingBuffer.cpp +++ b/src/core/LocklessRingBuffer.cpp @@ -25,12 +25,6 @@ #include "LocklessRingBuffer.h" -// This is required to force MSVC compilers to export symbols for template class methods. -// Any template instances that are not specified here will not work in plugins. -template class LMMS_EXPORT LocklessRingBuffer; -template class LMMS_EXPORT LocklessRingBufferReader; - - template LocklessRingBuffer::LocklessRingBuffer(std::size_t sz) : m_buffer(sz) @@ -107,3 +101,9 @@ void LocklessRingBufferReader::waitForData() m_notifier->wait(&useless_lock); useless_lock.unlock(); } + + +// This is required to force MSVC compilers to export symbols for template class methods. +// Any template instances that are not specified here will not work in plugins. +template class LMMS_EXPORT LocklessRingBuffer; +template class LMMS_EXPORT LocklessRingBufferReader; From 228dd2f571114925af6d06d56293999e2df814b9 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sat, 9 Nov 2019 18:42:56 +0100 Subject: [PATCH 26/36] Revert "Move instances to the bottom of file" This reverts commit 0d56ae89933e2a0662ba4bdf6bb382c1d0405888. --- src/core/LocklessRingBuffer.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/LocklessRingBuffer.cpp b/src/core/LocklessRingBuffer.cpp index 0c8d2b691a4..713624a9cad 100644 --- a/src/core/LocklessRingBuffer.cpp +++ b/src/core/LocklessRingBuffer.cpp @@ -25,6 +25,12 @@ #include "LocklessRingBuffer.h" +// This is required to force MSVC compilers to export symbols for template class methods. +// Any template instances that are not specified here will not work in plugins. +template class LMMS_EXPORT LocklessRingBuffer; +template class LMMS_EXPORT LocklessRingBufferReader; + + template LocklessRingBuffer::LocklessRingBuffer(std::size_t sz) : m_buffer(sz) @@ -101,9 +107,3 @@ void LocklessRingBufferReader::waitForData() m_notifier->wait(&useless_lock); useless_lock.unlock(); } - - -// This is required to force MSVC compilers to export symbols for template class methods. -// Any template instances that are not specified here will not work in plugins. -template class LMMS_EXPORT LocklessRingBuffer; -template class LMMS_EXPORT LocklessRingBufferReader; From 22e916335014f51a01fb4012a85066aeb9a7cce9 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sat, 9 Nov 2019 19:08:24 +0100 Subject: [PATCH 27/36] Move specialized write() above the non-specialized one --- src/core/LocklessRingBuffer.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/LocklessRingBuffer.cpp b/src/core/LocklessRingBuffer.cpp index 713624a9cad..48cc0e6944d 100644 --- a/src/core/LocklessRingBuffer.cpp +++ b/src/core/LocklessRingBuffer.cpp @@ -44,14 +44,6 @@ LocklessRingBuffer::~LocklessRingBuffer() } -template -std::size_t LocklessRingBuffer::write(const T *src, size_t cnt) -{ - std::size_t written = m_buffer.write(src, cnt); - m_notifier.wakeAll(); // Let all waiting readers know new data are available. - return written; -} - //! Specialized write function modified to support sampleFrame. template <> std::size_t LocklessRingBuffer::write(const sampleFrame *src, size_t cnt) @@ -64,6 +56,15 @@ std::size_t LocklessRingBuffer::write(const sampleFrame *src, size_ } +template +std::size_t LocklessRingBuffer::write(const T *src, size_t cnt) +{ + std::size_t written = m_buffer.write(src, cnt); + m_notifier.wakeAll(); // Let all waiting readers know new data are available. + return written; +} + + template std::size_t LocklessRingBuffer::capacity() { From dc2bd91281bc64da317a6c21ef362aa115348522 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sat, 9 Nov 2019 21:58:19 +0100 Subject: [PATCH 28/36] Move sampleFrame instantiation to the header file --- include/LocklessRingBuffer.h | 7 +++++++ src/core/LocklessRingBuffer.cpp | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index e9e78c271a1..ee46631ac79 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -83,4 +83,11 @@ class LMMS_EXPORT LocklessRingBufferReader : public ringbuffer_reader_t private: QWaitCondition *m_notifier; }; + + +// This is required to force MSVC compilers to export symbols for template class methods. +// Any template instances that are not specified here will not work in plugins. +template class LMMS_EXPORT LocklessRingBuffer; +template class LMMS_EXPORT LocklessRingBufferReader; + #endif //LOCKLESSRINGBUFFER_H diff --git a/src/core/LocklessRingBuffer.cpp b/src/core/LocklessRingBuffer.cpp index 48cc0e6944d..f0ecf26be08 100644 --- a/src/core/LocklessRingBuffer.cpp +++ b/src/core/LocklessRingBuffer.cpp @@ -25,12 +25,6 @@ #include "LocklessRingBuffer.h" -// This is required to force MSVC compilers to export symbols for template class methods. -// Any template instances that are not specified here will not work in plugins. -template class LMMS_EXPORT LocklessRingBuffer; -template class LMMS_EXPORT LocklessRingBufferReader; - - template LocklessRingBuffer::LocklessRingBuffer(std::size_t sz) : m_buffer(sz) From 8739abb1fd8a858668b05acc10aa47accc20db5d Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 14 Nov 2019 21:38:08 +0100 Subject: [PATCH 29/36] Try to remove LMMS_EXPORT from LocklessRingBuffer template --- include/LocklessRingBuffer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index ee46631ac79..3b5372235ec 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -34,7 +34,7 @@ //! A convenience layer for a realtime-safe and thread-safe multi-reader ring buffer library. template -class LMMS_EXPORT LocklessRingBuffer +class LocklessRingBuffer { template friend class LocklessRingBufferReader; @@ -73,7 +73,7 @@ class LMMS_EXPORT sampleFrame_copier //! Wrapper for lockless ringbuffer reader template -class LMMS_EXPORT LocklessRingBufferReader : public ringbuffer_reader_t +class LocklessRingBufferReader : public ringbuffer_reader_t { public: LocklessRingBufferReader(LocklessRingBuffer &rb); From 7a0fc5a25669a066ec861b8d622c62e5ccbdb8de Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 14 Nov 2019 22:35:38 +0100 Subject: [PATCH 30/36] Go back to 'everything in the header' to fix Mac and hope it does not break MSVC again --- include/LocklessRingBuffer.h | 60 +++++++++++++++--- src/core/CMakeLists.txt | 1 - src/core/LocklessRingBuffer.cpp | 104 -------------------------------- 3 files changed, 50 insertions(+), 115 deletions(-) delete mode 100644 src/core/LocklessRingBuffer.cpp diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 3b5372235ec..b15c4cd252a 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -39,13 +39,18 @@ class LocklessRingBuffer template friend class LocklessRingBufferReader; public: - LocklessRingBuffer(std::size_t sz); - ~LocklessRingBuffer(); + LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + ~LocklessRingBuffer() {}; - std::size_t write(const T *src, size_t cnt); - std::size_t capacity(); - std::size_t free(); - void wakeAll(); + std::size_t write(const T *src, size_t cnt) + { + std::size_t written = m_buffer.write(src, cnt); + m_notifier.wakeAll(); // Let all waiting readers know new data are available. + return written; + } + std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t free() {return m_buffer.write_space();} + void wakeAll() {m_notifier.wakeAll();} private: ringbuffer_t m_buffer; @@ -71,15 +76,51 @@ class LMMS_EXPORT sampleFrame_copier }; +//! Specialized version with write function modified to support sampleFrame. +template <> +class LocklessRingBuffer +{ + template + friend class LocklessRingBufferReader; +public: + LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; + ~LocklessRingBuffer() {}; + + std::size_t write(const sampleFrame *src, size_t cnt) + { + sampleFrame_copier copier(src); + std::size_t written = m_buffer.write_func(copier, cnt); + // Let all waiting readers know new data are available. + m_notifier.wakeAll(); + return written; + } + + std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} + std::size_t free() {return m_buffer.write_space();} + void wakeAll() {m_notifier.wakeAll();} + +private: + ringbuffer_t m_buffer; + QWaitCondition m_notifier; +}; + + //! Wrapper for lockless ringbuffer reader template class LocklessRingBufferReader : public ringbuffer_reader_t { public: - LocklessRingBufferReader(LocklessRingBuffer &rb); + LocklessRingBufferReader(LocklessRingBuffer &rb) : + ringbuffer_reader_t(rb.m_buffer), + m_notifier(&rb.m_notifier) {}; - bool empty(); - void waitForData(); + bool empty() {return !this->read_space();} + void waitForData() + { + QMutex useless_lock; + m_notifier->wait(&useless_lock); + useless_lock.unlock(); + } private: QWaitCondition *m_notifier; }; @@ -87,7 +128,6 @@ class LocklessRingBufferReader : public ringbuffer_reader_t // This is required to force MSVC compilers to export symbols for template class methods. // Any template instances that are not specified here will not work in plugins. -template class LMMS_EXPORT LocklessRingBuffer; template class LMMS_EXPORT LocklessRingBufferReader; #endif //LOCKLESSRINGBUFFER_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 03c2fec3756..48d55b500f0 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -33,7 +33,6 @@ set(LMMS_SRCS core/LadspaManager.cpp core/LfoController.cpp core/LocklessAllocator.cpp - core/LocklessRingBuffer.cpp core/MemoryHelper.cpp core/MemoryManager.cpp core/MeterModel.cpp diff --git a/src/core/LocklessRingBuffer.cpp b/src/core/LocklessRingBuffer.cpp deleted file mode 100644 index f0ecf26be08..00000000000 --- a/src/core/LocklessRingBuffer.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - * LocklessRingBuffer.cpp - LMMS wrapper for a lockless ringbuffer library - * - * Copyright (c) 2019 Martin Pavelek - * - * This file is part of LMMS - https://lmms.io - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program (see COPYING); if not, write to the - * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301 USA. - * - */ - -#include "LocklessRingBuffer.h" - - -template -LocklessRingBuffer::LocklessRingBuffer(std::size_t sz) : - m_buffer(sz) -{ -} - - -template -LocklessRingBuffer::~LocklessRingBuffer() -{ -} - - -//! Specialized write function modified to support sampleFrame. -template <> -std::size_t LocklessRingBuffer::write(const sampleFrame *src, size_t cnt) -{ - sampleFrame_copier copier(src); - std::size_t written = m_buffer.write_func(copier, cnt); - // Let all waiting readers know new data are available. - m_notifier.wakeAll(); - return written; -} - - -template -std::size_t LocklessRingBuffer::write(const T *src, size_t cnt) -{ - std::size_t written = m_buffer.write(src, cnt); - m_notifier.wakeAll(); // Let all waiting readers know new data are available. - return written; -} - - -template -std::size_t LocklessRingBuffer::capacity() -{ - return m_buffer.maximum_eventual_write_space(); -} - - -template -std::size_t LocklessRingBuffer::free() -{ - return m_buffer.write_space(); -} - - -template -void LocklessRingBuffer::wakeAll() -{ - m_notifier.wakeAll(); -} - - -template -LocklessRingBufferReader::LocklessRingBufferReader(LocklessRingBuffer &rb) : - ringbuffer_reader_t(rb.m_buffer), - m_notifier(&rb.m_notifier) -{ -} - - -template -bool LocklessRingBufferReader::empty() -{ - return !this->read_space(); -} - - -template -void LocklessRingBufferReader::waitForData() -{ - QMutex useless_lock; - m_notifier->wait(&useless_lock); - useless_lock.unlock(); -} From bf793dad5c1c1d415bce3b99448c3a4747fea72e Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Fri, 15 Nov 2019 01:39:44 +0100 Subject: [PATCH 31/36] Try removing all LMMS_EXPORTs from LocklessRingBuffer --- include/LocklessRingBuffer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index b15c4cd252a..5e4530a703d 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -60,7 +60,7 @@ class LocklessRingBuffer // The sampleFrame_copier is required because sampleFrame is just a two-element // array and therefore does not have a copy constructor needed by std::copy. -class LMMS_EXPORT sampleFrame_copier +class sampleFrame_copier { const sampleFrame* src; public: @@ -128,6 +128,6 @@ class LocklessRingBufferReader : public ringbuffer_reader_t // This is required to force MSVC compilers to export symbols for template class methods. // Any template instances that are not specified here will not work in plugins. -template class LMMS_EXPORT LocklessRingBufferReader; +//template class LMMS_EXPORT LocklessRingBufferReader; #endif //LOCKLESSRINGBUFFER_H From a0acc8a6e2a6cd006b8f9bdf382123973c8dda15 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sat, 16 Nov 2019 11:35:24 +0100 Subject: [PATCH 32/36] Implement LocklessRingBuffer changes requested in review --- include/LocklessRingBuffer.h | 64 +++++++++++++-------------- plugins/SpectrumAnalyzer/Analyzer.cpp | 2 +- src/3rdparty/ringbuffer | 2 +- src/CMakeLists.txt | 2 + src/core/CMakeLists.txt | 1 - 5 files changed, 35 insertions(+), 36 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 5e4530a703d..4152850afdd 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -34,25 +34,22 @@ //! A convenience layer for a realtime-safe and thread-safe multi-reader ring buffer library. template -class LocklessRingBuffer +class LocklessRingBufferBase { template friend class LocklessRingBufferReader; public: - LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - ~LocklessRingBuffer() {}; - - std::size_t write(const T *src, size_t cnt) + LocklessRingBufferBase(std::size_t sz) : m_buffer(sz) { - std::size_t written = m_buffer.write(src, cnt); - m_notifier.wakeAll(); // Let all waiting readers know new data are available. - return written; + m_buffer.touch(); // reserve storage space before realtime operation starts } - std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t free() {return m_buffer.write_space();} + ~LocklessRingBufferBase() {}; + + std::size_t capacity() const {return m_buffer.maximum_eventual_write_space();} + std::size_t free() const {return m_buffer.write_space();} void wakeAll() {m_notifier.wakeAll();} -private: +protected: ringbuffer_t m_buffer; QWaitCondition m_notifier; }; @@ -76,32 +73,38 @@ class sampleFrame_copier }; -//! Specialized version with write function modified to support sampleFrame. -template <> -class LocklessRingBuffer +//! Standard ring buffer template for data types with copy constructor. +template +class LocklessRingBuffer : public LocklessRingBufferBase { - template - friend class LocklessRingBufferReader; public: - LocklessRingBuffer(std::size_t sz) : m_buffer(sz) {}; - ~LocklessRingBuffer() {}; + LocklessRingBuffer(std::size_t sz) : LocklessRingBufferBase(sz) {}; - std::size_t write(const sampleFrame *src, size_t cnt) + std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false) { - sampleFrame_copier copier(src); - std::size_t written = m_buffer.write_func(copier, cnt); + std::size_t written = LocklessRingBufferBase::m_buffer.write(src, cnt); // Let all waiting readers know new data are available. - m_notifier.wakeAll(); + if (notify) {LocklessRingBufferBase::m_notifier.wakeAll();} return written; } +}; - std::size_t capacity() {return m_buffer.maximum_eventual_write_space();} - std::size_t free() {return m_buffer.write_space();} - void wakeAll() {m_notifier.wakeAll();} -private: - ringbuffer_t m_buffer; - QWaitCondition m_notifier; +//! Specialized ring buffer template with write function modified to support sampleFrame. +template <> +class LocklessRingBuffer : public LocklessRingBufferBase +{ +public: + LocklessRingBuffer(std::size_t sz) : LocklessRingBufferBase(sz) {}; + + std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false) + { + sampleFrame_copier copier(src); + std::size_t written = LocklessRingBufferBase::m_buffer.write_func(copier, cnt); + // Let all waiting readers know new data are available. + if (notify) {LocklessRingBufferBase::m_notifier.wakeAll();} + return written; + } }; @@ -125,9 +128,4 @@ class LocklessRingBufferReader : public ringbuffer_reader_t QWaitCondition *m_notifier; }; - -// This is required to force MSVC compilers to export symbols for template class methods. -// Any template instances that are not specified here will not work in plugins. -//template class LMMS_EXPORT LocklessRingBufferReader; - #endif //LOCKLESSRINGBUFFER_H diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index c5f9ce19795..91c2dec0e3f 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -93,7 +93,7 @@ bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) if (m_controls.isViewVisible()) { // To avoid processing spikes on audio thread, data are stored in // a lockless ringbuffer and processed in a separate thread. - m_inputBuffer.write(buffer, frame_count); + m_inputBuffer.write(buffer, frame_count, true); } #ifdef SA_DEBUG audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - audio_time; diff --git a/src/3rdparty/ringbuffer b/src/3rdparty/ringbuffer index 5332e6d03c7..82ed7cfb9ad 160000 --- a/src/3rdparty/ringbuffer +++ b/src/3rdparty/ringbuffer @@ -1 +1 @@ -Subproject commit 5332e6d03c7449a3c343866f654aeb2526a1a7d1 +Subproject commit 82ed7cfb9ad40467421d8b14ca1af0350e92613c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5688e5410ce..59710926d86 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -90,6 +90,8 @@ IF(NOT ("${LAME_INCLUDE_DIRS}" STREQUAL "")) INCLUDE_DIRECTORIES("${LAME_INCLUDE_DIRS}") ENDIF() +LIST(APPEND LMMS_SRCS "${RINGBUFFER_DIR}/src/lib/ringbuffer.cpp") + # Use libraries in non-standard directories (e.g., another version of Qt) IF(LMMS_BUILD_LINUX) LINK_LIBRARIES(-Wl,--enable-new-dtags) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 48d55b500f0..a50b32a0ff2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,6 +1,5 @@ set(LMMS_SRCS ${LMMS_SRCS} - 3rdparty/ringbuffer/src/lib/ringbuffer.cpp core/AutomatableModel.cpp core/AutomationPattern.cpp From 8ca05a3b94d92b60713b531f0ed8acfbe56b0599 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 17 Nov 2019 10:52:30 +0100 Subject: [PATCH 33/36] Fix code conventions, make some includes harder to read --- include/LocklessRingBuffer.h | 21 ++++++++++---------- plugins/SpectrumAnalyzer/Analyzer.cpp | 12 ++++++----- plugins/SpectrumAnalyzer/Analyzer.h | 6 ++++-- plugins/SpectrumAnalyzer/README.md | 2 +- plugins/SpectrumAnalyzer/SaProcessor.cpp | 18 ++++++++++------- plugins/SpectrumAnalyzer/SaWaterfallView.cpp | 7 +++---- 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 4152850afdd..00affa01fac 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -25,12 +25,13 @@ #ifndef LOCKLESSRINGBUFFER_H #define LOCKLESSRINGBUFFER_H -#include "lmms_export.h" -#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" -#include "lmms_basics.h" #include #include +#include "lmms_basics.h" +#include "lmms_export.h" +#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" + //! A convenience layer for a realtime-safe and thread-safe multi-reader ring buffer library. template @@ -57,17 +58,17 @@ class LocklessRingBufferBase // The sampleFrame_copier is required because sampleFrame is just a two-element // array and therefore does not have a copy constructor needed by std::copy. -class sampleFrame_copier +class SampleFrameCopier { - const sampleFrame* src; + const sampleFrame* m_src; public: - sampleFrame_copier(const sampleFrame* src) : src(src) {} + SampleFrameCopier(const sampleFrame* src) : m_src(src) {} void operator()(std::size_t src_offset, std::size_t count, sampleFrame* dest) { for (std::size_t i = src_offset; i < src_offset + count; i++, dest++) { - (*dest)[0] = src[i][0]; - (*dest)[1] = src[i][1]; + (*dest)[0] = m_src[i][0]; + (*dest)[1] = m_src[i][1]; } } }; @@ -100,7 +101,7 @@ class LocklessRingBuffer : public LocklessRingBufferBase::m_buffer.write_func(copier, cnt); + std::size_t written = LocklessRingBufferBase::m_buffer.write_func(copier, cnt); // Let all waiting readers know new data are available. if (notify) {LocklessRingBufferBase::m_notifier.wakeAll();} return written; @@ -117,7 +118,7 @@ class LocklessRingBufferReader : public ringbuffer_reader_t ringbuffer_reader_t(rb.m_buffer), m_notifier(&rb.m_notifier) {}; - bool empty() {return !this->read_space();} + bool empty() const {return !this->read_space();} void waitForData() { QMutex useless_lock; diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index 91c2dec0e3f..e410364fa9a 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -27,15 +27,16 @@ #include "Analyzer.h" -#include "embed.h" -#include "lmms_basics.h" -#include "plugin_export.h" - #ifdef SA_DEBUG #include #include #endif +#include "embed.h" +#include "lmms_basics.h" +#include "plugin_export.h" + + extern "C" { Plugin::Descriptor PLUGIN_EXPORT analyzer_plugin_descriptor = { @@ -90,7 +91,8 @@ bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) if (!isEnabled() || !isRunning ()) {return false;} // Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. - if (m_controls.isViewVisible()) { + if (m_controls.isViewVisible()) + { // To avoid processing spikes on audio thread, data are stored in // a lockless ringbuffer and processed in a separate thread. m_inputBuffer.write(buffer, frame_count, true); diff --git a/plugins/SpectrumAnalyzer/Analyzer.h b/plugins/SpectrumAnalyzer/Analyzer.h index 1f49fe6e7e7..304777c9a09 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.h +++ b/plugins/SpectrumAnalyzer/Analyzer.h @@ -27,12 +27,14 @@ #ifndef ANALYZER_H #define ANALYZER_H +#include + #include "DataprocLauncher.h" #include "Effect.h" +#include "LocklessRingBuffer.h" #include "SaControls.h" #include "SaProcessor.h" -#include -#include "LocklessRingBuffer.h" + //! Top level class; handles LMMS interface and feeds data to the data processor. class Analyzer : public Effect diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md index 965460f004f..42c8a501da8 100644 --- a/plugins/SpectrumAnalyzer/README.md +++ b/plugins/SpectrumAnalyzer/README.md @@ -18,7 +18,7 @@ The Spectrum Analyzer is involved in three different threads: ## Changelog 1.1.1 2019-10-13 - - improved interface for accessing SaProcessor provate data + - improved interface for accessing SaProcessor private data - readme file update - other small improvements based on reviews 1.1.0 2019-08-29 diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index f984dd8ff92..aef99e64ec0 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -26,17 +26,19 @@ #include "SaProcessor.h" #include +#ifdef SA_DEBUG + #include +#endif #include +#ifdef SA_DEBUG + #include + #include +#endif #include #include "lmms_math.h" #include "LocklessRingBuffer.h" -#ifdef SA_DEBUG - #include - #include - #include -#endif SaProcessor::SaProcessor(const SaControls *controls) : m_controls(controls), @@ -93,7 +95,8 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) LocklessRingBufferReader reader(ring_buffer); // Processing thread loop - while (!m_terminate) { + while (!m_terminate) + { // If there is nothing to read, wait for notification from the writing side. if (reader.empty()) {reader.waitForData();} @@ -205,7 +208,8 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) for (unsigned int i = 0; i < waterfallWidth(); i++) { // fill line with red color to indicate lost data if CPU cannot keep up - if (overload) { + if (overload) + { pixel[i] = qRgb(42, 0, 0); continue; } diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp index 136157495ca..e015d31ef74 100644 --- a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -23,6 +23,9 @@ #include "SaWaterfallView.h" #include +#ifdef SA_DEBUG + #include +#endif #include #include #include @@ -36,10 +39,6 @@ #include "MainWindow.h" #include "SaProcessor.h" -#ifdef SA_DEBUG - #include -#endif - SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, QWidget *_parent) : QWidget(_parent), From c6942778294f046e8dccf1c9fc765a0d987a6235 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Sun, 17 Nov 2019 10:54:26 +0100 Subject: [PATCH 34/36] Forgotten rename --- include/LocklessRingBuffer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 00affa01fac..3b18dd475d6 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -56,7 +56,7 @@ class LocklessRingBufferBase }; -// The sampleFrame_copier is required because sampleFrame is just a two-element +// The SampleFrameCopier is required because sampleFrame is just a two-element // array and therefore does not have a copy constructor needed by std::copy. class SampleFrameCopier { @@ -100,7 +100,7 @@ class LocklessRingBuffer : public LocklessRingBufferBase::m_buffer.write_func(copier, cnt); // Let all waiting readers know new data are available. if (notify) {LocklessRingBufferBase::m_notifier.wakeAll();} From 0caa7486af1fc9ba245babf1358978abd100f962 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Mon, 18 Nov 2019 20:25:21 +0100 Subject: [PATCH 35/36] Fix missing part of waterfall when its width limit is reached --- plugins/SpectrumAnalyzer/SaProcessor.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index aef99e64ec0..2e7cf4ead56 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -204,8 +204,7 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) int target; // pixel being constructed float accL = 0; // accumulators for merging multiple bins float accR = 0; - - for (unsigned int i = 0; i < waterfallWidth(); i++) + for (unsigned int i = 0; i < binCount(); i++) { // fill line with red color to indicate lost data if CPU cannot keep up if (overload) From a7388b141a324dd034a0e81e9a0f14a728809e1b Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Mon, 18 Nov 2019 23:40:33 +0100 Subject: [PATCH 36/36] Fix drawing bounds of "overload fill-in"; improve comment on waterfallWidth(); update changelog and version number --- plugins/SpectrumAnalyzer/Analyzer.cpp | 2 +- plugins/SpectrumAnalyzer/README.md | 3 +++ plugins/SpectrumAnalyzer/SaProcessor.cpp | 7 +++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp index e410364fa9a..656d18bd4d6 100644 --- a/plugins/SpectrumAnalyzer/Analyzer.cpp +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -44,7 +44,7 @@ extern "C" { "Spectrum Analyzer", QT_TRANSLATE_NOOP("pluginBrowser", "A graphical spectrum analyzer."), "Martin Pavelek ", - 0x0111, + 0x0112, Plugin::Effect, new PluginPixmapLoader("logo"), NULL, diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md index 42c8a501da8..473083da81e 100644 --- a/plugins/SpectrumAnalyzer/README.md +++ b/plugins/SpectrumAnalyzer/README.md @@ -17,6 +17,9 @@ The Spectrum Analyzer is involved in three different threads: ## Changelog + 1.1.2 2019-11-18 + - waterfall is no longer cut short when width limit is reached + - various small tweaks based on final review 1.1.1 2019-10-13 - improved interface for accessing SaProcessor private data - readme file update diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp index 2e7cf4ead56..9d83f2916f0 100644 --- a/plugins/SpectrumAnalyzer/SaProcessor.cpp +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -207,7 +207,7 @@ void SaProcessor::analyze(LocklessRingBuffer &ring_buffer) for (unsigned int i = 0; i < binCount(); i++) { // fill line with red color to indicate lost data if CPU cannot keep up - if (overload) + if (overload && i < waterfallWidth()) { pixel[i] = qRgb(42, 0, 0); continue; @@ -517,8 +517,11 @@ unsigned int SaProcessor::binCount() const } +// Return the final width of waterfall display buffer. +// Normally the waterfall width equals the number of frequency bins, but the // FFT transform can easily produce more bins than can be reasonably useful for -// display. Cap the width at 3840: full screen on UHD display should be enough. +// currently used display resolutions. This function limits width of the final +// image to a given size, which is then used during waterfall render and display. unsigned int SaProcessor::waterfallWidth() const { return binCount() < m_waterfallMaxWidth ? binCount() : m_waterfallMaxWidth;