Skip to content

Commit

Permalink
Spectrum analyzer update (LMMS#5160)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
he29-net authored and JohannesLorenz committed Nov 21, 2019
1 parent 2f00102 commit da73ddd
Show file tree
Hide file tree
Showing 26 changed files with 1,865 additions and 362 deletions.
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)
{
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
#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

0 comments on commit da73ddd

Please sign in to comment.