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 all 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
132 changes: 132 additions & 0 deletions include/LocklessRingBuffer.h
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

srcOffset

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was there really a camel case thing in the conventions before? I often used underscore in local variable names before in all the other classes.
I feel like the conventions are a moving target... There could at least be an announcement when they change.

Copy link
Contributor

Choose a reason for hiding this comment

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

They changed a lot recently. It earlier said: "Variable and method names begin with a lower case letter". @Veratil changed it to "SHOULD be camelCase". So it's indeed optional. But clang-format will eradicate any style freedom anyways

{
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
2 changes: 2 additions & 0 deletions include/RingBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
51 changes: 47 additions & 4 deletions plugins/SpectrumAnalyzer/Analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@

#include "Analyzer.h"

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

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


Expand All @@ -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,
Expand All @@ -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();
}

Expand Down
24 changes: 23 additions & 1 deletion plugins/SpectrumAnalyzer/Analyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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;}
Expand All @@ -47,6 +51,24 @@ 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);});
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
Expand Down
4 changes: 3 additions & 1 deletion 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})

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)
52 changes: 52 additions & 0 deletions plugins/SpectrumAnalyzer/DataprocLauncher.h
Original file line number Diff line number Diff line change
@@ -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
Loading