Add SlewDistortion effect and oversampling support (#7641)

This commit is contained in:
Lost Robot
2025-10-13 14:18:34 -05:00
committed by GitHub
parent 807751dc4d
commit a809c03f87
38 changed files with 2232 additions and 7 deletions

76
include/Draggable.h Normal file
View File

@@ -0,0 +1,76 @@
/*
* Draggable.h
*
* Copyright (c) 2025 Lost Robot <r94231/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 LMMS_GUI_DRAGGABLE_H
#define LMMS_GUI_DRAGGABLE_H
#include "FloatModelEditorBase.h"
namespace lmms::gui
{
/**
* @brief A pixmap that can be dragged from one location to another to control a FloatModel
*/
class LMMS_EXPORT Draggable : public FloatModelEditorBase
{
Q_OBJECT
public:
using FloatModelEditorBase::DirectionOfManipulation;
/**
* @param directionOfManipulation Direction the user can drag the control
* @param floatModel Pointer to the FloatModel this draggable modifies
* @param pixmap The pixmap shown for this handle
* @param pointA Starting pixel position (Y if vertical, X if horizontal)
* @param pointB Ending pixel position (Y if vertical, X if horizontal)
* @param parent Parent QWidget
*/
Draggable(DirectionOfManipulation directionOfManipulation,
FloatModel* floatModel, const QPixmap& pixmap, int pointA, int pointB, QWidget* parent = nullptr);
QSize sizeHint() const override;
void setPixmap(const QPixmap& pixmap);
void setDefaultValPixmap(const QPixmap& pixmap, float value = 0.f);
protected:
void paintEvent(QPaintEvent* event) override;
void mouseMoveEvent(QMouseEvent* me) override;
protected slots:
void handleMovement();
private:
QPixmap m_pixmap;
QPixmap m_defaultValPixmap;
float m_pointA;
float m_pointB;
float m_defaultValue;
bool m_hasDefaultValPixmap;
};
} // namespace lmms::gui
#endif // LMMS_GUI_DRAGGABLE_H

View File

@@ -85,13 +85,6 @@ protected:
void leaveEvent(QEvent *event) override;
virtual float getValue(const QPoint & p);
private slots:
virtual void enterValue();
void friendlyUpdate();
void toggleScale();
private:
virtual QString displayValue() const;
void doConnections() override;
@@ -114,6 +107,11 @@ private:
bool m_buttonPressed;
DirectionOfManipulation m_directionOfManipulation;
private slots:
virtual void enterValue();
void friendlyUpdate();
void toggleScale();
};
} // namespace lmms::gui

249
include/OversamplingHelpers.h Executable file
View File

@@ -0,0 +1,249 @@
/*
* OversamplingHelpers.h
*
* Copyright (c) 2025 Lost Robot <r94231/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 LMMS_OVERSAMPLING_HELPERS_H
#define LMMS_OVERSAMPLING_HELPERS_H
#include <algorithm>
#include <array>
#include <hiir/PolyphaseIir2Designer.h>
#ifdef __SSE2__
#include <hiir/Downsampler2xSse.h>
#include <hiir/Upsampler2xSse.h>
#else
#include <hiir/Downsampler2xFpu.h>
#include <hiir/Upsampler2xFpu.h>
#endif
inline constexpr float HIIR_DEFAULT_PASSBAND = 19600;
inline constexpr int HIIR_DEFAULT_MAX_COEFS = 8;
namespace lmms
{
template<int MaxStages, int MaxCoefs = 8>
class Upsampler
{
public:
void reset()
{
float bw = 0.5f - m_passband / m_sampleRate;
// Stage 1
double coefsFirst[s_firstCoefCount];
hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsFirst, s_firstCoefCount, bw);
m_upsampleFirst.set_coefs(coefsFirst);
m_upsampleFirst.clear_buffers();
bw = (bw + 0.5f) * 0.5f;
// Stage 2
double coefsSecond[s_secondCoefCount];
hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsSecond, s_secondCoefCount, bw);
m_upsampleSecond.set_coefs(coefsSecond);
m_upsampleSecond.clear_buffers();
bw = (bw + 0.5f) * 0.5f;
// Remaining stages
for (int i = 0; i < m_stages - 2; ++i)
{
double coefsRest[s_restCoefCount];
hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsRest, s_restCoefCount, bw);
m_upsampleRest[i].set_coefs(coefsRest);
m_upsampleRest[i].clear_buffers();
bw = (bw + 0.5f) * 0.5f;
}
}
void setup(int stages, float sampleRate, float passband = HIIR_DEFAULT_PASSBAND)
{
assert(stages <= MaxStages);
m_stages = stages;
m_sampleRate = sampleRate;
m_passband = passband;
reset();
}
// expects `2 ^ m_stages` elements for the output
void processSample(float* outSamples, float inSample)
{
int total = 1 << m_stages;
outSamples[0] = inSample;
int gap1 = total / 2;
if (m_stages >= 1)
{
m_upsampleFirst.process_sample(outSamples[0], outSamples[gap1], outSamples[0]);
}
if (m_stages >= 2)
{
int gap2 = gap1 / 2;
m_upsampleSecond.process_sample(outSamples[0], outSamples[gap2], outSamples[0]);
m_upsampleSecond.process_sample(outSamples[gap1], outSamples[gap1 + gap2], outSamples[gap1]);
}
for (int i = 2; i < m_stages; ++i)
{
int count = 1 << i;
int gap = total / count;
for (int j = 0; j < count; ++j)
{
int temp = j * gap;
m_upsampleRest[i - 2].process_sample(outSamples[temp], outSamples[temp + gap / 2], outSamples[temp]);
}
}
}
int getStages() const { return m_stages; }
float getSampleRate() const { return m_sampleRate; }
float getPassband() const { return m_passband; }
void setStages(int stages) { m_stages = stages; reset(); }
void setSampleRate(float sampleRate) { m_sampleRate = sampleRate; reset(); }
void setPassband(float passband) { m_passband = passband; reset(); }
private:
static constexpr int s_firstCoefCount = MaxCoefs;
static constexpr int s_secondCoefCount = std::max(MaxCoefs / 2, 2);
static constexpr int s_restCoefCount = std::max(MaxCoefs / 4, 2);
#ifdef __SSE2__
hiir::Upsampler2xSse<s_firstCoefCount> m_upsampleFirst;
hiir::Upsampler2xSse<s_secondCoefCount> m_upsampleSecond;
std::array<hiir::Upsampler2xSse<s_restCoefCount>, MaxStages - 2> m_upsampleRest;
#else
hiir::Upsampler2xFpu<s_firstCoefCount> m_upsampleFirst;
hiir::Upsampler2xFpu<s_secondCoefCount> m_upsampleSecond;
std::array<hiir::Upsampler2xFpu<s_restCoefCount>, MaxStages - 2> m_upsampleRest;
#endif
int m_stages = 0;
float m_sampleRate = 44100;
float m_passband = HIIR_DEFAULT_PASSBAND;
};
template<int MaxStages, int MaxCoefs = 8>
class Downsampler
{
public:
void reset()
{
float bw = 0.5f - m_passband / m_sampleRate;
// Stage 1
double coefsFirst[s_firstCoefCount];
hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsFirst, s_firstCoefCount, bw);
m_downsampleFirst.set_coefs(coefsFirst);
m_downsampleFirst.clear_buffers();
bw = (bw + 0.5f) * 0.5f;
// Stage 2
double coefsSecond[s_secondCoefCount];
hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsSecond, s_secondCoefCount, bw);
m_downsampleSecond.set_coefs(coefsSecond);
m_downsampleSecond.clear_buffers();
bw = (bw + 0.5f) * 0.5f;
// Remaining stages
for (int i = 0; i < m_stages - 2; ++i)
{
double coefsRest[s_restCoefCount];
hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefsRest, s_restCoefCount, bw);
m_downsampleRest[i].set_coefs(coefsRest);
m_downsampleRest[i].clear_buffers();
bw = (bw + 0.5f) * 0.5f;
}
}
void setup(int stages, float sampleRate, float passband = HIIR_DEFAULT_PASSBAND)
{
assert(stages <= MaxStages);
m_stages = stages;
m_sampleRate = sampleRate;
m_passband = passband;
reset();
}
// expects `2 ^ m_stages` elements for the input
float processSample(float* inSamples)
{
for (int i = m_stages - 1; i >= 2; --i)
{
for (int j = 0; j < 1 << i; ++j)
{
inSamples[j] = m_downsampleRest[i - 2].process_sample(&inSamples[j * 2]);
}
}
if (m_stages >= 2)
{
for (int j = 0; j < 2; ++j)
{
inSamples[j] = m_downsampleSecond.process_sample(&inSamples[j * 2]);
}
}
if (m_stages >= 1)
{
inSamples[0] = m_downsampleFirst.process_sample(&inSamples[0]);
}
return inSamples[0];
}
int getStages() const { return m_stages; }
float getSampleRate() const { return m_sampleRate; }
float getPassband() const { return m_passband; }
void setStages(int stages) { m_stages = stages; reset(); }
void setSampleRate(float sampleRate) { m_sampleRate = sampleRate; reset(); }
void setPassband(float passband) { m_passband = passband; reset(); }
private:
static constexpr int s_firstCoefCount = MaxCoefs;
static constexpr int s_secondCoefCount = std::max(MaxCoefs / 2, 2);
static constexpr int s_restCoefCount = std::max(MaxCoefs / 4, 2);
#ifdef __SSE2__
hiir::Downsampler2xSse<s_firstCoefCount> m_downsampleFirst;
hiir::Downsampler2xSse<s_secondCoefCount> m_downsampleSecond;
std::array<hiir::Downsampler2xSse<s_restCoefCount>, MaxStages - 2> m_downsampleRest;
#else
hiir::Downsampler2xFpu<s_firstCoefCount> m_downsampleFirst;
hiir::Downsampler2xFpu<s_secondCoefCount> m_downsampleSecond;
std::array<hiir::Downsampler2xFpu<s_restCoefCount>, MaxStages - 2> m_downsampleRest;
#endif
int m_stages = 0;
float m_sampleRate = 44100;
float m_passband = HIIR_DEFAULT_PASSBAND;
};
} // namespace lmms
#endif // LMMS_OVERSAMPLING_HELPERS_H

View File

@@ -36,6 +36,10 @@
#include "lmms_constants.h"
#ifdef __SSE2__
#include <emmintrin.h>
#endif
namespace lmms
{
@@ -280,6 +284,77 @@ private:
T m_b;
};
#ifdef __SSE2__
// exp approximation for SSE2: https://stackoverflow.com/a/47025627/5759631
// Maximum relative error of 1.72863156e-3 on [-87.33654, 88.72283]
inline __m128 fastExp(__m128 x)
{
__m128 f, p, r;
__m128i t, j;
const __m128 a = _mm_set1_ps (12102203.0f); /* (1 << 23) / log(2) */
const __m128i m = _mm_set1_epi32 (0xff800000); /* mask for integer bits */
const __m128 ttm23 = _mm_set1_ps (1.1920929e-7f); /* exp2(-23) */
const __m128 c0 = _mm_set1_ps (0.3371894346f);
const __m128 c1 = _mm_set1_ps (0.657636276f);
const __m128 c2 = _mm_set1_ps (1.00172476f);
t = _mm_cvtps_epi32 (_mm_mul_ps (a, x));
j = _mm_and_si128 (t, m); /* j = (int)(floor (x/log(2))) << 23 */
t = _mm_sub_epi32 (t, j);
f = _mm_mul_ps (ttm23, _mm_cvtepi32_ps (t)); /* f = (x/log(2)) - floor (x/log(2)) */
p = c0; /* c0 */
p = _mm_mul_ps (p, f); /* c0 * f */
p = _mm_add_ps (p, c1); /* c0 * f + c1 */
p = _mm_mul_ps (p, f); /* (c0 * f + c1) * f */
p = _mm_add_ps (p, c2); /* p = (c0 * f + c1) * f + c2 ~= 2^f */
r = _mm_castsi128_ps (_mm_add_epi32 (j, _mm_castps_si128 (p))); /* r = p * 2^i*/
return r;
}
// Lost Robot's SSE2 adaptation of Kari's vectorized log approximation: https://stackoverflow.com/a/65537754/5759631
// Maximum relative error of 7.922410e-4 on [1.0279774e-38f, 3.4028235e+38f]
inline __m128 fastLog(__m128 a)
{
__m128i aInt = _mm_castps_si128(a);
__m128i e = _mm_sub_epi32(aInt, _mm_set1_epi32(0x3f2aaaab));
e = _mm_and_si128(e, _mm_set1_epi32(0xff800000));
__m128i subtr = _mm_sub_epi32(aInt, e);
__m128 m = _mm_castsi128_ps(subtr);
__m128 i = _mm_mul_ps(_mm_cvtepi32_ps(e), _mm_set1_ps(1.19209290e-7f));
__m128 f = _mm_sub_ps(m, _mm_set1_ps(1.0f));
__m128 s = _mm_mul_ps(f, f);
__m128 r = _mm_add_ps(_mm_mul_ps(_mm_set1_ps(0.230836749f), f), _mm_set1_ps(-0.279208571f));
__m128 t = _mm_add_ps(_mm_mul_ps(_mm_set1_ps(0.331826031f), f), _mm_set1_ps(-0.498910338f));
r = _mm_add_ps(_mm_mul_ps(r, s), t);
r = _mm_add_ps(_mm_mul_ps(r, s), f);
r = _mm_add_ps(_mm_mul_ps(i, _mm_set1_ps(0.693147182f)), r);
return r;
}
inline __m128 sse2Abs(__m128 x)
{
return _mm_and_ps(x, _mm_castsi128_ps(_mm_set1_epi32(0x7fffffff)));// clear sign bit
}
inline __m128 sse2Floor(__m128 x)
{
__m128 t = _mm_cvtepi32_ps(_mm_cvttps_epi32(x)); // trunc toward 0
__m128 needs_correction = _mm_cmplt_ps(x, t); // checks if x < trunc
return _mm_sub_ps(t, _mm_and_ps(needs_correction, _mm_set1_ps(1.0f)));
}
inline __m128 sse2Round(__m128 x)
{
__m128 sign_mask = _mm_cmplt_ps(x, _mm_setzero_ps());// checks if x < 0
__m128 bias_pos = _mm_set1_ps(0.5f);
__m128 bias_neg = _mm_set1_ps(-0.5f);
__m128 bias = _mm_or_ps(_mm_and_ps(sign_mask, bias_neg), _mm_andnot_ps(sign_mask, bias_pos));
__m128 y = _mm_add_ps(x, bias);
return _mm_cvtepi32_ps(_mm_cvttps_epi32(y));
}
#endif // __SSE2__
} // namespace lmms
#endif // LMMS_MATH_H