mirror of
https://github.com/LMMS/lmms.git
synced 2026-01-25 23:08:12 -05:00
Summary: * `NULL` -> `nullptr` * `gui` -> Function `getGUI()` * `pluginFactory` -> Function `getPluginFactory()` * `assert` (redefinition) -> using `NDEBUG` instead, which standard `assert` respects. * `powf` (C stdlib symbol clash) -> removed and all expansions replaced with calls to `std::pow`. * `exp10` (nonstandard function symbol clash) -> removed and all expansions replaced with calls to `std::pow`. * `PATH_DEV_DSP` -> File-scope QString of identical name and value. * `VST_SNC_SHM_KEY_FILE` -> constexpr char* with identical name and value. * `MM_ALLOC` and `MM_FREE` -> Functions with identical name and implementation. * `INVAL`, `OUTVAL`, etc. for automation nodes -> Functions with identical names and implementations. * BandLimitedWave.h: All integer constant macros replaced with constexpr ints of same name and value. * `FAST_RAND_MAX` -> constexpr int of same name and value. * `QSTR_TO_STDSTR` -> Function with identical name and equivalent implementation. * `CCONST` -> constexpr function template with identical name and implementation. * `F_OPEN_UTF8` -> Function with identical name and equivalent implementation. * `LADSPA_PATH_SEPARATOR` -> constexpr char with identical name and value. * `UI_CTRL_KEY` -> constexpr char* with identical name and value. * `ALIGN_SIZE` -> Renamed to `LMMS_ALIGN_SIZE` and converted from a macro to a constexpr size_t. * `JACK_MIDI_BUFFER_MAX` -> constexpr size_t with identical name and value. * versioninfo.h: `PLATFORM`, `MACHINE` and `COMPILER_VERSION` -> prefixed with `LMMS_BUILDCONF_` and converted from macros to constexpr char* literals. * Header guard _OSCILLOSCOPE -> renamed to OSCILLOSCOPE_H * Header guard _TIME_DISPLAY_WIDGET -> renamed to TIME_DISPLAY_WIDGET_H * C-style typecasts in DrumSynth.cpp have been replaced with `static_cast`. * constexpr numerical constants are initialized with assignment notation instead of curly brace intializers. * In portsmf, `Alg_seq::operator[]` will throw an exception instead of returning null if the operator index is out of range. Additionally, in many places, global constants that were declared as `const T foo = bar;` were changed from const to constexpr, leaving them const and making them potentially evaluable at compile time. Some macros that only appeared in single source files and were unused in those files have been removed entirely.
693 lines
23 KiB
C++
693 lines
23 KiB
C++
/* SaProcessor.cpp - implementation of SaProcessor class.
|
|
*
|
|
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
|
*
|
|
* Based partially on Eq plugin code,
|
|
* Copyright (c) 2014-2017, David French <dave/dot/french3/at/googlemail/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.
|
|
*
|
|
*/
|
|
|
|
#include "SaProcessor.h"
|
|
|
|
#include <algorithm>
|
|
#ifdef SA_DEBUG
|
|
#include <chrono>
|
|
#endif
|
|
#include <cmath>
|
|
#ifdef SA_DEBUG
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#endif
|
|
#include <QMutexLocker>
|
|
|
|
#include "lmms_math.h"
|
|
#include "LocklessRingBuffer.h"
|
|
|
|
|
|
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::audioEngine()->processingSampleRate()),
|
|
m_framesFilledUp(0),
|
|
m_spectrumActive(false),
|
|
m_waterfallActive(false),
|
|
m_waterfallNotEmpty(0),
|
|
m_reallocating(false)
|
|
{
|
|
m_fftWindow.resize(m_inBlockSize, 1.0);
|
|
precomputeWindow(m_fftWindow.data(), m_inBlockSize, BLACKMAN_HARRIS);
|
|
|
|
m_bufferL.resize(m_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_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_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);
|
|
}
|
|
|
|
|
|
SaProcessor::~SaProcessor()
|
|
{
|
|
if (m_fftPlanL != nullptr) {fftwf_destroy_plan(m_fftPlanL);}
|
|
if (m_fftPlanR != nullptr) {fftwf_destroy_plan(m_fftPlanR);}
|
|
if (m_spectrumL != nullptr) {fftwf_free(m_spectrumL);}
|
|
if (m_spectrumR != nullptr) {fftwf_free(m_spectrumR);}
|
|
|
|
m_fftPlanL = nullptr;
|
|
m_fftPlanR = nullptr;
|
|
m_spectrumL = nullptr;
|
|
m_spectrumR = nullptr;
|
|
}
|
|
|
|
|
|
// Load data from audio thread ringbuffer and run FFT analysis if buffer is full enough.
|
|
void SaProcessor::analyze(LocklessRingBuffer<sampleFrame> &ring_buffer)
|
|
{
|
|
LocklessRingBufferReader<sampleFrame> reader(ring_buffer);
|
|
|
|
// Processing thread loop
|
|
while (!m_terminate)
|
|
{
|
|
// 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)
|
|
{
|
|
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::audioEngine()->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)
|
|
{
|
|
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_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++)
|
|
{
|
|
// fill line with red color to indicate lost data if CPU cannot keep up
|
|
if (overload && i < waterfallWidth())
|
|
{
|
|
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)
|
|
{
|
|
// 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]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
m_history = m_history_work;
|
|
m_flipRequest = false;
|
|
}
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
#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
|
|
}
|
|
|
|
|
|
// Produce a spectrogram pixel from normalized spectrum data.
|
|
// Values over 1.0 will cause the color components to overflow: this is left
|
|
// intentionally untreated as it clearly indicates which frequency is clipping.
|
|
// Gamma correction is applied to make small values more visible and to make
|
|
// a linear gradient actually appear roughly linear. The correction should be
|
|
// around 0.42 to 0.45 for sRGB displays (or lower for bigger visibility boost).
|
|
QRgb SaProcessor::makePixel(float left, float right) const
|
|
{
|
|
const float gamma_correction = m_controls->m_waterfallGammaModel.value();
|
|
if (m_controls->m_stereoModel.value())
|
|
{
|
|
float ampL = pow(left, gamma_correction);
|
|
float ampR = pow(right, gamma_correction);
|
|
return qRgb(m_controls->m_colorL.red() * ampL + m_controls->m_colorR.red() * ampR,
|
|
m_controls->m_colorL.green() * ampL + m_controls->m_colorR.green() * ampR,
|
|
m_controls->m_colorL.blue() * ampL + m_controls->m_colorR.blue() * ampR);
|
|
}
|
|
else
|
|
{
|
|
float ampL = pow(left, gamma_correction);
|
|
// make mono color brighter to compensate for the fact it is not summed
|
|
return qRgb(m_controls->m_colorMonoW.red() * ampL,
|
|
m_controls->m_colorMonoW.green() * ampL,
|
|
m_controls->m_colorMonoW.blue() * ampL);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Inform the processor whether any display widgets actually need it.
|
|
void SaProcessor::setSpectrumActive(bool active)
|
|
{
|
|
m_spectrumActive = active;
|
|
}
|
|
|
|
void SaProcessor::setWaterfallActive(bool active)
|
|
{
|
|
m_waterfallActive = active;
|
|
}
|
|
|
|
|
|
// Reallocate data buffers according to newly set block size.
|
|
void SaProcessor::reallocateBuffers()
|
|
{
|
|
unsigned int new_size_index = m_controls->m_blockSizeModel.value();
|
|
unsigned int new_in_size, new_fft_size;
|
|
unsigned int new_bins;
|
|
|
|
// get new block sizes and bin count based on selected index
|
|
if (new_size_index < FFT_BLOCK_SIZES.size())
|
|
{
|
|
new_in_size = FFT_BLOCK_SIZES[new_size_index];
|
|
}
|
|
else
|
|
{
|
|
new_in_size = FFT_BLOCK_SIZES.back();
|
|
}
|
|
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];
|
|
}
|
|
else
|
|
{
|
|
new_fft_size = FFT_BLOCK_SIZES.back();
|
|
}
|
|
|
|
new_bins = new_fft_size / 2 +1;
|
|
|
|
// 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;
|
|
|
|
// 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 != nullptr) {fftwf_destroy_plan(m_fftPlanL);}
|
|
if (m_fftPlanR != nullptr) {fftwf_destroy_plan(m_fftPlanR);}
|
|
if (m_spectrumL != nullptr) {fftwf_free(m_spectrumL);}
|
|
if (m_spectrumR != nullptr) {fftwf_free(m_spectrumR);}
|
|
|
|
// allocate new space, create new plan and resize containers
|
|
m_fftWindow.resize(new_in_size, 1.0);
|
|
precomputeWindow(m_fftWindow.data(), new_in_size, (FFT_WINDOWS) m_controls->m_windowModel.value());
|
|
m_bufferL.resize(new_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_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 == nullptr || m_fftPlanR == nullptr)
|
|
{
|
|
#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_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;
|
|
|
|
data_lock.unlock();
|
|
reloc_lock.unlock();
|
|
m_reallocating = false;
|
|
|
|
clear();
|
|
}
|
|
|
|
|
|
// Precompute a new FFT window based on currently selected type.
|
|
void SaProcessor::rebuildWindow()
|
|
{
|
|
// computation is done in fft_helpers
|
|
QMutexLocker lock(&m_dataAccess);
|
|
precomputeWindow(m_fftWindow.data(), m_inBlockSize, (FFT_WINDOWS) m_controls->m_windowModel.value());
|
|
}
|
|
|
|
|
|
// Clear all data buffers and replace contents with zeros.
|
|
// Note: may take a few milliseconds, do not call in a loop!
|
|
void SaProcessor::clear()
|
|
{
|
|
const unsigned int overlaps = m_controls->m_windowOverlapModel.value();
|
|
QMutexLocker lock(&m_dataAccess);
|
|
// 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
|
|
//
|
|
|
|
// Get sample rate value that is valid for currently stored results.
|
|
unsigned int SaProcessor::getSampleRate() const
|
|
{
|
|
return m_sampleRate;
|
|
}
|
|
|
|
|
|
// Maximum frequency of a sampled signal is equal to half of its sample rate.
|
|
float SaProcessor::getNyquistFreq() const
|
|
{
|
|
return getSampleRate() / 2.0f;
|
|
}
|
|
|
|
|
|
// FFTW automatically discards upper half of the symmetric FFT output, so
|
|
// the useful bin count is the transform size divided by 2, plus zero.
|
|
unsigned int SaProcessor::binCount() const
|
|
{
|
|
return m_fftBlockSize / 2 + 1;
|
|
}
|
|
|
|
|
|
// Return the 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
|
|
{
|
|
return getNyquistFreq() * bin_index / binCount();
|
|
}
|
|
|
|
|
|
// Return width of the frequency range that falls into one bin.
|
|
// The binCount is lowered by one since half of the first and last bin is
|
|
// actually outside the frequency range.
|
|
float SaProcessor::binBandwidth() const
|
|
{
|
|
return getNyquistFreq() / (binCount() - 1);
|
|
}
|
|
|
|
|
|
float SaProcessor::getFreqRangeMin(bool linear) const
|
|
{
|
|
switch (m_controls->m_freqRangeModel.value())
|
|
{
|
|
case FRANGE_AUDIBLE: return FRANGE_AUDIBLE_START;
|
|
case FRANGE_BASS: return FRANGE_BASS_START;
|
|
case FRANGE_MIDS: return FRANGE_MIDS_START;
|
|
case FRANGE_HIGH: return FRANGE_HIGH_START;
|
|
default:
|
|
case FRANGE_FULL: return linear ? 0 : LOWEST_LOG_FREQ;
|
|
}
|
|
}
|
|
|
|
|
|
float SaProcessor::getFreqRangeMax() const
|
|
{
|
|
switch (m_controls->m_freqRangeModel.value())
|
|
{
|
|
case FRANGE_AUDIBLE: return FRANGE_AUDIBLE_END;
|
|
case FRANGE_BASS: return FRANGE_BASS_END;
|
|
case FRANGE_MIDS: return FRANGE_MIDS_END;
|
|
case FRANGE_HIGH: return FRANGE_HIGH_END;
|
|
default:
|
|
case FRANGE_FULL: return getNyquistFreq();
|
|
}
|
|
}
|
|
|
|
|
|
// Map frequency to pixel x position on a display of given width.
|
|
float SaProcessor::freqToXPixel(float freq, unsigned int width) const
|
|
{
|
|
if (m_controls->m_logXModel.value())
|
|
{
|
|
if (freq <= 1) {return 0;}
|
|
float min = log10(getFreqRangeMin());
|
|
float range = log10(getFreqRangeMax()) - min;
|
|
return (log10(freq) - min) / range * width;
|
|
}
|
|
else
|
|
{
|
|
float min = getFreqRangeMin();
|
|
float range = getFreqRangeMax() - min;
|
|
return (freq - min) / range * width;
|
|
}
|
|
}
|
|
|
|
|
|
// Map pixel x position on display of given width back to frequency.
|
|
float SaProcessor::xPixelToFreq(float x, unsigned int width) const
|
|
{
|
|
if (m_controls->m_logXModel.value())
|
|
{
|
|
float min = log10(getFreqRangeMin());
|
|
float max = log10(getFreqRangeMax());
|
|
float range = max - min;
|
|
return pow(10, min + x / width * range);
|
|
}
|
|
else
|
|
{
|
|
float min = getFreqRangeMin();
|
|
float range = getFreqRangeMax() - min;
|
|
return min + x / width * range;
|
|
}
|
|
}
|
|
|
|
|
|
// --------------------------------------
|
|
// Amplitude conversion helpers
|
|
//
|
|
float SaProcessor::getAmpRangeMin(bool linear) const
|
|
{
|
|
// return very low limit to make sure zero gets included at linear grid
|
|
if (linear) {return -900;}
|
|
switch (m_controls->m_ampRangeModel.value())
|
|
{
|
|
case ARANGE_EXTENDED: return ARANGE_EXTENDED_START;
|
|
case ARANGE_SILENT: return ARANGE_SILENT_START;
|
|
case ARANGE_LOUD: return ARANGE_LOUD_START;
|
|
default:
|
|
case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_START;
|
|
}
|
|
}
|
|
|
|
|
|
float SaProcessor::getAmpRangeMax() const
|
|
{
|
|
switch (m_controls->m_ampRangeModel.value())
|
|
{
|
|
case ARANGE_EXTENDED: return ARANGE_EXTENDED_END;
|
|
case ARANGE_SILENT: return ARANGE_SILENT_END;
|
|
case ARANGE_LOUD: return ARANGE_LOUD_END;
|
|
default:
|
|
case ARANGE_AUDIBLE: return ARANGE_AUDIBLE_END;
|
|
}
|
|
}
|
|
|
|
|
|
// Map linear amplitude to pixel y position on a display of given height.
|
|
// Note that display coordinates are flipped: amplitude grows from [height] to zero.
|
|
float SaProcessor::ampToYPixel(float amplitude, unsigned int height) const
|
|
{
|
|
if (m_controls->m_logYModel.value())
|
|
{
|
|
// logarithmic scale: convert linear amplitude to dB (relative to 1.0)
|
|
float amplitude_dB = 10 * log10(amplitude);
|
|
if (amplitude_dB < getAmpRangeMin())
|
|
{
|
|
return height;
|
|
}
|
|
else
|
|
{
|
|
float max = getAmpRangeMax();
|
|
float range = getAmpRangeMin() - max;
|
|
return (amplitude_dB - max) / range * height;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// linear scale: convert returned ranges from dB to linear scale
|
|
float max = pow(10, getAmpRangeMax() / 10);
|
|
float range = pow(10, getAmpRangeMin() / 10) - max;
|
|
return (amplitude - max) / range * height;
|
|
}
|
|
}
|
|
|
|
|
|
// Map pixel y position on display of given height back to amplitude.
|
|
// Note that display coordinates are flipped: amplitude grows from [height] to zero.
|
|
// Also note that in logarithmic Y mode the returned amplitude is in dB, not linear.
|
|
float SaProcessor::yPixelToAmp(float y, unsigned int height) const
|
|
{
|
|
if (m_controls->m_logYModel.value())
|
|
{
|
|
float max = getAmpRangeMax();
|
|
float range = getAmpRangeMin() - max;
|
|
return max + range * (y / height);
|
|
}
|
|
else
|
|
{
|
|
// linear scale: convert returned ranges from dB to linear scale
|
|
float max = pow(10, getAmpRangeMax() / 10);
|
|
float range = pow(10, getAmpRangeMin() / 10) - max;
|
|
return max + range * (y / height);
|
|
}
|
|
}
|
|
|