diff --git a/include/EffectControlDialog.h b/include/EffectControlDialog.h index 712f3121792..c0a60cfa65a 100644 --- a/include/EffectControlDialog.h +++ b/include/EffectControlDialog.h @@ -40,6 +40,8 @@ class LMMS_EXPORT EffectControlDialog : public QWidget, public ModelView EffectControlDialog( EffectControls * _controls ); virtual ~EffectControlDialog(); + virtual bool isResizable() const {return false;} + signals: void closed(); diff --git a/include/SubWindow.h b/include/SubWindow.h index f6247d06169..5d7a810c291 100644 --- a/include/SubWindow.h +++ b/include/SubWindow.h @@ -30,7 +30,6 @@ #include #include #include -#include #include #include diff --git a/include/fft_helpers.h b/include/fft_helpers.h index 23450d2ca5a..876510f8bbf 100644 --- a/include/fft_helpers.h +++ b/include/fft_helpers.h @@ -2,6 +2,7 @@ * fft_helpers.h - some functions around FFT analysis * * Copyright (c) 2008-2012 Tobias Doerffel + * Copyright (c) 2019 Martin Pavelek * * This file is part of LMMS - https://lmms.io * @@ -28,57 +29,90 @@ #include "lmms_export.h" +#include #include -const int FFT_BUFFER_SIZE = 2048; +// NOTE: FFT_BUFFER_SIZE should be considered deprecated! +// It is used by Eq plugin and some older code here, but this should be a user +// switchable parameter, not a constant. Use a value from FFT_BLOCK_SIZES +const unsigned int FFT_BUFFER_SIZE = 2048; -enum WINDOWS +// Allowed FFT block sizes. Ranging from barely useful to barely acceptable +// because of performance and latency reasons. +const std::vector FFT_BLOCK_SIZES = {256, 512, 1024, 2048, 4096, 8192, 16384}; + +// List of FFT window functions supported by precomputeWindow() +enum FFT_WINDOWS { - KAISER=1, - RECTANGLE, - HANNING, - HAMMING + RECTANGULAR = 0, + BLACKMAN_HARRIS, + HAMMING, + HANNING }; -/* returns biggest value from abs_spectrum[spec_size] array + +/** Returns biggest value from abs_spectrum[spec_size] array. + * + * @return -1 on error, 0 on success + */ +float LMMS_EXPORT maximum(const float *abs_spectrum, unsigned int spec_size); +float LMMS_EXPORT maximum(const std::vector &abs_spectrum); + + +/** Normalize the abs_spectrum array of absolute values to a 0..1 range + * based on supplied energy and stores it in the norm_spectrum array. + * + * @return -1 on error + */ +int LMMS_EXPORT normalize(const float *abs_spectrum, float *norm_spectrum, unsigned int bin_count, unsigned int block_size); +int LMMS_EXPORT normalize(const std::vector &abs_spectrum, std::vector &norm_spectrum, unsigned int block_size); + + +/** Check if the spectrum contains any non-zero value. * - * returns -1 on error + * @return 1 if spectrum contains any non-zero value + * @return 0 otherwise */ -float LMMS_EXPORT maximum( float * _abs_spectrum, unsigned int _spec_size ); +int LMMS_EXPORT notEmpty(const std::vector &spectrum); + -/* apply hanning or hamming window to channel +/** Precompute a window function for later real-time use. + * Set normalized to false if you do not want to apply amplitude correction. * - * returns -1 on error + * @return -1 on error */ -int LMMS_EXPORT hanming( float * _timebuffer, int _length, WINDOWS _type ); +int LMMS_EXPORT precomputeWindow(float *window, unsigned int length, FFT_WINDOWS type, bool normalized = true); -/* compute absolute values of complex_buffer, save to absspec_buffer - * take care that - compl_len is not bigger than complex_buffer! - * - absspec buffer is big enough! + +/** Compute absolute values of complex_buffer, save to absspec_buffer. + * Take care that - compl_len is not bigger than complex_buffer! + * - absspec buffer is big enough! * - * returns 0 on success, else -1 + * @return 0 on success, else -1 */ -int LMMS_EXPORT absspec( fftwf_complex * _complex_buffer, float * _absspec_buffer, - int _compl_length ); +int LMMS_EXPORT absspec(const fftwf_complex *complex_buffer, float *absspec_buffer, + unsigned int compl_length); + -/* build fewer subbands from many absolute spectrum values - * take care that - compressedbands[] array num_new elements long - * - num_old > num_new +/** Build fewer subbands from many absolute spectrum values. + * Take care that - compressedbands[] array num_new elements long + * - num_old > num_new * - * returns 0 on success, else -1 + * @return 0 on success, else -1 */ -int LMMS_EXPORT compressbands( float * _absspec_buffer, float * _compressedband, - int _num_old, int _num_new, int _bottom, int _top ); +int LMMS_EXPORT compressbands(const float * _absspec_buffer, float * _compressedband, + int _num_old, int _num_new, int _bottom, int _top); + +int LMMS_EXPORT calc13octaveband31(float * _absspec_buffer, float * _subbands, + int _num_spec, float _max_frequency); -int LMMS_EXPORT calc13octaveband31( float * _absspec_buffer, float * _subbands, - int _num_spec, float _max_frequency ); -/* compute power of finite time sequence - * take care num_values is length of timesignal[] +/** Compute power of finite time sequence. + * Take care num_values is length of timesignal[]. * - * returns power on success, else -1 + * @return power on success, else -1 */ -float LMMS_EXPORT signalpower(float *timesignal, int num_values); +float LMMS_EXPORT signalpower(const float *timesignal, int num_values); #endif diff --git a/plugins/SpectrumAnalyzer/Analyzer.cpp b/plugins/SpectrumAnalyzer/Analyzer.cpp new file mode 100644 index 00000000000..9c3fe0814ca --- /dev/null +++ b/plugins/SpectrumAnalyzer/Analyzer.cpp @@ -0,0 +1,75 @@ +/* + * Analyzer.cpp - definition of Analyzer class. + * + * Copyright (c) 2019 Martin Pavelek + * + * Based partially on Eq plugin code, + * Copyright (c) 2014-2017, David French + * + * 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 "Analyzer.h" + +#include "embed.h" +#include "plugin_export.h" + + +extern "C" { + Plugin::Descriptor PLUGIN_EXPORT analyzer_plugin_descriptor = + { + "spectrumanalyzer", + "Spectrum Analyzer", + QT_TRANSLATE_NOOP("pluginBrowser", "A graphical spectrum analyzer."), + "Martin Pavelek ", + 0x0100, + Plugin::Effect, + new PluginPixmapLoader("logo"), + NULL, + NULL + }; +} + + +Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::Key *key) : + Effect(&analyzer_plugin_descriptor, parent, key), + m_processor(&m_controls), + m_controls(this) +{ +} + + +// Take audio data and pass them to the spectrum processor. +// Skip processing if the controls dialog isn't visible, it would only waste CPU cycles. +bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) +{ + if (!isEnabled() || !isRunning ()) {return false;} + if (m_controls.isViewVisible()) {m_processor.analyse(buffer, frame_count);} + return isRunning(); +} + + +extern "C" { + // needed for getting plugin out of shared lib + PLUGIN_EXPORT Plugin *lmms_plugin_main(Model *parent, void *data) + { + return new Analyzer(parent, static_cast(data)); + } +} + diff --git a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControlDialog.h b/plugins/SpectrumAnalyzer/Analyzer.h similarity index 50% rename from plugins/SpectrumAnalyzer/SpectrumAnalyzerControlDialog.h rename to plugins/SpectrumAnalyzer/Analyzer.h index 17e2f772489..157cc1eae20 100644 --- a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControlDialog.h +++ b/plugins/SpectrumAnalyzer/Analyzer.h @@ -1,7 +1,9 @@ -/* - * SpectrumAnalyzerControlDialog.h - view for spectrum analyzer +/* Analyzer.h - declaration of Analyzer class. * - * Copyright (c) 2008-2014 Tobias Doerffel + * Copyright (c) 2019 Martin Pavelek + * + * Based partially on Eq plugin code, + * Copyright (c) 2014-2017, David French * * This file is part of LMMS - https://lmms.io * @@ -22,32 +24,30 @@ * */ -#ifndef _SPECTRUM_ANALYZER_CONTROL_DIALOG_H -#define _SPECTRUM_ANALYZER_CONTROL_DIALOG_H - -#include "EffectControlDialog.h" - +#ifndef ANALYZER_H +#define ANALYZER_H -class SpectrumAnalyzerControls; +#include "Effect.h" +#include "SaControls.h" +#include "SaProcessor.h" -class SpectrumAnalyzerControlDialog : public EffectControlDialog +//! Top level class; handles LMMS interface and feeds data to the data processor. +class Analyzer : public Effect { - Q_OBJECT public: - SpectrumAnalyzerControlDialog( SpectrumAnalyzerControls* controls ); - virtual ~SpectrumAnalyzerControlDialog() - { - } + Analyzer(Model *parent, const Descriptor::SubPluginFeatures::Key *key); + virtual ~Analyzer() {}; -private: - virtual void paintEvent( QPaintEvent* event ); + bool processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) override; + EffectControls *controls() override {return &m_controls;} - SpectrumAnalyzerControls* m_controls; + SaProcessor *getProcessor() {return &m_processor;} - QPixmap m_logXAxis; - QPixmap m_logYAxis; +private: + SaProcessor m_processor; + SaControls m_controls; +}; -} ; +#endif // ANALYZER_H -#endif diff --git a/plugins/SpectrumAnalyzer/CMakeLists.txt b/plugins/SpectrumAnalyzer/CMakeLists.txt index 29187b39d99..630fbf1be01 100644 --- a/plugins/SpectrumAnalyzer/CMakeLists.txt +++ b/plugins/SpectrumAnalyzer/CMakeLists.txt @@ -1,4 +1,5 @@ INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) LINK_LIBRARIES(${FFTW3F_LIBRARIES}) -BUILD_PLUGIN(spectrumanalyzer SpectrumAnalyzer.cpp SpectrumAnalyzerControls.cpp SpectrumAnalyzerControlDialog.cpp SpectrumAnalyzer.h SpectrumAnalyzerControls.h SpectrumAnalyzerControlDialog.h MOCFILES SpectrumAnalyzerControlDialog.h SpectrumAnalyzerControls.h EMBEDDED_RESOURCES *.png) +BUILD_PLUGIN(analyzer Analyzer.cpp SaProcessor.cpp SaControls.cpp SaControlsDialog.cpp SaSpectrumView.cpp SaWaterfallView.cpp +MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h EMBEDDED_RESOURCES *.svg logo.png) diff --git a/plugins/SpectrumAnalyzer/README.md b/plugins/SpectrumAnalyzer/README.md new file mode 100644 index 00000000000..3d3506d6540 --- /dev/null +++ b/plugins/SpectrumAnalyzer/README.md @@ -0,0 +1,19 @@ +# Spectrum Analyzer plugin + +## Overview + +This plugin consists of three widgets and back-end code to provide them with required data. + +The top-level widget is SaControlDialog. It populates a configuration widget (created dynamically) and instantiates spectrum display widgets. Its main back-end class is SaControls, which holds all configuration values and globally valid constants (e.g. range definitions). + +SaSpectrumDisplay and SaWaterfallDisplay show the result of spectrum analysis. Their main back-end class is SaProcessor, which performs FFT analysis on data received from the Analyzer class, which in turn handles the interface with LMMS. + + +## Changelog + + 1.0.1 2019-06-02 + - code style changes + - added tool-tips + - use const for unmodified arrays passed to fft_helpers + 1.0.0 2019-04-07 + - initial release diff --git a/plugins/SpectrumAnalyzer/SaControls.cpp b/plugins/SpectrumAnalyzer/SaControls.cpp new file mode 100644 index 00000000000..5691c0ae44a --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaControls.cpp @@ -0,0 +1,144 @@ +/* + * SaControls.cpp - definition of SaControls class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SaControls.h" + +#include + +#include "Analyzer.h" +#include "SaControlsDialog.h" + + +SaControls::SaControls(Analyzer *effect) : + EffectControls(effect), + m_effect(effect), + + // initialize bool models and set default values + m_pauseModel(false, this, tr("Pause")), + m_refFreezeModel(false, this, tr("Reference freeze")), + + m_waterfallModel(false, this, tr("Waterfall")), + m_smoothModel(false, this, tr("Averaging")), + m_stereoModel(false, this, tr("Stereo")), + m_peakHoldModel(false, this, tr("Peak hold")), + + m_logXModel(true, this, tr("Logarithmic frequency")), + m_logYModel(true, this, tr("Logarithmic amplitude")), + + // default values of combo boxes are set after they are populated + m_freqRangeModel(this, tr("Frequency range")), + m_ampRangeModel(this, tr("Amplitude range")), + m_blockSizeModel(this, tr("FFT block size")), + m_windowModel(this, tr("FFT window type")) +{ + // Frequency and amplitude ranges; order must match + // FREQUENCY_RANGES and AMPLITUDE_RANGES defined in SaControls.h + m_freqRangeModel.addItem(tr("Full (auto)")); + m_freqRangeModel.addItem(tr("Audible")); + m_freqRangeModel.addItem(tr("Bass")); + m_freqRangeModel.addItem(tr("Mids")); + m_freqRangeModel.addItem(tr("High")); + m_freqRangeModel.setValue(m_freqRangeModel.findText(tr("Full (auto)"))); + + m_ampRangeModel.addItem(tr("Extended")); + m_ampRangeModel.addItem(tr("Default")); + m_ampRangeModel.addItem(tr("Audible")); + m_ampRangeModel.addItem(tr("Noise")); + m_ampRangeModel.setValue(m_ampRangeModel.findText(tr("Default"))); + + // FFT block size labels are generated automatically, based on + // FFT_BLOCK_SIZES vector defined in fft_helpers.h + for (unsigned int i = 0; i < FFT_BLOCK_SIZES.size(); i++) + { + if (i == 0) + { + m_blockSizeModel.addItem((std::to_string(FFT_BLOCK_SIZES[i]) + " ").c_str() + tr("(High time res.)")); + } + else if (i == FFT_BLOCK_SIZES.size() - 1) + { + m_blockSizeModel.addItem((std::to_string(FFT_BLOCK_SIZES[i]) + " ").c_str() + tr("(High freq. res.)")); + } + else + { + m_blockSizeModel.addItem(std::to_string(FFT_BLOCK_SIZES[i]).c_str()); + } + } + m_blockSizeModel.setValue(m_blockSizeModel.findText("2048")); + + // Window type order must match FFT_WINDOWS defined in fft_helpers.h + m_windowModel.addItem(tr("Rectangular (Off)")); + m_windowModel.addItem(tr("Blackman-Harris (Default)")); + m_windowModel.addItem(tr("Hamming")); + m_windowModel.addItem(tr("Hanning")); + m_windowModel.setValue(m_windowModel.findText(tr("Blackman-Harris (Default)"))); + + // Colors + // Background color is defined by Qt / theme. + // Make sure the sum of colors for L and R channel stays lower or equal + // to 255. Otherwise the Waterfall pixels may overflow back to 0 even when + // the input signal isn't clipping (over 1.0). + m_colorL = QColor(51, 148, 204, 135); + m_colorR = QColor(204, 107, 51, 135); + m_colorMono = QColor(51, 148, 204, 204); + m_colorBG = QColor(7, 7, 7, 255); // ~20 % gray (after gamma correction) + m_colorGrid = QColor(30, 34, 38, 255); // ~40 % gray (slightly cold / blue) + m_colorLabels = QColor(192, 202, 212, 255); // ~90 % gray (slightly cold / blue) +} + + +// Create the SaControlDialog widget which handles display of GUI elements. +EffectControlDialog* SaControls::createView() +{ + return new SaControlsDialog(this, m_effect->getProcessor()); +} + + +void SaControls::loadSettings(const QDomElement &_this) +{ + m_waterfallModel.loadSettings(_this, "Waterfall"); + m_smoothModel.loadSettings(_this, "Smooth"); + m_stereoModel.loadSettings(_this, "Stereo"); + m_peakHoldModel.loadSettings(_this, "PeakHold"); + m_logXModel.loadSettings(_this, "LogX"); + m_logYModel.loadSettings(_this, "LogY"); + m_freqRangeModel.loadSettings(_this, "RangeX"); + m_ampRangeModel.loadSettings(_this, "RangeY"); + m_blockSizeModel.loadSettings(_this, "BlockSize"); + m_windowModel.loadSettings(_this, "WindowType"); +} + + +void SaControls::saveSettings(QDomDocument &doc, QDomElement &parent) +{ + m_waterfallModel.saveSettings(doc, parent, "Waterfall"); + m_smoothModel.saveSettings(doc, parent, "Smooth"); + m_stereoModel.saveSettings(doc, parent, "Stereo"); + m_peakHoldModel.saveSettings(doc, parent, "PeakHold"); + m_logXModel.saveSettings(doc, parent, "LogX"); + m_logYModel.saveSettings(doc, parent, "LogY"); + m_freqRangeModel.saveSettings(doc, parent, "RangeX"); + m_ampRangeModel.saveSettings(doc, parent, "RangeY"); + m_blockSizeModel.saveSettings(doc, parent, "BlockSize"); + m_windowModel.saveSettings(doc, parent, "WindowType"); +} diff --git a/plugins/SpectrumAnalyzer/SaControls.h b/plugins/SpectrumAnalyzer/SaControls.h new file mode 100644 index 00000000000..e0b54e6a2ba --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaControls.h @@ -0,0 +1,126 @@ +/* + * SaControls.h - declaration of SaControls class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef SACONTROLS_H +#define SACONTROLS_H + +#include "ComboBoxModel.h" +#include "EffectControls.h" + +//#define SA_DEBUG 1 // define SA_DEBUG to enable performance measurements + +// Frequency ranges (in Hz). +// Full range is defined by LOWEST_LOG_FREQ and current sample rate. +const int LOWEST_LOG_FREQ = 10; // arbitrary low limit for log. scale, >1 + +enum FREQUENCY_RANGES +{ + FRANGE_FULL = 0, + FRANGE_AUDIBLE, + FRANGE_BASS, + FRANGE_MIDS, + FRANGE_HIGH +}; + +const int FRANGE_AUDIBLE_START = 20; +const int FRANGE_AUDIBLE_END = 20000; +const int FRANGE_BASS_START = 20; +const int FRANGE_BASS_END = 300; +const int FRANGE_MIDS_START = 200; +const int FRANGE_MIDS_END = 5000; +const int FRANGE_HIGH_START = 4000; +const int FRANGE_HIGH_END = 20000; + +// Amplitude ranges. +// Reference: sine wave from -1.0 to 1.0 = 0 dB. +// I.e. if master volume is 100 %, positive values signify clipping. +// Doubling or halving the amplitude produces 3 dB difference. +enum AMPLITUDE_RANGES +{ + ARANGE_EXTENDED = 0, + ARANGE_DEFAULT, + ARANGE_AUDIBLE, + ARANGE_NOISE +}; + +const int ARANGE_EXTENDED_START = -80; +const int ARANGE_EXTENDED_END = 20; +const int ARANGE_DEFAULT_START = -30; +const int ARANGE_DEFAULT_END = 0; +const int ARANGE_AUDIBLE_START = -50; +const int ARANGE_AUDIBLE_END = 10; +const int ARANGE_NOISE_START = -60; +const int ARANGE_NOISE_END = -20; + + +class Analyzer; + +// Holds all the configuration values +class SaControls : public EffectControls +{ + Q_OBJECT +public: + explicit SaControls(Analyzer* effect); + virtual ~SaControls() {} + + EffectControlDialog* createView() override; + + void saveSettings (QDomDocument& doc, QDomElement& parent) override; + void loadSettings (const QDomElement &_this) override; + + QString nodeName() const override {return "Analyzer";} + int controlCount() override {return 12;} + +private: + Analyzer *m_effect; + + BoolModel m_pauseModel; + BoolModel m_refFreezeModel; + + BoolModel m_waterfallModel; + BoolModel m_smoothModel; + BoolModel m_stereoModel; + BoolModel m_peakHoldModel; + + BoolModel m_logXModel; + BoolModel m_logYModel; + + ComboBoxModel m_freqRangeModel; + ComboBoxModel m_ampRangeModel; + ComboBoxModel m_blockSizeModel; + ComboBoxModel m_windowModel; + + QColor m_colorL; + QColor m_colorR; + QColor m_colorMono; + QColor m_colorBG; + QColor m_colorGrid; + QColor m_colorLabels; + + friend class SaControlsDialog; + friend class SaSpectrumView; + friend class SaWaterfallView; + friend class SaProcessor; +}; +#endif // SACONTROLS_H diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.cpp b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp new file mode 100644 index 00000000000..4ba307a4def --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.cpp @@ -0,0 +1,227 @@ +/* + * SaControlsDialog.cpp - definition of SaControlsDialog class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SaControlsDialog.h" + +#include +#include +#include +#include +#include + +#include "ComboBox.h" +#include "ComboBoxModel.h" +#include "embed.h" +#include "Engine.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); + + // QSplitter top: configuration section + QWidget *config_widget = new QWidget; + QGridLayout *config_layout = new QGridLayout; + config_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + config_widget->setMaximumHeight(m_configHeight); + config_widget->setLayout(config_layout); + display_splitter->addWidget(config_widget); + + // 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; + + // 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); + + 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); + + // 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, 22); + freqRangeCombo->setMaximumSize(200, 22); + 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, 22); + ampRangeCombo->setMaximumSize(200, 22); + 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 bize")); + 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, 22); + windowCombo->setMaximumSize(200, 22); + windowCombo->setModel(&controls->m_windowModel); + config_layout->addWidget(windowCombo, 2, 5, 2, 1); + processor->rebuildWindow(); + connect(&controls->m_windowModel, &ComboBoxModel::dataChanged, [=] {processor->rebuildWindow();}); + + + // 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); + } +} + diff --git a/plugins/SpectrumAnalyzer/SaControlsDialog.h b/plugins/SpectrumAnalyzer/SaControlsDialog.h new file mode 100644 index 00000000000..e5a35f186bc --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaControlsDialog.h @@ -0,0 +1,57 @@ +/* + * SaControlsDialog.h - declatation of SaControlsDialog class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef 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 diff --git a/plugins/SpectrumAnalyzer/SaProcessor.cpp b/plugins/SpectrumAnalyzer/SaProcessor.cpp new file mode 100644 index 00000000000..9261658aa49 --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaProcessor.cpp @@ -0,0 +1,571 @@ +/* SaProcessor.cpp - implementation of SaProcessor class. + * + * Copyright (c) 2019 Martin Pavelek + * + * Based partially on Eq plugin code, + * Copyright (c) 2014-2017, David French + * + * 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 +#include +#include +#include + +#include "lmms_math.h" + + +SaProcessor::SaProcessor(SaControls *controls) : + m_controls(controls), + 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_fftBlockSize, 0); + m_bufferR.resize(m_fftBlockSize, 0); + m_spectrumL = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex)); + m_spectrumR = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex)); + m_fftPlanL = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferL.data(), m_spectrumL, FFTW_MEASURE); + m_fftPlanR = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferR.data(), m_spectrumR, FFTW_MEASURE); + + m_absSpectrumL.resize(binCount(), 0); + m_absSpectrumR.resize(binCount(), 0); + m_normSpectrumL.resize(binCount(), 0); + m_normSpectrumR.resize(binCount(), 0); + + m_history.resize(binCount() * m_waterfallHeight * sizeof qRgb(0,0,0), 0); + + clear(); +} + + +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 a batch of data from LMMS; run FFT analysis if buffer is full enough. +void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count) +{ + #ifdef SA_DEBUG + int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + #endif + // only take in data if any view is visible and not paused + if ((m_spectrumActive || m_waterfallActive) && !m_controls->m_pauseModel.value()) + { + const bool stereo = m_controls->m_stereoModel.value(); + fpp_t in_frame = 0; + while (in_frame < frame_count) + { + // 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. + // Also, to prevent audio interruption and a momentary GUI freeze, + // skip analysis if buffers are being reallocated. + if (m_framesFilledUp < m_inBlockSize || m_reallocating) {return;} + + // update sample rate + m_sampleRate = Engine::mixer()->processingSampleRate(); + + // apply FFT window + for (unsigned int i = 0; i < m_inBlockSize; i++) + { + m_bufferL[i] = m_bufferL[i] * m_fftWindow[i]; + m_bufferR[i] = m_bufferR[i] * m_fftWindow[i]; + } + + // lock data shared with SaSpectrumView and SaWaterfallView + QMutexLocker lock(&m_dataAccess); + + // Run FFT on left channel, convert the result to absolute magnitude + // spectrum and normalize it. + fftwf_execute(m_fftPlanL); + absspec(m_spectrumL, m_absSpectrumL.data(), binCount()); + normalize(m_absSpectrumL, m_normSpectrumL, m_inBlockSize); + + // repeat analysis for right channel if stereo processing is enabled + if (stereo) + { + fftwf_execute(m_fftPlanR); + absspec(m_spectrumR, m_absSpectrumR.data(), binCount()); + normalize(m_absSpectrumR, m_normSpectrumR, m_inBlockSize); + } + + // count empty lines so that empty history does not have to update + if (block_empty && m_waterfallNotEmpty) + { + m_waterfallNotEmpty -= 1; + } + else if (!block_empty) + { + m_waterfallNotEmpty = m_waterfallHeight + 2; + } + + if (m_waterfallActive && m_waterfallNotEmpty) + { + // move waterfall history one line down and clear the top line + QRgb *pixel = (QRgb *)m_history.data(); + std::copy(pixel, + pixel + binCount() * m_waterfallHeight - binCount(), + pixel + binCount()); + memset(pixel, 0, binCount() * sizeof (QRgb)); + + // add newest result on top + int target; // pixel being constructed + float accL = 0; // accumulators for merging multiple bins + float accR = 0; + + for (unsigned int i = 0; i < binCount(); i++) + { + // Every frequency bin spans a frequency range that must be + // partially or fully mapped to a pixel. Any inconsistency + // may be seen in the spectrogram as dark or white lines -- + // play white noise to confirm your change did not break it. + float band_start = freqToXPixel(binToFreq(i) - binBandwidth() / 2.0, binCount()); + float band_end = freqToXPixel(binToFreq(i + 1) - binBandwidth() / 2.0, binCount()); + if (m_controls->m_logXModel.value()) + { + // 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 < binCount()) + { + 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 < binCount()) {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 < binCount()) + { + pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]); + } + } + } + } + } + #ifdef SA_DEBUG + // report FFT processing speed + start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time; + std::cout << "Processed " << m_framesFilledUp << " samples in " << start_time / 1000000.0 << " ms" << std::endl; + #endif + + // clean up before checking for more data from input buffer + m_framesFilledUp = 0; + } + } +} + + +// 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, float gamma_correction) const +{ + 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_colorMono.lighter().red() * ampL, + m_controls->m_colorMono.lighter().green() * ampL, + m_controls->m_colorMono.lighter().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(); + } + 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; + + // Lock data shared with SaSpectrumView and SaWaterfallView. + // The m_reallocating is here to tell analyse() to avoid asking for the + // lock, since fftw3 can take a while to find the fastest FFT algorithm + // for given machine, which would produce interruption in the audio stream. + m_reallocating = true; + QMutexLocker 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_fft_size, 0); + m_bufferR.resize(new_fft_size, 0); + m_spectrumL = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex)); + m_spectrumR = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex)); + m_fftPlanL = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferL.data(), m_spectrumL, FFTW_MEASURE); + m_fftPlanR = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferR.data(), m_spectrumR, FFTW_MEASURE); + + if (m_fftPlanL == NULL || m_fftPlanR == NULL) + { + std::cerr << "Failed to create new FFT plan!" << std::endl; + } + 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_history.resize(new_bins * m_waterfallHeight * sizeof qRgb(0,0,0), 0); + + // done; publish new sizes and clean up + m_inBlockSize = new_in_size; + m_fftBlockSize = new_fft_size; + + lock.unlock(); + 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() +{ + QMutexLocker lock(&m_dataAccess); + m_framesFilledUp = 0; + std::fill(m_bufferL.begin(), m_bufferL.end(), 0); + std::fill(m_bufferR.begin(), m_bufferR.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.begin(), m_history.end(), 0); +} + + +// -------------------------------------- +// 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 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_AUDIBLE: return ARANGE_AUDIBLE_START; + case ARANGE_NOISE: return ARANGE_NOISE_START; + default: + case ARANGE_DEFAULT: return ARANGE_DEFAULT_START; + } +} + + +float SaProcessor::getAmpRangeMax() const +{ + switch (m_controls->m_ampRangeModel.value()) + { + case ARANGE_EXTENDED: return ARANGE_EXTENDED_END; + case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END; + case ARANGE_NOISE: return ARANGE_NOISE_END; + default: + case ARANGE_DEFAULT: return ARANGE_DEFAULT_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); + } +} + diff --git a/plugins/SpectrumAnalyzer/SaProcessor.h b/plugins/SpectrumAnalyzer/SaProcessor.h new file mode 100644 index 00000000000..ae2df16f8c8 --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaProcessor.h @@ -0,0 +1,122 @@ +/* SaProcessor.h - declaration of SaProcessor class. + * + * Copyright (c) 2019 Martin Pavelek + * + * Based partially on Eq plugin code, + * Copyright (c) 2014 David French + * + * 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 +#include +#include + +#include "fft_helpers.h" +#include "SaControls.h" + + +//! Receives audio data, runs FFT analysis and stores the result. +class SaProcessor +{ +public: + explicit SaProcessor(SaControls *controls); + virtual ~SaProcessor(); + + void analyse(sampleFrame *in_buffer, const fpp_t frame_count); + + // inform processor if any processing is actually required + void setSpectrumActive(bool active); + void setWaterfallActive(bool active); + + // configuration is taken from models in SaControls; some changes require + // an exlicit update request (reallocation and window rebuild) + void reallocateBuffers(); + void rebuildWindow(); + void clear(); + + // information about results and unit conversion helpers + 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; + + // data access lock must be acquired by any friendly class that touches + // the results, mainly to prevent unexpected mid-way reallocation + QMutex m_dataAccess; + +private: + SaControls *m_controls; + + // currently valid configuration + const unsigned int m_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size + unsigned int m_inBlockSize; //!< size of input (time domain) data block + unsigned int m_fftBlockSize; //!< size of padded block for FFT processing + unsigned int m_sampleRate; + + unsigned int binCount() const; //!< size of output (frequency domain) data block + + // data buffers (roughly in the order of processing, from input to output) + unsigned int m_framesFilledUp; + std::vector m_bufferL; //!< time domain samples (left) + std::vector m_bufferR; //!< time domain samples (right) + std::vector m_fftWindow; //!< precomputed window function coefficients + 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 m_absSpectrumL; //!< frequency domain samples (absolute) (left) + std::vector m_absSpectrumR; //!< frequency domain samples (absolute) (right) + std::vector m_normSpectrumL; //!< frequency domain samples (normalized) (left) + std::vector m_normSpectrumR; //!< frequency domain samples (normalized) (right) + + // spectrum history for waterfall: new normSpectrum lines are added on top + std::vector m_history; + const unsigned int m_waterfallHeight = 200; // Number of stored lines. + // Note: high values may make it harder to see transients. + + // book keeping + bool m_spectrumActive; + bool m_waterfallActive; + unsigned int m_waterfallNotEmpty; + bool m_reallocating; + + // merge L and R channels and apply gamma correction to make a spectrogram pixel + QRgb makePixel(float left, float right, float gamma_correction = 0.30) const; + + friend class SaSpectrumView; + friend class SaWaterfallView; +}; +#endif // SAPROCESSOR_H + diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.cpp b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp new file mode 100644 index 00000000000..746d52cfdc1 --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.cpp @@ -0,0 +1,796 @@ +/* SaSpectrumView.cpp - implementation of SaSpectrumView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * Based partially on Eq plugin code, + * Copyright (c) 2014-2017, David French + * + * 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 +#include +#include +#include +#include +#include + +#include "GuiApplication.h" +#include "MainWindow.h" +#include "SaProcessor.h" + +#ifdef SA_DEBUG + #include + #include +#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 = QPoint(0, 0); +} + + +// 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 what FPS would be achieved if spectrum display ran in a loop + total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; + painter.setPen(QPen(m_controls->m_colorLabels, 1, + Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft, + QString(std::string("Max FPS: " + std::to_string(1000000000.0 / total_time)).c_str())); + #endif +} + + +// Refresh data and draw the spectrum. +void SaSpectrumView::drawSpectrum(QPainter &painter) +{ + #ifdef SA_DEBUG + int path_time = 0, draw_time = 0; + #endif + + // draw the graph only if there is any input, averaging residue or peaks + QMutexLocker lock(&m_processor->m_dataAccess); + if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR)) + { + lock.unlock(); + #ifdef SA_DEBUG + path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + #endif + // update data buffers and reconstruct paths + refreshPaths(); + #ifdef SA_DEBUG + path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time; + #endif + + // draw stored paths + #ifdef SA_DEBUG + 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 + } + else + { + lock.unlock(); + } + + #ifdef SA_DEBUG + // display measurement results + painter.drawText(m_displayRight -100, 90, 100, 16, Qt::AlignLeft, + QString(std::string("Path ms: " + std::to_string(path_time / 1000000.0)).c_str())); + painter.drawText(m_displayRight -100, 110, 100, 16, Qt::AlignLeft, + QString(std::string("Draw ms: " + std::to_string(draw_time / 1000000.0)).c_str())); + #endif +} + + +// Read newest FFT results from SaProcessor, update local display buffers +// and build QPainter paths. +void SaSpectrumView::refreshPaths() +{ + // Lock is required for the entire function, mainly to prevent block size + // changes from causing reallocation of data structures mid-way. + QMutexLocker lock(&m_processor->m_dataAccess); + + // 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->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data()); + updateBuffers(m_processor->m_normSpectrumR.data(), 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 make_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, 1.5); + if (m_controls->m_stereoModel.value()) + { + m_pathR = makePath(m_displayBufferR, 1.5); + } + if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value()) + { + m_pathPeakL = makePath(m_peakBufferL, 0.25); + if (m_controls->m_stereoModel.value()) + { + m_pathPeakR = makePath(m_peakBufferR, 0.25); + } + } + #ifdef SA_DEBUG + make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - make_time; + #endif + + #ifdef SA_DEBUG + // print measurement results + std::cout << "Buffer update ms: " << std::to_string(refresh_time / 1000000.0) << ", "; + std::cout << "Path-make ms: " << std::to_string(make_time / 1000000.0) << std::endl; + #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 data +// access lock! +void SaSpectrumView::updateBuffers(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()) + { + displayBuffer[n] = spectrum[n] * m_smoothFactor + displayBuffer[n] * (1 - m_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_peakDecayFactor; + } + } + 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 &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> *freqTics = NULL; + std::vector> *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(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); + painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); + + // coordinates + painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawText(m_displayRight -60, 5, 100, 16, Qt::AlignLeft, "Cursor"); + + QString tmps; + // frequency + int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); + tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str()); + painter.drawText(m_displayRight -60, 18, 100, 16, Qt::AlignLeft, tmps); + + // amplitude + float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); + if (m_controls->m_logYModel.value()) + { + tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dB").c_str()); + } + else + { + // add 0.0005 to get proper rounding to 3 decimal places + tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str()); + } + painter.drawText(m_displayRight -60, 30, 100, 16, Qt::AlignLeft, tmps); + } +} + + +// 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> SaSpectrumView::makeLogFreqTics(int low, int high) +{ + std::vector> 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> SaSpectrumView::makeLinearFreqTics(int low, int high) +{ + std::vector> 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> SaSpectrumView::makeLogAmpTics(int low, int high) +{ + std::vector> 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> SaSpectrumView::makeLinearAmpTics(int low, int high) +{ + std::vector> 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. +void SaSpectrumView::mouseMoveEvent(QMouseEvent *event) +{ + m_cursor = event->pos(); +} + +void SaSpectrumView::mousePressEvent(QMouseEvent *event) +{ + m_cursor = event->pos(); +} + + +// 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()); +} + diff --git a/plugins/SpectrumAnalyzer/SaSpectrumView.h b/plugins/SpectrumAnalyzer/SaSpectrumView.h new file mode 100644 index 00000000000..0db5852e19d --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaSpectrumView.h @@ -0,0 +1,126 @@ +/* SaSpectrumView.h - declaration of SaSpectrumView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * Based partially on Eq plugin code, + * Copyright (c) 2014 David French + * + * 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 +#include +#include +#include + +class QMouseEvent; +class QPainter; +class SaControls; +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> m_logFreqTics; // 10-20-50... Hz + std::vector> m_linearFreqTics; // 2k-4k-6k... Hz + std::vector> m_logAmpTics; // dB + std::vector> m_linearAmpTics; // 0..1 + + std::vector> makeLogFreqTics(int low, int high); + std::vector> makeLinearFreqTics(int low, int high); + std::vector> makeLogAmpTics(int low, int high); + std::vector> 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 m_displayBufferL; + std::vector m_displayBufferR; + std::vector m_peakBufferL; + std::vector m_peakBufferR; + void updateBuffers(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 &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 + + const float m_smoothFactor = 0.15; // alpha for exponential smoothing + const float m_peakDecayFactor = 0.992; // multiplier for gradual peak decay + + // top level: refresh buffers, make paths and draw the spectrum + void drawSpectrum(QPainter &painter); + + // current cursor location and a method to draw it + QPoint 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; +}; +#endif // SASPECTRUMVIEW_H + diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.cpp b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp new file mode 100644 index 00000000000..617e80b2c49 --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.cpp @@ -0,0 +1,230 @@ +/* SaWaterfallViewView.cpp - implementation of SaWaterfallViewView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SaWaterfallView.h" + +#include +#include +#include +#include +#include +#include +#include + +#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_timeTics = makeTimeTics(); + m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate(); +} + + +// 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 + int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + #endif + + // all drawing done here, local variables are sufficient for the boundary + const int displayTop = 1; + const int displayBottom = height() -2; + const int displayLeft = 26; + const int displayRight = width() -26; + const int displayWidth = displayRight - displayLeft; + 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 ((float)m_processor->m_inBlockSize / m_processor->getSampleRate() != m_oldTimePerLine) + { + m_timeTics = makeTimeTics(); + m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate(); + } + + // 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, displayBottom); + // align first and last label to the edge if needed, otherwise center them + if (line == m_timeTics.front() && pos < label_height / 2) + { + painter.drawText(displayLeft - label_width - margin, displayTop - 1, + label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip, + QString(line.second.c_str())); + painter.drawText(displayRight + margin, displayTop - 1, + label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, + QString(line.second.c_str())); + } + else if (line == m_timeTics.back() && pos > displayBottom - label_height + 2) + { + painter.drawText(displayLeft - label_width - margin, displayBottom - label_height, + label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip, + QString(line.second.c_str())); + painter.drawText(displayRight + margin, displayBottom - label_height + 2, + label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip, + QString(line.second.c_str())); + } + else + { + painter.drawText(displayLeft - label_width - margin, pos - label_height / 2, + label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, + QString(line.second.c_str())); + painter.drawText(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->m_waterfallNotEmpty) + { + QMutexLocker lock(&m_processor->m_dataAccess); + painter.drawImage(displayLeft, displayTop, // top left corner coordinates + QImage(m_processor->m_history.data(), // raw pixel data to display + m_processor->binCount(), // width = number of frequency bins + m_processor->m_waterfallHeight, // height = number of history lines + QImage::Format_RGB32 + ).scaled(displayWidth, // scale to fit view.. + displayBottom, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + lock.unlock(); + } + else + { + painter.fillRect(displayLeft, displayTop, displayWidth, displayBottom, QColor(0,0,0)); + } + + // always draw the outline + painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawRoundedRect(displayLeft, displayTop, displayWidth, displayBottom, 2.0, 2.0); + + #ifdef SA_DEBUG + // display what FPS would be achieved if waterfall ran in a loop + start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time; + painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); + painter.drawText(displayRight -100, 10, 100, 16, Qt::AlignLeft, + QString(std::string("Max FPS: " + std::to_string(1000000000.0 / start_time)).c_str())); + #endif +} + + +// Convert time value to Y coordinate for display of given height. +float SaWaterfallView::timeToYPixel(float time, int height) +{ + float pixels_per_line = (float)height / m_processor->m_waterfallHeight; + float seconds_per_line = ((float)m_processor->m_inBlockSize / m_processor->getSampleRate()); + + return pixels_per_line * time / seconds_per_line; +} + + +// Generate labels for linear time scale. +std::vector> SaWaterfallView::makeTimeTics() +{ + std::vector> result; + float i; + + // upper limit defined by number of lines * time per line + float limit = m_processor->m_waterfallHeight * ((float)m_processor->m_inBlockSize / m_processor->getSampleRate()); + + // set increment so that about 8 tics are generated + float increment = std::round(10 * limit / 7) / 10; + + // NOTE: labels positions are rounded to match the (rounded) label value + for (i = 0; i <= limit; i += increment) + { + 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 + QMutexLocker lock(&m_processor->m_dataAccess); + std::fill(m_processor->m_history.begin(), m_processor->m_history.end(), 0); + lock.unlock(); + + 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()); + } +} + diff --git a/plugins/SpectrumAnalyzer/SaWaterfallView.h b/plugins/SpectrumAnalyzer/SaWaterfallView.h new file mode 100644 index 00000000000..0e104c0a168 --- /dev/null +++ b/plugins/SpectrumAnalyzer/SaWaterfallView.h @@ -0,0 +1,66 @@ +/* SaWaterfallView.h - declaration of SaWaterfallView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ +#ifndef SAWATERFALLVIEW_H +#define SAWATERFALLVIEW_H + +#include +#include +#include +#include +#include + +#include "SaControls.h" +#include "SaProcessor.h" + + +// 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; + +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_oldTimePerLine; + float timeToYPixel(float time, int height); + std::vector> makeTimeTics(); + std::vector> m_timeTics; // 0..n (s) +}; +#endif // SAWATERFALLVIEW_H diff --git a/plugins/SpectrumAnalyzer/SpectrumAnalyzer.cpp b/plugins/SpectrumAnalyzer/SpectrumAnalyzer.cpp deleted file mode 100644 index 0b947a3ba36..00000000000 --- a/plugins/SpectrumAnalyzer/SpectrumAnalyzer.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/* - * SpectrumAnalyzer.cpp - spectrum analyzer effect plugin - * - * Copyright (c) 2008-2014 Tobias Doerffel - * - * 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 "SpectrumAnalyzer.h" - -#include "embed.h" -#include "plugin_export.h" - -extern "C" -{ - -Plugin::Descriptor PLUGIN_EXPORT spectrumanalyzer_plugin_descriptor = -{ - STRINGIFY( PLUGIN_NAME ), - "Spectrum Analyzer", - QT_TRANSLATE_NOOP( "pluginBrowser", "Graphical spectrum analyzer plugin" ), - "Tobias Doerffel ", - 0x0100, - Plugin::Effect, - new PluginPixmapLoader(), - NULL, - NULL -} ; - -} - - - -SpectrumAnalyzer::SpectrumAnalyzer( Model * _parent, - const Descriptor::SubPluginFeatures::Key * _key ) : - Effect( &spectrumanalyzer_plugin_descriptor, _parent, _key ), - m_saControls( this ), - m_framesFilledUp( 0 ), - m_energy( 0 ) -{ - memset( m_buffer, 0, sizeof( m_buffer ) ); - - m_specBuf = (fftwf_complex *) fftwf_malloc( ( FFT_BUFFER_SIZE + 1 ) * sizeof( fftwf_complex ) ); - m_fftPlan = fftwf_plan_dft_r2c_1d( FFT_BUFFER_SIZE*2, m_buffer, m_specBuf, FFTW_MEASURE ); -} - - - - -SpectrumAnalyzer::~SpectrumAnalyzer() -{ - fftwf_destroy_plan( m_fftPlan ); - fftwf_free( m_specBuf ); -} - - - - -bool SpectrumAnalyzer::processAudioBuffer( sampleFrame* _buf, const fpp_t _frames ) -{ - if( !isEnabled() || !isRunning () ) - { - return false; - } - - if( !m_saControls.isViewVisible() ) - { - return true; - } - - fpp_t f = 0; - if( _frames > FFT_BUFFER_SIZE ) - { - m_framesFilledUp = 0; - f = _frames - FFT_BUFFER_SIZE; - } - - const int cm = m_saControls.m_channelMode.value(); - - switch( cm ) - { - case MergeChannels: - for( ; f < _frames; ++f ) - { - m_buffer[m_framesFilledUp] = - ( _buf[f][0] + _buf[f][1] ) * 0.5; - ++m_framesFilledUp; - } - break; - case LeftChannel: - for( ; f < _frames; ++f ) - { - m_buffer[m_framesFilledUp] = _buf[f][0]; - ++m_framesFilledUp; - } - break; - case RightChannel: - for( ; f < _frames; ++f ) - { - m_buffer[m_framesFilledUp] = _buf[f][1]; - ++m_framesFilledUp; - } - break; - } - - if( m_framesFilledUp < FFT_BUFFER_SIZE ) - { - return isRunning(); - } - - -// hanming( m_buffer, FFT_BUFFER_SIZE, HAMMING ); - - const sample_rate_t sr = Engine::mixer()->processingSampleRate(); - const int LOWEST_FREQ = 0; - const int HIGHEST_FREQ = sr / 2; - - fftwf_execute( m_fftPlan ); - absspec( m_specBuf, m_absSpecBuf, FFT_BUFFER_SIZE+1 ); - if( m_saControls.m_linearSpec.value() ) - { - compressbands( m_absSpecBuf, m_bands, FFT_BUFFER_SIZE+1, - MAX_BANDS, - (int)(LOWEST_FREQ*(FFT_BUFFER_SIZE+1)/(float)(sr/2)), - (int)(HIGHEST_FREQ*(FFT_BUFFER_SIZE+1)/(float)(sr/2))); - m_energy = maximum( m_bands, MAX_BANDS ) / maximum( m_buffer, FFT_BUFFER_SIZE ); - } - else - { - calc13octaveband31( m_absSpecBuf, m_bands, FFT_BUFFER_SIZE+1, sr/2.0 ); - m_energy = signalpower( m_buffer, FFT_BUFFER_SIZE ) / maximum( m_buffer, FFT_BUFFER_SIZE ); - } - - - m_framesFilledUp = 0; - - checkGate( 1 ); - - return isRunning(); -} - - - - - -extern "C" -{ - -// necessary for getting instance out of shared lib -PLUGIN_EXPORT Plugin * lmms_plugin_main( Model* parent, void* data ) -{ - return new SpectrumAnalyzer( parent, static_cast( data ) ); -} - -} - diff --git a/plugins/SpectrumAnalyzer/SpectrumAnalyzer.h b/plugins/SpectrumAnalyzer/SpectrumAnalyzer.h deleted file mode 100644 index c9235117680..00000000000 --- a/plugins/SpectrumAnalyzer/SpectrumAnalyzer.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * SpectrumAnalyzer.h - spectrum anaylzer effect plugin - * - * Copyright (c) 2008-2014 Tobias Doerffel - * - * 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 _SPECTRUM_ANALYZER_H -#define _SPECTRUM_ANALYZER_H - -#include "Effect.h" -#include "fft_helpers.h" -#include "SpectrumAnalyzerControls.h" - - -const int MAX_BANDS = 249; - - -class SpectrumAnalyzer : public Effect -{ -public: - enum ChannelModes - { - MergeChannels, - LeftChannel, - RightChannel - } ; - - SpectrumAnalyzer( Model * _parent, - const Descriptor::SubPluginFeatures::Key * _key ); - virtual ~SpectrumAnalyzer(); - virtual bool processAudioBuffer( sampleFrame * _buf, - const fpp_t _frames ); - - virtual EffectControls * controls() - { - return( &m_saControls ); - } - - -private: - SpectrumAnalyzerControls m_saControls; - - fftwf_plan m_fftPlan; - - fftwf_complex * m_specBuf; - float m_absSpecBuf[FFT_BUFFER_SIZE+1]; - float m_buffer[FFT_BUFFER_SIZE*2]; - int m_framesFilledUp; - - float m_bands[MAX_BANDS]; - float m_energy; - - friend class SpectrumAnalyzerControls; - friend class SpectrumView; - -} ; - - -#endif diff --git a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControlDialog.cpp b/plugins/SpectrumAnalyzer/SpectrumAnalyzerControlDialog.cpp deleted file mode 100644 index 05b1ed08a5d..00000000000 --- a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControlDialog.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* - * SpectrumAnalyzerControlDialog.cpp - view for spectrum analyzer - * - * Copyright (c) 2008-2014 Tobias Doerffel - * - * 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 - -#include -#include - -#include "SpectrumAnalyzer.h" -#include "MainWindow.h" -#include "GuiApplication.h" -#include "LedCheckbox.h" -#include "embed.h" - - -static inline void darken( QImage& img, int x, int y, int w, int h ) -{ - int imgWidth = img.width(); - QRgb * base = ( (QRgb *) img.bits() ) + y*imgWidth + x; - for( int y = 0; y < h; ++y ) - { - QRgb * d = base + y*imgWidth; - for( int x = 0; x < w; ++x ) - { - // shift each color component by 1 bit and set alpha - // to 0xff - d[x] = ( ( d[x] >> 1 ) & 0x7f7f7f7f ) | 0xff000000; - } - } -} - - - -class SpectrumView : public QWidget -{ -public: - SpectrumView( SpectrumAnalyzer* s, QWidget * _parent ) : - QWidget( _parent ), - m_sa( s ), - m_backgroundPlain( PLUGIN_NAME::getIconPixmap( "spectrum_background_plain" ).toImage() ), - m_background( PLUGIN_NAME::getIconPixmap( "spectrum_background" ).toImage() ) - { - setFixedSize( 249, 151 ); - connect( gui->mainWindow(), SIGNAL( periodicUpdate() ), this, SLOT( update() ) ); - setAttribute( Qt::WA_OpaquePaintEvent, true ); - } - - virtual ~SpectrumView() - { - } - - virtual void paintEvent( QPaintEvent* event ) - { - QPainter p( this ); - QImage i = m_sa->m_saControls.m_linearSpec.value() ? - m_backgroundPlain : m_background; - const float e = m_sa->m_energy; - if( e <= 0 ) - { - darken( i, 0, 0, i.width(), i.height() ); - p.drawImage( 0, 0, i ); - return; - } - - const bool lin_y = m_sa->m_saControls.m_linearYAxis.value(); - float * b = m_sa->m_bands; - const int LOWER_Y = -60; // dB - int h; - const int fh = height(); - if( m_sa->m_saControls.m_linearSpec.value() ) - { - if( lin_y ) - { - for( int x = 0; x < MAX_BANDS; ++x, ++b ) - { - h = fh * 2.0 / 3.0 * (*b / e ); - if( h < 0 ) h = 0; else if( h >= fh ) continue; - darken( i, x, 0, 1, fh-h ); - } - } - else - { - for( int x = 0; x < MAX_BANDS; ++x, ++b ) - { - h = (int)( fh * 2.0 / 3.0 * (20*(log10( *b / e ) ) - LOWER_Y ) / (-LOWER_Y ) ); - if( h < 0 ) h = 0; else if( h >= fh ) continue; - darken( i, x, 0, 1, fh-h ); - } - } - } - else - { - if( lin_y ) - { - for( int x = 0; x < 31; ++x, ++b ) - { - h = fh * 2.0 / 3.0 * ( 1.2 * *b / e ); - if( h < 0 ) h = 0; else if( h >= fh ) continue; else h = ( h / 3 ) * 3; - darken( i, x*8, 0, 8, fh-h ); - } - } - else - { - for( int x = 0; x < 31; ++x, ++b ) - { - h = (int)( fh * 2.0 / 3.0 * (20*(log10( *b / e ) ) - LOWER_Y ) / (-LOWER_Y ) ); - if( h < 0 ) h = 0; else if( h >= fh ) continue; else h = ( h / 3 ) * 3; - darken( i, x*8, 0, 8, fh-h ); - } - } - darken( i, 31*8, 0, 1, fh ); - } - p.drawImage( 0, 0, i ); - } - - -private: - SpectrumAnalyzer * m_sa; - QImage m_backgroundPlain; - QImage m_background; - -} ; - - - - -SpectrumAnalyzerControlDialog::SpectrumAnalyzerControlDialog( SpectrumAnalyzerControls* controls ) : - EffectControlDialog( controls ), - m_controls( controls ), - m_logXAxis( PLUGIN_NAME::getIconPixmap( "log_x_axis" ) ), - m_logYAxis( PLUGIN_NAME::getIconPixmap( "log_y_axis" ) ) -{ - setAutoFillBackground( true ); - QPalette pal; - pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "background" ) ); - setFixedSize( 293, 205 ); - setPalette( pal ); -/* QVBoxLayout * l = new QVBoxLayout( this );*/ - SpectrumView* v = new SpectrumView( controls->m_effect, this ); - v->move( 34, 10 ); - - LedCheckBox * lin_spec = new LedCheckBox( tr( "Linear spectrum" ), this ); - lin_spec->move( 32, 182 ); - lin_spec->setModel( &controls->m_linearSpec ); - - LedCheckBox * lin_y = new LedCheckBox( tr( "Linear Y axis" ), this ); - lin_y->move( 137, 182 ); - lin_y->setModel( &controls->m_linearYAxis ); - - connect( &controls->m_linearSpec, SIGNAL( dataChanged() ), this, SLOT( update() ) ); - connect( &controls->m_linearYAxis, SIGNAL( dataChanged() ), this, SLOT( update() ) ); -/* l->addWidget( v ); - l->addWidget( lin_spec ); - l->addWidget( lin_y );*/ - -} - - -void SpectrumAnalyzerControlDialog::paintEvent( QPaintEvent * ) -{ - QPainter p( this ); - - if( !m_controls->m_linearSpec.value() ) - { - p.drawPixmap( 33, 165, m_logXAxis ); - } - - if( !m_controls->m_linearYAxis.value() ) - { - p.drawPixmap( 10, 29, m_logYAxis); - } - -} diff --git a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControls.cpp b/plugins/SpectrumAnalyzer/SpectrumAnalyzerControls.cpp deleted file mode 100644 index 4a59b3c0a8a..00000000000 --- a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControls.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SpectrumAnalyzerControls.cpp - controls for spectrum analyzer - * - * Copyright (c) 2008-2014 Tobias Doerffel - * - * 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 "SpectrumAnalyzer.h" -#include "SpectrumAnalyzerControls.h" - - - -SpectrumAnalyzerControls::SpectrumAnalyzerControls( SpectrumAnalyzer* effect ) : - EffectControls( effect ), - m_effect( effect ), - m_linearSpec( false, this, tr( "Linear spectrum" ) ), - m_linearYAxis( false, this, tr( "Linear Y axis" ) ), - m_channelMode( SpectrumAnalyzer::MergeChannels, - SpectrumAnalyzer::MergeChannels, - SpectrumAnalyzer::RightChannel, - this, tr( "Channel mode" ) ) -{ -} - - - - -void SpectrumAnalyzerControls::loadSettings( const QDomElement & _this ) -{ -} - - - - -void SpectrumAnalyzerControls::saveSettings( QDomDocument & _doc, - QDomElement & _this ) -{ -} - - - - - diff --git a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControls.h b/plugins/SpectrumAnalyzer/SpectrumAnalyzerControls.h deleted file mode 100644 index e46407863ae..00000000000 --- a/plugins/SpectrumAnalyzer/SpectrumAnalyzerControls.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SpectrumAnalyzerControls.h - controls for spectrum-analyzer - * - * Copyright (c) 2008-2014 Tobias Doerffel - * - * 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 SPECTRUM_ANALYZER_CONTROLS_H -#define SPECTRUM_ANALYZER_CONTROLS_H - -#include "EffectControls.h" -#include "SpectrumAnalyzerControlDialog.h" -#include "Knob.h" - - -class SpectrumAnalyzer; - - -class SpectrumAnalyzerControls : public EffectControls -{ - Q_OBJECT -public: - SpectrumAnalyzerControls( SpectrumAnalyzer* effect ); - virtual ~SpectrumAnalyzerControls() - { - } - - virtual void saveSettings( QDomDocument & _doc, QDomElement & _parent ); - virtual void loadSettings( const QDomElement & _this ); - inline virtual QString nodeName() const - { - return "spectrumanaylzercontrols"; - } - - virtual int controlCount() - { - return 1; - } - - virtual EffectControlDialog * createView() - { - return new SpectrumAnalyzerControlDialog( this ); - } - - -private: - SpectrumAnalyzer* m_effect; - BoolModel m_linearSpec; - BoolModel m_linearYAxis; - IntModel m_channelMode; - - friend class SpectrumAnalyzer; - friend class SpectrumAnalyzerControlDialog; - friend class SpectrumView; - -} ; - -#endif diff --git a/plugins/SpectrumAnalyzer/background.png b/plugins/SpectrumAnalyzer/background.png deleted file mode 100644 index e375f7d3403..00000000000 Binary files a/plugins/SpectrumAnalyzer/background.png and /dev/null differ diff --git a/plugins/SpectrumAnalyzer/block_size.svg b/plugins/SpectrumAnalyzer/block_size.svg new file mode 100644 index 00000000000..e9c4806ec94 --- /dev/null +++ b/plugins/SpectrumAnalyzer/block_size.svg @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/freeze.svg b/plugins/SpectrumAnalyzer/freeze.svg new file mode 100644 index 00000000000..4c9a43c1284 --- /dev/null +++ b/plugins/SpectrumAnalyzer/freeze.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/freeze_off.svg b/plugins/SpectrumAnalyzer/freeze_off.svg new file mode 100644 index 00000000000..b6353d7a768 --- /dev/null +++ b/plugins/SpectrumAnalyzer/freeze_off.svg @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/log_x_axis.png b/plugins/SpectrumAnalyzer/log_x_axis.png deleted file mode 100644 index 97c5e729dff..00000000000 Binary files a/plugins/SpectrumAnalyzer/log_x_axis.png and /dev/null differ diff --git a/plugins/SpectrumAnalyzer/log_y_axis.png b/plugins/SpectrumAnalyzer/log_y_axis.png deleted file mode 100644 index e20d27001fa..00000000000 Binary files a/plugins/SpectrumAnalyzer/log_y_axis.png and /dev/null differ diff --git a/plugins/SpectrumAnalyzer/pause.svg b/plugins/SpectrumAnalyzer/pause.svg new file mode 100644 index 00000000000..d28a93aa515 --- /dev/null +++ b/plugins/SpectrumAnalyzer/pause.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/play.svg b/plugins/SpectrumAnalyzer/play.svg new file mode 100644 index 00000000000..eb3178284b9 --- /dev/null +++ b/plugins/SpectrumAnalyzer/play.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/spectrum_background.png b/plugins/SpectrumAnalyzer/spectrum_background.png deleted file mode 100644 index b65491bc39a..00000000000 Binary files a/plugins/SpectrumAnalyzer/spectrum_background.png and /dev/null differ diff --git a/plugins/SpectrumAnalyzer/spectrum_background_plain.png b/plugins/SpectrumAnalyzer/spectrum_background_plain.png deleted file mode 100644 index ba9bcd1c89d..00000000000 Binary files a/plugins/SpectrumAnalyzer/spectrum_background_plain.png and /dev/null differ diff --git a/plugins/SpectrumAnalyzer/window.svg b/plugins/SpectrumAnalyzer/window.svg new file mode 100644 index 00000000000..5d0bd7dbdf7 --- /dev/null +++ b/plugins/SpectrumAnalyzer/window.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/x_linear.svg b/plugins/SpectrumAnalyzer/x_linear.svg new file mode 100644 index 00000000000..cef7886709f --- /dev/null +++ b/plugins/SpectrumAnalyzer/x_linear.svg @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/x_log.svg b/plugins/SpectrumAnalyzer/x_log.svg new file mode 100644 index 00000000000..8b8dc8b5a26 --- /dev/null +++ b/plugins/SpectrumAnalyzer/x_log.svg @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/y_linear.svg b/plugins/SpectrumAnalyzer/y_linear.svg new file mode 100644 index 00000000000..69c39bb0592 --- /dev/null +++ b/plugins/SpectrumAnalyzer/y_linear.svg @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/plugins/SpectrumAnalyzer/y_log.svg b/plugins/SpectrumAnalyzer/y_log.svg new file mode 100644 index 00000000000..645e09e5ac6 --- /dev/null +++ b/plugins/SpectrumAnalyzer/y_log.svg @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/core/fft_helpers.cpp b/src/core/fft_helpers.cpp index a17ebf719a6..bc7d289e337 100644 --- a/src/core/fft_helpers.cpp +++ b/src/core/fft_helpers.cpp @@ -2,6 +2,7 @@ * fft_helpers.cpp - some functions around FFT analysis * * Copyright (c) 2008-2012 Tobias Doerffel + * Copyright (c) 2019 Martin Pavelek * * This file is part of LMMS - https://lmms.io * @@ -28,127 +29,194 @@ #include #include "lmms_constants.h" -/* returns biggest value from abs_spectrum[spec_size] array - - returns -1 on error -*/ -float maximum(float *abs_spectrum, unsigned int spec_size) +/* Returns biggest value from abs_spectrum[spec_size] array. + * + * return -1 on error, otherwise the maximum value + */ +float maximum(const float *abs_spectrum, unsigned int spec_size) { - float maxi=0; + float maxi = 0; unsigned int i; - if ( abs_spectrum==NULL ) - return -1; - - if (spec_size<=0) - return -1; + if (abs_spectrum == NULL) {return -1;} + if (spec_size <= 0) {return -1;} - for ( i=0; imaxi ) - maxi=abs_spectrum[i]; + if (abs_spectrum[i] > maxi) {maxi = abs_spectrum[i];} } - return maxi; } +float maximum(const std::vector &abs_spectrum) +{ + return maximum(abs_spectrum.data(), abs_spectrum.size()); +} -/* apply hanning or hamming window to channel - returns -1 on error */ -int hanming(float *timebuffer, int length, WINDOWS type) +/* Normalize the array of absolute magnitudes to a 0..1 range. + * Block size refers to FFT block size before any zero padding. + * + * return -1 on error, 0 on success + */ +int normalize(const float *abs_spectrum, float *norm_spectrum, unsigned int bin_count, unsigned int block_size) { int i; - float alpha; - if ( (timebuffer==NULL)||(length<=0) ) - return -1; + if (abs_spectrum == NULL || norm_spectrum == NULL) {return -1;} + if (bin_count == 0 || block_size == 0) {return -1;} + + for (i = 0; i < bin_count; i++) + { + norm_spectrum[i] = abs_spectrum[i] / block_size; + } + return 0; +} + +int normalize(const std::vector &abs_spectrum, std::vector &norm_spectrum, unsigned int block_size) +{ + if (abs_spectrum.size() != norm_spectrum.size()) {return -1;} + + return normalize(abs_spectrum.data(), norm_spectrum.data(), abs_spectrum.size(), block_size); +} + + +/* Check if the spectrum contains any non-zero value. + * + * return 1 if spectrum contains any non-zero value + * return 0 otherwise + */ +int notEmpty(const std::vector &spectrum) +{ + for (int i = 0; i < spectrum.size(); i++) + { + if (spectrum[i] != 0) {return 1;} + } + return 0; +} + + +/* Precompute an FFT window function for later real-time use. + * + * return -1 on error + */ +int precomputeWindow(float *window, unsigned int length, FFT_WINDOWS type, bool normalized) +{ + unsigned int i; + float gain = 0; + float a0; + float a1; + float a2; + float a3; + + if (window == NULL) {return -1;} + // constants taken from + // https://en.wikipedia.org/wiki/Window_function#AList_of_window_functions switch (type) { - case HAMMING: alpha=0.54; break; + default: + case RECTANGULAR: + for (i = 0; i < length; i++) {window[i] = 1.0;} + gain = 1; + return 0; + case BLACKMAN_HARRIS: + a0 = 0.35875; + a1 = 0.48829; + a2 = 0.14128; + a3 = 0.01168; + break; + case HAMMING: + a0 = 0.54; + a1 = 1.0 - a0; + a2 = 0; + a3 = 0; + break; case HANNING: - default: alpha=0.5; break; + a0 = 0.5; + a1 = 1.0 - a0; + a2 = 0; + a3 = 0; + break; } - for ( i=0; i num_new - - returns 0 on success, else -1 */ -int compressbands(float *absspec_buffer, float *compressedband, int num_old, int num_new, int bottom, int top) +/* Build fewer subbands from many absolute spectrum values. + * Take care that - compressedbands[] array num_new elements long + * - num_old > num_new + * + * return 0 on success, else -1 + */ +int compressbands(const float *absspec_buffer, float *compressedband, int num_old, int num_new, int bottom, int top) { float ratio; int i, usefromold; float j; float j_min, j_max; - if ( (absspec_buffer==NULL)||(compressedband==NULL) ) - return -1; + if (absspec_buffer == NULL || compressedband == NULL) {return -1;} + if (num_old < num_new) {return -1;} + if (num_old <= 0 || num_new <= 0) {return -1;} + if (bottom < 0) {bottom = 0;} + if (top >= num_old) {top = num_old - 1;} - if ( num_old=num_old ) - top=num_old-1; - - usefromold=num_old-(num_old-top)-bottom; - - ratio=(float)usefromold/(float)num_new; + ratio = (float)usefromold / (float)num_new; // for each new subband - for ( i=0; iviewport(), windowFlags); win->setAttribute(Qt::WA_DeleteOnClose); win->setWidget(w); + if (w->sizeHint().isValid()) {win->resize(w->sizeHint());} m_workspace->addSubWindow(win); return win; } diff --git a/src/gui/SubWindow.cpp b/src/gui/SubWindow.cpp index 63bdee7a8ac..5cbac4cd6fa 100644 --- a/src/gui/SubWindow.cpp +++ b/src/gui/SubWindow.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include "embed.h" diff --git a/src/gui/widgets/EffectView.cpp b/src/gui/widgets/EffectView.cpp index 2a492128bc0..a159eedf956 100644 --- a/src/gui/widgets/EffectView.cpp +++ b/src/gui/widgets/EffectView.cpp @@ -97,9 +97,14 @@ EffectView::EffectView( Effect * _model, QWidget * _parent ) : if( m_controlView ) { m_subWindow = gui->mainWindow()->addWindowedWidget( m_controlView ); - m_subWindow->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); - if (m_subWindow->layout()) { - m_subWindow->layout()->setSizeConstraint(QLayout::SetFixedSize); + + if ( !m_controlView->isResizable() ) + { + m_subWindow->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); + if (m_subWindow->layout()) + { + m_subWindow->layout()->setSizeConstraint(QLayout::SetFixedSize); + } } Qt::WindowFlags flags = m_subWindow->windowFlags(); diff --git a/src/gui/widgets/PixmapButton.cpp b/src/gui/widgets/PixmapButton.cpp index 9858c221d3b..eb2553cf777 100644 --- a/src/gui/widgets/PixmapButton.cpp +++ b/src/gui/widgets/PixmapButton.cpp @@ -134,11 +134,11 @@ QSize PixmapButton::sizeHint() const { if( ( model() != NULL && model()->value() ) || m_pressed ) { - return m_activePixmap.size(); + return m_activePixmap.size() / devicePixelRatio(); } else { - return m_inactivePixmap.size(); + return m_inactivePixmap.size() / devicePixelRatio(); } }