Spectrum analyzer update (#5160)

* advanced config: expose hidden constants to user screen
* advanced config: add support for FFT window overlapping
* waterfall: display at native resolution on high-DPI screens
* waterfall: add cursor and improve label density
* FFT: fix normalization so that 0 dBFS matches full-scale sinewave
* FFT: decouple data acquisition from processing and display
* FFT: separate lock for reallocation (to avoid some needless waiting)
* moved ranges and other constants to a separate file
* debug: better performance measurements
* minor fixes
* build the ringbuffer library as part of LMMS core
This commit is contained in:
Martin Pavelek
2019-11-21 14:44:18 +01:00
committed by Johannes Lorenz
parent 2f0010270e
commit da73ddd242
26 changed files with 1867 additions and 364 deletions

3
.gitmodules vendored
View File

@@ -34,3 +34,6 @@
[submodule "doc/wiki"]
path = doc/wiki
url = https://github.com/lmms/lmms.wiki.git
[submodule "src/3rdparty/ringbuffer"]
path = src/3rdparty/ringbuffer
url = https://github.com/JohannesLorenz/ringbuffer.git

View File

@@ -0,0 +1,132 @@
/*
* LocklessRingBuffer.h - LMMS wrapper for a lockless ringbuffer library
*
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LOCKLESSRINGBUFFER_H
#define LOCKLESSRINGBUFFER_H
#include <QMutex>
#include <QWaitCondition>
#include "lmms_basics.h"
#include "lmms_export.h"
#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h"
//! A convenience layer for a realtime-safe and thread-safe multi-reader ring buffer library.
template <class T>
class LocklessRingBufferBase
{
template<class _T>
friend class LocklessRingBufferReader;
public:
LocklessRingBufferBase(std::size_t sz) : m_buffer(sz)
{
m_buffer.touch(); // reserve storage space before realtime operation starts
}
~LocklessRingBufferBase() {};
std::size_t capacity() const {return m_buffer.maximum_eventual_write_space();}
std::size_t free() const {return m_buffer.write_space();}
void wakeAll() {m_notifier.wakeAll();}
protected:
ringbuffer_t<T> m_buffer;
QWaitCondition m_notifier;
};
// The SampleFrameCopier is required because sampleFrame is just a two-element
// array and therefore does not have a copy constructor needed by std::copy.
class SampleFrameCopier
{
const sampleFrame* m_src;
public:
SampleFrameCopier(const sampleFrame* src) : m_src(src) {}
void operator()(std::size_t src_offset, std::size_t count, sampleFrame* dest)
{
for (std::size_t i = src_offset; i < src_offset + count; i++, dest++)
{
(*dest)[0] = m_src[i][0];
(*dest)[1] = m_src[i][1];
}
}
};
//! Standard ring buffer template for data types with copy constructor.
template <class T>
class LocklessRingBuffer : public LocklessRingBufferBase<T>
{
public:
LocklessRingBuffer(std::size_t sz) : LocklessRingBufferBase<T>(sz) {};
std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false)
{
std::size_t written = LocklessRingBufferBase<T>::m_buffer.write(src, cnt);
// Let all waiting readers know new data are available.
if (notify) {LocklessRingBufferBase<T>::m_notifier.wakeAll();}
return written;
}
};
//! Specialized ring buffer template with write function modified to support sampleFrame.
template <>
class LocklessRingBuffer<sampleFrame> : public LocklessRingBufferBase<sampleFrame>
{
public:
LocklessRingBuffer(std::size_t sz) : LocklessRingBufferBase<sampleFrame>(sz) {};
std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false)
{
SampleFrameCopier copier(src);
std::size_t written = LocklessRingBufferBase<sampleFrame>::m_buffer.write_func<SampleFrameCopier>(copier, cnt);
// Let all waiting readers know new data are available.
if (notify) {LocklessRingBufferBase<sampleFrame>::m_notifier.wakeAll();}
return written;
}
};
//! Wrapper for lockless ringbuffer reader
template <class T>
class LocklessRingBufferReader : public ringbuffer_reader_t<T>
{
public:
LocklessRingBufferReader(LocklessRingBuffer<T> &rb) :
ringbuffer_reader_t<T>(rb.m_buffer),
m_notifier(&rb.m_notifier) {};
bool empty() const {return !this->read_space();}
void waitForData()
{
QMutex useless_lock;
m_notifier->wait(&useless_lock);
useless_lock.unlock();
}
private:
QWaitCondition *m_notifier;
};
#endif //LOCKLESSRINGBUFFER_H

View File

@@ -32,6 +32,8 @@
#include "lmms_math.h"
#include "MemoryManager.h"
/** \brief A basic LMMS ring buffer for single-thread use. For thread and realtime safe alternative see LocklessRingBuffer.
*/
class LMMS_EXPORT RingBuffer : public QObject
{
Q_OBJECT

View File

@@ -49,4 +49,47 @@ const float F_PI_SQR = (float) LD_PI_SQR;
const float F_E = (float) LD_E;
const float F_E_R = (float) LD_E_R;
// Frequency ranges (in Hz).
// Arbitrary low limit for logarithmic frequency scale; >1 Hz.
const int LOWEST_LOG_FREQ = 10;
// Full range is defined by LOWEST_LOG_FREQ and current sample rate.
enum FREQUENCY_RANGES
{
FRANGE_FULL = 0,
FRANGE_AUDIBLE,
FRANGE_BASS,
FRANGE_MIDS,
FRANGE_HIGH
};
const int FRANGE_AUDIBLE_START = 20;
const int FRANGE_AUDIBLE_END = 20000;
const int FRANGE_BASS_START = 20;
const int FRANGE_BASS_END = 300;
const int FRANGE_MIDS_START = 200;
const int FRANGE_MIDS_END = 5000;
const int FRANGE_HIGH_START = 4000;
const int FRANGE_HIGH_END = 20000;
// Amplitude ranges (in dBFS).
// Reference: full scale sine wave (-1.0 to 1.0) is 0 dB.
// Doubling or halving the amplitude produces 3 dB difference.
enum AMPLITUDE_RANGES
{
ARANGE_EXTENDED = 0,
ARANGE_AUDIBLE,
ARANGE_LOUD,
ARANGE_SILENT
};
const int ARANGE_EXTENDED_START = -80;
const int ARANGE_EXTENDED_END = 20;
const int ARANGE_AUDIBLE_START = -50;
const int ARANGE_AUDIBLE_END = 0;
const int ARANGE_LOUD_START = -30;
const int ARANGE_LOUD_END = 0;
const int ARANGE_SILENT_START = -60;
const int ARANGE_SILENT_END = -10;
#endif

View File

@@ -27,7 +27,13 @@
#include "Analyzer.h"
#ifdef SA_DEBUG
#include <chrono>
#include <iostream>
#endif
#include "embed.h"
#include "lmms_basics.h"
#include "plugin_export.h"
@@ -38,7 +44,7 @@ extern "C" {
"Spectrum Analyzer",
QT_TRANSLATE_NOOP("pluginBrowser", "A graphical spectrum analyzer."),
"Martin Pavelek <he29/dot/HS/at/gmail/dot/com>",
0x0100,
0x0112,
Plugin::Effect,
new PluginPixmapLoader("logo"),
NULL,
@@ -50,17 +56,54 @@ extern "C" {
Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::Key *key) :
Effect(&analyzer_plugin_descriptor, parent, key),
m_processor(&m_controls),
m_controls(this)
m_controls(this),
m_processorThread(m_processor, m_inputBuffer),
// Buffer is sized to cover 4* the current maximum LMMS audio buffer size,
// so that it has some reserve space in case data processor is busy.
m_inputBuffer(4 * m_maxBufferSize)
{
m_processorThread.start();
}
Analyzer::~Analyzer()
{
m_processor.terminate();
m_inputBuffer.wakeAll();
m_processorThread.wait();
}
// Take audio data and pass them to the spectrum processor.
// Skip processing if the controls dialog isn't visible, it would only waste CPU cycles.
bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count)
{
// Measure time spent in audio thread; both average and peak should be well under 1 ms.
#ifdef SA_DEBUG
unsigned int audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
if (audio_time - m_last_dump_time > 5000000000) // print every 5 seconds
{
std::cout << "Analyzer audio thread: " << m_sum_execution / m_dump_count << " ms avg / "
<< m_max_execution << " ms peak." << std::endl;
m_last_dump_time = audio_time;
m_sum_execution = m_max_execution = m_dump_count = 0;
}
#endif
if (!isEnabled() || !isRunning ()) {return false;}
if (m_controls.isViewVisible()) {m_processor.analyse(buffer, frame_count);}
// Skip processing if the controls dialog isn't visible, it would only waste CPU cycles.
if (m_controls.isViewVisible())
{
// To avoid processing spikes on audio thread, data are stored in
// a lockless ringbuffer and processed in a separate thread.
m_inputBuffer.write(buffer, frame_count, true);
}
#ifdef SA_DEBUG
audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - audio_time;
m_dump_count++;
m_sum_execution += audio_time / 1000000.0;
if (audio_time / 1000000.0 > m_max_execution) {m_max_execution = audio_time / 1000000.0;}
#endif
return isRunning();
}

View File

@@ -27,7 +27,11 @@
#ifndef ANALYZER_H
#define ANALYZER_H
#include <QWaitCondition>
#include "DataprocLauncher.h"
#include "Effect.h"
#include "LocklessRingBuffer.h"
#include "SaControls.h"
#include "SaProcessor.h"
@@ -37,7 +41,7 @@ class Analyzer : public Effect
{
public:
Analyzer(Model *parent, const Descriptor::SubPluginFeatures::Key *key);
virtual ~Analyzer() {};
virtual ~Analyzer();
bool processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) override;
EffectControls *controls() override {return &m_controls;}
@@ -47,6 +51,24 @@ public:
private:
SaProcessor m_processor;
SaControls m_controls;
// Maximum LMMS buffer size (hard coded, the actual constant is hard to get)
const unsigned int m_maxBufferSize = 4096;
// QThread::create() workaround
// Replace DataprocLauncher by QThread and replace initializer in constructor
// with the following commented line when LMMS CI starts using Qt > 5.9
//m_processorThread = QThread::create([=]{m_processor.analyze(m_inputBuffer);});
DataprocLauncher m_processorThread;
LocklessRingBuffer<sampleFrame> m_inputBuffer;
#ifdef SA_DEBUG
int m_last_dump_time;
int m_dump_count;
float m_sum_execution;
float m_max_execution;
#endif
};
#endif // ANALYZER_H

View File

@@ -1,5 +1,7 @@
INCLUDE(BuildPlugin)
INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS})
LINK_LIBRARIES(${FFTW3F_LIBRARIES})
BUILD_PLUGIN(analyzer Analyzer.cpp SaProcessor.cpp SaControls.cpp SaControlsDialog.cpp SaSpectrumView.cpp SaWaterfallView.cpp
MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h EMBEDDED_RESOURCES *.svg logo.png)
MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h DataprocLauncher.h EMBEDDED_RESOURCES *.svg logo.png)

View File

@@ -0,0 +1,52 @@
/*
* DataprocLauncher.h - QThread::create workaround for older Qt version
*
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef DATAPROCLAUNCHER_H
#define DATAPROCLAUNCHER_H
#include <QThread>
#include "SaProcessor.h"
#include "LocklessRingBuffer.h"
class DataprocLauncher : public QThread
{
public:
explicit DataprocLauncher(SaProcessor &proc, LocklessRingBuffer<sampleFrame> &buffer)
: m_processor(&proc),
m_inputBuffer(&buffer)
{
}
private:
void run() override
{
m_processor->analyze(*m_inputBuffer);
}
SaProcessor *m_processor;
LocklessRingBuffer<sampleFrame> *m_inputBuffer;
};
#endif // DATAPROCLAUNCHER_H

View File

@@ -4,13 +4,41 @@
This plugin consists of three widgets and back-end code to provide them with required data.
The top-level widget is SaControlDialog. It populates a configuration widget (created dynamically) and instantiates spectrum display widgets. Its main back-end class is SaControls, which holds all configuration values and globally valid constants (e.g. range definitions).
The top-level widget is `SaControlDialog`. It populates configuration widgets (created dynamically) and instantiates spectrum display widgets. Its main back-end class is `SaControls`, which holds all configuration values.
SaSpectrumDisplay and SaWaterfallDisplay show the result of spectrum analysis. Their main back-end class is SaProcessor, which performs FFT analysis on data received from the Analyzer class, which in turn handles the interface with LMMS.
`SaSpectrumView` and `SaWaterfallView` widgets show the result of spectrum analysis. Their main back-end class is `SaProcessor`, which performs FFT analysis on data received from the `Analyzer` class, which in turn handles the interface with LMMS.
## Threads
The Spectrum Analyzer is involved in three different threads:
- **Effect mixer thread**: periodically calls `Analyzer::processAudioBuffer()` to provide the plugin with more data. This thread is real-time sensitive -- any latency spikes can potentially cause interruptions in the audio stream. For this reason, `Analyzer::processAudioBuffer()` must finish as fast as possible and must not call any functions that could cause it to be delayed for unpredictable amount of time. A lock-less ring buffer is used to safely feed data to the FFT analysis thread without risking any latency spikes due to a shared mutex being unavailable at the time of writing.
- **FFT analysis thread**: a standalone thread formed by the `SaProcessor::analyze()` function. Takes in data from the ring buffer, performs FFT analysis and prepares results for display. This thread is not real-time sensitive but excessive locking is discouraged to maintain good performance.
- **GUI thread**: periodically triggers `paintEvent()` of all Qt widgets, including `SaSpectrumView` and `SaWaterfallView`. While it is not as sensitive to latency spikes as the effect mixer thread, the `paintEvent()`s appear to be called sequentially and the execution time of each widget therefore adds to the total time needed to complete one full refresh cycle. This means the maximum frame rate of the Qt GUI will be limited to `1 / total_execution_time`. Good performance of the `paintEvent()` functions should be therefore kept in mind.
## Changelog
1.1.2 2019-11-18
- waterfall is no longer cut short when width limit is reached
- various small tweaks based on final review
1.1.1 2019-10-13
- improved interface for accessing SaProcessor private data
- readme file update
- other small improvements based on reviews
1.1.0 2019-08-29
- advanced config: expose hidden constants to user
- advanced config: add support for FFT window overlapping
- waterfall: display at native resolution on high-DPI screens
- waterfall: add cursor and improve label density
- FFT: fix normalization so that 0 dBFS matches full-scale sinewave
- FFT: decouple data acquisition from processing and display
- FFT: separate lock for reallocation (to avoid some needless waiting)
- moved ranges and other constants to a separate file
- debug: better performance measurements
- various performance optimizations
1.0.3 2019-07-25
- rename and tweak amplitude ranges based on feedback
1.0.2 2019-07-12
- variety of small changes based on code review
1.0.1 2019-06-02
- code style changes
- added tool-tips

View File

@@ -50,7 +50,17 @@ SaControls::SaControls(Analyzer *effect) :
m_freqRangeModel(this, tr("Frequency range")),
m_ampRangeModel(this, tr("Amplitude range")),
m_blockSizeModel(this, tr("FFT block size")),
m_windowModel(this, tr("FFT window type"))
m_windowModel(this, tr("FFT window type")),
// Advanced settings knobs
m_envelopeResolutionModel(0.25f, 0.1f, 3.0f, 0.05f, this, tr("Peak envelope resolution")),
m_spectrumResolutionModel(1.5f, 0.1f, 3.0f, 0.05f, this, tr("Spectrum display resolution")),
m_peakDecayFactorModel(0.992f, 0.95f, 0.999f, 0.001f, this, tr("Peak decay multiplier")),
m_averagingWeightModel(0.15f, 0.01f, 0.5f, 0.01f, this, tr("Averaging weight")),
m_waterfallHeightModel(300.0f, 50.0f, 1000.0f, 50.0f, this, tr("Waterfall history size")),
m_waterfallGammaModel(0.30f, 0.10f, 1.00f, 0.05f, this, tr("Waterfall gamma correction")),
m_windowOverlapModel(2.0f, 1.0f, 4.0f, 1.0f, this, tr("FFT window overlap")),
m_zeroPaddingModel(2.0f, 0.0f, 4.0f, 1.0f, this, tr("FFT zero padding"))
{
// Frequency and amplitude ranges; order must match
// FREQUENCY_RANGES and AMPLITUDE_RANGES defined in SaControls.h
@@ -62,10 +72,10 @@ SaControls::SaControls(Analyzer *effect) :
m_freqRangeModel.setValue(m_freqRangeModel.findText(tr("Full (auto)")));
m_ampRangeModel.addItem(tr("Extended"));
m_ampRangeModel.addItem(tr("Default"));
m_ampRangeModel.addItem(tr("Audible"));
m_ampRangeModel.addItem(tr("Noise"));
m_ampRangeModel.setValue(m_ampRangeModel.findText(tr("Default")));
m_ampRangeModel.addItem(tr("Loud"));
m_ampRangeModel.addItem(tr("Silent"));
m_ampRangeModel.setValue(m_ampRangeModel.findText(tr("Audible")));
// FFT block size labels are generated automatically, based on
// FFT_BLOCK_SIZES vector defined in fft_helpers.h
@@ -95,12 +105,15 @@ SaControls::SaControls(Analyzer *effect) :
// Colors
// Background color is defined by Qt / theme.
// Make sure the sum of colors for L and R channel stays lower or equal
// to 255. Otherwise the Waterfall pixels may overflow back to 0 even when
// the input signal isn't clipping (over 1.0).
// Make sure the sum of colors for L and R channel results into a neutral
// color that has at least one component equal to 255 (i.e. ideally white).
// This means the color overflows to zero exactly when signal reaches
// clipping threshold, indicating the problematic frequency to user.
// Mono waterfall color should have similarly at least one component at 255.
m_colorL = QColor(51, 148, 204, 135);
m_colorR = QColor(204, 107, 51, 135);
m_colorMono = QColor(51, 148, 204, 204);
m_colorMonoW = QColor(64, 185, 255, 255);
m_colorBG = QColor(7, 7, 7, 255); // ~20 % gray (after gamma correction)
m_colorGrid = QColor(30, 34, 38, 255); // ~40 % gray (slightly cold / blue)
m_colorLabels = QColor(192, 202, 212, 255); // ~90 % gray (slightly cold / blue)
@@ -126,6 +139,15 @@ void SaControls::loadSettings(const QDomElement &_this)
m_ampRangeModel.loadSettings(_this, "RangeY");
m_blockSizeModel.loadSettings(_this, "BlockSize");
m_windowModel.loadSettings(_this, "WindowType");
m_envelopeResolutionModel.loadSettings(_this, "EnvelopeRes");
m_spectrumResolutionModel.loadSettings(_this, "SpectrumRes");
m_peakDecayFactorModel.loadSettings(_this, "PeakDecayFactor");
m_averagingWeightModel.loadSettings(_this, "AverageWeight");
m_waterfallHeightModel.loadSettings(_this, "WaterfallHeight");
m_waterfallGammaModel.loadSettings(_this, "WaterfallGamma");
m_windowOverlapModel.loadSettings(_this, "WindowOverlap");
m_zeroPaddingModel.loadSettings(_this, "ZeroPadding");
}
@@ -141,4 +163,14 @@ void SaControls::saveSettings(QDomDocument &doc, QDomElement &parent)
m_ampRangeModel.saveSettings(doc, parent, "RangeY");
m_blockSizeModel.saveSettings(doc, parent, "BlockSize");
m_windowModel.saveSettings(doc, parent, "WindowType");
m_envelopeResolutionModel.saveSettings(doc, parent, "EnvelopeRes");
m_spectrumResolutionModel.saveSettings(doc, parent, "SpectrumRes");
m_peakDecayFactorModel.saveSettings(doc, parent, "PeakDecayFactor");
m_averagingWeightModel.saveSettings(doc, parent, "AverageWeight");
m_waterfallHeightModel.saveSettings(doc, parent, "WaterfallHeight");
m_waterfallGammaModel.saveSettings(doc, parent, "WaterfallGamma");
m_windowOverlapModel.saveSettings(doc, parent, "WindowOverlap");
m_zeroPaddingModel.saveSettings(doc, parent, "ZeroPadding");
}

View File

@@ -27,52 +27,10 @@
#include "ComboBoxModel.h"
#include "EffectControls.h"
#include "lmms_constants.h"
//#define SA_DEBUG 1 // define SA_DEBUG to enable performance measurements
// Frequency ranges (in Hz).
// Full range is defined by LOWEST_LOG_FREQ and current sample rate.
const int LOWEST_LOG_FREQ = 10; // arbitrary low limit for log. scale, >1
enum FREQUENCY_RANGES
{
FRANGE_FULL = 0,
FRANGE_AUDIBLE,
FRANGE_BASS,
FRANGE_MIDS,
FRANGE_HIGH
};
const int FRANGE_AUDIBLE_START = 20;
const int FRANGE_AUDIBLE_END = 20000;
const int FRANGE_BASS_START = 20;
const int FRANGE_BASS_END = 300;
const int FRANGE_MIDS_START = 200;
const int FRANGE_MIDS_END = 5000;
const int FRANGE_HIGH_START = 4000;
const int FRANGE_HIGH_END = 20000;
// Amplitude ranges.
// Reference: sine wave from -1.0 to 1.0 = 0 dB.
// I.e. if master volume is 100 %, positive values signify clipping.
// Doubling or halving the amplitude produces 3 dB difference.
enum AMPLITUDE_RANGES
{
ARANGE_EXTENDED = 0,
ARANGE_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;
@@ -90,11 +48,12 @@ public:
void loadSettings (const QDomElement &_this) override;
QString nodeName() const override {return "Analyzer";}
int controlCount() override {return 12;}
int controlCount() override {return 20;}
private:
Analyzer *m_effect;
// basic settings
BoolModel m_pauseModel;
BoolModel m_refFreezeModel;
@@ -111,12 +70,24 @@ private:
ComboBoxModel m_blockSizeModel;
ComboBoxModel m_windowModel;
QColor m_colorL;
QColor m_colorR;
QColor m_colorMono;
QColor m_colorBG;
QColor m_colorGrid;
QColor m_colorLabels;
// advanced settings
FloatModel m_envelopeResolutionModel;
FloatModel m_spectrumResolutionModel;
FloatModel m_peakDecayFactorModel;
FloatModel m_averagingWeightModel;
FloatModel m_waterfallHeightModel;
FloatModel m_waterfallGammaModel;
FloatModel m_windowOverlapModel;
FloatModel m_zeroPaddingModel;
// colors (hard-coded, values must add up to specific numbers)
QColor m_colorL; //!< color of the left channel
QColor m_colorR; //!< color of the right channel
QColor m_colorMono; //!< mono color for spectrum display
QColor m_colorMonoW; //!< mono color for waterfall display
QColor m_colorBG; //!< spectrum display background color
QColor m_colorGrid; //!< color of grid lines
QColor m_colorLabels; //!< color of axis labels
friend class SaControlsDialog;
friend class SaSpectrumView;

View File

@@ -34,6 +34,7 @@
#include "ComboBoxModel.h"
#include "embed.h"
#include "Engine.h"
#include "Knob.h"
#include "LedCheckbox.h"
#include "PixmapButton.h"
#include "SaControls.h"
@@ -53,13 +54,24 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor)
master_layout->setContentsMargins(2, 6, 2, 8);
setLayout(master_layout);
// QSplitter top: configuration section
// Display splitter top: controls section
QWidget *controls_widget = new QWidget;
QHBoxLayout *controls_layout = new QHBoxLayout;
controls_layout->setContentsMargins(0, 0, 0, 0);
controls_widget->setLayout(controls_layout);
controls_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
controls_widget->setMaximumHeight(m_configHeight);
display_splitter->addWidget(controls_widget);
// Basic configuration
QWidget *config_widget = new QWidget;
QGridLayout *config_layout = new QGridLayout;
config_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
config_widget->setMaximumHeight(m_configHeight);
config_widget->setLayout(config_layout);
display_splitter->addWidget(config_widget);
controls_layout->addWidget(config_widget);
controls_layout->setStretchFactor(config_widget, 10);
// Pre-compute target pixmap size based on monitor DPI.
// Using setDevicePixelRatio() on pixmap allows the SVG image to be razor
@@ -67,6 +79,8 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor)
// enlarged. No idea how to make Qt do it in a more reasonable way.
QSize iconSize = QSize(22.0 * devicePixelRatio(), 22.0 * devicePixelRatio());
QSize buttonSize = 1.2 * iconSize;
QSize advButtonSize = QSize((m_configHeight * devicePixelRatio()) / 3, m_configHeight * devicePixelRatio());
// pause and freeze buttons
PixmapButton *pauseButton = new PixmapButton(this, tr("Pause"));
@@ -79,7 +93,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor)
pauseButton->setInactiveGraphic(*pauseOffPixmap);
pauseButton->setCheckable(true);
pauseButton->setModel(&controls->m_pauseModel);
config_layout->addWidget(pauseButton, 0, 0, 2, 1);
config_layout->addWidget(pauseButton, 0, 0, 2, 1, Qt::AlignHCenter);
PixmapButton *refFreezeButton = new PixmapButton(this, tr("Reference freeze"));
refFreezeButton->setToolTip(tr("Freeze current input as a reference / disable falloff in peak-hold mode."));
@@ -91,7 +105,7 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor)
refFreezeButton->setInactiveGraphic(*freezeOffPixmap);
refFreezeButton->setCheckable(true);
refFreezeButton->setModel(&controls->m_refFreezeModel);
config_layout->addWidget(refFreezeButton, 2, 0, 2, 1);
config_layout->addWidget(refFreezeButton, 2, 0, 2, 1, Qt::AlignHCenter);
// misc configuration switches
LedCheckBox *waterfallButton = new LedCheckBox(tr("Waterfall"), this);
@@ -194,6 +208,117 @@ SaControlsDialog::SaControlsDialog(SaControls *controls, SaProcessor *processor)
processor->rebuildWindow();
connect(&controls->m_windowModel, &ComboBoxModel::dataChanged, [=] {processor->rebuildWindow();});
// set stretch factors so that combo boxes expand first
config_layout->setColumnStretch(3, 2);
config_layout->setColumnStretch(5, 3);
// Advanced configuration
QWidget *advanced_widget = new QWidget;
QGridLayout *advanced_layout = new QGridLayout;
advanced_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
advanced_widget->setMaximumHeight(m_configHeight);
advanced_widget->setLayout(advanced_layout);
advanced_widget->hide();
controls_layout->addWidget(advanced_widget);
controls_layout->setStretchFactor(advanced_widget, 10);
// Peak envelope resolution
Knob *envelopeResolutionKnob = new Knob(knobSmall_17, this);
envelopeResolutionKnob->setModel(&controls->m_envelopeResolutionModel);
envelopeResolutionKnob->setLabel(tr("Envelope res."));
envelopeResolutionKnob->setToolTip(tr("Increase envelope resolution for better details, decrease for better GUI performance."));
envelopeResolutionKnob->setHintText(tr("Draw at most"), tr(" envelope points per pixel"));
advanced_layout->addWidget(envelopeResolutionKnob, 0, 0, 1, 1, Qt::AlignCenter);
// Spectrum graph resolution
Knob *spectrumResolutionKnob = new Knob(knobSmall_17, this);
spectrumResolutionKnob->setModel(&controls->m_spectrumResolutionModel);
spectrumResolutionKnob->setLabel(tr("Spectrum res."));
spectrumResolutionKnob->setToolTip(tr("Increase spectrum resolution for better details, decrease for better GUI performance."));
spectrumResolutionKnob->setHintText(tr("Draw at most"), tr(" spectrum points per pixel"));
advanced_layout->addWidget(spectrumResolutionKnob, 1, 0, 1, 1, Qt::AlignCenter);
// Peak falloff speed
Knob *peakDecayFactorKnob = new Knob(knobSmall_17, this);
peakDecayFactorKnob->setModel(&controls->m_peakDecayFactorModel);
peakDecayFactorKnob->setLabel(tr("Falloff factor"));
peakDecayFactorKnob->setToolTip(tr("Decrease to make peaks fall faster."));
peakDecayFactorKnob->setHintText(tr("Multiply buffered value by"), "");
advanced_layout->addWidget(peakDecayFactorKnob, 0, 1, 1, 1, Qt::AlignCenter);
// Averaging weight
Knob *averagingWeightKnob = new Knob(knobSmall_17, this);
averagingWeightKnob->setModel(&controls->m_averagingWeightModel);
averagingWeightKnob->setLabel(tr("Averaging weight"));
averagingWeightKnob->setToolTip(tr("Decrease to make averaging slower and smoother."));
averagingWeightKnob->setHintText(tr("New sample contributes"), "");
advanced_layout->addWidget(averagingWeightKnob, 1, 1, 1, 1, Qt::AlignCenter);
// Waterfall history size
Knob *waterfallHeightKnob = new Knob(knobSmall_17, this);
waterfallHeightKnob->setModel(&controls->m_waterfallHeightModel);
waterfallHeightKnob->setLabel(tr("Waterfall height"));
waterfallHeightKnob->setToolTip(tr("Increase to get slower scrolling, decrease to see fast transitions better. Warning: medium CPU usage."));
waterfallHeightKnob->setHintText(tr("Keep"), tr(" lines"));
advanced_layout->addWidget(waterfallHeightKnob, 0, 2, 1, 1, Qt::AlignCenter);
processor->reallocateBuffers();
connect(&controls->m_waterfallHeightModel, &FloatModel::dataChanged, [=] {processor->reallocateBuffers();});
// Waterfall gamma correction
Knob *waterfallGammaKnob = new Knob(knobSmall_17, this);
waterfallGammaKnob->setModel(&controls->m_waterfallGammaModel);
waterfallGammaKnob->setLabel(tr("Waterfall gamma"));
waterfallGammaKnob->setToolTip(tr("Decrease to see very weak signals, increase to get better contrast."));
waterfallGammaKnob->setHintText(tr("Gamma value:"), "");
advanced_layout->addWidget(waterfallGammaKnob, 1, 2, 1, 1, Qt::AlignCenter);
// FFT window overlap
Knob *windowOverlapKnob = new Knob(knobSmall_17, this);
windowOverlapKnob->setModel(&controls->m_windowOverlapModel);
windowOverlapKnob->setLabel(tr("Window overlap"));
windowOverlapKnob->setToolTip(tr("Increase to prevent missing fast transitions arriving near FFT window edges. Warning: high CPU usage."));
windowOverlapKnob->setHintText(tr("Each sample processed"), tr(" times"));
advanced_layout->addWidget(windowOverlapKnob, 0, 3, 1, 1, Qt::AlignCenter);
// FFT zero padding
Knob *zeroPaddingKnob = new Knob(knobSmall_17, this);
zeroPaddingKnob->setModel(&controls->m_zeroPaddingModel);
zeroPaddingKnob->setLabel(tr("Zero padding"));
zeroPaddingKnob->setToolTip(tr("Increase to get smoother-looking spectrum. Warning: high CPU usage."));
zeroPaddingKnob->setHintText(tr("Processing buffer is"), tr(" steps larger than input block"));
advanced_layout->addWidget(zeroPaddingKnob, 1, 3, 1, 1, Qt::AlignCenter);
processor->reallocateBuffers();
connect(&controls->m_zeroPaddingModel, &FloatModel::dataChanged, [=] {processor->reallocateBuffers();});
// Advanced settings button
PixmapButton *advancedButton = new PixmapButton(this, tr("Advanced settings"));
advancedButton->setToolTip(tr("Access advanced settings"));
QPixmap *advancedOnPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_on").scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
QPixmap *advancedOffPixmap = new QPixmap(PLUGIN_NAME::getIconPixmap("advanced_off").scaled(advButtonSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
advancedOnPixmap->setDevicePixelRatio(devicePixelRatio());
advancedOffPixmap->setDevicePixelRatio(devicePixelRatio());
advancedButton->setActiveGraphic(*advancedOnPixmap);
advancedButton->setInactiveGraphic(*advancedOffPixmap);
advancedButton->setCheckable(true);
controls_layout->addStretch(0);
controls_layout->addWidget(advancedButton);
connect(advancedButton, &PixmapButton::toggled, [=](bool checked)
{
if (checked)
{
config_widget->hide();
advanced_widget->show();
}
else
{
config_widget->show();
advanced_widget->hide();
}
}
);
// QSplitter middle and bottom: spectrum display widgets
m_spectrum = new SaSpectrumView(controls, processor, this);

View File

@@ -26,15 +26,23 @@
#include "SaProcessor.h"
#include <algorithm>
#ifdef SA_DEBUG
#include <chrono>
#endif
#include <cmath>
#include <iostream>
#ifdef SA_DEBUG
#include <iomanip>
#include <iostream>
#endif
#include <QMutexLocker>
#include "lmms_math.h"
#include "LocklessRingBuffer.h"
SaProcessor::SaProcessor(SaControls *controls) :
SaProcessor::SaProcessor(const SaControls *controls) :
m_controls(controls),
m_terminate(false),
m_inBlockSize(FFT_BLOCK_SIZES[0]),
m_fftBlockSize(FFT_BLOCK_SIZES[0]),
m_sampleRate(Engine::mixer()->processingSampleRate()),
@@ -47,21 +55,23 @@ SaProcessor::SaProcessor(SaControls *controls) :
m_fftWindow.resize(m_inBlockSize, 1.0);
precomputeWindow(m_fftWindow.data(), m_inBlockSize, BLACKMAN_HARRIS);
m_bufferL.resize(m_fftBlockSize, 0);
m_bufferR.resize(m_fftBlockSize, 0);
m_bufferL.resize(m_inBlockSize, 0);
m_bufferR.resize(m_inBlockSize, 0);
m_filteredBufferL.resize(m_fftBlockSize, 0);
m_filteredBufferR.resize(m_fftBlockSize, 0);
m_spectrumL = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex));
m_spectrumR = (fftwf_complex *) fftwf_malloc(binCount() * sizeof (fftwf_complex));
m_fftPlanL = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferL.data(), m_spectrumL, FFTW_MEASURE);
m_fftPlanR = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_bufferR.data(), m_spectrumR, FFTW_MEASURE);
m_fftPlanL = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_filteredBufferL.data(), m_spectrumL, FFTW_MEASURE);
m_fftPlanR = fftwf_plan_dft_r2c_1d(m_fftBlockSize, m_filteredBufferR.data(), m_spectrumR, FFTW_MEASURE);
m_absSpectrumL.resize(binCount(), 0);
m_absSpectrumR.resize(binCount(), 0);
m_normSpectrumL.resize(binCount(), 0);
m_normSpectrumR.resize(binCount(), 0);
m_history.resize(binCount() * m_waterfallHeight * sizeof qRgb(0,0,0), 0);
clear();
m_waterfallHeight = 100; // a small safe value
m_history_work.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0);
m_history.resize(waterfallWidth() * m_waterfallHeight * sizeof qRgb(0,0,0), 0);
}
@@ -79,169 +89,229 @@ SaProcessor::~SaProcessor()
}
// Load a batch of data from LMMS; run FFT analysis if buffer is full enough.
void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count)
// Load data from audio thread ringbuffer and run FFT analysis if buffer is full enough.
void SaProcessor::analyze(LocklessRingBuffer<sampleFrame> &ring_buffer)
{
#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())
LocklessRingBufferReader<sampleFrame> reader(ring_buffer);
// Processing thread loop
while (!m_terminate)
{
const bool stereo = m_controls->m_stereoModel.value();
fpp_t in_frame = 0;
while (in_frame < frame_count)
// If there is nothing to read, wait for notification from the writing side.
if (reader.empty()) {reader.waitForData();}
// skip waterfall render if processing can't keep up with input
bool overload = ring_buffer.free() < ring_buffer.capacity() / 2;
auto in_buffer = reader.read_max(ring_buffer.capacity() / 4);
std::size_t frame_count = in_buffer.size();
// Process received data only if any view is visible and not paused.
// Also, to prevent a momentary GUI freeze under high load (due to lock
// starvation), skip analysis when buffer reallocation is requested.
if ((m_spectrumActive || m_waterfallActive) && !m_controls->m_pauseModel.value() && !m_reallocating)
{
// fill sample buffers and check for zero input
bool block_empty = true;
for (; in_frame < frame_count && m_framesFilledUp < m_inBlockSize; in_frame++, m_framesFilledUp++)
const bool stereo = m_controls->m_stereoModel.value();
fpp_t in_frame = 0;
while (in_frame < frame_count)
{
// Lock data access to prevent reallocation from changing
// buffers and control variables.
QMutexLocker data_lock(&m_dataAccess);
// Fill sample buffers and check for zero input.
bool block_empty = true;
for (; in_frame < frame_count && m_framesFilledUp < m_inBlockSize; in_frame++, m_framesFilledUp++)
{
if (stereo)
{
m_bufferL[m_framesFilledUp] = in_buffer[in_frame][0];
m_bufferR[m_framesFilledUp] = in_buffer[in_frame][1];
}
else
{
m_bufferL[m_framesFilledUp] =
m_bufferR[m_framesFilledUp] = (in_buffer[in_frame][0] + in_buffer[in_frame][1]) * 0.5f;
}
if (in_buffer[in_frame][0] != 0.f || in_buffer[in_frame][1] != 0.f)
{
block_empty = false;
}
}
// Run analysis only if buffers contain enough data.
if (m_framesFilledUp < m_inBlockSize) {break;}
// Print performance analysis once per 2 seconds if debug is enabled
#ifdef SA_DEBUG
unsigned int total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
if (total_time - m_last_dump_time > 2000000000)
{
std::cout << "FFT analysis: " << std::fixed << std::setprecision(2)
<< m_sum_execution / m_dump_count << " ms avg / "
<< m_max_execution << " ms peak, executing "
<< m_dump_count << " times per second ("
<< m_sum_execution / 20.0 << " % CPU usage)." << std::endl;
m_last_dump_time = total_time;
m_sum_execution = m_max_execution = m_dump_count = 0;
}
#endif
// update sample rate
m_sampleRate = Engine::mixer()->processingSampleRate();
// apply FFT window
for (unsigned int i = 0; i < m_inBlockSize; i++)
{
m_filteredBufferL[i] = m_bufferL[i] * m_fftWindow[i];
m_filteredBufferR[i] = m_bufferR[i] * m_fftWindow[i];
}
// Run FFT on left channel, convert the result to absolute magnitude
// spectrum and normalize it.
fftwf_execute(m_fftPlanL);
absspec(m_spectrumL, m_absSpectrumL.data(), binCount());
normalize(m_absSpectrumL, m_normSpectrumL, m_inBlockSize);
// repeat analysis for right channel if stereo processing is enabled
if (stereo)
{
m_bufferL[m_framesFilledUp] = in_buffer[in_frame][0];
m_bufferR[m_framesFilledUp] = in_buffer[in_frame][1];
fftwf_execute(m_fftPlanR);
absspec(m_spectrumR, m_absSpectrumR.data(), binCount());
normalize(m_absSpectrumR, m_normSpectrumR, m_inBlockSize);
}
else
// count empty lines so that empty history does not have to update
if (block_empty && m_waterfallNotEmpty)
{
m_bufferL[m_framesFilledUp] =
m_bufferR[m_framesFilledUp] = (in_buffer[in_frame][0] + in_buffer[in_frame][1]) * 0.5f;
m_waterfallNotEmpty -= 1;
}
if (in_buffer[in_frame][0] != 0.f || in_buffer[in_frame][1] != 0.f)
else if (!block_empty)
{
block_empty = false;
m_waterfallNotEmpty = m_waterfallHeight + 2;
}
}
// 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++)
if (m_waterfallActive && m_waterfallNotEmpty)
{
// Every frequency bin spans a frequency range that must be
// partially or fully mapped to a pixel. Any inconsistency
// may be seen in the spectrogram as dark or white lines --
// play white noise to confirm your change did not break it.
float band_start = freqToXPixel(binToFreq(i) - binBandwidth() / 2.0, binCount());
float band_end = freqToXPixel(binToFreq(i + 1) - binBandwidth() / 2.0, binCount());
if (m_controls->m_logXModel.value())
// move waterfall history one line down and clear the top line
QRgb *pixel = (QRgb *)m_history_work.data();
std::copy(pixel,
pixel + waterfallWidth() * m_waterfallHeight - waterfallWidth(),
pixel + waterfallWidth());
memset(pixel, 0, waterfallWidth() * sizeof (QRgb));
// add newest result on top
int target; // pixel being constructed
float accL = 0; // accumulators for merging multiple bins
float accR = 0;
for (unsigned int i = 0; i < binCount(); i++)
{
// Logarithmic scale
if (band_end - band_start > 1.0)
// fill line with red color to indicate lost data if CPU cannot keep up
if (overload && i < waterfallWidth())
{
// band spans multiple pixels: draw all pixels it covers
for (target = (int)band_start; target < (int)band_end; target++)
pixel[i] = qRgb(42, 0, 0);
continue;
}
// Every frequency bin spans a frequency range that must be
// partially or fully mapped to a pixel. Any inconsistency
// may be seen in the spectrogram as dark or white lines --
// play white noise to confirm your change did not break it.
float band_start = freqToXPixel(binToFreq(i) - binBandwidth() / 2.0, waterfallWidth());
float band_end = freqToXPixel(binToFreq(i + 1) - binBandwidth() / 2.0, waterfallWidth());
if (m_controls->m_logXModel.value())
{
// Logarithmic scale
if (band_end - band_start > 1.0)
{
if (target >= 0 && target < binCount())
// band spans multiple pixels: draw all pixels it covers
for (target = (int)band_start; target < (int)band_end; target++)
{
if (target >= 0 && target < waterfallWidth())
{
pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]);
}
}
// save remaining portion of the band for the following band / pixel
// (in case the next band uses sub-pixel drawing)
accL = (band_end - (int)band_end) * m_normSpectrumL[i];
accR = (band_end - (int)band_end) * m_normSpectrumR[i];
}
else
{
// sub-pixel drawing; add contribution of current band
target = (int)band_start;
if ((int)band_start == (int)band_end)
{
// band ends within current target pixel, accumulate
accL += (band_end - band_start) * m_normSpectrumL[i];
accR += (band_end - band_start) * m_normSpectrumR[i];
}
else
{
// Band ends in the next pixel -- finalize the current pixel.
// Make sure contribution is split correctly on pixel boundary.
accL += ((int)band_end - band_start) * m_normSpectrumL[i];
accR += ((int)band_end - band_start) * m_normSpectrumR[i];
if (target >= 0 && target < waterfallWidth()) {pixel[target] = makePixel(accL, accR);}
// save remaining portion of the band for the following band / pixel
accL = (band_end - (int)band_end) * m_normSpectrumL[i];
accR = (band_end - (int)band_end) * m_normSpectrumR[i];
}
}
}
else
{
// Linear: always draws one or more pixels per band
for (target = (int)band_start; target < band_end; target++)
{
if (target >= 0 && target < waterfallWidth())
{
pixel[target] = makePixel(m_normSpectrumL[i], m_normSpectrumR[i]);
}
}
// 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
// Copy work buffer to result buffer. Done only if requested, so
// that time isn't wasted on updating faster than display FPS.
// (The copy is about as expensive as the movement.)
if (m_flipRequest)
{
// 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]);
}
}
m_history = m_history_work;
m_flipRequest = false;
}
}
}
#ifdef SA_DEBUG
// report FFT processing speed
start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time;
std::cout << "Processed " << m_framesFilledUp << " samples in " << start_time / 1000000.0 << " ms" << std::endl;
#endif
// clean up before checking for more data from input buffer
const unsigned int overlaps = m_controls->m_windowOverlapModel.value();
if (overlaps == 1) // Discard buffer, each sample used only once
{
m_framesFilledUp = 0;
}
else
{
// Drop only a part of the buffer from the beginning, so that new
// data can be added to the end. This means the older samples will
// be analyzed again, but in a different position in the window,
// making short transient signals show up better in the waterfall.
const unsigned int drop = m_inBlockSize / overlaps;
std::move(m_bufferL.begin() + drop, m_bufferL.end(), m_bufferL.begin());
std::move(m_bufferR.begin() + drop, m_bufferR.end(), m_bufferR.begin());
m_framesFilledUp -= drop;
}
// clean up before checking for more data from input buffer
m_framesFilledUp = 0;
}
}
#ifdef SA_DEBUG
// measure overall FFT processing speed
total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time;
m_dump_count++;
m_sum_execution += total_time / 1000000.0;
if (total_time / 1000000.0 > m_max_execution) {m_max_execution = total_time / 1000000.0;}
#endif
} // frame filler and processing
} // process if active
} // thread loop end
}
@@ -251,8 +321,9 @@ void SaProcessor::analyse(sampleFrame *in_buffer, const fpp_t frame_count)
// Gamma correction is applied to make small values more visible and to make
// a linear gradient actually appear roughly linear. The correction should be
// around 0.42 to 0.45 for sRGB displays (or lower for bigger visibility boost).
QRgb SaProcessor::makePixel(float left, float right, float gamma_correction) const
QRgb SaProcessor::makePixel(float left, float right) const
{
const float gamma_correction = m_controls->m_waterfallGammaModel.value();
if (m_controls->m_stereoModel.value())
{
float ampL = pow(left, gamma_correction);
@@ -265,9 +336,9 @@ QRgb SaProcessor::makePixel(float left, float right, float gamma_correction) con
{
float ampL = pow(left, gamma_correction);
// make mono color brighter to compensate for the fact it is not summed
return qRgb(m_controls->m_colorMono.lighter().red() * ampL,
m_controls->m_colorMono.lighter().green() * ampL,
m_controls->m_colorMono.lighter().blue() * ampL);
return qRgb(m_controls->m_colorMonoW.red() * ampL,
m_controls->m_colorMonoW.green() * ampL,
m_controls->m_colorMonoW.blue() * ampL);
}
}
@@ -301,6 +372,7 @@ void SaProcessor::reallocateBuffers()
{
new_in_size = FFT_BLOCK_SIZES.back();
}
m_zeroPadFactor = m_controls->m_zeroPaddingModel.value();
if (new_size_index + m_zeroPadFactor < FFT_BLOCK_SIZES.size())
{
new_fft_size = FFT_BLOCK_SIZES[new_size_index + m_zeroPadFactor];
@@ -312,12 +384,16 @@ void SaProcessor::reallocateBuffers()
new_bins = new_fft_size / 2 +1;
// Lock data shared with SaSpectrumView and SaWaterfallView.
// The m_reallocating is here to tell analyse() to avoid asking for the
// lock, since fftw3 can take a while to find the fastest FFT algorithm
// for given machine, which would produce interruption in the audio stream.
// Use m_reallocating to tell analyze() to avoid asking for the lock. This
// is needed because under heavy load the FFT thread requests data lock so
// often that this routine could end up waiting even for several seconds.
m_reallocating = true;
QMutexLocker lock(&m_dataAccess);
// Lock data shared with SaSpectrumView and SaWaterfallView.
// Reallocation lock must be acquired first to avoid deadlock (a view class
// may already have it and request the "stronger" data lock on top of that).
QMutexLocker reloc_lock(&m_reallocationAccess);
QMutexLocker data_lock(&m_dataAccess);
// destroy old FFT plan and free the result buffer
if (m_fftPlanL != NULL) {fftwf_destroy_plan(m_fftPlanL);}
@@ -328,30 +404,42 @@ void SaProcessor::reallocateBuffers()
// allocate new space, create new plan and resize containers
m_fftWindow.resize(new_in_size, 1.0);
precomputeWindow(m_fftWindow.data(), new_in_size, (FFT_WINDOWS) m_controls->m_windowModel.value());
m_bufferL.resize(new_fft_size, 0);
m_bufferR.resize(new_fft_size, 0);
m_bufferL.resize(new_in_size, 0);
m_bufferR.resize(new_in_size, 0);
m_filteredBufferL.resize(new_fft_size, 0);
m_filteredBufferR.resize(new_fft_size, 0);
m_spectrumL = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex));
m_spectrumR = (fftwf_complex *) fftwf_malloc(new_bins * sizeof (fftwf_complex));
m_fftPlanL = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferL.data(), m_spectrumL, FFTW_MEASURE);
m_fftPlanR = fftwf_plan_dft_r2c_1d(new_fft_size, m_bufferR.data(), m_spectrumR, FFTW_MEASURE);
m_fftPlanL = fftwf_plan_dft_r2c_1d(new_fft_size, m_filteredBufferL.data(), m_spectrumL, FFTW_MEASURE);
m_fftPlanR = fftwf_plan_dft_r2c_1d(new_fft_size, m_filteredBufferR.data(), m_spectrumR, FFTW_MEASURE);
if (m_fftPlanL == NULL || m_fftPlanR == NULL)
{
std::cerr << "Failed to create new FFT plan!" << std::endl;
#ifdef SA_DEBUG
std::cerr << "Analyzer: failed to create new FFT plan!" << std::endl;
#endif
}
m_absSpectrumL.resize(new_bins, 0);
m_absSpectrumR.resize(new_bins, 0);
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);
m_waterfallHeight = m_controls->m_waterfallHeightModel.value();
m_history_work.resize((new_bins < m_waterfallMaxWidth ? new_bins : m_waterfallMaxWidth)
* m_waterfallHeight
* sizeof qRgb(0,0,0), 0);
m_history.resize((new_bins < m_waterfallMaxWidth ? new_bins : m_waterfallMaxWidth)
* m_waterfallHeight
* sizeof qRgb(0,0,0), 0);
// done; publish new sizes and clean up
m_inBlockSize = new_in_size;
m_fftBlockSize = new_fft_size;
lock.unlock();
data_lock.unlock();
reloc_lock.unlock();
m_reallocating = false;
clear();
}
@@ -369,17 +457,39 @@ void SaProcessor::rebuildWindow()
// Note: may take a few milliseconds, do not call in a loop!
void SaProcessor::clear()
{
const unsigned int overlaps = m_controls->m_windowOverlapModel.value();
QMutexLocker lock(&m_dataAccess);
m_framesFilledUp = 0;
// If there is any window overlap, leave space only for the new samples
// and treat the rest at initialized with zeros. Prevents missing
// transients at the start of the very first block.
m_framesFilledUp = m_inBlockSize - m_inBlockSize / overlaps;
std::fill(m_bufferL.begin(), m_bufferL.end(), 0);
std::fill(m_bufferR.begin(), m_bufferR.end(), 0);
std::fill(m_filteredBufferL.begin(), m_filteredBufferL.end(), 0);
std::fill(m_filteredBufferR.begin(), m_filteredBufferR.end(), 0);
std::fill(m_absSpectrumL.begin(), m_absSpectrumL.end(), 0);
std::fill(m_absSpectrumR.begin(), m_absSpectrumR.end(), 0);
std::fill(m_normSpectrumL.begin(), m_normSpectrumL.end(), 0);
std::fill(m_normSpectrumR.begin(), m_normSpectrumR.end(), 0);
std::fill(m_history_work.begin(), m_history_work.end(), 0);
std::fill(m_history.begin(), m_history.end(), 0);
}
// Clear only history work buffer. Used to flush old data when waterfall
// is shown after a period of inactivity.
void SaProcessor::clearHistory()
{
QMutexLocker lock(&m_dataAccess);
std::fill(m_history_work.begin(), m_history_work.end(), 0);
}
// Check if result buffers contain any non-zero values
bool SaProcessor::spectrumNotEmpty()
{
QMutexLocker lock(&m_reallocationAccess);
return notEmpty(m_normSpectrumL) || notEmpty(m_normSpectrumR);
}
// --------------------------------------
// Frequency conversion helpers
@@ -407,6 +517,17 @@ unsigned int SaProcessor::binCount() const
}
// Return the final width of waterfall display buffer.
// Normally the waterfall width equals the number of frequency bins, but the
// FFT transform can easily produce more bins than can be reasonably useful for
// currently used display resolutions. This function limits width of the final
// image to a given size, which is then used during waterfall render and display.
unsigned int SaProcessor::waterfallWidth() const
{
return binCount() < m_waterfallMaxWidth ? binCount() : m_waterfallMaxWidth;
}
// Return the center frequency of given frequency bin.
float SaProcessor::binToFreq(unsigned int bin_index) const
{
@@ -499,10 +620,10 @@ float SaProcessor::getAmpRangeMin(bool linear) const
switch (m_controls->m_ampRangeModel.value())
{
case ARANGE_EXTENDED: return ARANGE_EXTENDED_START;
case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START;
case ARANGE_NOISE: return ARANGE_NOISE_START;
case ARANGE_SILENT: return ARANGE_SILENT_START;
case ARANGE_LOUD: return ARANGE_LOUD_START;
default:
case ARANGE_DEFAULT: return ARANGE_DEFAULT_START;
case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START;
}
}
@@ -512,10 +633,10 @@ float SaProcessor::getAmpRangeMax() const
switch (m_controls->m_ampRangeModel.value())
{
case ARANGE_EXTENDED: return ARANGE_EXTENDED_END;
case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END;
case ARANGE_NOISE: return ARANGE_NOISE_END;
case ARANGE_SILENT: return ARANGE_SILENT_END;
case ARANGE_LOUD: return ARANGE_LOUD_END;
default:
case ARANGE_DEFAULT: return ARANGE_DEFAULT_END;
case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END;
}
}

View File

@@ -27,6 +27,7 @@
#ifndef SAPROCESSOR_H
#define SAPROCESSOR_H
#include <atomic>
#include <QColor>
#include <QMutex>
#include <vector>
@@ -34,27 +35,45 @@
#include "fft_helpers.h"
#include "SaControls.h"
template<class T>
class LocklessRingBuffer;
//! Receives audio data, runs FFT analysis and stores the result.
class SaProcessor
{
public:
explicit SaProcessor(SaControls *controls);
explicit SaProcessor(const SaControls *controls);
virtual ~SaProcessor();
void analyse(sampleFrame *in_buffer, const fpp_t frame_count);
// analysis thread and a method to terminate it
void analyze(LocklessRingBuffer<sampleFrame> &ring_buffer);
void terminate() {m_terminate = true;}
// inform processor if any processing is actually required
void setSpectrumActive(bool active);
void setWaterfallActive(bool active);
void flipRequest() {m_flipRequest = true;} // request refresh of history buffer
// configuration is taken from models in SaControls; some changes require
// an exlicit update request (reallocation and window rebuild)
void reallocateBuffers();
void rebuildWindow();
void clear();
void clearHistory();
const float *getSpectrumL() const {return m_normSpectrumL.data();}
const float *getSpectrumR() const {return m_normSpectrumR.data();}
const uchar *getHistory() const {return m_history.data();}
// information about results and unit conversion helpers
unsigned int inBlockSize() const {return m_inBlockSize;}
unsigned int binCount() const; //!< size of output (frequency domain) data block
bool spectrumNotEmpty(); //!< check if result buffers contain any non-zero values
unsigned int waterfallWidth() const; //!< binCount value capped at 3840 (for display)
unsigned int waterfallHeight() const {return m_waterfallHeight;}
bool waterfallNotEmpty() const {return m_waterfallNotEmpty;}
float binToFreq(unsigned int bin_index) const;
float binBandwidth() const;
@@ -72,26 +91,38 @@ public:
float getAmpRangeMin(bool linear = false) const;
float getAmpRangeMax() const;
// data access lock must be acquired by any friendly class that touches
// the results, mainly to prevent unexpected mid-way reallocation
// Reallocation lock prevents the processor from changing size of its buffers.
// It is used to keep consistent bin-to-frequency mapping while drawing the
// spectrum and to make sure reading side does not find itself out of bounds.
// The processor is meanwhile free to work on another block.
QMutex m_reallocationAccess;
// Data access lock prevents the processor from changing both size and content
// of its buffers. It is used when writing to a result buffer, or when a friendly
// class reads them and needs guaranteed data consistency.
// It causes FFT analysis to be paused, so this lock should be used sparingly.
// If using both locks at the same time, reallocation lock MUST be acquired first.
QMutex m_dataAccess;
private:
SaControls *m_controls;
const SaControls *m_controls;
// thread communication and control
bool m_terminate;
// 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_zeroPadFactor = 2; //!< use n-steps bigger FFT for given block size
std::atomic<unsigned int> m_inBlockSize;//!< size of input (time domain) data block
unsigned int m_fftBlockSize; //!< size of padded block for FFT processing
unsigned int m_sampleRate;
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<float> m_bufferL; //!< time domain samples (left)
std::vector<float> m_bufferR; //!< time domain samples (right)
std::vector<float> m_fftWindow; //!< precomputed window function coefficients
std::vector<float> m_filteredBufferL; //!< time domain samples with window function applied (left)
std::vector<float> m_filteredBufferR; //!< time domain samples with window function applied (right)
fftwf_plan m_fftPlanL;
fftwf_plan m_fftPlanR;
fftwf_complex *m_spectrumL; //!< frequency domain samples (complex) (left)
@@ -102,21 +133,28 @@ private:
std::vector<float> m_normSpectrumR; //!< frequency domain samples (normalized) (right)
// spectrum history for waterfall: new normSpectrum lines are added on top
std::vector<uchar> m_history;
const unsigned int m_waterfallHeight = 200; // Number of stored lines.
// Note: high values may make it harder to see transients.
std::vector<uchar> m_history_work; //!< local history buffer for render
std::vector<uchar> m_history; //!< public buffer for reading
bool m_flipRequest; //!< update public buffer only when requested
std::atomic<unsigned int> m_waterfallHeight; //!< number of stored lines in history buffer
// Note: high values may make it harder to see transients.
const unsigned int m_waterfallMaxWidth = 3840;
// book keeping
bool m_spectrumActive;
bool m_waterfallActive;
unsigned int m_waterfallNotEmpty;
std::atomic<unsigned int> m_waterfallNotEmpty; //!< number of lines remaining visible on display
bool m_reallocating;
// merge L and R channels and apply gamma correction to make a spectrogram pixel
QRgb makePixel(float left, float right, float gamma_correction = 0.30) const;
QRgb makePixel(float left, float right) const;
friend class SaSpectrumView;
friend class SaWaterfallView;
#ifdef SA_DEBUG
unsigned int m_last_dump_time;
unsigned int m_dump_count;
float m_sum_execution;
float m_max_execution;
#endif
};
#endif // SAPROCESSOR_H

View File

@@ -39,7 +39,6 @@
#ifdef SA_DEBUG
#include <chrono>
#include <iostream>
#endif
@@ -68,7 +67,11 @@ SaSpectrumView::SaSpectrumView(SaControls *controls, SaProcessor *processor, QWi
m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
m_cursor = QPoint(0, 0);
m_cursor = QPointF(0, 0);
#ifdef SA_DEBUG
m_execution_avg = m_path_avg = m_draw_avg = 0;
#endif
}
@@ -134,12 +137,20 @@ void SaSpectrumView::paintEvent(QPaintEvent *event)
2.0, 2.0);
#ifdef SA_DEBUG
// display what FPS would be achieved if spectrum display ran in a loop
// display performance measurements if enabled
total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time;
m_execution_avg = 0.95 * m_execution_avg + 0.05 * total_time / 1000000.0;
painter.setPen(QPen(m_controls->m_colorLabels, 1,
Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft,
QString(std::string("Max FPS: " + std::to_string(1000000000.0 / total_time)).c_str()));
painter.drawText(m_displayRight -150, 10, 130, 16, Qt::AlignLeft,
QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms"));
painter.drawText(m_displayRight -150, 30, 130, 16, Qt::AlignLeft,
QString("Buff. upd. avg: ").append(std::to_string(m_refresh_avg).substr(0, 5).c_str()).append(" ms"));
painter.drawText(m_displayRight -150, 50, 130, 16, Qt::AlignLeft,
QString("Path build avg: ").append(std::to_string(m_path_avg).substr(0, 5).c_str()).append(" ms"));
painter.drawText(m_displayRight -150, 70, 130, 16, Qt::AlignLeft,
QString("Path draw avg: ").append(std::to_string(m_draw_avg).substr(0, 5).c_str()).append(" ms"));
#endif
}
@@ -148,22 +159,14 @@ void SaSpectrumView::paintEvent(QPaintEvent *event)
void SaSpectrumView::drawSpectrum(QPainter &painter)
{
#ifdef SA_DEBUG
int path_time = 0, draw_time = 0;
int draw_time = 0;
#endif
// draw the graph only if there is any input, averaging residue or peaks
QMutexLocker lock(&m_processor->m_dataAccess);
if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR))
if (m_decaySum > 0 || m_processor->spectrumNotEmpty())
{
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
@@ -199,17 +202,10 @@ void SaSpectrumView::drawSpectrum(QPainter &painter)
draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time;
#endif
}
else
{
lock.unlock();
}
#ifdef SA_DEBUG
// display measurement results
painter.drawText(m_displayRight -100, 90, 100, 16, Qt::AlignLeft,
QString(std::string("Path ms: " + std::to_string(path_time / 1000000.0)).c_str()));
painter.drawText(m_displayRight -100, 110, 100, 16, Qt::AlignLeft,
QString(std::string("Draw ms: " + std::to_string(draw_time / 1000000.0)).c_str()));
// save performance measurement result
m_draw_avg = 0.95 * m_draw_avg + 0.05 * draw_time / 1000000.0;
#endif
}
@@ -218,9 +214,9 @@ void SaSpectrumView::drawSpectrum(QPainter &painter)
// and build QPainter paths.
void SaSpectrumView::refreshPaths()
{
// Lock is required for the entire function, mainly to prevent block size
// changes from causing reallocation of data structures mid-way.
QMutexLocker lock(&m_processor->m_dataAccess);
// Reallocation lock is required for the entire function, to keep display
// buffer size consistent with block size.
QMutexLocker reloc_lock(&m_processor->m_reallocationAccess);
// check if bin count changed and reallocate display buffers accordingly
if (m_processor->binCount() != m_displayBufferL.size())
@@ -240,8 +236,8 @@ void SaSpectrumView::refreshPaths()
int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
#endif
m_decaySum = 0;
updateBuffers(m_processor->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data());
updateBuffers(m_processor->m_normSpectrumR.data(), m_displayBufferR.data(), m_peakBufferR.data());
updateBuffers(m_processor->getSpectrumL(), m_displayBufferL.data(), m_peakBufferL.data());
updateBuffers(m_processor->getSpectrumR(), m_displayBufferR.data(), m_peakBufferR.data());
#ifdef SA_DEBUG
refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time;
#endif
@@ -254,41 +250,43 @@ void SaSpectrumView::refreshPaths()
}
#ifdef SA_DEBUG
int make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
int path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
#endif
// Use updated display buffers to prepare new paths for QPainter.
// This is the second slowest action (first is the subsequent drawing); use
// the resolution parameter to balance display quality and performance.
m_pathL = makePath(m_displayBufferL, 1.5);
m_pathL = makePath(m_displayBufferL, m_controls->m_spectrumResolutionModel.value());
if (m_controls->m_stereoModel.value())
{
m_pathR = makePath(m_displayBufferR, 1.5);
m_pathR = makePath(m_displayBufferR, m_controls->m_spectrumResolutionModel.value());
}
if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value())
{
m_pathPeakL = makePath(m_peakBufferL, 0.25);
m_pathPeakL = makePath(m_peakBufferL, m_controls->m_envelopeResolutionModel.value());
if (m_controls->m_stereoModel.value())
{
m_pathPeakR = makePath(m_peakBufferR, 0.25);
m_pathPeakR = makePath(m_peakBufferR, m_controls->m_envelopeResolutionModel.value());
}
}
#ifdef SA_DEBUG
make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - make_time;
path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time;
#endif
#ifdef SA_DEBUG
// print measurement results
std::cout << "Buffer update ms: " << std::to_string(refresh_time / 1000000.0) << ", ";
std::cout << "Path-make ms: " << std::to_string(make_time / 1000000.0) << std::endl;
// save performance measurement results
m_refresh_avg = 0.95 * m_refresh_avg + 0.05 * refresh_time / 1000000.0;
m_path_avg = .95f * m_path_avg + .05f * path_time / 1000000.f;
#endif
}
// Update display buffers: add new data, update average and peaks / reference.
// Output the sum of all displayed values -- draw only if it is non-zero.
// NOTE: The calling function is responsible for acquiring SaProcessor data
// access lock!
void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer)
// NOTE: The calling function is responsible for acquiring SaProcessor
// reallocation access lock! Data access lock is not needed: the final result
// buffer is updated very quickly and the worst case is that one frame will be
// part new, part old. At reasonable frame rate, such difference is invisible..
void SaSpectrumView::updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer)
{
for (int n = 0; n < m_processor->binCount(); n++)
{
@@ -297,7 +295,8 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float
{
if (m_controls->m_smoothModel.value())
{
displayBuffer[n] = spectrum[n] * m_smoothFactor + displayBuffer[n] * (1 - m_smoothFactor);
const float smoothFactor = m_controls->m_averagingWeightModel.value();
displayBuffer[n] = spectrum[n] * smoothFactor + displayBuffer[n] * (1 - smoothFactor);
}
else
{
@@ -319,7 +318,7 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float
}
else if (!m_controls->m_refFreezeModel.value())
{
peakBuffer[n] = peakBuffer[n] * m_peakDecayFactor;
peakBuffer[n] = peakBuffer[n] * m_controls->m_peakDecayFactorModel.value();
}
}
else if (!m_controls->m_refFreezeModel.value() && !m_controls->m_peakHoldModel.value())
@@ -539,38 +538,52 @@ void SaSpectrumView::drawGrid(QPainter &painter)
// Draw cursor and its coordinates if it is within display bounds.
void SaSpectrumView::drawCursor(QPainter &painter)
{
if( m_cursor.x() >= m_displayLeft
if ( m_cursor.x() >= m_displayLeft
&& m_cursor.x() <= m_displayRight
&& m_cursor.y() >= m_displayTop
&& m_cursor.y() <= m_displayBottom)
{
// cursor lines
painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom);
painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y());
painter.drawLine(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom));
painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y()));
// coordinates
// coordinates: background box
QFontMetrics fontMetrics = painter.fontMetrics();
unsigned int const box_left = 5;
unsigned int const box_top = 5;
unsigned int const box_margin = 3;
unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 HzdBFS").height() + box_margin);
unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "-99.9 dBFS").width() + 2*box_margin;
painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawText(m_displayRight -60, 5, 100, 16, Qt::AlignLeft, "Cursor");
painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top,
box_width, box_height, QColor(0, 0, 0, 64));
// coordinates: text
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
QString tmps;
// frequency
int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth);
tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str());
painter.drawText(m_displayRight -60, 18, 100, 16, Qt::AlignLeft, tmps);
tmps = QString("%1 Hz").arg(xFreq);
painter.drawText(m_displayLeft + box_left + box_margin,
m_displayTop + box_top + box_margin,
box_width, box_height / 2, Qt::AlignLeft, tmps);
// amplitude
float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom);
if (m_controls->m_logYModel.value())
{
tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dB").c_str());
tmps = QString(std::to_string(yAmp).substr(0, 5).c_str()).append(" dBFS");
}
else
{
// add 0.0005 to get proper rounding to 3 decimal places
tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str());
tmps = QString(std::to_string(0.0005f + yAmp).substr(0, 5).c_str());
}
painter.drawText(m_displayRight -60, 30, 100, 16, Qt::AlignLeft, tmps);
painter.drawText(m_displayLeft + box_left + box_margin,
m_displayTop + box_top + box_height / 2,
box_width, box_height / 2, Qt::AlignLeft, tmps);
}
}
@@ -774,14 +787,18 @@ void SaSpectrumView::periodicUpdate()
// Handle mouse input: set new cursor position.
// For some reason (a bug?), localPos() only returns integers. As a workaround
// the fractional part is taken from windowPos() (which works correctly).
void SaSpectrumView::mouseMoveEvent(QMouseEvent *event)
{
m_cursor = event->pos();
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}
void SaSpectrumView::mousePressEvent(QMouseEvent *event)
{
m_cursor = event->pos();
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}

View File

@@ -27,6 +27,8 @@
#ifndef SASPECTRUMVIEW_H
#define SASPECTRUMVIEW_H
#include "SaControls.h"
#include <string>
#include <utility>
#include <QPainterPath>
@@ -34,7 +36,6 @@
class QMouseEvent;
class QPainter;
class SaControls;
class SaProcessor;
//! Widget that displays a spectrum curve and frequency / amplitude grid
@@ -84,7 +85,7 @@ private:
std::vector<float> m_displayBufferR;
std::vector<float> m_peakBufferL;
std::vector<float> m_peakBufferR;
void updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer);
void updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer);
// final paths to be drawn by QPainter and methods to build them
QPainterPath m_pathL;
@@ -99,14 +100,11 @@ private:
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;
QPointF m_cursor;
void drawCursor(QPainter &painter);
// wrappers for most used SaProcessor conversion helpers
@@ -121,6 +119,13 @@ private:
unsigned int m_displayLeft;
unsigned int m_displayRight;
unsigned int m_displayWidth;
#ifdef SA_DEBUG
float m_execution_avg;
float m_refresh_avg;
float m_path_avg;
float m_draw_avg;
#endif
};
#endif // SASPECTRUMVIEW_H

View File

@@ -23,8 +23,12 @@
#include "SaWaterfallView.h"
#include <algorithm>
#ifdef SA_DEBUG
#include <chrono>
#endif
#include <cmath>
#include <QImage>
#include <QMouseEvent>
#include <QMutexLocker>
#include <QPainter>
#include <QSplitter>
@@ -47,8 +51,22 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q
connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
m_displayTop = 1;
m_displayBottom = height() -2;
m_displayLeft = 26;
m_displayRight = width() -26;
m_displayWidth = m_displayRight - m_displayLeft;
m_displayHeight = m_displayBottom - m_displayTop;
m_timeTics = makeTimeTics();
m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate();
m_oldSecondsPerLine = 0;
m_oldHeight = 0;
m_cursor = QPointF(0, 0);
#ifdef SA_DEBUG
m_execution_avg = 0;
#endif
}
@@ -58,15 +76,14 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q
void SaWaterfallView::paintEvent(QPaintEvent *event)
{
#ifdef SA_DEBUG
int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
unsigned int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
#endif
// all drawing done here, local variables are sufficient for the boundary
const int displayTop = 1;
const int displayBottom = height() -2;
const int displayLeft = 26;
const int displayRight = width() -26;
const int displayWidth = displayRight - displayLeft;
// update boundary
m_displayBottom = height() -2;
m_displayRight = width() -26;
m_displayWidth = m_displayRight - m_displayLeft;
m_displayHeight = m_displayBottom - m_displayTop;
float label_width = 20;
float label_height = 16;
float margin = 2;
@@ -75,10 +92,11 @@ void SaWaterfallView::paintEvent(QPaintEvent *event)
painter.setRenderHint(QPainter::Antialiasing, true);
// check if time labels need to be rebuilt
if ((float)m_processor->m_inBlockSize / m_processor->getSampleRate() != m_oldTimePerLine)
if (secondsPerLine() != m_oldSecondsPerLine || m_processor->waterfallHeight() != m_oldHeight)
{
m_timeTics = makeTimeTics();
m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate();
m_oldSecondsPerLine = secondsPerLine();
m_oldHeight = m_processor->waterfallHeight();
}
// print time labels
@@ -86,78 +104,104 @@ void SaWaterfallView::paintEvent(QPaintEvent *event)
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
for (auto & line: m_timeTics)
{
pos = timeToYPixel(line.first, displayBottom);
pos = timeToYPixel(line.first, m_displayHeight);
// align first and last label to the edge if needed, otherwise center them
if (line == m_timeTics.front() && pos < label_height / 2)
{
painter.drawText(displayLeft - label_width - margin, displayTop - 1,
painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 1,
label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip,
QString(line.second.c_str()));
painter.drawText(displayRight + margin, displayTop - 1,
painter.drawText(m_displayRight + margin, m_displayTop - 1,
label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
QString(line.second.c_str()));
}
else if (line == m_timeTics.back() && pos > displayBottom - label_height + 2)
else if (line == m_timeTics.back() && pos > m_displayBottom - label_height + 2)
{
painter.drawText(displayLeft - label_width - margin, displayBottom - label_height,
painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height,
label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip,
QString(line.second.c_str()));
painter.drawText(displayRight + margin, displayBottom - label_height + 2,
painter.drawText(m_displayRight + margin, m_displayBottom - label_height + 2,
label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip,
QString(line.second.c_str()));
}
else
{
painter.drawText(displayLeft - label_width - margin, pos - label_height / 2,
painter.drawText(m_displayLeft - label_width - margin, pos - label_height / 2,
label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip,
QString(line.second.c_str()));
painter.drawText(displayRight + margin, pos - label_height / 2,
painter.drawText(m_displayRight + margin, pos - label_height / 2,
label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip,
QString(line.second.c_str()));
}
}
// draw the spectrogram precomputed in SaProcessor
if (m_processor->m_waterfallNotEmpty)
if (m_processor->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));
QMutexLocker lock(&m_processor->m_reallocationAccess);
QImage temp = QImage(m_processor->getHistory(), // raw pixel data to display
m_processor->waterfallWidth(), // width = number of frequency bins
m_processor->waterfallHeight(), // height = number of history lines
QImage::Format_RGB32);
lock.unlock();
temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution
painter.drawImage(m_displayLeft, m_displayTop,
temp.scaled(m_displayWidth * devicePixelRatio(),
m_displayHeight * devicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
m_processor->flipRequest();
}
else
{
painter.fillRect(displayLeft, displayTop, displayWidth, displayBottom, QColor(0,0,0));
painter.fillRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, QColor(0,0,0));
}
// draw cursor (if it is within bounds)
drawCursor(painter);
// always draw the outline
painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawRoundedRect(displayLeft, displayTop, displayWidth, displayBottom, 2.0, 2.0);
painter.drawRoundedRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, 2.0, 2.0);
#ifdef SA_DEBUG
// display what FPS would be achieved if waterfall ran in a loop
start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time;
draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time;
m_execution_avg = 0.95 * m_execution_avg + 0.05 * draw_time / 1000000.0;
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawText(displayRight -100, 10, 100, 16, Qt::AlignLeft,
QString(std::string("Max FPS: " + std::to_string(1000000000.0 / start_time)).c_str()));
painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft,
QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms"));
#endif
}
// Helper functions for time conversion
float SaWaterfallView::samplesPerLine()
{
return (float)m_processor->inBlockSize() / m_controls->m_windowOverlapModel.value();
}
float SaWaterfallView::secondsPerLine()
{
return samplesPerLine() / m_processor->getSampleRate();
}
// Convert time value to Y coordinate for display of given height.
float SaWaterfallView::timeToYPixel(float time, int height)
{
float pixels_per_line = (float)height / m_processor->m_waterfallHeight;
float seconds_per_line = ((float)m_processor->m_inBlockSize / m_processor->getSampleRate());
float pixels_per_line = (float)height / m_processor->waterfallHeight();
return pixels_per_line * time / seconds_per_line;
return pixels_per_line * time / secondsPerLine();
}
// Convert Y coordinate on display of given height back to time value.
float SaWaterfallView::yPixelToTime(float position, int height)
{
if (height == 0) {height = 1;}
float pixels_per_line = (float)height / m_processor->waterfallHeight();
return (position / pixels_per_line) * secondsPerLine();
}
@@ -167,16 +211,21 @@ std::vector<std::pair<float, std::string>> SaWaterfallView::makeTimeTics()
std::vector<std::pair<float, std::string>> result;
float i;
// upper limit defined by number of lines * time per line
float limit = m_processor->m_waterfallHeight * ((float)m_processor->m_inBlockSize / m_processor->getSampleRate());
// get time value of the last line
float limit = yPixelToTime(m_displayBottom, m_displayHeight);
// set increment so that about 8 tics are generated
float increment = std::round(10 * limit / 7) / 10;
// set increment to about 30 pixels (but min. 0.1 s)
float increment = std::round(10 * limit / (m_displayHeight / 30)) / 10;
if (increment < 0.1) {increment = 0.1;}
// NOTE: labels positions are rounded to match the (rounded) label value
for (i = 0; i <= limit; i += increment)
{
if (i < 10)
if (i > 99)
{
result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 3));
}
else if (i < 10)
{
result.emplace_back(std::round(i * 10) / 10, std::to_string(std::round(i * 10) / 10).substr(0, 3));
}
@@ -208,10 +257,7 @@ void SaWaterfallView::updateVisibility()
if (m_controls->m_waterfallModel.value())
{
// clear old data before showing the waterfall
QMutexLocker lock(&m_processor->m_dataAccess);
std::fill(m_processor->m_history.begin(), m_processor->m_history.end(), 0);
lock.unlock();
m_processor->clearHistory();
setVisible(true);
// increase window size if it is too small
@@ -228,3 +274,70 @@ void SaWaterfallView::updateVisibility()
}
}
// Draw cursor and its coordinates if it is within display bounds.
void SaWaterfallView::drawCursor(QPainter &painter)
{
if ( m_cursor.x() >= m_displayLeft
&& m_cursor.x() <= m_displayRight
&& m_cursor.y() >= m_displayTop
&& m_cursor.y() <= m_displayBottom)
{
// cursor lines
painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawLine(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom));
painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y()));
// coordinates: background box
QFontMetrics fontMetrics = painter.fontMetrics();
unsigned int const box_left = 5;
unsigned int const box_top = 5;
unsigned int const box_margin = 3;
unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 Hz").height() + box_margin);
unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "20000 Hz ").width() + 2*box_margin;
painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top,
box_width, box_height, QColor(0, 0, 0, 64));
// coordinates: text
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
QString tmps;
// frequency
int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth);
tmps = QString("%1 Hz").arg(freq);
painter.drawText(m_displayLeft + box_left + box_margin,
m_displayTop + box_top + box_margin,
box_width, box_height / 2, Qt::AlignLeft, tmps);
// time
float time = yPixelToTime(m_cursor.y(), m_displayBottom);
tmps = QString(std::to_string(time).substr(0, 5).c_str()).append(" s");
painter.drawText(m_displayLeft + box_left + box_margin,
m_displayTop + box_top + box_height / 2,
box_width, box_height / 2, Qt::AlignLeft, tmps);
}
}
// Handle mouse input: set new cursor position.
// For some reason (a bug?), localPos() only returns integers. As a workaround
// the fractional part is taken from windowPos() (which works correctly).
void SaWaterfallView::mouseMoveEvent(QMouseEvent *event)
{
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}
void SaWaterfallView::mousePressEvent(QMouseEvent *event)
{
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}
// Handle resize event: rebuild time labels
void SaWaterfallView::resizeEvent(QResizeEvent *event)
{
m_timeTics = makeTimeTics();
}

View File

@@ -32,6 +32,7 @@
#include "SaControls.h"
#include "SaProcessor.h"
class QMouseEvent;
// Widget that displays a spectrum waterfall (spectrogram) and time labels.
class SaWaterfallView : public QWidget
@@ -48,6 +49,9 @@ public:
protected:
void paintEvent(QPaintEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private slots:
void periodicUpdate();
@@ -58,9 +62,29 @@ private:
const EffectControlDialog *m_controlDialog;
// Methods and data used to make time labels
float m_oldTimePerLine;
float m_oldSecondsPerLine;
float m_oldHeight;
float samplesPerLine();
float secondsPerLine();
float timeToYPixel(float time, int height);
float yPixelToTime(float position, int height);
std::vector<std::pair<float, std::string>> makeTimeTics();
std::vector<std::pair<float, std::string>> m_timeTics; // 0..n (s)
// current cursor location and a method to draw it
QPointF m_cursor;
void drawCursor(QPainter &painter);
// current boundaries for drawing
unsigned int m_displayTop;
unsigned int m_displayBottom;
unsigned int m_displayLeft;
unsigned int m_displayRight;
unsigned int m_displayWidth;
unsigned int m_displayHeight;
#ifdef SA_DEBUG
float m_execution_avg;
#endif
};
#endif // SAWATERFALLVIEW_H

View File

@@ -0,0 +1,243 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50mm"
height="150mm"
viewBox="0 0 50 150"
version="1.1"
id="svg8"
inkscape:version="0.92.1 r15371"
sodipodi:docname="advanced_off.svg">
<defs
id="defs2">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker8614"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleOutM">
<path
inkscape:connector-curvature="0"
transform="scale(0.4)"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path8612" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker8538"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleInM">
<path
inkscape:connector-curvature="0"
transform="scale(-0.4)"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path8536" />
</marker>
<marker
inkscape:stockid="Arrow2Mstart"
orient="auto"
refY="0"
refX="0"
id="marker9923"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path9921"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mend"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8461"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(-0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mstart"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8458"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker9470"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Send">
<path
transform="matrix(-0.3,0,0,-0.3,0.69,0)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path9468"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker9424"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Sstart">
<path
transform="matrix(0.3,0,0,0.3,-0.69,0)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path9422"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Mstart"
orient="auto"
refY="0"
refX="0"
id="Arrow1Mstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8440"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(0.4,0,0,0.4,4,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="TriangleOutL"
orient="auto"
refY="0"
refX="0"
id="TriangleOutL"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8576"
d="M 5.77,0 -2.88,5 V -5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="scale(0.8)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="TriangleInL"
orient="auto"
refY="0"
refX="0"
id="TriangleInL"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8567"
d="M 5.77,0 -2.88,5 V -5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="scale(-0.8)"
inkscape:connector-curvature="0" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.3196389"
inkscape:cx="94.488189"
inkscape:cy="283.46457"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-center="false"
inkscape:snap-object-midpoints="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-59.999998,-57)">
<g
aria-label="A D V."
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.39999962px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.32664606px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text8560"
transform="matrix(0.81,0,0,0.81,16.15003,20.403867)">
<path
d="M 87.278317,86.62654 H 79.88652 l -1.835547,3.373437 H 73.152048 L 83.619626,71.483278 h 5.457031 l 3.274219,18.516699 H 87.811618 Z M 81.68486,83.265505 h 4.97334 l -1.116211,-7.131348 z"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.39999962px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.32664606px"
id="path13111"
inkscape:connector-curvature="0" />
<path
d="m 83.036716,106.84236 -2.22002,11.29853 h 1.711523 q 3.336231,0 5.283399,-1.87275 1.947168,-1.87275 1.947168,-5.06016 0,-2.158 -1.277442,-3.26181 -1.265039,-1.10381 -3.770312,-1.10381 z m -4.092774,-3.60908 h 5.022949 q 3.59668,0 5.39502,0.43408 1.79834,0.42168 2.988965,1.42627 1.203027,1.00459 1.810742,2.44326 0.607715,1.42627 0.607715,3.23701 0,2.51768 -1.041797,4.7501 -1.029395,2.22002 -2.902149,3.73311 -1.637109,1.35185 -3.807519,1.92236 -2.158008,0.57051 -6.647656,0.57051 H 75.33486 Z"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.39999962px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.32664606px"
id="path13113"
inkscape:connector-curvature="0" />
<path
d="m 76.736325,134.98328 h 4.539258 l 2.269629,13.84101 7.664648,-13.84101 h 4.92373 l -10.455175,18.5167 h -5.692676 z"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.39999962px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.32664606px"
id="path13115"
inkscape:connector-curvature="0" />
<path
d="m 92.58652,148.70027 h 4.464844 l -0.930176,4.79971 h -4.477246 z"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.39999962px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.32664606px"
id="path13117"
inkscape:connector-curvature="0" />
</g>
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 85.084602,154.05774 c -4.167529,-0.0223 -0.480916,3.48589 -4.24523,5.30715 -3.764312,1.82128 -4.106432,-3.30495 -6.721962,-8.5e-4 -2.61553,3.30412 2.376484,2.55635 1.427774,6.68889 -0.94871,4.13255 -5.097666,1.20865 -4.191651,5.35112 0.906015,4.14247 3.444154,-0.29848 6.025446,3.03344 2.581291,3.33193 -2.250566,4.81237 1.494746,6.67382 3.745312,1.86146 1.918612,-2.92849 6.086139,-2.90618 4.167528,0.0223 2.291691,4.79234 6.056002,2.97107 3.764314,-1.82128 -1.052137,-3.35339 1.563395,-6.6575 2.61553,-3.30412 5.10813,1.16355 6.056835,-2.96899 0.948717,-4.13254 -3.230381,-1.25301 -4.136395,-5.39548 -0.906015,-4.14246 4.077715,-3.34148 1.496421,-6.67341 -2.581293,-3.33192 -2.976232,1.79071 -6.721544,-0.0707 -3.745312,-1.86145 -0.02245,-5.33007 -4.189976,-5.35237 z m -0.07367,11.20577 a 3.04264,3.04264 0 0 1 3.042652,3.04266 3.04264,3.04264 0 0 1 -3.042652,3.04265 3.04264,3.04264 0 0 1 -3.042652,-3.04265 3.04264,3.04264 0 0 1 3.042652,-3.04266 z"
id="path8568"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,224 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50mm"
height="150mm"
viewBox="0 0 50 150"
version="1.1"
id="svg8"
inkscape:version="0.92.1 r15371"
sodipodi:docname="advanced_on.svg">
<defs
id="defs2">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker8614"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleOutM">
<path
inkscape:connector-curvature="0"
transform="scale(0.4)"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path8612" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker8538"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleInM">
<path
inkscape:connector-curvature="0"
transform="scale(-0.4)"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path8536" />
</marker>
<marker
inkscape:stockid="Arrow2Mstart"
orient="auto"
refY="0"
refX="0"
id="marker9923"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path9921"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mend"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8461"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(-0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mstart"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8458"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker9470"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Send">
<path
transform="matrix(-0.3,0,0,-0.3,0.69,0)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path9468"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker9424"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Sstart">
<path
transform="matrix(0.3,0,0,0.3,-0.69,0)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path9422"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Mstart"
orient="auto"
refY="0"
refX="0"
id="Arrow1Mstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8440"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(0.4,0,0,0.4,4,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="TriangleOutL"
orient="auto"
refY="0"
refX="0"
id="TriangleOutL"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8576"
d="M 5.77,0 -2.88,5 V -5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="scale(0.8)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="TriangleInL"
orient="auto"
refY="0"
refX="0"
id="TriangleInL"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8567"
d="M 5.77,0 -2.88,5 V -5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="scale(-0.8)"
inkscape:connector-curvature="0" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.3196389"
inkscape:cx="94.488189"
inkscape:cy="283.46457"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-center="false"
inkscape:snap-object-midpoints="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-59.999998,-57)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:48.00000381;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 65.999998,66.768297 V 186.64851 H 104 V 66.768297 Z m 17.881999,10.921566 h 4.420194 L 90.954309,92.68839 H 87.27751 l -0.431972,-2.732485 h -5.987356 l -1.486793,2.732485 h -3.96813 z m 1.557114,3.767211 -3.124274,5.776392 h 4.028405 z m -5.344418,21.950276 h 4.068589 c 1.942207,0 3.398863,0.11721 4.369966,0.35162 0.971104,0.22771 1.778123,0.6128 2.421061,1.15527 0.649635,0.54248 1.138535,1.20216 1.466701,1.97905 0.328166,0.77018 0.49225,1.64417 0.49225,2.62198 0,1.35954 -0.281285,2.64207 -0.843855,3.84758 -0.555873,1.19881 -1.339454,2.20674 -2.350741,3.02381 -0.884039,0.73 -1.91207,1.24904 -3.084092,1.55712 -1.165322,0.30807 -2.96019,0.4621 -5.384601,0.4621 h -4.078634 z m 3.315147,2.92337 -1.798216,9.15181 h 1.386335 c 1.801564,0 3.22808,-0.50565 4.279551,-1.51693 1.051471,-1.01129 1.577208,-2.37753 1.577208,-4.09873 0,-1.16532 -0.344911,-2.04601 -1.03473,-2.64207 -0.68312,-0.59606 -1.701104,-0.89408 -3.053952,-0.89408 z m -5.103317,22.79413 h 3.676799 l 1.838399,11.21123 6.208365,-11.21123 h 3.988223 l -8.468693,14.99853 h -4.611067 z m 12.838659,11.11077 h 3.616524 l -0.753444,3.88776 h -3.626569 z m -6.060607,13.2066 c 4.167527,0.0223 0.444665,3.49092 4.189976,5.35237 3.745312,1.86144 4.140251,-3.26119 6.721545,0.0707 2.581288,3.33192 -2.402437,2.53093 -1.496422,6.6734 0.906015,4.14247 5.085107,1.26294 4.136399,5.39548 -0.948709,4.13255 -3.441308,-0.33513 -6.056839,2.96898 -2.615531,3.30413 2.20092,4.83623 -1.563394,6.65751 -3.764311,1.82127 -1.888475,-2.94878 -6.056002,-2.97108 -4.167527,-0.0223 -2.340827,4.76765 -6.086139,2.9062 -3.745313,-1.86145 1.086544,-3.34191 -1.494747,-6.67382 -2.581291,-3.33193 -5.119431,1.10901 -6.025446,-3.03345 -0.906014,-4.14246 3.242942,-1.21858 4.191652,-5.35112 0.948709,-4.13254 -4.043304,-3.38477 -1.427774,-6.68889 2.61553,-3.30412 2.957649,1.82211 6.721961,8.6e-4 3.764314,-1.82128 0.0777,-5.32946 4.24523,-5.30717 z"
id="path8424"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccscccscccsccccscscscccccccccccccccccssccsscsssccc" />
<circle
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path8570"
cx="85.011032"
cy="167.69077"
r="3.04264" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,238 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50mm"
height="150mm"
viewBox="0 0 50 150"
version="1.1"
id="svg8"
inkscape:version="0.92.1 r15371"
sodipodi:docname="advanced_src.svg">
<defs
id="defs2">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker8614"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleOutM">
<path
inkscape:connector-curvature="0"
transform="scale(0.4)"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path8612" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker8538"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleInM">
<path
inkscape:connector-curvature="0"
transform="scale(-0.4)"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path8536" />
</marker>
<marker
inkscape:stockid="Arrow2Mstart"
orient="auto"
refY="0"
refX="0"
id="marker9923"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path9921"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mend"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8461"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(-0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mstart"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8458"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker9470"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Send">
<path
transform="matrix(-0.3,0,0,-0.3,0.69,0)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path9468"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker9424"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow2Sstart">
<path
transform="matrix(0.3,0,0,0.3,-0.69,0)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path9422"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Mstart"
orient="auto"
refY="0"
refX="0"
id="Arrow1Mstart"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8440"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(0.4,0,0,0.4,4,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="TriangleOutL"
orient="auto"
refY="0"
refX="0"
id="TriangleOutL"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8576"
d="M 5.77,0 -2.88,5 V -5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="scale(0.8)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="TriangleInL"
orient="auto"
refY="0"
refX="0"
id="TriangleInL"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path8567"
d="M 5.77,0 -2.88,5 V -5 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="scale(-0.8)"
inkscape:connector-curvature="0" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.3196389"
inkscape:cx="94.488189"
inkscape:cy="283.46457"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-center="false"
inkscape:snap-object-midpoints="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-59.999998,-57)">
<text
xml:space="preserve"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20.57400131px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="76.739296"
y="93.303856"
id="text8560"><tspan
sodipodi:role="line"
id="tspan8558"
x="76.739296"
y="93.303856"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20.57400131px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.26458332px">A</tspan><tspan
sodipodi:role="line"
x="76.739296"
y="119.02135"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20.57400131px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.26458332px"
id="tspan8562">D</tspan><tspan
sodipodi:role="line"
x="76.739296"
y="144.73885"
style="font-style:oblique;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20.57400131px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold Oblique';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.26458332px"
id="tspan8564">V.</tspan></text>
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 85.084602,154.05774 c -4.167529,-0.0223 -0.480916,3.48589 -4.24523,5.30715 -3.764312,1.82128 -4.106432,-3.30495 -6.721962,-8.5e-4 -2.61553,3.30412 2.376484,2.55635 1.427774,6.68889 -0.94871,4.13255 -5.097666,1.20865 -4.191651,5.35112 0.906015,4.14247 3.444154,-0.29848 6.025446,3.03344 2.581291,3.33193 -2.250566,4.81237 1.494746,6.67382 3.745312,1.86146 1.918612,-2.92849 6.086139,-2.90618 4.167528,0.0223 2.291691,4.79234 6.056002,2.97107 3.764314,-1.82128 -1.052137,-3.35339 1.563395,-6.6575 2.61553,-3.30412 5.10813,1.16355 6.056835,-2.96899 0.948717,-4.13254 -3.230381,-1.25301 -4.136395,-5.39548 -0.906015,-4.14246 4.077715,-3.34148 1.496421,-6.67341 -2.581293,-3.33192 -2.976232,1.79071 -6.721544,-0.0707 -3.745312,-1.86145 -0.02245,-5.33007 -4.189976,-5.35237 z m -0.07367,11.20577 a 3.04264,3.04264 0 0 1 3.042652,3.04266 3.04264,3.04264 0 0 1 -3.042652,3.04265 3.04264,3.04264 0 0 1 -3.042652,-3.04265 3.04264,3.04264 0 0 1 3.042652,-3.04266 z"
id="path8568"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -10,3 +10,22 @@ ENDIF()
ADD_SUBDIRECTORY(rpmalloc)
ADD_SUBDIRECTORY(weakjack)
# The lockless ring buffer library is compiled as part of the core
SET(RINGBUFFER_DIR "${CMAKE_SOURCE_DIR}/src/3rdparty/ringbuffer/")
SET(RINGBUFFER_DIR ${RINGBUFFER_DIR} PARENT_SCOPE)
# Create a dummy ringbuffer_export.h, since ringbuffer is not compiled as a library
FILE(WRITE ${CMAKE_BINARY_DIR}/src/ringbuffer_export.h
"#include \"${CMAKE_BINARY_DIR}/src/lmms_export.h\"\n
#define RINGBUFFER_EXPORT LMMS_EXPORT")
# Enable MLOCK support for ringbuffer if available
INCLUDE(CheckIncludeFiles)
CHECK_INCLUDE_FILES(sys/mman.h HAVE_SYS_MMAN)
IF(HAVE_SYS_MMAN)
SET(USE_MLOCK ON)
ELSE()
SET(USE_MLOCK OFF)
ENDIF()
# Generate ringbuffer configuration headers
CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-config.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-config.h)
CONFIGURE_FILE(${RINGBUFFER_DIR}/src/ringbuffer-version.h.in ${CMAKE_BINARY_DIR}/src/ringbuffer-version.h)

1
src/3rdparty/ringbuffer vendored Submodule

Submodule src/3rdparty/ringbuffer added at 82ed7cfb9a

View File

@@ -27,6 +27,7 @@ INCLUDE_DIRECTORIES(
"${CMAKE_BINARY_DIR}/include"
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/include"
"${RINGBUFFER_DIR}/include"
)
IF(WIN32 AND MSVC)
@@ -89,6 +90,8 @@ IF(NOT ("${LAME_INCLUDE_DIRS}" STREQUAL ""))
INCLUDE_DIRECTORIES("${LAME_INCLUDE_DIRS}")
ENDIF()
LIST(APPEND LMMS_SRCS "${RINGBUFFER_DIR}/src/lib/ringbuffer.cpp")
# Use libraries in non-standard directories (e.g., another version of Qt)
IF(LMMS_BUILD_LINUX)
LINK_LIBRARIES(-Wl,--enable-new-dtags)

View File

@@ -1,5 +1,6 @@
set(LMMS_SRCS
${LMMS_SRCS}
core/AutomatableModel.cpp
core/AutomationPattern.cpp
core/BandLimitedWave.cpp

View File

@@ -66,6 +66,7 @@ int normalize(const float *abs_spectrum, float *norm_spectrum, unsigned int bin_
if (abs_spectrum == NULL || norm_spectrum == NULL) {return -1;}
if (bin_count == 0 || block_size == 0) {return -1;}
block_size /= 2;
for (i = 0; i < bin_count; i++)
{
norm_spectrum[i] = abs_spectrum[i] / block_size;