mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-28 02:43:11 -04:00
* Remove the struct StereoSample Remove the struct `StereoSample`. Let `AudioEngine::getPeakValues` return a `sampleFrame` instead. Adjust the calls in `Mixer` and `Oscilloscope`. * Simplify AudioEngine::getPeakValues * Remove surroundSampleFrame Some code assumes that `surroundSampleFrame` is interchangeable with `sampleFrame`. Thus, if the line `#define LMMS_DISABLE_SURROUND` is commented out in `lmms_basics.h` then the code does not compile anymore because `surroundSampleFrame` now is defined to be an array with four values instead of two. There also does not seem to be any support for surround sound (four channels instead of two) in the application. The faders and mixers do not seem to support more that two channels and the instruments and effects all expect a `sampleFrame`, i.e. stereo channels. It therefore makes sense to remove the "feature" because it also hinders the improvement of `sampleFrame`, e.g. by making it a class with some convenience methods that act on `sampleFrame` instances. All occurrences of `surroundSampleFrame` are replaced with `sampleFrame`. The version of `BufferManager::clear` that takes a `surroundSampleFrame` is removed completely. The define `SURROUND_CHANNELS` is removed. All its occurrences are replaced with `DEFAULT_CHANNELS`. Most of the audio devices classes, i.e. classes that inherit from `AudioDevice`, now clamp the configuration parameter between two values of `DEFAULT_CHANNELS`. This can be improved/streamlined later. `BYTES_PER_SURROUND_FRAME` has been removed as it was not used anywhere anyway. * Make sampleFrame a class Make `sampleFrame` a class with several convenience methods. As a first step and demonstration adjust the follow methods to make use of the new functionality: * `AudioEngine::getPeakValues`: Much more concise now. * `lmms::MixHelpers::sanitize`: Better structure, better readable, less dereferencing and juggling with indices. * `AddOp`, `AddMultipliedOp`, `multiply`: Make use of operators. Might become superfluous in the future. * More operators and methods for sampleFrame Add some more operators and methods to `sampleFrame`: * Constructor which initializes both channels from a single sample value * Assignment operator from a single sample value * Addition/multiplication operators * Scalar product Adjust some more plugins to the new functionality of `sampleFrame`. * Adjust DelayEffect to methods in sampleFrame * Use composition instead of inheritance Using inheritance was the quickest way to enable adding methods to `sampleFrame` without having to reimpement much of `std::array`s interface. This is changed with this commit. The array is now a member of `sampleFrame` and the interface is extended with the necessary methods `data` and the index operator. An `average` method was added so that no iterators need to be implemented (see changes in `SampleWaveform.cpp`). * Apply suggestions from code review Apply Veratil's suggestions from the code review Co-authored-by: Kevin Zander <veratil@gmail.com> * Fix warnings: zeroing non-trivial type Fix several warnings of the following form: Warnung: »void* memset(void*, int, size_t)« Säubern eines Objekts von nichttrivialem Typ »class lmms::sampleFrame«; use assignment or value-initialization instead [-Wclass-memaccess] * Remove unnecessary reinterpret_casts Remove some unnecessary reinterpret_casts with regards to `sampleFrame` buffers. `PlayHandle::m_playHandleBuffer` already is a `sampleFrame*` and does not need a reinterpret_cast anymore. In `LadspaEffect::processAudioBuffer` the `QVarLengthArray` is now directly initialized as an array of `sampleFrame` instances. I guess in both places the `sampleFrame` previously was a `surroundSampleFrame` which has been removed. * Clean up zeroSampleFrames code * Fix warnings in RemotePlugin Fix some warnings related to calls to `memcpy` in conjunction with`sampleFrame` which is now a class. Add the helper functions `copyToSampleFrames` and `copyFromSampleFrames` and use them. The first function copies data from a `float` buffer into a `sampleFrame` buffer and the second copies vice versa. * Rename "sampleFrame" to "SampleFrame" Uppercase the name of `sampleFrame` so that it uses UpperCamelCase convention. * Move SampleFrame into its own file Move the class `SampleFrame` into its own class and remove it from `lmms_basics.h`. Add forward includes to all headers where possible or include the `SampleFrame` header if it's not just referenced but used. Add include to all cpp files where necessary. It's a bit surprising that the `SampleFrame` header does not need to be included much more often in the implementation/cpp files. This is an indicator that it seems to be included via an include chain that at one point includes one of the headers where an include instead of a forward declaration had to be added in this commit. * Return reference for += and *= Return a reference for the compound assignment operators `+=` and `-=`. * Explicit float constructor Make the constructor that takes a `float` explicit. Remove the assignment operator that takes a `float`. Clients must use the explicit `float` constructor and assign the result. Adjust the code in "BitInvader" accordingly. * Use std::fill in zeroSampleFrames * Use zeroSampleFrames in sanitize * Replace max with absMax Replace `SampleFrame::max` with `SampleFrame::absMax`. Use `absMax` in `DelayEffect::processAudioBuffer`. This should also fix a buggy implementation of the peak computation. Add the function `getAbsPeakValues`. It computes the absolute peak values for a buffer. Remove `AudioEngine::getPeakValues`. It's not really the business of the audio engine. Let `Mixer` and `Oscilloscope` use `getAbsPeakValues`. * Replace scalarProduct Replace the rather mathematical method `scalarProduct` with `sumOfSquaredAmplitudes`. It was always called on itself anyway. * Remove comment/TODO * Simplify sanitize Simplify the `sanitize` function by getting rid of the `bool found` and by zeroing the buffer as soon as a problem is found. * Put pointer symbols next to type * Code review adjustments * Remove "#pragme once" * Adjust name of include guard * Remove superfluous includes (leftovers from previous code changes) --------- Co-authored-by: Kevin Zander <veratil@gmail.com>
692 lines
24 KiB
C++
692 lines
24 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 "fft_helpers.h"
|
|
#include "lmms_constants.h"
|
|
#include "LocklessRingBuffer.h"
|
|
#include "SaControls.h"
|
|
|
|
#include <cassert>
|
|
#include <limits>
|
|
|
|
namespace lmms
|
|
{
|
|
|
|
|
|
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()->outputSampleRate()),
|
|
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, FFTWindow::BlackmanHarris);
|
|
|
|
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()->outputSampleRate();
|
|
|
|
// 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
|
|
auto 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
|
|
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 (int target = std::max(static_cast<int>(band_start), 0);
|
|
target < band_end && target < waterfallWidth(); target++)
|
|
{
|
|
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
|
|
int target = static_cast<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 (int target = std::max(static_cast<int>(band_start), 0);
|
|
target < band_end && target < waterfallWidth(); target++)
|
|
{
|
|
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()
|
|
{
|
|
m_zeroPadFactor = m_controls->m_zeroPaddingModel.value();
|
|
|
|
// get new block sizes and bin count based on selected index
|
|
const unsigned int new_size_index = m_controls->m_blockSizeModel.value();
|
|
|
|
const unsigned int new_in_size = new_size_index < FFT_BLOCK_SIZES.size()
|
|
? FFT_BLOCK_SIZES[new_size_index]
|
|
: FFT_BLOCK_SIZES.back();
|
|
|
|
const unsigned int new_fft_size = (new_size_index + m_zeroPadFactor < FFT_BLOCK_SIZES.size())
|
|
? FFT_BLOCK_SIZES[new_size_index + m_zeroPadFactor]
|
|
: FFT_BLOCK_SIZES.back();
|
|
|
|
const unsigned int 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, (FFTWindow) 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, (FFTWindow) 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 (static_cast<FrequencyRange>(m_controls->m_freqRangeModel.value()))
|
|
{
|
|
case FrequencyRange::Audible: return FRANGE_AUDIBLE_START;
|
|
case FrequencyRange::Bass: return FRANGE_BASS_START;
|
|
case FrequencyRange::Mids: return FRANGE_MIDS_START;
|
|
case FrequencyRange::High: return FRANGE_HIGH_START;
|
|
default:
|
|
case FrequencyRange::Full: return linear ? 0 : LOWEST_LOG_FREQ;
|
|
}
|
|
}
|
|
|
|
|
|
float SaProcessor::getFreqRangeMax() const
|
|
{
|
|
switch (static_cast<FrequencyRange>(m_controls->m_freqRangeModel.value()))
|
|
{
|
|
case FrequencyRange::Audible: return FRANGE_AUDIBLE_END;
|
|
case FrequencyRange::Bass: return FRANGE_BASS_END;
|
|
case FrequencyRange::Mids: return FRANGE_MIDS_END;
|
|
case FrequencyRange::High: return FRANGE_HIGH_END;
|
|
default:
|
|
case FrequencyRange::Full: return getNyquistFreq();
|
|
}
|
|
}
|
|
|
|
|
|
// Map frequency to pixel x position on a display of given width.
|
|
// NOTE: Results of this function may be cached by SaSpectrumView. If you use
|
|
// a new function call or variable that can affect results of this function,
|
|
// make sure to also add it as a trigger for cache update in SaSpectrumView.
|
|
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 (static_cast<AmplitudeRange>(m_controls->m_ampRangeModel.value()))
|
|
{
|
|
case AmplitudeRange::Extended: return ARANGE_EXTENDED_START;
|
|
case AmplitudeRange::Silent: return ARANGE_SILENT_START;
|
|
case AmplitudeRange::Loud: return ARANGE_LOUD_START;
|
|
default:
|
|
case AmplitudeRange::Audible: return ARANGE_AUDIBLE_START;
|
|
}
|
|
}
|
|
|
|
|
|
float SaProcessor::getAmpRangeMax() const
|
|
{
|
|
switch (static_cast<AmplitudeRange>(m_controls->m_ampRangeModel.value()))
|
|
{
|
|
case AmplitudeRange::Extended: return ARANGE_EXTENDED_END;
|
|
case AmplitudeRange::Silent: return ARANGE_SILENT_END;
|
|
case AmplitudeRange::Loud: return ARANGE_LOUD_END;
|
|
default:
|
|
case AmplitudeRange::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)
|
|
assert (amplitude >= 0);
|
|
float amplitude_dB = 10 * std::log10(std::max(amplitude, std::numeric_limits<float>::min()));
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
} // namespace lmms
|