Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spectrum analyzer update #5160

Merged
merged 37 commits into from
Nov 21, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9be31e0
tweak amplitude ranges, update and fix readme.md
he29-net Jul 21, 2019
4e2f168
Add and implement advanced settings
he29-net Aug 15, 2019
f5eda87
Display waterfall at native resolution
he29-net Aug 23, 2019
7d630e1
Add waterfall cursor, fix time labels and make density change with wi…
he29-net Aug 23, 2019
249d161
Fix normalization so that full scale sinewave is 0 dBFS; tweak perf. …
he29-net Aug 23, 2019
25aa537
Move FFT analysis to a separate thread for better performance and rea…
he29-net Aug 23, 2019
3707eeb
Performance optimizations and some final touches here and there
he29-net Sep 1, 2019
cb3e701
Improve cursor coordinates display
he29-net Sep 1, 2019
8d639e5
Fix missed transient in the first block despite having overlapping en…
he29-net Sep 16, 2019
36feb65
workaround for QMouseEvent::localPos() bug
he29-net Oct 8, 2019
b650838
Update plugins/SpectrumAnalyzer/SaSpectrumView.cpp
he29-net Oct 13, 2019
42d74db
Make SaProcessor unfriendly to view classes
he29-net Oct 13, 2019
9dd9ef0
Update and improve readme file; use consistent "analyzer" spelling
he29-net Oct 13, 2019
6a5089d
Use QString directly where possible
he29-net Oct 13, 2019
6939702
Fix bug introduced in previous commit
he29-net Oct 13, 2019
11013cc
SaProcessor: make some variables accessed by other classes atomic; ma…
he29-net Oct 17, 2019
edefc93
test a change required to make analyzer work after make install
he29-net Oct 24, 2019
e3c89d5
Build the ringbuffer libary as part of LMMS core
he29-net Oct 28, 2019
5b1f28c
Attempted fix of missing ringbuffer.cpp symbols on Win platforms
he29-net Oct 30, 2019
7cf189c
Move most ringbuffer cmake setup to 3rdparty/, hijack RINGBUFFER_EXPO…
he29-net Oct 30, 2019
11bb2c7
Add LMMS_EXPORT to LocklessRingBuffer methods
he29-net Nov 7, 2019
2963fb6
Revert "Add LMMS_EXPORT to LocklessRingBuffer methods"
PhysSong Nov 7, 2019
6dd2619
Try to fix an export error
PhysSong Nov 7, 2019
6856b3d
Rework LocklessRingBuffer and force export of <sampleFrame> template …
he29-net Nov 9, 2019
0d56ae8
Move <sampleFrame> instances to the bottom of file
he29-net Nov 9, 2019
228dd2f
Revert "Move <sampleFrame> instances to the bottom of file"
he29-net Nov 9, 2019
22e9163
Move specialized write() above the non-specialized one
he29-net Nov 9, 2019
dc2bd91
Move sampleFrame instantiation to the header file
he29-net Nov 9, 2019
8739abb
Try to remove LMMS_EXPORT from LocklessRingBuffer template
he29-net Nov 14, 2019
7a0fc5a
Go back to 'everything in the header' to fix Mac and hope it does not…
he29-net Nov 14, 2019
bf793da
Try removing all LMMS_EXPORTs from LocklessRingBuffer
he29-net Nov 15, 2019
ad49d36
Merge remote-tracking branch 'upstream/master' into analyzer-update
he29-net Nov 15, 2019
a0acc8a
Implement LocklessRingBuffer changes requested in review
he29-net Nov 16, 2019
8ca05a3
Fix code conventions, make some includes harder to read
he29-net Nov 17, 2019
c694277
Forgotten rename
he29-net Nov 17, 2019
0caa748
Fix missing part of waterfall when its width limit is reached
he29-net Nov 18, 2019
a7388b1
Fix drawing bounds of "overload fill-in"; improve comment on waterfal…
he29-net Nov 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions include/lmms_basics.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,24 @@ typedef sample_t surroundSampleFrame[SURROUND_CHANNELS];
typedef sample_t sampleFrameA[DEFAULT_CHANNELS] __attribute__((__aligned__(ALIGN_SIZE)));
#endif

// The sampleFrame_copier is required to store samples into the lockless ringbuffer.
// This is because sampleFrame is just a two-element array and therefore does
// not have a copy constructor which the ringbuffer class needs.
class sampleFrame_copier
{
const sampleFrame* src;
public:
sampleFrame_copier(const sampleFrame* src) : src(src) {}
void operator()(std::size_t src_offset, std::size_t count, sampleFrame* dest)
{
for (std::size_t i = src_offset; i < src_offset + count; i++, dest++)
{
(*dest)[0] = src[i][0];
(*dest)[1] = src[i][1];
}
}
};


#define STRINGIFY(s) STR(s)
#define STR(PN) #PN
Expand Down
43 changes: 43 additions & 0 deletions include/lmms_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
53 changes: 49 additions & 4 deletions plugins/SpectrumAnalyzer/Analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@
#include "Analyzer.h"

#include "embed.h"
#include "lmms_basics.h"
#include "plugin_export.h"

#ifdef SA_DEBUG
he29-net marked this conversation as resolved.
Show resolved Hide resolved
#include <chrono>
#include <iostream>
#endif

extern "C" {
Plugin::Descriptor PLUGIN_EXPORT analyzer_plugin_descriptor =
Expand All @@ -38,7 +43,7 @@ extern "C" {
"Spectrum Analyzer",
QT_TRANSLATE_NOOP("pluginBrowser", "A graphical spectrum analyzer."),
"Martin Pavelek <he29/dot/HS/at/gmail/dot/com>",
0x0100,
0x0111,
Plugin::Effect,
new PluginPixmapLoader("logo"),
NULL,
Expand All @@ -50,17 +55,57 @@ extern "C" {
Analyzer::Analyzer(Model *parent, const Plugin::Descriptor::SubPluginFeatures::Key *key) :
Effect(&analyzer_plugin_descriptor, parent, key),
m_processor(&m_controls),
m_controls(this)
m_controls(this),
m_processorThread(m_processor, m_inputBuffer, m_notifier),
// Buffer is sized to cover 4* the current maximum LMMS audio buffer size,
// so that it has some reserve space in case data processor is busy.
m_inputBuffer(4 * m_maxBufferSize)
{
m_processorThread.start();
}


Analyzer::~Analyzer()
{
m_processor.terminate();
m_notifier.wakeAll();
m_processorThread.wait();
}

// Take audio data and pass them to the spectrum processor.
// Skip processing if the controls dialog isn't visible, it would only waste CPU cycles.
bool Analyzer::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count)
{
// Measure time spent in audio thread; both average and peak should be well under 1 ms.
#ifdef SA_DEBUG
unsigned int audio_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
if (audio_time - m_last_dump_time > 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()) {
he29-net marked this conversation as resolved.
Show resolved Hide resolved
// To avoid processing spikes on audio thread, data are stored in
// a lockless ringbuffer and processed in a separate thread.
sampleFrame_copier copier(buffer);
m_inputBuffer.write_func<sampleFrame_copier>(copier, frame_count);

// Inform processor to check the buffer (to avoid busy waiting).
m_notifier.wakeAll();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely (dependent on the implementation) not realtime safe. If you want to keep the realtime constraint up, it may be better to leave the other thread busy waiting 😕 I'm still thinking about a good solution, it's not easy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain how exactly does it cause a behavior that is not real-time safe?

Assuming you the effect processing thread has real-time priority (otherwise it does not matter), even if the QWaitCondition uses a mutex internally and it causes a syscall which returns control to the OS kernel, the kernel will just handle the syscall and return control back to your process (because you have real-time priority).

The way the mutex is used guarantees that it will always be free and available, because it only prevents multiple threads from trying to call the "wake up" at the same time. In this case there is only one thread such thread, so there is nothing else that could take the mutex, i.e. it will be always acquired instantly.

We also discussed delays that could occur because of context switch from user space to kernel space -- but these will occur anyway, since to achieve multi-tasking the kernel has to re-schedule what processes and threads are running every few milliseconds.

So, to summarize: syscall: no delay because you have realtime priority and context switches occur all the time anyway. Mutex: no delay because it is always free to take. So I still don't see the problem.

On the other hand, busy waiting has definite drawbacks: with high poll rate it needlessly burns CPU time (with cumulative effect over multiple plugin instances), and when the poll rate is low it wastes available CPU time by sleeping too long when you could have been processing data already.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really sorry, I've changed this text, but it seemed like my internet connection was broken at the time of editing.

I agree to all what you say, if you replace mutex by condition variable. Especially, I think you're right that this code does not hurt any of the 3 arguments not to use "mutexes" mentioned in the article from Ross Bencina. So, in theory, we could whitelist waking up threads in condition variables in stoat.

In practice, however:

  1. QWaitCondition uses a pthread_condition variable and a pthread_mutex_t.
  2. pthread_cond_wait uses __pthread_cond_mutex_lock (e.g. a mutex lock) plus a spinlock.

I imagine both cases are equally dangerous as the ones with the sem_post? @fundamental should we whitelist pthread_cond_wait, or do you consider it worse than sem_post?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The programming patterns that pthread_cond_wait tend to have worse semantics than sem_post. sem_post does involve an internal futex lock, but that lock is virtually never blocked while pthread_cond_wait assumes that the section should fairly regularly block waiting for an event to occur.

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

Expand Down
25 changes: 23 additions & 2 deletions plugins/SpectrumAnalyzer/Analyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,19 @@
#ifndef ANALYZER_H
#define ANALYZER_H

#include "DataprocLauncher.h"
#include "Effect.h"
#include "SaControls.h"
#include "SaProcessor.h"

#include <QWaitCondition>
he29-net marked this conversation as resolved.
Show resolved Hide resolved
#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h"

//! Top level class; handles LMMS interface and feeds data to the data processor.
class Analyzer : public Effect
{
public:
Analyzer(Model *parent, const Descriptor::SubPluginFeatures::Key *key);
virtual ~Analyzer() {};
virtual ~Analyzer();

bool processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) override;
EffectControls *controls() override {return &m_controls;}
Expand All @@ -47,6 +49,25 @@ class Analyzer : public Effect
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, m_notifier);});
DataprocLauncher m_processorThread;

ringbuffer_t<sampleFrame> m_inputBuffer;
QWaitCondition m_notifier;

#ifdef SA_DEBUG
int m_last_dump_time;
int m_dump_count;
float m_sum_execution;
float m_max_execution;
#endif
};

#endif // ANALYZER_H
Expand Down
6 changes: 4 additions & 2 deletions plugins/SpectrumAnalyzer/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
INCLUDE(BuildPlugin)
INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS})
LINK_LIBRARIES(${FFTW3F_LIBRARIES})

LINK_LIBRARIES(${FFTW3F_LIBRARIES} ringbuffer)

BUILD_PLUGIN(analyzer Analyzer.cpp SaProcessor.cpp SaControls.cpp SaControlsDialog.cpp SaSpectrumView.cpp SaWaterfallView.cpp
MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h EMBEDDED_RESOURCES *.svg logo.png)
MOCFILES SaProcessor.h SaControls.h SaControlsDialog.h SaSpectrumView.h SaWaterfallView.h DataprocLauncher.h EMBEDDED_RESOURCES *.svg logo.png)
55 changes: 55 additions & 0 deletions plugins/SpectrumAnalyzer/DataprocLauncher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 <QWaitCondition>
JohannesLorenz marked this conversation as resolved.
Show resolved Hide resolved

#include "SaProcessor.h"
#include "../../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h"

class DataprocLauncher : public QThread
{
public:
explicit DataprocLauncher(SaProcessor &proc, ringbuffer_t<sampleFrame> &buffer, QWaitCondition &notifier)
: m_processor(&proc),
m_inputBuffer(&buffer),
m_notifier(&notifier)
{
}

private:
void run() override
{
m_processor->analyze(*m_inputBuffer, *m_notifier);
}

SaProcessor *m_processor;
ringbuffer_t<sampleFrame> *m_inputBuffer;
QWaitCondition *m_notifier;
};

#endif // DATAPROCLAUNCHER_H
31 changes: 28 additions & 3 deletions plugins/SpectrumAnalyzer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,38 @@

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

he29-net marked this conversation as resolved.
Show resolved Hide resolved
## Changelog
1.1.1 2019-10-13
- improved interface for accessing SaProcessor provate data
- readme file update
- other small improvements based on reviews
1.1.0 2019-08-29
- advanced config: expose hidden constants to user
- advanced config: add support for FFT window overlapping
- 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
Expand Down
Loading