code:
/*
 * SaControlsDialog.cpp - definition of SaControlsDialog class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * 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 "SaControlsDialog.h"

#include <QGridLayout>
#include <QLabel>
#include <QSizePolicy>
#include <QSplitter>
#include <QWidget>

#include "ComboBox.h"
#include "ComboBoxModel.h"
#include "embed.h"
#include "Engine.h"
#include "Knob.h"
#include "LedCheckbox.h"
#include "PixmapButton.h"
#include "SaControls.h"
#include "SaProcessor.h"

// The entire GUI layout is built here.
SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor) :
	EffectControlDialog(controls),
	m_controls(controls),
	m_processor(processor)
{
	// Top level placement of sections is handled by QSplitter widget.
	QHBoxLayout *master_layout = new QHBoxLayout;
	QSplitter *display_splitter = new QSplitter(Qt::Vertical);
	master_layout->addWidget(display_splitter);
	master_layout->setContentsMargins(2, 6, 2, 8);
	setLayout(master_layout);

	// 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);
	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
	// sharp on High-DPI screens, but the desired size must be manually
	// 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"));
	pauseButton->setToolTip(tr("Pause data acquisition"));
	QPixmap *pauseOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("play").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	QPixmap *pauseOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("pause").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	pauseOnPixmap->setDevicePixelRatio(devicePixelRatio());
	pauseOffPixmap->setDevicePixelRatio(devicePixelRatio());
	pauseButton->setActiveGraphic(*pauseOnPixmap);
	pauseButton->setInactiveGraphic(*pauseOffPixmap);
	pauseButton->setCheckable(true);
	pauseButton->setModel(&controls->m_pauseModel);
	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."));
	QPixmap *freezeOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("freeze").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	QPixmap *freezeOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("freeze_off").scaled(buttonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	freezeOnPixmap->setDevicePixelRatio(devicePixelRatio());
	freezeOffPixmap->setDevicePixelRatio(devicePixelRatio());
	refFreezeButton->setActiveGraphic(*freezeOnPixmap);
	refFreezeButton->setInactiveGraphic(*freezeOffPixmap);
	refFreezeButton->setCheckable(true);
	refFreezeButton->setModel(&controls->m_refFreezeModel);
	config_layout->addWidget(refFreezeButton, 2, 0, 2, 1, Qt::AlignHCenter);

	// misc configuration switches
	LedCheckBox *waterfallButton = new LedCheckBox(tr("Waterfall"), this);
	waterfallButton->setToolTip(tr("Display real-time spectrogram"));
	waterfallButton->setCheckable(true);
	waterfallButton->setMinimumSize(70, 12);
	waterfallButton->setModel(&controls->m_waterfallModel);
	config_layout->addWidget(waterfallButton, 0, 1);

	LedCheckBox *smoothButton = new LedCheckBox(tr("Averaging"), this);
	smoothButton->setToolTip(tr("Enable exponential moving average"));
	smoothButton->setCheckable(true);
	smoothButton->setMinimumSize(70, 12);
	smoothButton->setModel(&controls->m_smoothModel);
	config_layout->addWidget(smoothButton, 1, 1);

	LedCheckBox *stereoButton = new LedCheckBox(tr("Stereo"), this);
	stereoButton->setToolTip(tr("Display stereo channels separately"));
	stereoButton->setCheckable(true);
	stereoButton->setMinimumSize(70, 12);
	stereoButton->setModel(&controls->m_stereoModel);
	config_layout->addWidget(stereoButton, 2, 1);

	LedCheckBox *peakHoldButton = new LedCheckBox(tr("Peak hold"), this);
	peakHoldButton->setToolTip(tr("Display envelope of peak values"));
	peakHoldButton->setCheckable(true);
	peakHoldButton->setMinimumSize(70, 12);
	peakHoldButton->setModel(&controls->m_peakHoldModel);
	config_layout->addWidget(peakHoldButton, 3, 1);

	// frequency: linear / log. switch and range selector
	PixmapButton *logXButton = new PixmapButton(this, tr("Logarithmic frequency"));
	logXButton->setToolTip(tr("Switch between logarithmic and linear frequency scale"));
	QPixmap *logXOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("x_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	QPixmap *logXOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("x_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	logXOnPixmap->setDevicePixelRatio(devicePixelRatio());
	logXOffPixmap->setDevicePixelRatio(devicePixelRatio());
	logXButton->setActiveGraphic(*logXOnPixmap);
	logXButton->setInactiveGraphic(*logXOffPixmap);
	logXButton->setCheckable(true);
	logXButton->setModel(&controls->m_logXModel);
	config_layout->addWidget(logXButton, 0, 2, 2, 1, Qt::AlignRight);

	ComboBox *freqRangeCombo = new ComboBox(this, tr("Frequency range"));
	freqRangeCombo->setToolTip(tr("Frequency range"));
	freqRangeCombo->setMinimumSize(100, ComboBox::DEFAULT_HEIGHT);
	freqRangeCombo->setMaximumSize(200, ComboBox::DEFAULT_HEIGHT);
	freqRangeCombo->setModel(&controls->m_freqRangeModel);
	config_layout->addWidget(freqRangeCombo, 0, 3, 2, 1);

	// amplitude: linear / log switch and range selector
	PixmapButton *logYButton = new PixmapButton(this, tr("Logarithmic amplitude"));
	logYButton->setToolTip(tr("Switch between logarithmic and linear amplitude scale"));
	QPixmap *logYOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("y_log").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	QPixmap *logYOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("y_linear").scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	logYOnPixmap->setDevicePixelRatio(devicePixelRatio());
	logYOffPixmap->setDevicePixelRatio(devicePixelRatio());
	logYButton->setActiveGraphic(*logYOnPixmap);
	logYButton->setInactiveGraphic(*logYOffPixmap);
	logYButton->setCheckable(true);
	logYButton->setModel(&controls->m_logYModel);
	config_layout->addWidget(logYButton, 2, 2, 2, 1, Qt::AlignRight);

	ComboBox *ampRangeCombo = new ComboBox(this, tr("Amplitude range"));
	ampRangeCombo->setToolTip(tr("Amplitude range"));
	ampRangeCombo->setMinimumSize(100, ComboBox::DEFAULT_HEIGHT);
	ampRangeCombo->setMaximumSize(200, ComboBox::DEFAULT_HEIGHT);
	ampRangeCombo->setModel(&controls->m_ampRangeModel);
	config_layout->addWidget(ampRangeCombo, 2, 3, 2, 1);

	// FFT: block size: icon and selector
	QLabel *blockSizeLabel = new QLabel("", this);
	QPixmap *blockSizeIcon = new QPixmap(PLUGIN_NAME::getIconPixmap("block_size"));
	blockSizeIcon->setDevicePixelRatio(devicePixelRatio());
	blockSizeLabel->setPixmap(blockSizeIcon->scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	config_layout->addWidget(blockSizeLabel, 0, 4, 2, 1, Qt::AlignRight);

	ComboBox *blockSizeCombo = new ComboBox(this, tr("FFT block size"));
	blockSizeCombo->setToolTip(tr("FFT block size"));
	blockSizeCombo->setMinimumSize(100, 22);
	blockSizeCombo->setMaximumSize(200, 22);
	blockSizeCombo->setModel(&controls->m_blockSizeModel);
	config_layout->addWidget(blockSizeCombo, 0, 5, 2, 1);
	processor->reallocateBuffers();
	connect(&controls->m_blockSizeModel, &ComboBoxModel::dataChanged, [=] {processor->reallocateBuffers();});

	// FFT: window type: icon and selector
	QLabel *windowLabel = new QLabel("", this);
	QPixmap *windowIcon = new QPixmap(PLUGIN_NAME::getIconPixmap("window"));
	windowIcon->setDevicePixelRatio(devicePixelRatio());
	windowLabel->setPixmap(windowIcon->scaled(iconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
	config_layout->addWidget(windowLabel, 2, 4, 2, 1, Qt::AlignRight);

	ComboBox *windowCombo = new ComboBox(this, tr("FFT window type"));
	windowCombo->setToolTip(tr("FFT window type"));
	windowCombo->setMinimumSize(100, ComboBox::DEFAULT_HEIGHT);
	windowCombo->setMaximumSize(200, ComboBox::DEFAULT_HEIGHT);
	windowCombo->setModel(&controls->m_windowModel);
	config_layout->addWidget(windowCombo, 2, 5, 2, 1);
	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->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);

	// Spectrum graph resolution
	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);

	// Peak falloff speed
	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);

	// Averaging weight
	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);

	// Waterfall history size
	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. Warning: medium CPU usage."));
	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->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);

	// FFT window overlap
	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);

	// FFT zero padding
	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();
	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);
	display_splitter->addWidget(m_spectrum);

	m_waterfall = new SaWaterfallView(controls, processor, this);
	display_splitter->addWidget(m_waterfall);
	m_waterfall->setVisible(m_controls->m_waterfallModel.value());
	connect(&controls->m_waterfallModel, &BoolModel::dataChanged, [=] {m_waterfall->updateVisibility();});
}

// Suggest the best current widget size.
QSize SaControlsDialog::sizeHint() const
{
	// Best width is determined by spectrum display sizeHint.
	// Best height depends on whether waterfall is visible and
	// consists of heights of the config section, spectrum, waterfall
	// and some reserve for margins.
	if (m_waterfall->isVisible())
	{
		return QSize(m_spectrum->sizeHint().width(),
					 m_configHeight + m_spectrum->sizeHint().height() + m_waterfall->sizeHint().height() + 50);
	}
	else
	{
		return QSize(m_spectrum->sizeHint().width(),
					 m_configHeight + m_spectrum->sizeHint().height() + 50);
	}
}


code:
/*
 * SaControlsDialog.h - declatation of SaControlsDialog class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * 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 SACONTROLSDIALOG_H
#define SACONTROLSDIALOG_H

#include "EffectControlDialog.h"
#include "SaControls.h"
#include "SaSpectrumView.h"
#include "SaProcessor.h"
#include "SaWaterfallView.h"

//! Top-level widget holding the configuration GUI and spectrum displays
class SaControlsDialog : public EffectControlDialog
{
	Q_OBJECT
public:
	explicit SaControlsDialog(SaControls *controls, SaProcessor *processor);
	virtual ~SaControlsDialog() {}

	bool isResizable() const override {return true;}
	QSize sizeHint() const override;

private:
	SaControls *m_controls;
	SaProcessor *m_processor;

	// Pointers to created widgets are needed to keep track of their sizeHint() changes.
	// Config widget is a plain QWidget so it has just a fixed height instead.
	const int m_configHeight = 75;
	SaSpectrumView *m_spectrum;
	SaWaterfallView *m_waterfall;
};

#endif // SACONTROLSDIALOG_H

repo_name: CYBERDEViL/LMMS

license: mit


code:
/* SaProcessor.cpp - implementation of SaProcessor class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * Based partially on Eq plugin code,
 * Copyright (c) 2014-2017, David French <dave/dot/french3/at/googlemail/dot/com>
 *
 * 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 "SaProcessor.h"

#include <algorithm>
#ifdef SA_DEBUG
	#include <chrono>
#endif
#include <cmath>
#ifdef SA_DEBUG
	#include <iomanip>
	#include <iostream>
#endif
#include <QMutexLocker>

#include "lmms_math.h"
#include "LocklessRingBuffer.h"

SaProcessor::SaProcessor(const 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()),
	m_framesFilledUp(0),
	m_spectrumActive(false),
	m_waterfallActive(false),
	m_waterfallNotEmpty(0),
	m_reallocating(false)
{
	m_fftWindow.resize(m_inBlockSize, 1.0);
	precomputeWindow(m_fftWindow.data(), m_inBlockSize, BLACKMAN_HARRIS);

	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_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);
	m_normSpectrumL.resize(binCount(), 0);
	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);
}

SaProcessor::~SaProcessor()
{
	if (m_fftPlanL != NULL) {fftwf_destroy_plan(m_fftPlanL);}
	if (m_fftPlanR != NULL) {fftwf_destroy_plan(m_fftPlanR);}
	if (m_spectrumL != NULL) {fftwf_free(m_spectrumL);}
	if (m_spectrumR != NULL) {fftwf_free(m_spectrumR);}

	m_fftPlanL = NULL;
	m_fftPlanR = NULL;
	m_spectrumL = NULL;
	m_spectrumR = NULL;
}

// Load data from audio thread ringbuffer and run FFT analysis if buffer is full enough.
void SaProcessor::analyze(LocklessRingBuffer<sampleFrame> &ring_buffer)
{
	LocklessRingBufferReader<sampleFrame> reader(ring_buffer);

	// Processing thread loop
	while (!m_terminate)
	{
		// If there is nothing to read, wait for notification from the writing side.
		if (reader.empty()) {reader.waitForData();}

		// skip waterfall render if processing can't keep up with input
		bool overload = ring_buffer.free() < ring_buffer.capacity() / 2;

		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.
		// 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)
		{
			const bool stereo = m_controls->m_stereoModel.value();
			fpp_t in_frame = 0;
			while (in_frame < frame_count)
			{
				// 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++)
				{
					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;
					}
				}

				// 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 / 20.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++)
				{
					m_filteredBufferL[i] = m_bufferL[i] * m_fftWindow[i];
					m_filteredBufferR[i] = m_bufferR[i] * m_fftWindow[i];
				}

				// 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);
				}

				// 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_work.data();
					std::copy(pixel,
							  pixel + waterfallWidth() * m_waterfallHeight - waterfallWidth(),
							  pixel + waterfallWidth());
					memset(pixel, 0, waterfallWidth() * 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++)
					{
						// fill line with red color to indicate lost data if CPU cannot keep up
						if (overload && i < waterfallWidth())
						{
							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())
						{
							// Logarithmic scale
							if (band_end - band_start > 1.0)
							{
								// band spans multiple pixels: draw all pixels it covers
								for (target = (int)band_start; target < (int)band_end; target++)
								{
									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];
								}
								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 < 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++)
							{
								if (target >= 0 && target < waterfallWidth())
								{
									pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]);
								}
							}
						}
					}

					// 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;
					}
				}
				// clean up before checking for more data from input buffer
				const unsigned int overlaps = m_controls->m_windowOverlapModel.value();
				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());
					m_framesFilledUp -= drop;
				}

				#ifdef SA_DEBUG
					// 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
}

// Produce a spectrogram pixel from normalized spectrum data.
// Values over 1.0 will cause the color components to overflow: this is left
// intentionally untreated as it clearly indicates which frequency is clipping.
// 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) const
{
	const float gamma_correction = m_controls->m_waterfallGammaModel.value();
	if (m_controls->m_stereoModel.value())
	{
		float ampL = pow(left, gamma_correction);
		float ampR = pow(right, gamma_correction);
		return qRgb(m_controls->m_colorL.red() * ampL + m_controls->m_colorR.red() * ampR,
					m_controls->m_colorL.green() * ampL + m_controls->m_colorR.green() * ampR,
					m_controls->m_colorL.blue() * ampL + m_controls->m_colorR.blue() * ampR);
	}
	else
	{
		float ampL = pow(left, gamma_correction);
		// make mono color brighter to compensate for the fact it is not summed
		return qRgb(m_controls->m_colorMonoW.red() * ampL,
					m_controls->m_colorMonoW.green() * ampL,
					m_controls->m_colorMonoW.blue() * ampL);
	}
}

// Inform the processor whether any display widgets actually need it.
void SaProcessor::setSpectrumActive(bool active)
{
	m_spectrumActive = active;
}

void SaProcessor::setWaterfallActive(bool active)
{
	m_waterfallActive = active;
}

// Reallocate data buffers according to newly set block size.
void SaProcessor::reallocateBuffers()
{
	unsigned int new_size_index = m_controls->m_blockSizeModel.value();
	unsigned int new_in_size, new_fft_size;
	unsigned int new_bins;

	// get new block sizes and bin count based on selected index
	if (new_size_index < FFT_BLOCK_SIZES.size())
	{
		new_in_size = FFT_BLOCK_SIZES[new_size_index];
	}
	else
	{
		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];
	}
	else
	{
		new_fft_size = FFT_BLOCK_SIZES.back();
	}

	new_bins = new_fft_size / 2 +1;

	// 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;

	// 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);}
	if (m_fftPlanR != NULL) {fftwf_destroy_plan(m_fftPlanR);}
	if (m_spectrumL != NULL) {fftwf_free(m_spectrumL);}
	if (m_spectrumR != NULL) {fftwf_free(m_spectrumR);}

	// 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_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_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)
	{
		#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);
	m_normSpectrumL.resize(new_bins, 0);
	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);

	// done; publish new sizes and clean up
	m_inBlockSize = new_in_size;
	m_fftBlockSize = new_fft_size;

	data_lock.unlock();
	reloc_lock.unlock();
	m_reallocating = false;

	clear();
}

// Precompute a new FFT window based on currently selected type.
void SaProcessor::rebuildWindow()
{
	// computation is done in fft_helpers
	QMutexLocker lock(&m_dataAccess);
	precomputeWindow(m_fftWindow.data(), m_inBlockSize, (FFT_WINDOWS) m_controls->m_windowModel.value());
}

// Clear all data buffers and replace contents with zeros.
// 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);
	// 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);
	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);
	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);
}

// 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
//

// Get sample rate value that is valid for currently stored results.
unsigned int SaProcessor::getSampleRate() const
{
	return m_sampleRate;
}

// Maximum frequency of a sampled signal is equal to half of its sample rate.
float SaProcessor::getNyquistFreq() const
{
	return getSampleRate() / 2.0f;
}

// FFTW automatically discards upper half of the symmetric FFT output, so
// the useful bin count is the transform size divided by 2, plus zero.
unsigned int SaProcessor::binCount() const
{
	return m_fftBlockSize / 2 + 1;
}

// 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
// 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;
}

// Return the center frequency of given frequency bin.
float SaProcessor::binToFreq(unsigned int bin_index) const
{
	return getNyquistFreq() * bin_index / binCount();
}

// Return width of the frequency range that falls into one bin.
// The binCount is lowered by one since half of the first and last bin is
// actually outside the frequency range.
float SaProcessor::binBandwidth() const
{
	return getNyquistFreq() / (binCount() - 1);
}

float SaProcessor::getFreqRangeMin(bool linear) const
{
	switch (m_controls->m_freqRangeModel.value())
	{
		case FRANGE_AUDIBLE: return FRANGE_AUDIBLE_START;
		case FRANGE_BASS: return FRANGE_BASS_START;
		case FRANGE_MIDS: return FRANGE_MIDS_START;
		case FRANGE_HIGH: return FRANGE_HIGH_START;
		default:
		case FRANGE_FULL: return linear ? 0 : LOWEST_LOG_FREQ;
	}
}

float SaProcessor::getFreqRangeMax() const
{
	switch (m_controls->m_freqRangeModel.value())
	{
		case FRANGE_AUDIBLE: return FRANGE_AUDIBLE_END;
		case FRANGE_BASS: return FRANGE_BASS_END;
		case FRANGE_MIDS: return FRANGE_MIDS_END;
		case FRANGE_HIGH: return FRANGE_HIGH_END;
		default:
		case FRANGE_FULL: return getNyquistFreq();
	}
}

// Map frequency to pixel x position on a display of given width.
float SaProcessor::freqToXPixel(float freq, unsigned int width) const
{
	if (m_controls->m_logXModel.value())
	{
		if (freq <= 1) {return 0;}
		float min = log10(getFreqRangeMin());
		float range = log10(getFreqRangeMax()) - min;
		return (log10(freq) - min) / range * width;
	}
	else
	{
		float min = getFreqRangeMin();
		float range = getFreqRangeMax() - min;
		return (freq - min) / range * width;
	}
}

// Map pixel x position on display of given width back to frequency.
float SaProcessor::xPixelToFreq(float x, unsigned int width) const
{
	if (m_controls->m_logXModel.value())
	{
		float min = log10(getFreqRangeMin());
		float max = log10(getFreqRangeMax());
		float range = max - min;
		return pow(10, min + x / width * range);
	}
	else
	{
		float min = getFreqRangeMin();
		float range = getFreqRangeMax() - min;
		return min + x / width * range;
	}
}

// --------------------------------------
// Amplitude conversion helpers
//
float SaProcessor::getAmpRangeMin(bool linear) const
{
	// return very low limit to make sure zero gets included at linear grid
	if (linear) {return -900;}
	switch (m_controls->m_ampRangeModel.value())
	{
		case ARANGE_EXTENDED: return ARANGE_EXTENDED_START;
		case ARANGE_SILENT: return ARANGE_SILENT_START;
		case ARANGE_LOUD: return ARANGE_LOUD_START;
		default:
		case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START;
	}
}

float SaProcessor::getAmpRangeMax() const
{
	switch (m_controls->m_ampRangeModel.value())
	{
		case ARANGE_EXTENDED: return ARANGE_EXTENDED_END;
		case ARANGE_SILENT: return ARANGE_SILENT_END;
		case ARANGE_LOUD: return ARANGE_LOUD_END;
		default:
		case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END;
	}
}

// Map linear amplitude to pixel y position on a display of given height.
// Note that display coordinates are flipped: amplitude grows from [height] to zero.
float SaProcessor::ampToYPixel(float amplitude, unsigned int height) const
{
	if (m_controls->m_logYModel.value())
	{
		// logarithmic scale: convert linear amplitude to dB (relative to 1.0)
		float amplitude_dB = 10 * log10(amplitude);
		if (amplitude_dB < getAmpRangeMin())
		{
			return height;
		}
		else
		{
			float max = getAmpRangeMax();
			float range = getAmpRangeMin() - max;
			return (amplitude_dB - max) / range * height;
		}
	}
	else
	{
		// linear scale: convert returned ranges from dB to linear scale
		float max = pow(10, getAmpRangeMax() / 10);
		float range = pow(10, getAmpRangeMin() / 10) - max;
		return (amplitude - max) / range * height;
	}
}

// Map pixel y position on display of given height back to amplitude.
// Note that display coordinates are flipped: amplitude grows from [height] to zero.
// Also note that in logarithmic Y mode the returned amplitude is in dB, not linear.
float SaProcessor::yPixelToAmp(float y, unsigned int height) const
{
	if (m_controls->m_logYModel.value())
	{
		float max = getAmpRangeMax();
		float range = getAmpRangeMin() - max;
		return max + range * (y / height);
	}
	else
	{
		// linear scale: convert returned ranges from dB to linear scale
		float max = pow(10, getAmpRangeMax() / 10);
		float range = pow(10, getAmpRangeMin() / 10) - max;
		return max + range * (y / height);
	}
}


code:
/* SaProcessor.h - declaration of SaProcessor class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * Based partially on Eq plugin code,
 * Copyright (c) 2014 David French <dave/dot/french3/at/googlemail/dot/com>
 *
 * 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 SAPROCESSOR_H
#define SAPROCESSOR_H

#include <atomic>
#include <QColor>
#include <QMutex>
#include <vector>

#include "fft_helpers.h"
#include "SaControls.h"

template<class T>
class LocklessRingBuffer;

//! Receives audio data, runs FFT analysis and stores the result.
class SaProcessor
{
public:
	explicit SaProcessor(const SaControls *controls);
	virtual ~SaProcessor();

	// analysis thread and a method to terminate it
	void analyze(LocklessRingBuffer<sampleFrame> &ring_buffer);
	void terminate() {m_terminate = true;}

	// 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)
	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
	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)
	unsigned int waterfallHeight() const {return m_waterfallHeight;}
	bool waterfallNotEmpty() const {return m_waterfallNotEmpty;}

	float binToFreq(unsigned int bin_index) const;
	float binBandwidth() const;

	float freqToXPixel(float frequency, unsigned int width) const;
	float xPixelToFreq(float x, unsigned int width) const;

	float ampToYPixel(float amplitude, unsigned int height) const;
	float yPixelToAmp(float y, unsigned int height) const;

	unsigned int getSampleRate() const;
	float getNyquistFreq() const;

	float getFreqRangeMin(bool linear = false) const;
	float getFreqRangeMax() const;
	float getAmpRangeMin(bool linear = false) const;
	float getAmpRangeMax() const;

	// 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 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 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;

private:
	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
	std::atomic<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;

	// data buffers (roughly in the order of processing, from input to output)
	unsigned int m_framesFilledUp;
	std::vector<float> m_bufferL;			//!< time domain samples (left)
	std::vector<float> m_bufferR;			//!< time domain samples (right)
	std::vector<float> m_fftWindow;			//!< precomputed window function coefficients
	std::vector<float> m_filteredBufferL;	//!< time domain samples with window function applied (left)
	std::vector<float> 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)
	fftwf_complex *m_spectrumR;				//!< frequency domain samples (complex) (right)
	std::vector<float> m_absSpectrumL;		//!< frequency domain samples (absolute) (left)
	std::vector<float> m_absSpectrumR;		//!< frequency domain samples (absolute) (right)
	std::vector<float> m_normSpectrumL;		//!< frequency domain samples (normalized) (left)
	std::vector<float> m_normSpectrumR;     //!< frequency domain samples (normalized) (right)

	// spectrum history for waterfall: new normSpectrum lines are added on top
	std::vector<uchar> m_history_work;		//!< local history buffer for render
	std::vector<uchar> m_history;			//!< public buffer for reading
	bool m_flipRequest;						//!< update public buffer only when requested
	std::atomic<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;

	// book keeping
	bool m_spectrumActive;
	bool m_waterfallActive;
	std::atomic<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;

	#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


code:
/* SaSpectrumView.cpp - implementation of SaSpectrumView class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * Based partially on Eq plugin code,
 * Copyright (c) 2014-2017, David French <dave/dot/french3/at/googlemail/dot/com>
 *
 * 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 "SaSpectrumView.h"

#include <algorithm>
#include <cmath>
#include <QMouseEvent>
#include <QMutexLocker>
#include <QPainter>
#include <QString>

#include "GuiApplication.h"
#include "MainWindow.h"
#include "SaProcessor.h"

#ifdef SA_DEBUG
	#include <chrono>
#endif

SaSpectrumView::SaSpectrumView(SaControls *controls, SaProcessor *processor, QWidget *_parent) :
	QWidget(_parent),
	m_controls(controls),
	m_processor(processor),
	m_freezeRequest(false),
	m_frozen(false)
{
	setMinimumSize(360, 170);
	setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

	connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));

	m_displayBufferL.resize(m_processor->binCount(), 0);
	m_displayBufferR.resize(m_processor->binCount(), 0);
	m_peakBufferL.resize(m_processor->binCount(), 0);
	m_peakBufferR.resize(m_processor->binCount(), 0);

	m_freqRangeIndex = m_controls->m_freqRangeModel.value();
	m_ampRangeIndex = m_controls->m_ampRangeModel.value();

	m_logFreqTics = makeLogFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax());
	m_linearFreqTics = makeLinearFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax());
	m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
	m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());

	m_cursor = QPointF(0, 0);

	#ifdef SA_DEBUG
		m_execution_avg = m_path_avg = m_draw_avg = 0;
	#endif
}

// Compose and draw all the content; periodically called by Qt.
// NOTE: Performance sensitive! If the drawing takes too long, it will drag
// the FPS down for the entire program! Use SA_DEBUG to display timings.
void SaSpectrumView::paintEvent(QPaintEvent *event)
{
	#ifdef SA_DEBUG
		int total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
	#endif

	// 0) Constants and init
	QPainter painter(this);
	painter.setRenderHint(QPainter::Antialiasing, true);

	// drawing and path-making are split into multiple methods for clarity;
	// display boundaries are updated here and shared as member variables
	m_displayTop = 1;
	m_displayBottom = height() -20;
	m_displayLeft = 26;
	m_displayRight = width() -26;
	m_displayWidth = m_displayRight - m_displayLeft;

	// recompute range labels if needed
	if (m_freqRangeIndex != m_controls->m_freqRangeModel.value())
	{
		m_logFreqTics = makeLogFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax());
		m_linearFreqTics = makeLinearFreqTics(m_processor->getFreqRangeMin(true), m_processor->getFreqRangeMax());
		m_freqRangeIndex = m_controls->m_freqRangeModel.value();
	}
	if (m_ampRangeIndex != m_controls->m_ampRangeModel.value())
	{
		m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
		m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(true), m_processor->getAmpRangeMax());
		m_ampRangeIndex = m_controls->m_ampRangeModel.value();
	}

	// generate freeze request or clear "frozen" status based on freeze button
	if (!m_frozen && m_controls->m_refFreezeModel.value())
	{
		m_freezeRequest = true;
	}
	else if (!m_controls->m_refFreezeModel.value())
	{
		m_frozen = false;
	}

	// 1) Background, grid and labels
	drawGrid(painter);

	// 2) Spectrum display
	drawSpectrum(painter);

	// 3) Overlays
	// draw cursor (if it is within bounds)
	drawCursor(painter);

	// always draw the display outline
	painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
	painter.drawRoundedRect(m_displayLeft, 1,
							m_displayWidth, m_displayBottom,
							2.0, 2.0);

	#ifdef SA_DEBUG
		// 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 -150, 10, 130, 16, Qt::AlignLeft,
						 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).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).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).c_str()).append(" ms"));

	#endif
}

// Refresh data and draw the spectrum.
void SaSpectrumView::drawSpectrum(QPainter &painter)
{
	#ifdef SA_DEBUG
		int draw_time = 0;
	#endif

	// draw the graph only if there is any input, averaging residue or peaks
	if (m_decaySum > 0 || m_processor->spectrumNotEmpty())
	{
		// update data buffers and reconstruct paths
		refreshPaths();

		// draw stored paths
		#ifdef SA_DEBUG
			draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
		#endif
		// in case stereo is disabled, mono data are stored in left channel structures
		if (m_controls->m_stereoModel.value())
		{
			painter.fillPath(m_pathR, QBrush(m_controls->m_colorR));
			painter.fillPath(m_pathL, QBrush(m_controls->m_colorL));
		}
		else
		{
			painter.fillPath(m_pathL, QBrush(m_controls->m_colorMono));
		}
		// draw the peakBuffer only if peak hold or reference freeze is active
		if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value())
		{
			if (m_controls->m_stereoModel.value())
			{
				painter.setPen(QPen(m_controls->m_colorR, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
				painter.drawPath(m_pathPeakR);
				painter.setPen(QPen(m_controls->m_colorL, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
				painter.drawPath(m_pathPeakL);
			}
			else
			{
				painter.setPen(QPen(m_controls->m_colorL, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
				painter.drawPath(m_pathPeakL);
			}
		}
		#ifdef SA_DEBUG
			draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time;
		#endif
	}

	#ifdef SA_DEBUG
		// save performance measurement result
		m_draw_avg = 0.95 * m_draw_avg + 0.05 * draw_time / 1000000.0;
	#endif
}

// Read newest FFT results from SaProcessor, update local display buffers
// and build QPainter paths.
void SaSpectrumView::refreshPaths()
{
	// 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())
	{
		m_displayBufferL.clear();
		m_displayBufferR.clear();
		m_peakBufferL.clear();
		m_peakBufferR.clear();
		m_displayBufferL.resize(m_processor->binCount(), 0);
		m_displayBufferR.resize(m_processor->binCount(), 0);
		m_peakBufferL.resize(m_processor->binCount(), 0);
		m_peakBufferR.resize(m_processor->binCount(), 0);
	}

	// update display buffers for left and right channel
	#ifdef SA_DEBUG
		int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
	#endif
	m_decaySum = 0;
	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

	// if there was a freeze request, it was taken care of during the update
	if (m_controls->m_refFreezeModel.value() && m_freezeRequest)
	{
		m_freezeRequest = false;
		m_frozen = true;
	}

	#ifdef SA_DEBUG
		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
	// the resolution parameter to balance display quality and performance.
	m_pathL = makePath(m_displayBufferL, m_controls->m_spectrumResolutionModel.value());
	if (m_controls->m_stereoModel.value())
	{
		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, m_controls->m_envelopeResolutionModel.value());
		if (m_controls->m_stereoModel.value())
		{
			m_pathPeakR = makePath(m_peakBufferR, m_controls->m_envelopeResolutionModel.value());
		}
	}
	#ifdef SA_DEBUG
		path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time;
	#endif

	#ifdef SA_DEBUG
		// save performance measurement results
		m_refresh_avg = 0.95 * m_refresh_avg + 0.05 * refresh_time / 1000000.0;
		m_path_avg = .95f * m_path_avg + .05f * path_time / 1000000.f;
	#endif
}

// 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
// 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(const float *spectrum, float *displayBuffer, float *peakBuffer)
{
	for (int n = 0; n < m_processor->binCount(); n++)
	{
		// Update the exponential average if enabled, or simply copy the value.
		if (!m_controls->m_pauseModel.value())
		{
			if (m_controls->m_smoothModel.value())
			{
				const float smoothFactor = m_controls->m_averagingWeightModel.value();
				displayBuffer[n] = spectrum[n] * smoothFactor + displayBuffer[n] * (1 - smoothFactor);
			}
			else
			{
				displayBuffer[n] = spectrum[n];
			}
		}
		// Update peak-hold and reference freeze data (using a shared curve).
		// Peak hold and freeze can be combined: decay only if not frozen.
		// Ref. freeze operates on the (possibly averaged) display buffer.
		if (m_controls->m_refFreezeModel.value() && m_freezeRequest)
		{
			peakBuffer[n] = displayBuffer[n];
		}
		else if (m_controls->m_peakHoldModel.value() && !m_controls->m_pauseModel.value())
		{
			if (spectrum[n] > peakBuffer[n])
			{
				peakBuffer[n] = spectrum[n];
			}
			else if (!m_controls->m_refFreezeModel.value())
			{
				peakBuffer[n] = peakBuffer[n] * m_controls->m_peakDecayFactorModel.value();
			}
		}
		else if (!m_controls->m_refFreezeModel.value() && !m_controls->m_peakHoldModel.value())
		{
			peakBuffer[n] = 0;
		}
		// take note if there was actually anything to display
		m_decaySum += displayBuffer[n] + peakBuffer[n];
	}
}

// Use display buffer to build a path that can be drawn or filled by QPainter.
// Resolution controls the performance / quality tradeoff; the value specifies
// number of points in x axis per device pixel. Values over 1.0 still
// contribute to quality and accuracy thanks to anti-aliasing.
QPainterPath SaSpectrumView::makePath(std::vector<float> &displayBuffer, float resolution = 1.0)
{
	// convert resolution to number of path points per logical pixel
	float pixel_limit = resolution * window()->devicePixelRatio();

	QPainterPath path;
	path.moveTo(m_displayLeft, m_displayBottom);

	// Translate frequency bins to path points.
	// Display is flipped: y values grow towards zero, initial max is bottom.
	// Bins falling to interval [x_start, x_next) contribute to a single point.
	float max = m_displayBottom;
	float x_start = -1;		// lower bound of currently constructed point
	for (unsigned int n = 0; n < m_processor->binCount(); n++)
	{
		float x = freqToXPixel(binToFreq(n), m_displayWidth);
		float x_next = freqToXPixel(binToFreq(n + 1), m_displayWidth);
		float y = ampToYPixel(displayBuffer[n], m_displayBottom);

		// consider making a point only if x falls within display bounds
		if (0 < x && x < m_displayWidth)
		{
			if (x_start == -1)
			{
				x_start = x;
				// the first displayed bin is stretched to the left edge to prevent
				// creating a misleading slope leading to zero (at log. scale)
				path.lineTo(m_displayLeft, y + m_displayTop);
			}
			// Opt.: QPainter is very slow -- draw at most [pixel_limit] points
			// per logical pixel. As opposed to limiting the bin count, this
			// allows high resolution display if user resizes the analyzer.
			// Look at bins that share the pixel and use the highest value:
			max = y < max ? y : max;
			// And make the final point in the middle of current interval.
			if ((int)(x * pixel_limit) != (int)(x_next * pixel_limit))
			{
				x = (x + x_start) / 2;
				path.lineTo(x + m_displayLeft, max + m_displayTop);
				max = m_displayBottom;
				x_start = x_next;
			}
		}
		else
		{
			// stop processing after a bin falls outside right edge
			// and align it to the edge to prevent a gap
			if (n > 0 && x > 0)
			{
				path.lineTo(m_displayRight,	y + m_displayTop);
				break;
			}
		}
	}
	path.lineTo(m_displayRight, m_displayBottom);
	path.closeSubpath();
	return path;
}

// Draw background, grid and associated frequency and amplitude labels.
void SaSpectrumView::drawGrid(QPainter &painter)
{
	std::vector<std::pair<int, std::string>> *freqTics = NULL;
	std::vector<std::pair<float, std::string>> *ampTics = NULL;
	float pos = 0;
	float label_width = 24;
	float label_height = 15;
	float margin = 5;

	// always draw the background
	painter.fillRect(m_displayLeft, m_displayTop,
					 m_displayWidth, m_displayBottom,
					 m_controls->m_colorBG);

	// select logarithmic or linear frequency grid and draw it
	if (m_controls->m_logXModel.value())
	{
		freqTics = &m_logFreqTics;
	}
	else
	{
		freqTics = &m_linearFreqTics;
	}
	// draw frequency grid (line.first is display position)
	painter.setPen(QPen(m_controls->m_colorGrid, 1,	Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
	for (auto &line: *freqTics)
	{
		painter.drawLine(m_displayLeft + freqToXPixel(line.first, m_displayWidth),
						 2,
						 m_displayLeft + freqToXPixel(line.first, m_displayWidth),
						 m_displayBottom);
	}
	// print frequency labels (line.second is label)
	painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
	for (auto & line: *freqTics)
	{
		pos = m_displayLeft + freqToXPixel(line.first, m_displayWidth);
		// align first and last label to the edge if needed, otherwise center them
		if (line == freqTics->front() && pos - label_width / 2 < m_displayLeft)
		{
			painter.drawText(m_displayLeft, m_displayBottom + margin,
							 label_width, label_height, Qt::AlignLeft | Qt::TextDontClip,
							 QString(line.second.c_str()));
		}
		else if (line == freqTics->back() && pos + label_width / 2 > m_displayRight)
		{
			painter.drawText(m_displayRight - label_width, m_displayBottom + margin,
							 label_width, label_height, Qt::AlignRight | Qt::TextDontClip,
							 QString(line.second.c_str()));
		}
		else
		{
			painter.drawText(pos - label_width / 2, m_displayBottom + margin,
							 label_width, label_height, Qt::AlignHCenter | Qt::TextDontClip,
							 QString(line.second.c_str()));
		}
	}

	margin = 2;
	// select logarithmic or linear amplitude grid and draw it
	if (m_controls->m_logYModel.value())
	{
		ampTics = &m_logAmpTics;
	}
	else
	{
		ampTics = &m_linearAmpTics;
	}
	// draw amplitude grid
	painter.setPen(QPen(m_controls->m_colorGrid, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
	for (auto & line: *ampTics)
	{
		painter.drawLine(m_displayLeft + 1,
						 ampToYPixel(line.first, m_displayBottom),
						 m_displayRight - 1,
						 ampToYPixel(line.first, m_displayBottom));
	}
	// print amplitude labels
	painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
	bool stereo = m_controls->m_stereoModel.value();
	for (auto & line: *ampTics)
	{
		pos = ampToYPixel(line.first, m_displayBottom);
		// align first and last labels to edge if needed, otherwise center them
		if (line == ampTics->back() && pos < 8)
		{
			if (stereo)
			{
				painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
			}
			painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 2,
							 label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip,
							 QString(line.second.c_str()));
			if (stereo)
			{
				painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
			}
			painter.drawText(m_displayRight + margin, m_displayTop - 2,
							 label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
							 QString(line.second.c_str()));
		}
		else if (line == ampTics->front() && pos > m_displayBottom - label_height)
		{
			if (stereo)
			{
				painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
			}
			painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height + 2,
							 label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip,
							 QString(line.second.c_str()));
			if (stereo)
			{
				painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
			}
			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
		{
			if (stereo)
			{
				painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
			}
			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()));
			if (stereo)
			{
				painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
			}
			painter.drawText(m_displayRight + margin, pos - label_height / 2,
							 label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip,
							 QString(line.second.c_str()));
		}
	}
}

// Draw cursor and its coordinates if it is within display bounds.
void SaSpectrumView::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(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();
		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 + 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));
		QString tmps;

		// frequency
		int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth);
		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);

		// amplitude
		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).c_str()).append(" dBFS");
		}
		else
		{
			// add 0.0005 to get proper rounding to 3 decimal places
			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,
						 box_width, box_height / 2, Qt::AlignLeft, tmps);
	}
}

// Wrappers for most used SaProcessor helpers (to make local code more compact).
float SaSpectrumView::binToFreq(unsigned int bin_index)
{
	return m_processor->binToFreq(bin_index);
}

float SaSpectrumView::freqToXPixel(float frequency, unsigned int width)
{
	return m_processor->freqToXPixel(frequency, width);
}

float SaSpectrumView::ampToYPixel(float amplitude, unsigned int height)
{
	return m_processor->ampToYPixel(amplitude, height);
}

// Generate labels suitable for logarithmic frequency scale.
// Low / high limits are in Hz. Lowest possible label is 10 Hz.
std::vector<std::pair<int, std::string>> SaSpectrumView::makeLogFreqTics(int low, int high)
{
	std::vector<std::pair<int, std::string>> result;
	int i, j;
	int a[] = {10, 20, 50};		// sparse series multipliers
	int b[] = {14, 30, 70};		// additional (denser) series

	// generate main steps (powers of 10); use the series to specify smaller steps
	for (i = 1; i <= high; i *= 10)
	{
		for (j = 0; j < 3; j++)
		{
			// insert a label from sparse series if it falls within bounds
			if (i * a[j] >= low && i * a[j] <= high)
			{
				if (i * a[j] < 1000)
				{
					result.emplace_back(i * a[j], std::to_string(i * a[j]));
				}
				else
				{
					result.emplace_back(i * a[j], std::to_string(i * a[j] / 1000) + "k");
				}
			}
			// also insert denser series if high and low values are close
			if ((log10(high) - log10(low) < 2) && (i * b[j] >= low && i * b[j] <= high))
			{
				if (i * b[j] < 1500)
				{
					result.emplace_back(i * b[j], std::to_string(i * b[j]));
				}
				else
				{
					result.emplace_back(i * b[j], std::to_string(i * b[j] / 1000) + "k");
				}
			}
		}
	}
	return result;
}

// Generate labels suitable for linear frequency scale.
// Low / high limits are in Hz.
std::vector<std::pair<int, std::string>> SaSpectrumView::makeLinearFreqTics(int low, int high)
{
	std::vector<std::pair<int, std::string>> result;
	int i, increment;

	// select a suitable increment based on zoom level
	if (high - low < 500) {increment = 50;}
	else if (high - low < 1000) {increment = 100;}
	else if (high - low < 5000) {increment = 1000;}
	else {increment = 2000;}

	// generate steps based on increment, starting at 0
	for (i = 0; i <= high; i += increment)
	{
		if (i >= low)
		{
			if (i < 1000)
			{
				result.emplace_back(i, std::to_string(i));
			}
			else
			{
				result.emplace_back(i, std::to_string(i/1000) + "k");
			}
		}
	}
	return result;
}

// Generate labels suitable for logarithmic (dB) amplitude scale.
// Low / high limits are in dB; 0 dB amplitude = 1.0 linear.
// Treating results as power ratio, i.e., 3 dB should be about twice as loud.
std::vector<std::pair<float, std::string>> SaSpectrumView::makeLogAmpTics(int low, int high)
{
	std::vector<std::pair<float, std::string>> result;
	float i;
	double increment;

	// Base zoom level on selected range and how close is the current height
	// to the sizeHint() (denser scale for bigger window).
	if ((high - low) < 20 * ((float)height() / sizeHint().height()))
	{
		increment = pow(10, 0.3);	// 3 dB steps when really zoomed in
	}
	else if (high - low < 45 * ((float)height() / sizeHint().height()))
	{
		increment = pow(10, 0.6);	// 6 dB steps when sufficiently zoomed in
	}
	else
	{
		increment = 10;				// 10 dB steps otherwise
	}

	// Generate n dB increments, start checking at -90 dB. Limits are tweaked
	// just a little bit to make sure float comparisons do not miss edges.
	for (i = 0.000000001; 10 * log10(i) <= (high + 0.001); i *= increment)
	{
		if (10 * log10(i) >= (low - 0.001))
		{
			result.emplace_back(i, std::to_string((int)std::round(10 * log10(i))));
		}
	}
	return result;
}

// Generate labels suitable for linear amplitude scale.
// Low / high limits are in dB; 0 dB amplitude = 1.0 linear.
// Smallest possible label is 0.001, largest is 999. This includes the majority
// of useful labels; going lower or higher would require increasing margin size
// so that the text can fit. That would be a waste of space -- the linear scale
// would only make the experience worse for the main, logarithmic (dB) scale.
std::vector<std::pair<float, std::string>> SaSpectrumView::makeLinearAmpTics(int low, int high)
{
	std::vector<std::pair<float, std::string>> result;
	double i, nearest;

	// make about 5 labels when window is small, 10 if it is big
	float split = (float)height() / sizeHint().height() >= 1.5 ? 10.0 : 5.0;

	// convert limits to linear scale
	float lin_low = pow(10, low / 10.0);
	float lin_high = pow(10, high / 10.0);

	// Linear scale will vary widely, so instead of trying to craft extra nice
	// multiples, just generate a few evenly spaced increments across the range,
	// paying attention only to the decimal places to keep labels short.
	// Limits are shifted a bit so that float comparisons do not miss edges.
	for (i = 0; i <= (lin_high + 0.0001); i += (lin_high - lin_low) / split)
	{
		if (i >= (lin_low - 0.0001))
		{
			if (i >= 9.99 && i < 99.9)
			{
				nearest = std::round(i);
				result.emplace_back(nearest, std::to_string(nearest).substr(0, 2));
			}
			else if (i >= 0.099)
			{	// also covers numbers above 100
				nearest = std::round(i * 10) / 10;
				result.emplace_back(nearest, std::to_string(nearest).substr(0, 3));
			}
			else if (i >= 0.0099)
			{
				nearest = std::round(i * 1000) / 1000;
				result.emplace_back(nearest, std::to_string(nearest).substr(0, 4));
			}
			else if	(i >= 0.00099)
			{
				nearest = std::round(i * 10000) / 10000;
				result.emplace_back(nearest, std::to_string(nearest).substr(1, 4));
			}
			else if (i > -0.01 && i < 0.01)
			{
				result.emplace_back(i, "0");	// an exception, zero is short..
			}
		}
	}
	return result;
}

// Periodic update is called by LMMS.
void SaSpectrumView::periodicUpdate()
{
	// check if the widget is visible; if it is not, processing can be paused
	m_processor->setSpectrumActive(isVisible());
	// tell Qt it is time for repaint
	update();
}

// 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 = 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 = QPointF(	event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
						event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}

// Handle resize event: rebuild grid and labels
void SaSpectrumView::resizeEvent(QResizeEvent *event)
{
	// frequency does not change density with size
	// amplitude does: rebuild labels
	m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
	m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
}


code:
/* SaSpectrumView.h - declaration of SaSpectrumView class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * Based partially on Eq plugin code,
 * Copyright (c) 2014 David French <dave/dot/french3/at/googlemail/dot/com>
 *
 * 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 SASPECTRUMVIEW_H
#define SASPECTRUMVIEW_H

#include "SaControls.h"

#include <string>
#include <utility>
#include <QPainterPath>
#include <QWidget>

class QMouseEvent;
class QPainter;
class SaProcessor;

//! Widget that displays a spectrum curve and frequency / amplitude grid
class SaSpectrumView : public QWidget
{
	Q_OBJECT
public:
	explicit SaSpectrumView(SaControls *controls, SaProcessor *processor, QWidget *_parent = 0);
	virtual ~SaSpectrumView() {}

	QSize sizeHint() const override {return QSize(400, 200);}

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();

private:
	const SaControls *m_controls;
	SaProcessor *m_processor;

	// grid labels (position, label) and methods to generate them
	std::vector<std::pair<int, std::string>> m_logFreqTics;		// 10-20-50... Hz
	std::vector<std::pair<int, std::string>> m_linearFreqTics;	// 2k-4k-6k... Hz
	std::vector<std::pair<float, std::string>> m_logAmpTics;	// dB
	std::vector<std::pair<float, std::string>> m_linearAmpTics;	// 0..1

	std::vector<std::pair<int, std::string>> makeLogFreqTics(int low, int high);
	std::vector<std::pair<int, std::string>> makeLinearFreqTics(int low, int high);
	std::vector<std::pair<float, std::string>> makeLogAmpTics(int low, int high);
	std::vector<std::pair<float, std::string>> makeLinearAmpTics(int low, int high);

	// currently selected ranges (see SaControls.h for enum definitions)
	int m_freqRangeIndex;
	int m_ampRangeIndex;

	// draw the grid and all labels based on selected ranges
	void drawGrid(QPainter &painter);

	// local buffers for frequency bin values and a method to update them
	// (mainly needed for averaging and to keep track of peak values)
	std::vector<float> m_displayBufferL;
	std::vector<float> m_displayBufferR;
	std::vector<float> m_peakBufferL;
	std::vector<float> m_peakBufferR;
	void updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer);

	// final paths to be drawn by QPainter and methods to build them
	QPainterPath m_pathL;
	QPainterPath m_pathR;
	QPainterPath m_pathPeakL;
	QPainterPath m_pathPeakR;
	void refreshPaths();
	QPainterPath makePath(std::vector<float> &displayBuffer, float resolution);

	// helper variables for path drawing
	float m_decaySum;		// indicates if there is anything left to draw
	bool m_freezeRequest;	// new reference should be acquired
	bool m_frozen;			// a reference is currently stored in the peakBuffer

	// top level: refresh buffers, make paths and draw the spectrum
	void drawSpectrum(QPainter &painter);

	// current cursor location and a method to draw it
	QPointF m_cursor;
	void drawCursor(QPainter &painter);

	// wrappers for most used SaProcessor conversion helpers
	// (to make local code more readable)
	float binToFreq(unsigned int bin_index);
	float freqToXPixel(float frequency, unsigned int width);
	float ampToYPixel(float amplitude, unsigned int height);

	// current boundaries for drawing
	unsigned int m_displayTop;
	unsigned int m_displayBottom;
	unsigned int m_displayLeft;
	unsigned int m_displayRight;
	unsigned int m_displayWidth;

	#ifdef SA_DEBUG
		float m_execution_avg;
		float m_refresh_avg;
		float m_path_avg;
		float m_draw_avg;
	#endif
};
#endif // SASPECTRUMVIEW_H


code:
/* SaWaterfallViewView.cpp - implementation of SaWaterfallViewView class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * 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 "SaWaterfallView.h"

#include <algorithm>
#ifdef SA_DEBUG
	#include <chrono>
#endif
#include <cmath>
#include <QImage>
#include <QMouseEvent>
#include <QMutexLocker>
#include <QPainter>
#include <QSplitter>
#include <QString>

#include "EffectControlDialog.h"
#include "GuiApplication.h"
#include "MainWindow.h"
#include "SaProcessor.h"

SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, QWidget *_parent) :
	QWidget(_parent),
	m_controls(controls),
	m_processor(processor)
{
	m_controlDialog = (EffectControlDialog*) _parent;
	setMinimumSize(300, 150);
	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

	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;

	m_cursor = QPointF(0, 0);

	#ifdef SA_DEBUG
		m_execution_avg = 0;
	#endif
}

// Compose and draw all the content; called by Qt.
// Not as performance sensitive as SaSpectrumView, most of the processing is
// done directly in SaProcessor.
void SaWaterfallView::paintEvent(QPaintEvent *event)
{
	#ifdef SA_DEBUG
		unsigned int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
	#endif

	// update boundary
	m_displayBottom = height() -2;
	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;

	QPainter painter(this);
	painter.setRenderHint(QPainter::Antialiasing, true);

	// check if time labels need to be rebuilt
	if (secondsPerLine() != m_oldSecondsPerLine || m_processor->waterfallHeight() != m_oldHeight)
	{
		m_timeTics = makeTimeTics();
		m_oldSecondsPerLine = secondsPerLine();
		m_oldHeight = m_processor->waterfallHeight();
	}

	// print time labels
	float pos = 0;
	painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
	for (auto & line: m_timeTics)
	{
		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(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(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 > m_displayBottom - label_height + 2)
		{
			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(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(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(m_displayRight + margin, pos - label_height / 2,
							 label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip,
							 QString(line.second.c_str()));
		}
	}

	// draw the spectrogram precomputed in SaProcessor
	if (m_processor->waterfallNotEmpty())
	{
		QMutexLocker lock(&m_processor->m_reallocationAccess);
		QImage temp = QImage(m_processor->getHistory(),			// raw pixel data to display
							 m_processor->waterfallWidth(),		// width = number of frequency bins
							 m_processor->waterfallHeight(),	// height = number of history lines
							 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));
		m_processor->flipRequest();
	}
	else
	{
		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(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, 2.0, 2.0);

	#ifdef SA_DEBUG
		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 -150, 10, 100, 16, Qt::AlignLeft,
						 QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms"));
	#endif
}

// Helper functions for time conversion
float SaWaterfallView::samplesPerLine()
{
	return (float)m_processor->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->waterfallHeight();

	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)
{
	if (height == 0) {height = 1;}
	float pixels_per_line = (float)height / m_processor->waterfallHeight();

	return (position / pixels_per_line) * secondsPerLine();
}

// Generate labels for linear time scale.
std::vector<std::pair<float, std::string>> SaWaterfallView::makeTimeTics()
{
	std::vector<std::pair<float, std::string>> result;
	float i;

	// get time value of the last line
	float limit = yPixelToTime(m_displayBottom, m_displayHeight);

	// 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 > 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));
		}
		else
		{
			result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 2));
		}
	}
	return result;
}

// Periodically trigger repaint and check if the widget is visible.
// If it is not, stop drawing and inform the processor.
void SaWaterfallView::periodicUpdate()
{
	m_processor->setWaterfallActive(isVisible());
	if (isVisible()) {update();}
}

// Adjust window size and widget visibility when waterfall is enabled or disabbled.
void SaWaterfallView::updateVisibility()
{
	// get container of the control dialog to be resized if needed
	QWidget *subWindow = m_controlDialog->parentWidget();

	if (m_controls->m_waterfallModel.value())
	{
		// clear old data before showing the waterfall
		m_processor->clearHistory();
		setVisible(true);

		// increase window size if it is too small
		if (subWindow->size().height() < m_controlDialog->sizeHint().height())
		{
			subWindow->resize(subWindow->size().width(), m_controlDialog->sizeHint().height());
		}
	}
	else
	{
		setVisible(false);
		// decrease window size only if it does not violate sizeHint
		subWindow->resize(subWindow->size().width(), m_controlDialog->sizeHint().height());
	}
}

// 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(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();
		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 + 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));
		QString tmps;

		// frequency
		int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth);
		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::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);
	}
}

// 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 = 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 = QPointF(	event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
						event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}

// Handle resize event: rebuild time labels
void SaWaterfallView::resizeEvent(QResizeEvent *event)
{
	m_timeTics = makeTimeTics();
}


code:
/* SaWaterfallView.h - declaration of SaWaterfallView class.
 *
 * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
 *
 * 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 SAWATERFALLVIEW_H
#define SAWATERFALLVIEW_H

#include <string>
#include <utility>
#include <vector>
#include <QPainter>
#include <QWidget>

#include "SaControls.h"
#include "SaProcessor.h"

class QMouseEvent;

// Widget that displays a spectrum waterfall (spectrogram) and time labels.
class SaWaterfallView : public QWidget
{
	Q_OBJECT
public:
	explicit SaWaterfallView(SaControls *controls, SaProcessor *processor, QWidget *_parent = 0);
	virtual ~SaWaterfallView() {}

	QSize sizeHint() const override {return QSize(400, 350);}

	// Check if waterfall should be displayed and adjust window size if needed.
	void updateVisibility();

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();

private:
	const SaControls *m_controls;
	SaProcessor *m_processor;
	const EffectControlDialog *m_controlDialog;

	// Methods and data used to make time labels
	float m_oldSecondsPerLine;
	float m_oldHeight;
	float samplesPerLine();
	float secondsPerLine();
	float timeToYPixel(float time, int height);
	float yPixelToTime(float position, int height);
	std::vector<std::pair<float, std::string>> makeTimeTics();
	std::vector<std::pair<float, std::string>> 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