Add Frequency Shifter effect (not a pitch shifter) (#8140)
3
plugins/FrequencyShifter/CMakeLists.txt
Executable file
@@ -0,0 +1,3 @@
|
||||
INCLUDE(BuildPlugin)
|
||||
|
||||
BUILD_PLUGIN(frequencyshifter FrequencyShifterEffect.cpp FrequencyShifterControls.cpp FrequencyShifterControlDialog.cpp MOCFILES FrequencyShifterEffect.h FrequencyShifterControls.h FrequencyShifterControlDialog.h EMBEDDED_RESOURCES *.png)
|
||||
259
plugins/FrequencyShifter/FrequencyShifterControlDialog.cpp
Executable file
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* FrequencyShifterControlDialog.cpp
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "FrequencyShifterControlDialog.h"
|
||||
#include "FrequencyShifterControls.h"
|
||||
|
||||
#include <QTextEdit>
|
||||
|
||||
#include "AutomatableButton.h"
|
||||
#include "embed.h"
|
||||
#include "GuiApplication.h"
|
||||
#include "Knob.h"
|
||||
#include "LcdFloatSpinBox.h"
|
||||
#include "MainWindow.h"
|
||||
#include "PixmapButton.h"
|
||||
|
||||
namespace lmms::gui
|
||||
{
|
||||
|
||||
static inline void setupKnobGeometry(Knob* k, int w, int h)
|
||||
{
|
||||
k->setFixedSize(w, h);
|
||||
|
||||
const int cx = w / 2;
|
||||
const int cy = h / 2;
|
||||
k->setCenterPointX(cx);
|
||||
k->setCenterPointY(cy);
|
||||
|
||||
int outer = std::max(1, cx - 3);
|
||||
int inner = std::max(1, outer - ((w >= 40) ? 16 : (w >= 24) ? 10 : 6));
|
||||
|
||||
if (w <= 16)
|
||||
{
|
||||
outer = cx - 2;
|
||||
inner = 2;
|
||||
}
|
||||
|
||||
k->setOuterRadius(outer);
|
||||
k->setInnerRadius(inner);
|
||||
}
|
||||
|
||||
FrequencyShifterControlDialog::FrequencyShifterControlDialog(FrequencyShifterControls* c) :
|
||||
EffectControlDialog(c)
|
||||
{
|
||||
setAutoFillBackground(true);
|
||||
QPalette pal;
|
||||
pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork"));
|
||||
setPalette(pal);
|
||||
setFixedSize(288, 360);
|
||||
|
||||
auto mk = [this](int x, int y,
|
||||
const QString& lbl, FloatModel* m, const QString& unit,
|
||||
const char* objName,
|
||||
QSize sz)
|
||||
{
|
||||
Knob* k = new Knob(KnobType::Styled, this);
|
||||
k->setObjectName(objName);
|
||||
k->move(x, y);
|
||||
k->setModel(m);
|
||||
k->setHintText(lbl, unit);
|
||||
setupKnobGeometry(k, sz.width(), sz.height());
|
||||
return k;
|
||||
};
|
||||
|
||||
const QSize K60(60, 60);
|
||||
const QSize K36(36, 36);
|
||||
const QSize K24(24, 24);
|
||||
const QSize K19(19, 19);
|
||||
|
||||
LcdFloatSpinBox* shiftSpin = new LcdFloatSpinBox(6, 3, "19red", tr("Frequency Shift"), this);
|
||||
shiftSpin->move(100, 43);
|
||||
shiftSpin->setModel(&c->m_freqShift);
|
||||
shiftSpin->setSeamless(true, true);
|
||||
|
||||
mk(18, 30, "Mix", &c->m_mix, "", "fs_mix", K60);
|
||||
mk(235, 24, "Spread", &c->m_spreadShift,"Hz", "fs_spread", K24);
|
||||
mk(235, 72, "Phase",&c->m_phase, "", "fs_phase", K24);
|
||||
mk(24, 115, "Ring", &c->m_ring, "", "fs_ring", K36);
|
||||
mk(72, 115, "Harmonics", &c->m_harmonics, "", "fs_harm", K36);
|
||||
mk(120, 115, "Tone",&c->m_tone, "Hz", "fs_tone", K36);
|
||||
mk(200, 147, "Glide", &c->m_glide, "", "fs_glide", K19);
|
||||
|
||||
mk(18, 200, "LFO", &c->m_lfoAmount, "Hz", "fs_lfo", K36);
|
||||
mk(66, 200, "LFO Rate", &c->m_lfoRate, "Hz", "fs_lforate", K36);
|
||||
mk(114, 200, "LFO Stereo Phase", &c->m_lfoStereoPhase, "", "fs_lfost", K36);
|
||||
|
||||
mk(18, 282, "Delay Length", &c->m_delayLengthLong, "ms", "fs_delay", K36);
|
||||
mk(114, 282, "Feedback", &c->m_feedback, "", "fs_feedback", K36);
|
||||
mk(24, 324, "Delay Length (fine)", &c->m_delayLengthShort, "ms", "fs_finedelay", K24);
|
||||
mk(120, 324, "Delay Damping", &c->m_delayDamp, "Hz", "fs_damp", K24);
|
||||
mk(245, 315, "Delay Glide", &c->m_delayGlide, "", "fs_dglide", K19);
|
||||
|
||||
PixmapButton* antireflectButton = new PixmapButton(this, "Antireflect");
|
||||
antireflectButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("antireflect_on"));
|
||||
antireflectButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("antireflect_off"));
|
||||
antireflectButton->setToolTip("Anti-reflect");
|
||||
antireflectButton->move(188, 122);
|
||||
antireflectButton->setCheckable(true);
|
||||
antireflectButton->setModel(&c->m_antireflect);
|
||||
|
||||
PixmapButton* routeSend = new PixmapButton(this, tr("Send"));
|
||||
routeSend->setActiveGraphic(PLUGIN_NAME::getIconPixmap("send_on"));
|
||||
routeSend->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("send_off"));
|
||||
routeSend->setToolTip(tr("Route: Send"));
|
||||
routeSend->setCheckable(true);
|
||||
routeSend->move(188, 199);
|
||||
|
||||
PixmapButton* routePass = new PixmapButton(this, tr("Pass"));
|
||||
routePass->setActiveGraphic(PLUGIN_NAME::getIconPixmap("pass_on"));
|
||||
routePass->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("pass_off"));
|
||||
routePass->setToolTip(tr("Route: Pass"));
|
||||
routePass->setCheckable(true);
|
||||
routePass->move(188, 217);
|
||||
|
||||
PixmapButton* routeMute = new PixmapButton(this, tr("Mute"));
|
||||
routeMute->setActiveGraphic(PLUGIN_NAME::getIconPixmap("mute_on"));
|
||||
routeMute->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("mute_off"));
|
||||
routeMute->setToolTip(tr("Route: Mute"));
|
||||
routeMute->setCheckable(true);
|
||||
routeMute->move(188, 235);
|
||||
|
||||
AutomatableButtonGroup* routeGroup = new AutomatableButtonGroup(this);
|
||||
routeGroup->addButton(routeSend);
|
||||
routeGroup->addButton(routePass);
|
||||
routeGroup->addButton(routeMute);
|
||||
routeGroup->setModel(&c->m_routeMode);
|
||||
|
||||
PixmapButton* resetShifterBtn = new PixmapButton(this, tr("Reset Shifter"));
|
||||
resetShifterBtn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("reset_shifter_on"));
|
||||
resetShifterBtn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("reset_shifter_off"));
|
||||
resetShifterBtn->setToolTip(tr("Reset the shifter's oscillator phases to 0 (automatable)"));
|
||||
resetShifterBtn->setCheckable(false);
|
||||
resetShifterBtn->move(77, 5);
|
||||
resetShifterBtn->setModel(&c->m_resetShifter);
|
||||
|
||||
PixmapButton* resetLfoBtn = new PixmapButton(this, tr("Reset LFO"));
|
||||
resetLfoBtn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("reset_lfo_on"));
|
||||
resetLfoBtn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("reset_lfo_off"));
|
||||
resetLfoBtn->setToolTip(tr("Reset the LFO phase to 0 (automatable)"));
|
||||
resetLfoBtn->setCheckable(false);
|
||||
resetLfoBtn->move(60, 179);
|
||||
resetLfoBtn->setModel(&c->m_resetLfo);
|
||||
|
||||
PixmapButton* helpBtn = new PixmapButton(this, nullptr);
|
||||
helpBtn->move(256, 278);
|
||||
helpBtn->setActiveGraphic(PLUGIN_NAME::getIconPixmap("help_on"));
|
||||
helpBtn->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("help_off"));
|
||||
helpBtn->setToolTip(tr("Open help window"));
|
||||
connect(helpBtn, &PixmapButton::clicked, this, &FrequencyShifterControlDialog::showHelpWindow);
|
||||
}
|
||||
|
||||
void FrequencyShifterControlDialog::showHelpWindow()
|
||||
{
|
||||
FrequencyShifterHelpView::getInstance()->close();
|
||||
FrequencyShifterHelpView::getInstance()->show();
|
||||
}
|
||||
|
||||
QString FrequencyShifterHelpView::s_helpText = tr(
|
||||
"<div style='text-align: center;'>"
|
||||
"<b>Frequency Shifter</b><br><br>"
|
||||
"Plugin by Lost Robot<br>"
|
||||
"GUI by Haeleon<br>"
|
||||
"</div>"
|
||||
"<h3>Overview:</h3>"
|
||||
"Frequency Shifter is <b>not</b> a pitch shifter.<br><br>"
|
||||
"While "frequency" refers to Hz, "pitch" refers to octaves, semitones, cents, etc. <br>"
|
||||
"So, pitch shifting impacts all partials in the audio multiplicatively, while frequency shifting impacts it additively.<br>"
|
||||
"For example: If you have frequencies 100, 200, and 300 Hz, a pitch shift upward by 1.2x would result in 120, 240, and 360 Hz. "
|
||||
"Meanwhile, a frequency shift upward by 20 Hz would result in 120, 220, and 320 Hz.<br>"
|
||||
"Notice that a pitch shifter preserves the harmonic relationships between these frequencies, while frequency shifting destroys them entirely, "
|
||||
"resulting in an inharmonic timbre.<br><br>"
|
||||
"A frequency shifter can also be used as a "barberpole phaser". This is similar to other phasers, but unlike those, "
|
||||
"it can audibly move upward or downward infinitely, similar to a Shepard tone.<br>"
|
||||
"To achieve this, simply set the frequency shift amount to your desired phaser rate, and set the Mix to 50%. "
|
||||
"The resulting phase cancellation will filter the audio.<br>"
|
||||
"You may also achieve this by simply increasing the delay feedback, and keeping the delay length very low.<br><br>"
|
||||
"This frequency shifter sports a unique "anti-reflect" algorithm which eliminates all frequencies aliasing through Nyquist and 0 Hz.<br><br>"
|
||||
"This plugin may also be used as a ring modulator via the RING parameter. "
|
||||
"Ring modulation is the result of frequency shifting the audio upward and downward by the same amount in parallel.<br>"
|
||||
"<br><h3>Shifter:</h3>"
|
||||
"<b>Mix</b> - Blends between the wet and dry signals.<br>"
|
||||
"<b>Frequency Shift</b> - The amount of frequency shifting, in Hz.<br>"
|
||||
"<b>Spread</b> - Offsets the frequency shift amount in opposite directions for the left and right channels.<br>"
|
||||
"Even very small amounts will add a lot of stereo width to the signal.<br>"
|
||||
"<b>Phase</b> - Gives you manual control over the phase of the frequency shifter's internal oscillators.<br>"
|
||||
"When using the frequency shifter as a barberpole phaser, it is recommended to set the frequency shift amount to 0 and "
|
||||
"automate this Phase parameter.<br>"
|
||||
"<b>Ring</b> - Blends in ring modulation, instead of just frequency shifting.<br>"
|
||||
"<b>Harm</b> - Distorts the frequency shifter's internal sine oscillators. This brings them much closer to a smoothed square shape.<br>"
|
||||
"<b>Tone</b> - A basic 1-pole lowpass on the frequency shifter's output, helpful for taming harsh high frequencies.<br>"
|
||||
"<b>Glide</b> - Lowpass filters any frequency shift and phase parameter movements, so they move slowly over time rather than snapping "
|
||||
"to their target value instantly.<br>"
|
||||
"<b>Reset</b> - Instantly resets the phases of the frequency shifter's internal oscillators. This is automatable.<br>"
|
||||
"<b>Anti-reflect</b> - Magic.<br>"
|
||||
"It removes all aliased frequencies through Nyquist and through 0 Hz. "
|
||||
"This is done via clean and CPU-efficient math tricks, not oversampling.<br>"
|
||||
"<br><h3>LFO:</h3>"
|
||||
"This modulates the frequency shift amount. Audio-rate modulation is fully supported.<br><br>"
|
||||
"<b>Amount</b> - The amplitude of the LFO.<br>"
|
||||
"<b>Rate</b> - LFO rate, in Hz.<br>"
|
||||
"<b>Stereo Phase</b> - Offsets the phase of the LFO's right channel, making things stereo.<br>"
|
||||
"<b>Reset</b> - Instantly resets the phases of the LFO's oscillators. This is automatable.<br>"
|
||||
"<br><h3>Routing:</h3>"
|
||||
"<b>Send</b> - Sends the frequency shifter output into the delay.<br>"
|
||||
"<b>Pass</b> - The audio input bypasses the frequency shifter, and is sent to both the delay and the output. "
|
||||
"The frequency shifter is now located inside of the delay line. Use this if you want the frequency shifter to only impact the echoes.<br>"
|
||||
"<b>Mute</b> - Like "Pass" routing, except the input signal isn't sent to the output, "
|
||||
"so all you hear is the output from the delay line.<br>"
|
||||
"<br><h3>Delay:</h3>"
|
||||
"<b>Length</b> - Delay time in milliseconds.<br>"
|
||||
"<b>Fine</b> - Identical to delay Length, but with a smaller knob range. "
|
||||
"This is helpful when using the feedback to cause comb filtering, giving you access to a unique phaser/flanger hybrid.<br>"
|
||||
"<b>Feedback</b> - Feeds the output of the delay back into the input of the frequency shifter.<br>"
|
||||
"The delay's feedback path has very gentle saturation at high amplitudes, so the plugin can't break from high feedback values.<br>"
|
||||
"<b>Damping</b> - A 1-pole lowpass filter in the feedback loop, so high frequencies fade out sooner than low frequencies.<br>"
|
||||
"<b>Glide</b> - Lowpass filters any delay length changes, so they move slowly over time rather than snapping to their target value instantly.<br>"
|
||||
"<b>Help</b> - Instantly spawns a kiwano in a randomized location on the planet. 30 second cooldown.<br>"
|
||||
);
|
||||
|
||||
FrequencyShifterHelpView::FrequencyShifterHelpView() :
|
||||
QTextEdit(s_helpText)
|
||||
{
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(5,12,0))
|
||||
// Bug workaround: https://codereview.qt-project.org/c/qt/qtbase/+/225348
|
||||
using ::operator|;
|
||||
#endif
|
||||
setWindowTitle(tr("Frequency Shifter Help"));
|
||||
setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse);
|
||||
getGUI()->mainWindow()->addWindowedWidget(this);
|
||||
parentWidget()->setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
|
||||
// No maximize button
|
||||
Qt::WindowFlags flags = parentWidget()->windowFlags();
|
||||
flags &= ~Qt::WindowMaximizeButtonHint;
|
||||
parentWidget()->setWindowFlags(flags);
|
||||
}
|
||||
|
||||
} // namespace lmms::gui
|
||||
|
||||
70
plugins/FrequencyShifter/FrequencyShifterControlDialog.h
Executable file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* FrequencyShifterControlDialog.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_FREQUENCY_SHIFTER_CONTROL_DIALOG_H
|
||||
#define LMMS_FREQUENCY_SHIFTER_CONTROL_DIALOG_H
|
||||
|
||||
#include "EffectControlDialog.h"
|
||||
|
||||
#include <QTextEdit>
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
class FrequencyShifterControls;
|
||||
|
||||
namespace gui
|
||||
{
|
||||
|
||||
class FrequencyShifterControlDialog : public EffectControlDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FrequencyShifterControlDialog(FrequencyShifterControls* c);
|
||||
~FrequencyShifterControlDialog() override = default;
|
||||
|
||||
public slots:
|
||||
void showHelpWindow();
|
||||
};
|
||||
|
||||
|
||||
class FrequencyShifterHelpView : public QTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static FrequencyShifterHelpView* getInstance()
|
||||
{
|
||||
static FrequencyShifterHelpView* instance = new FrequencyShifterHelpView;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private:
|
||||
FrequencyShifterHelpView();
|
||||
static QString s_helpText;
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
#endif // LMMS_FREQUENCY_SHIFTER_CONTROL_DIALOG_H
|
||||
112
plugins/FrequencyShifter/FrequencyShifterControls.cpp
Executable file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* FrequencyShifterControls.cpp
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "FrequencyShifterEffect.h"
|
||||
#include "FrequencyShifterControls.h"
|
||||
|
||||
#include <QDomElement>
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
FrequencyShifterControls::FrequencyShifterControls(FrequencyShifterEffect* e) :
|
||||
EffectControls(e),
|
||||
m_effect(e),
|
||||
m_mix(1.f, 0.f, 1.f, 0.01f, this, "Mix"),
|
||||
m_freqShift(0.f, -20000.f, 20000.f, 0.001f, this, "Frequency Shift"),
|
||||
m_spreadShift(0.f, -50.f, 50.f, 0.01f, this, "Spread Shift"),
|
||||
m_ring(0.f, 0.f, 1.f, 0.01f, this, "Ring"),
|
||||
m_feedback(0.f, 0.f, 1.f, 0.001f, this, "Feedback"),
|
||||
m_delayLengthLong(0.f, 0.f, 2000.f, 0.001f, this, "Delay Length"),
|
||||
m_delayLengthShort(0.f, 0.f, 20.f, 0.001f, this, "Fine Delay Length"),
|
||||
m_delayDamp(22000.f, 100.f, 22000.f, 0.1f, this, "Delay Damping"),
|
||||
m_delayGlide(0.05f, 0.f, 1.f, 0.0001f, this, "Delay Glide"),
|
||||
m_lfoAmount(0.f, 0.f, 2000.f, 0.001f, this, "LFO Amount"),
|
||||
m_lfoRate(0.2f, 0.f, 200.f, 0.001f, this, "LFO Rate"),
|
||||
m_lfoStereoPhase(0.f, 0.f, 1.f, 0.001f, this, "LFO StereoPhase"),
|
||||
m_glide(0.05f, 0.f, 1.f, 0.0001f, this, "Glide"),
|
||||
m_tone(22000.f, 100.f, 22000.f, 0.1f, this, "Tone"),
|
||||
m_phase(0.f, -2.f, 2.f, 0.00001f, this, "Phase"),
|
||||
m_antireflect(false, this, "Antireflect"),
|
||||
m_routeMode(0, 0, 2, this, "Route Mode"),
|
||||
m_harmonics(0.f, 0.f, 1.f, 0.0001f, this, "Harmonics"),
|
||||
m_resetShifter(false, this, "Reset Shifter"),
|
||||
m_resetLfo(false, this, "Reset LFO")
|
||||
{
|
||||
m_spreadShift.setScaleLogarithmic(true);
|
||||
m_delayLengthLong.setScaleLogarithmic(true);
|
||||
m_delayLengthShort.setScaleLogarithmic(true);
|
||||
m_delayDamp.setScaleLogarithmic(true);
|
||||
m_delayGlide.setScaleLogarithmic(true);
|
||||
m_lfoAmount.setScaleLogarithmic(true);
|
||||
m_lfoRate.setScaleLogarithmic(true);
|
||||
m_glide.setScaleLogarithmic(true);
|
||||
m_tone.setScaleLogarithmic(true);
|
||||
}
|
||||
|
||||
void FrequencyShifterControls::loadSettings(const QDomElement& e)
|
||||
{
|
||||
m_mix.loadSettings(e, "mix");
|
||||
m_freqShift.loadSettings(e, "freqShift");
|
||||
m_spreadShift.loadSettings(e, "spreadShift");
|
||||
m_ring.loadSettings(e, "ring");
|
||||
m_feedback.loadSettings(e, "feedback");
|
||||
m_delayLengthLong.loadSettings(e, "m_delayLengthLong");
|
||||
m_delayLengthShort.loadSettings(e, "delayLengthShort");
|
||||
m_delayDamp.loadSettings(e, "delayDamp");
|
||||
m_delayGlide.loadSettings(e, "delayGlide");
|
||||
m_lfoAmount.loadSettings(e, "lfoAmount");
|
||||
m_lfoRate.loadSettings(e, "lfoRate");
|
||||
m_lfoStereoPhase.loadSettings(e, "lfoStereoPhase");
|
||||
m_antireflect.loadSettings(e, "antireflect");
|
||||
m_routeMode.loadSettings(e, "routeMode");
|
||||
m_harmonics.loadSettings(e, "harmonics");
|
||||
m_glide.loadSettings(e, "glide");
|
||||
m_tone.loadSettings(e, "tone");
|
||||
m_phase.loadSettings(e, "phase");
|
||||
}
|
||||
|
||||
void FrequencyShifterControls::saveSettings(QDomDocument& doc, QDomElement& e)
|
||||
{
|
||||
m_mix.saveSettings(doc, e, "mix");
|
||||
m_freqShift.saveSettings(doc, e, "freqShift");
|
||||
m_spreadShift.saveSettings(doc, e, "spreadShift");
|
||||
m_ring.saveSettings(doc, e, "ring");
|
||||
m_feedback.saveSettings(doc, e, "feedback");
|
||||
m_delayLengthLong.saveSettings(doc, e, "m_delayLengthLong");
|
||||
m_delayLengthShort.saveSettings(doc, e, "delayLengthShort");
|
||||
m_delayDamp.saveSettings(doc, e, "delayDamp");
|
||||
m_delayGlide.saveSettings(doc, e, "delayGlide");
|
||||
m_lfoAmount.saveSettings(doc, e, "lfoAmount");
|
||||
m_lfoRate.saveSettings(doc, e, "lfoRate");
|
||||
m_lfoStereoPhase.saveSettings(doc, e, "lfoStereoPhase");
|
||||
m_antireflect.saveSettings(doc, e, "antireflect");
|
||||
m_routeMode.saveSettings(doc, e, "routeMode");
|
||||
m_harmonics.saveSettings(doc, e, "harmonics");
|
||||
m_glide.saveSettings(doc, e, "glide");
|
||||
m_tone.saveSettings(doc, e, "tone");
|
||||
m_phase.saveSettings(doc, e, "phase");
|
||||
}
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
86
plugins/FrequencyShifter/FrequencyShifterControls.h
Executable file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* FrequencyShifterControls.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_FREQUENCY_SHIFTER_CONTROLS_H
|
||||
#define LMMS_FREQUENCY_SHIFTER_CONTROLS_H
|
||||
|
||||
#include "EffectControls.h"
|
||||
#include "FrequencyShifterControlDialog.h"
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
class FrequencyShifterEffect;
|
||||
|
||||
class FrequencyShifterControls : public EffectControls
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FrequencyShifterControls(FrequencyShifterEffect* e);
|
||||
~FrequencyShifterControls() override = default;
|
||||
|
||||
void saveSettings(QDomDocument& doc, QDomElement& e) override;
|
||||
void loadSettings(const QDomElement& e) override;
|
||||
QString nodeName() const override
|
||||
{
|
||||
return "FrequencyShifterControls";
|
||||
}
|
||||
gui::EffectControlDialog* createView() override
|
||||
{
|
||||
return new gui::FrequencyShifterControlDialog(this);
|
||||
}
|
||||
int controlCount() override
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
FrequencyShifterEffect* m_effect;
|
||||
FloatModel m_mix;
|
||||
FloatModel m_freqShift;
|
||||
FloatModel m_spreadShift;
|
||||
FloatModel m_ring;
|
||||
FloatModel m_feedback;
|
||||
FloatModel m_delayLengthLong;
|
||||
FloatModel m_delayLengthShort;
|
||||
FloatModel m_delayDamp;
|
||||
FloatModel m_delayGlide;
|
||||
FloatModel m_lfoAmount;
|
||||
FloatModel m_lfoRate;
|
||||
FloatModel m_lfoStereoPhase;
|
||||
FloatModel m_glide;
|
||||
FloatModel m_tone;
|
||||
|
||||
FloatModel m_phase;
|
||||
|
||||
BoolModel m_antireflect;
|
||||
IntModel m_routeMode;
|
||||
|
||||
FloatModel m_harmonics;
|
||||
|
||||
BoolModel m_resetShifter;
|
||||
BoolModel m_resetLfo;
|
||||
};
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
#endif // LMMS_FREQUENCY_SHIFTER_CONTROLS_H
|
||||
278
plugins/FrequencyShifter/FrequencyShifterEffect.cpp
Executable file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* FrequencyShifter.cpp
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "FrequencyShifterEffect.h"
|
||||
|
||||
#include "embed.h"
|
||||
#include "plugin_export.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
extern "C"
|
||||
{
|
||||
Plugin::Descriptor PLUGIN_EXPORT frequencyshifter_plugin_descriptor =
|
||||
{
|
||||
LMMS_STRINGIFY(PLUGIN_NAME),
|
||||
"Frequency Shifter",
|
||||
QT_TRANSLATE_NOOP("PluginBrowser", "A frequency shifter (not a pitch shifter) and barberpole phaser plugin"),
|
||||
"Lost Robot <r94231/at/gmail/dot/com>",
|
||||
0x0100,
|
||||
Plugin::Type::Effect,
|
||||
new PixmapLoader("lmms-plugin-logo"),
|
||||
nullptr,
|
||||
nullptr,
|
||||
};
|
||||
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data)
|
||||
{
|
||||
return new FrequencyShifterEffect(parent, static_cast<const Plugin::Descriptor::SubPluginFeatures::Key*>(data));
|
||||
}
|
||||
}// extern "C"
|
||||
|
||||
FrequencyShifterEffect::FrequencyShifterEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) :
|
||||
Effect(&frequencyshifter_plugin_descriptor, parent, key),
|
||||
m_controls(this)
|
||||
{
|
||||
connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged,
|
||||
this, &FrequencyShifterEffect::updateSampleRate);
|
||||
updateSampleRate();
|
||||
}
|
||||
|
||||
Effect::ProcessStatus FrequencyShifterEffect::processImpl(SampleFrame* buf, const fpp_t frames)
|
||||
{
|
||||
constexpr float twoPi = std::numbers::pi_v<float> * 2.0f;
|
||||
|
||||
const float mix = m_controls.m_mix.value() * wetLevel();
|
||||
const float fs = m_controls.m_freqShift.value();
|
||||
const float spread = m_controls.m_spreadShift.value();
|
||||
const float ring = m_controls.m_ring.value();
|
||||
const float feedback = m_controls.m_feedback.value();
|
||||
const float delayLen = (m_controls.m_delayLengthLong.value() + m_controls.m_delayLengthShort.value()) * 0.001f * m_sampleRate;
|
||||
const float delayDamp = m_controls.m_delayDamp.value();
|
||||
const float delayGlide = m_controls.m_delayGlide.value();
|
||||
const float lfoAmt = m_controls.m_lfoAmount.value();
|
||||
const float lfoRate = (m_controls.m_lfoRate.value() / m_sampleRate) * twoPi;
|
||||
const float lfoSt = m_controls.m_lfoStereoPhase.value() * twoPi;
|
||||
const bool antireflect = m_controls.m_antireflect.value();
|
||||
const int routeMode = m_controls.m_routeMode.value();
|
||||
const float harmonics = m_controls.m_harmonics.value();
|
||||
const float glide = m_controls.m_glide.value();
|
||||
const float tone = m_controls.m_tone.value();
|
||||
const float phase = m_controls.m_phase.value() * twoPi;
|
||||
|
||||
const bool resetShifterBtn = m_controls.m_resetShifter.value();
|
||||
const bool resetLfoBtn = m_controls.m_resetLfo.value();
|
||||
|
||||
if (!m_prevResetShifter && resetShifterBtn)
|
||||
{
|
||||
m_phase[0] = 0.f;
|
||||
m_phase[1] = 0.f;
|
||||
}
|
||||
if (!m_prevResetLfo && resetLfoBtn) { m_lfoPhase = 0.f; }
|
||||
m_prevResetShifter = resetShifterBtn;
|
||||
m_prevResetLfo = resetLfoBtn;
|
||||
|
||||
const float invRing = 1.f - ring;
|
||||
const bool parallelFB = (routeMode >= 1);
|
||||
const bool routeAdd = (routeMode == 1);
|
||||
|
||||
const bool doHarm = (harmonics > 0.f);
|
||||
const float harmFactor = harmonics * 20.f + 1.f;
|
||||
const float harmDiv = 1.f / (harmonics * 0.3f + 1.f);
|
||||
|
||||
const float dampCoeff = std::exp(-m_twoPiOverSr * delayDamp);
|
||||
const float toneCoeff = std::exp(-m_twoPiOverSr * tone);
|
||||
const float delayGlideCoeff = delayGlide ? std::exp(-1.f / (delayGlide * m_sampleRate)) : 0.f;
|
||||
const float glideCoeff = glide ? std::exp(-1.f / (glide * m_sampleRate)) : 0.f;
|
||||
|
||||
// we only bother with wrapping phases once per buffer
|
||||
m_lfoPhase = std::fmod(m_lfoPhase, twoPi);
|
||||
for (int ch = 0; ch < 2; ++ch)
|
||||
{
|
||||
m_phase[ch] = std::fmod(m_phase[ch], twoPi);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < frames; ++i)
|
||||
{
|
||||
float lfo0;
|
||||
float lfo1;
|
||||
if (lfoAmt > 0.f)
|
||||
{
|
||||
lfo0 = std::sin(m_lfoPhase) * lfoAmt;
|
||||
lfo1 = std::sin(m_lfoPhase + lfoSt) * lfoAmt;
|
||||
}
|
||||
else
|
||||
{
|
||||
lfo0 = 0.f;
|
||||
lfo1 = 0.f;
|
||||
}
|
||||
m_lfoPhase += lfoRate;
|
||||
|
||||
// parameter interpolation (glide)
|
||||
const float base0 = fs - spread;
|
||||
const float base1 = fs + spread;
|
||||
m_trueShift[0] = (1.f - glideCoeff) * base0 + glideCoeff * m_trueShift[0];
|
||||
m_trueShift[1] = (1.f - glideCoeff) * base1 + glideCoeff * m_trueShift[1];
|
||||
m_trueDelay = std::max((1.f - delayGlideCoeff) * delayLen + delayGlideCoeff * m_trueDelay, 1.f);
|
||||
m_truePhase = (1.f - glideCoeff) * phase + glideCoeff * m_truePhase;
|
||||
|
||||
// delay line with 4-point hermite interpolation
|
||||
float readIndex = static_cast<float>(m_writeIndex) - m_trueDelay;
|
||||
if (readIndex < 0.f) { readIndex += static_cast<float>(m_ringBufSize); }
|
||||
const int indexFloor = static_cast<int>(readIndex);
|
||||
const float frac = readIndex - static_cast<float>(indexFloor);
|
||||
const std::array<float, 2> dly = getHermiteSample(indexFloor, frac);
|
||||
if (++m_writeIndex == m_ringBufSize) { m_writeIndex = 0; }
|
||||
|
||||
// routing stuff
|
||||
const float inL = buf[i][0];
|
||||
const float inR = buf[i][1];
|
||||
const float fxInL = parallelFB ? (dly[0] * feedback) : (inL + dly[0] * feedback);
|
||||
const float fxInR = parallelFB ? (dly[1] * feedback) : (inR + dly[1] * feedback);
|
||||
|
||||
// delta phase
|
||||
const float dPh0 = (m_trueShift[0] + lfo0) * m_twoPiOverSr;
|
||||
const float dPh1 = (m_trueShift[1] + lfo1) * m_twoPiOverSr;
|
||||
|
||||
float outL;
|
||||
float outR;
|
||||
|
||||
{
|
||||
float fxIn[2] = {fxInL, fxInR};
|
||||
float dPh[2] = {dPh0, dPh1};
|
||||
float out[2];
|
||||
|
||||
for (int ch = 0; ch < 2; ++ch)
|
||||
{
|
||||
const float phaseValue = m_phase[ch] + m_truePhase;
|
||||
|
||||
float sinP = std::sin(phaseValue);
|
||||
float cosP = std::cos(phaseValue);
|
||||
|
||||
if (doHarm)
|
||||
{
|
||||
// arbitrary distortion function, crossfaded with original signal
|
||||
const float xc = std::clamp(harmFactor * cosP, -3.f, 3.f);
|
||||
const float xs = std::clamp(harmFactor * sinP, -3.f, 3.f);
|
||||
const float xc2 = xc * xc;
|
||||
const float xs2 = xs * xs;
|
||||
const float tc = xc * (27.f + xc2) / (27.f + 9.f * xc2);
|
||||
const float ts = xs * (27.f + xs2) / (27.f + 9.f * xs2);
|
||||
cosP = std::lerp(cosP, tc * harmDiv, harmonics);
|
||||
sinP = std::lerp(sinP, ts * harmDiv, harmonics);
|
||||
}
|
||||
|
||||
float analytic1[2];
|
||||
m_hilbert1.processReal(fxIn[ch], ch, analytic1);
|
||||
|
||||
// ring modulation frequency shifts both downward and upward simultaneously
|
||||
// oscI alongside the hilbert-transformed signal cancels out one of those two sidebands
|
||||
// so fading it out will bring us closer to ring modulation
|
||||
const float oscR = cosP;
|
||||
const float oscI = sinP * invRing;
|
||||
|
||||
const float modR = analytic1[0] * oscR - analytic1[1] * oscI;
|
||||
const float modI = analytic1[0] * oscI + analytic1[1] * oscR;
|
||||
|
||||
float shiftedR;
|
||||
|
||||
if (antireflect)
|
||||
{
|
||||
// use a second hilbert transform on the complex signal
|
||||
// in order to remove negative frequencies to
|
||||
// prevent aliasing through 0 Hz and Nyquist
|
||||
float mod[2] = {modR, modI};
|
||||
float analytic2[2];
|
||||
m_hilbert2.processComplex(mod, ch, analytic2);
|
||||
shiftedR = analytic2[0] * 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
shiftedR = modR;
|
||||
}
|
||||
|
||||
m_phase[ch] += dPh[ch];
|
||||
out[ch] = shiftedR;
|
||||
}
|
||||
|
||||
outL = out[0];
|
||||
outR = out[1];
|
||||
}
|
||||
|
||||
float delayInL = outL + (parallelFB ? inL : 0.f);
|
||||
float delayInR = outR + (parallelFB ? inR : 0.f);
|
||||
|
||||
// saturate feedback loop to ensure it doesn't explode
|
||||
constexpr float FbSaturation = 16.f;
|
||||
delayInL = (FbSaturation * delayInL) / (FbSaturation + std::fabs(delayInL));
|
||||
delayInR = (FbSaturation * delayInR) / (FbSaturation + std::fabs(delayInR));
|
||||
|
||||
// 1-pole lowpass in feedback loop
|
||||
m_dampState[0] = (1.f - dampCoeff) * delayInL + dampCoeff * m_dampState[0];
|
||||
m_dampState[1] = (1.f - dampCoeff) * delayInR + dampCoeff * m_dampState[1];
|
||||
m_ringBuf[m_writeIndex][0] = m_dampState[0];
|
||||
m_ringBuf[m_writeIndex][1] = m_dampState[1];
|
||||
|
||||
// 1-pole lowpass on entire signal
|
||||
m_toneState[0] = (1.f - toneCoeff) * outL + toneCoeff * m_toneState[0];
|
||||
m_toneState[1] = (1.f - toneCoeff) * outR + toneCoeff * m_toneState[1];
|
||||
outL = m_toneState[0];
|
||||
outR = m_toneState[1];
|
||||
|
||||
if (routeAdd)
|
||||
{
|
||||
buf[i][0] = inL + mix * outL;
|
||||
buf[i][1] = inR + mix * outR;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float dry = 1.f - mix;
|
||||
buf[i][0] = dry * inL + mix * outL;
|
||||
buf[i][1] = dry * inR + mix * outR;
|
||||
}
|
||||
}
|
||||
|
||||
return ProcessStatus::ContinueIfNotQuiet;
|
||||
}
|
||||
|
||||
void FrequencyShifterEffect::updateSampleRate()
|
||||
{
|
||||
m_sampleRate = Engine::audioEngine()->outputSampleRate();
|
||||
|
||||
constexpr float twoPi = std::numbers::pi_v<float> * 2.0f;
|
||||
m_twoPiOverSr = twoPi / m_sampleRate;
|
||||
|
||||
m_hilbert1 = HilbertIIRFloat<2>(m_sampleRate, 2.0f);
|
||||
m_hilbert2 = HilbertIIRFloat<2>(m_sampleRate, 2.0f);
|
||||
|
||||
// +6 provides space for interpolation
|
||||
m_ringBufSize = (m_controls.m_delayLengthLong.maxValue() + m_controls.m_delayLengthShort.maxValue()) * 0.001f * m_sampleRate + 6.f;
|
||||
m_ringBuf.resize(m_ringBufSize);
|
||||
}
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
115
plugins/FrequencyShifter/FrequencyShifterEffect.h
Executable file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* FrequencyShifter.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_FREQUENCY_SHIFTER_EFFECT_H
|
||||
#define LMMS_FREQUENCY_SHIFTER_EFFECT_H
|
||||
|
||||
#include "Effect.h"
|
||||
#include "FrequencyShifterControls.h"
|
||||
|
||||
#include "HilbertTransform.h"
|
||||
#include "interpolation.h"
|
||||
#include "lmms_math.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
#include <vector>
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
class FrequencyShifterEffect : public Effect
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FrequencyShifterEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key);
|
||||
~FrequencyShifterEffect() override = default;
|
||||
|
||||
ProcessStatus processImpl(SampleFrame* buf, const fpp_t frames) override;
|
||||
EffectControls* controls() override
|
||||
{
|
||||
return &m_controls;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void updateSampleRate();
|
||||
|
||||
private:
|
||||
std::array<float, 2> getHermiteSample(int indexFloor, float fraction)
|
||||
{
|
||||
const int size = m_ringBufSize;
|
||||
|
||||
const int i0 = (indexFloor == 0) ? (size - 1) : (indexFloor - 1);
|
||||
const int i1 = indexFloor;
|
||||
|
||||
int i2 = indexFloor + 1;
|
||||
int i3 = indexFloor + 2;
|
||||
if (i2 >= size) i2 -= size;
|
||||
if (i3 >= size) i3 -= size;
|
||||
|
||||
std::array<float, 2> out;
|
||||
|
||||
for (int ch = 0; ch < 2; ++ch)
|
||||
{
|
||||
const float v0 = m_ringBuf[i0][ch];
|
||||
const float v1 = m_ringBuf[i1][ch];
|
||||
const float v2 = m_ringBuf[i2][ch];
|
||||
const float v3 = m_ringBuf[i3][ch];
|
||||
|
||||
out[ch] = hermiteInterpolate(v0, v1, v2, v3, fraction);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
HilbertIIRFloat<2> m_hilbert1;
|
||||
HilbertIIRFloat<2> m_hilbert2;
|
||||
|
||||
std::vector<std::array<float, 2>> m_ringBuf;
|
||||
|
||||
std::array<float, 2> m_phase{};
|
||||
std::array<float, 2> m_trueShift{};
|
||||
std::array<float, 2> m_dampState{};
|
||||
std::array<float, 2> m_toneState{};
|
||||
|
||||
float m_lfoPhase{};
|
||||
float m_truePhase{};
|
||||
float m_trueDelay{1.f};
|
||||
|
||||
float m_twoPiOverSr{};
|
||||
float m_sampleRate{};
|
||||
|
||||
int m_ringBufSize{};
|
||||
int m_writeIndex{};
|
||||
|
||||
bool m_prevResetShifter{};
|
||||
bool m_prevResetLfo{};
|
||||
|
||||
FrequencyShifterControls m_controls;
|
||||
};
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
#endif // LMMS_FREQUENCY_SHIFTER_EFFECT_H
|
||||
|
||||
225
plugins/FrequencyShifter/HilbertTransform.h
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* HilbertTransform.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.
|
||||
*
|
||||
* -------------------------------------------------------------------------
|
||||
* Full credit for the Hilbert IIR coefficients and general usage goes to:
|
||||
*
|
||||
* Copyright (c) 2024 Geraint Luff / Signalsmith Audio Ltd.
|
||||
* Released under the 0BSD (Zero-Clause BSD) License.
|
||||
*
|
||||
* https://github.com/Signalsmith-Audio/hilbert-iir/blob/main/hilbert.h
|
||||
* -------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#ifndef LMMS_HILBERT_TRANSFORM_H
|
||||
#define LMMS_HILBERT_TRANSFORM_H
|
||||
#include <cmath>
|
||||
|
||||
#ifdef __SSE2__
|
||||
#include <emmintrin.h>
|
||||
#endif
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
template<int Channels>
|
||||
struct HilbertIIRFloat
|
||||
{
|
||||
static constexpr int order = 12;
|
||||
|
||||
alignas(16) static constexpr float baseCoeffsR[order] = {
|
||||
-0.000224352093802f, 0.0107500557815f, -0.0456795873917f,
|
||||
0.11282500582f, -0.208067578452f, 0.28717837501f,
|
||||
-0.254675294431f, 0.0481081835026f, 0.227861357867f,
|
||||
-0.365411839137f, 0.280729061131f, -0.0935061787728f
|
||||
};
|
||||
alignas(16) static constexpr float baseCoeffsI[order] = {
|
||||
0.00543499018201f, -0.0173890685681f, 0.0229166931429f,
|
||||
0.00278413661237f, -0.104628958675f, 0.33619239719f,
|
||||
-0.683033899655f, 0.954061589374f, -0.891273574569f,
|
||||
0.525088317271f, -0.155131206606f, 0.00512245855404f
|
||||
};
|
||||
alignas(16) static constexpr float basePolesR[order] = {
|
||||
-0.00495335976478f, -0.017859491302f, -0.0413714373155f,
|
||||
-0.0882148408885f, -0.17922965812f, -0.338261800753f,
|
||||
-0.557688699732f, -0.735157736148f, -0.719057381172f,
|
||||
-0.517871025209f, -0.280197469471f, -0.0852751354531f
|
||||
};
|
||||
alignas(16) static constexpr float basePolesI[order] = {
|
||||
0.0092579876872f, 0.0273493725543f, 0.0744756910287f,
|
||||
0.178349677457f, 0.39601340223f, 0.829229533354f,
|
||||
1.61298538328f, 2.79987398682f, 4.16396166128f,
|
||||
5.29724826804f, 5.99598602388f, 6.3048492377f
|
||||
};
|
||||
static constexpr float baseDirect = 0.000262057212648f;
|
||||
|
||||
alignas(16) float coeffsR[order];
|
||||
alignas(16) float coeffsI[order];
|
||||
alignas(16) float polesR[order];
|
||||
alignas(16) float polesI[order];
|
||||
alignas(16) float stateR[Channels][order];
|
||||
alignas(16) float stateI[Channels][order];
|
||||
float direct;
|
||||
|
||||
HilbertIIRFloat(float sampleRate = 48000.0f, float passbandGain = 2.0f)
|
||||
{
|
||||
const float freqFactor = std::fmin(0.46f, 20000.0f / sampleRate);
|
||||
const float coeffScale = freqFactor * passbandGain;
|
||||
direct = baseDirect * 2.0f * passbandGain * freqFactor;
|
||||
for (int i = 0; i < order; ++i)
|
||||
{
|
||||
coeffsR[i] = baseCoeffsR[i] * coeffScale;
|
||||
coeffsI[i] = baseCoeffsI[i] * coeffScale;
|
||||
const float a = basePolesR[i] * freqFactor;
|
||||
const float b = basePolesI[i] * freqFactor;
|
||||
const float ea = std::exp(a);
|
||||
polesR[i] = ea * std::cos(b);
|
||||
polesI[i] = ea * std::sin(b);
|
||||
}
|
||||
reset();
|
||||
}
|
||||
|
||||
inline void reset()
|
||||
{
|
||||
for (int ch = 0; ch < Channels; ++ch)
|
||||
{
|
||||
for (int i = 0; i < order; ++i)
|
||||
{
|
||||
stateR[ch][i] = stateI[ch][i] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void processReal(float x, int channel, float *out)
|
||||
{
|
||||
float *sR = stateR[channel], *sI = stateI[channel];
|
||||
#ifdef __SSE2__
|
||||
const __m128 vx = _mm_set1_ps(x);
|
||||
__m128 sumR = _mm_setzero_ps(), sumI = _mm_setzero_ps();
|
||||
for (int i = 0; i < order; i += 4)
|
||||
{
|
||||
__m128 vr = _mm_load_ps(&sR[i]);
|
||||
__m128 vi = _mm_load_ps(&sI[i]);
|
||||
__m128 vpr = _mm_load_ps(&polesR[i]);
|
||||
__m128 vpi = _mm_load_ps(&polesI[i]);
|
||||
__m128 vcr = _mm_load_ps(&coeffsR[i]);
|
||||
__m128 vci = _mm_load_ps(&coeffsI[i]);
|
||||
|
||||
__m128 rpr = _mm_mul_ps(vr, vpr);
|
||||
__m128 impi = _mm_mul_ps(vi, vpi);
|
||||
__m128 xcr = _mm_mul_ps(vx, vcr);
|
||||
__m128 nr = _mm_add_ps(_mm_sub_ps(rpr, impi), xcr);
|
||||
|
||||
__m128 rpi = _mm_mul_ps(vr, vpi);
|
||||
__m128 impr = _mm_mul_ps(vi, vpr);
|
||||
__m128 xci = _mm_mul_ps(vx, vci);
|
||||
__m128 ni = _mm_add_ps(_mm_add_ps(rpi, impr), xci);
|
||||
|
||||
_mm_store_ps(&sR[i], nr);
|
||||
_mm_store_ps(&sI[i], ni);
|
||||
|
||||
sumR = _mm_add_ps(sumR, nr);
|
||||
sumI = _mm_add_ps(sumI, ni);
|
||||
}
|
||||
float tmpR[4], tmpI[4];
|
||||
_mm_storeu_ps(tmpR, sumR);
|
||||
_mm_storeu_ps(tmpI, sumI);
|
||||
out[0] = x * direct + (tmpR[0] + tmpR[1] + tmpR[2] + tmpR[3]);
|
||||
out[1] = (tmpI[0] + tmpI[1] + tmpI[2] + tmpI[3]);
|
||||
#else
|
||||
float sumR = 0.0f, sumI = 0.0f;
|
||||
for (int i = 0; i < order; ++i)
|
||||
{
|
||||
const float r = sR[i], im = sI[i], pr = polesR[i], pi = polesI[i];
|
||||
const float nr = r * pr - im * pi + x * coeffsR[i];
|
||||
const float ni = r * pi + im * pr + x * coeffsI[i];
|
||||
sR[i] = nr; sI[i] = ni;
|
||||
sumR += nr; sumI += ni;
|
||||
}
|
||||
out[0] = x * direct + sumR;
|
||||
out[1] = sumI;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void processComplex(const float *x, int channel, float *out)
|
||||
{
|
||||
const float xr = x[0], xi = x[1];
|
||||
float *sR = stateR[channel], *sI = stateI[channel];
|
||||
#ifdef __SSE2__
|
||||
const __m128 vxr = _mm_set1_ps(xr), vxi = _mm_set1_ps(xi);
|
||||
__m128 sumR = _mm_setzero_ps(), sumI = _mm_setzero_ps();
|
||||
for (int i = 0; i < order; i += 4)
|
||||
{
|
||||
__m128 vr = _mm_load_ps(&sR[i]);
|
||||
__m128 vi = _mm_load_ps(&sI[i]);
|
||||
__m128 vpr = _mm_load_ps(&polesR[i]);
|
||||
__m128 vpi = _mm_load_ps(&polesI[i]);
|
||||
__m128 vcr = _mm_load_ps(&coeffsR[i]);
|
||||
__m128 vci = _mm_load_ps(&coeffsI[i]);
|
||||
|
||||
__m128 xrcr = _mm_mul_ps(vxr, vcr);
|
||||
__m128 xici = _mm_mul_ps(vxi, vci);
|
||||
__m128 xrci = _mm_mul_ps(vxr, vci);
|
||||
__m128 xicr = _mm_mul_ps(vxi, vcr);
|
||||
|
||||
__m128 rpr = _mm_mul_ps(vr, vpr);
|
||||
__m128 impi = _mm_mul_ps(vi, vpi);
|
||||
__m128 rpi = _mm_mul_ps(vr, vpi);
|
||||
__m128 impr = _mm_mul_ps(vi, vpr);
|
||||
|
||||
__m128 nr = _mm_add_ps(_mm_sub_ps(rpr, impi), _mm_sub_ps(xrcr, xici));
|
||||
__m128 ni = _mm_add_ps(_mm_add_ps(rpi, impr), _mm_add_ps(xrci, xicr));
|
||||
|
||||
_mm_store_ps(&sR[i], nr);
|
||||
_mm_store_ps(&sI[i], ni);
|
||||
|
||||
sumR = _mm_add_ps(sumR, nr);
|
||||
sumI = _mm_add_ps(sumI, ni);
|
||||
}
|
||||
float tmpR[4], tmpI[4];
|
||||
_mm_storeu_ps(tmpR, sumR);
|
||||
_mm_storeu_ps(tmpI, sumI);
|
||||
const float sr = tmpR[0] + tmpR[1] + tmpR[2] + tmpR[3];
|
||||
const float si = tmpI[0] + tmpI[1] + tmpI[2] + tmpI[3];
|
||||
out[0] = xr * direct + sr;
|
||||
out[1] = xi * direct + si;
|
||||
#else
|
||||
float sumR = 0.0f, sumI = 0.0f;
|
||||
for (int i = 0; i < order; ++i)
|
||||
{
|
||||
const float r = sR[i], im = sI[i];
|
||||
const float pr = polesR[i], pi = polesI[i], cr = coeffsR[i], ci = coeffsI[i];
|
||||
const float xrcr = xr * cr, xici = xi * ci, xrci = xr * ci, xicr = xi * cr;
|
||||
const float nr = r * pr - im * pi + (xrcr - xici);
|
||||
const float ni = r * pi + im * pr + (xrci + xicr);
|
||||
sR[i] = nr; sI[i] = ni;
|
||||
sumR += nr; sumI += ni;
|
||||
}
|
||||
out[0] = xr * direct + sumR;
|
||||
out[1] = xi * direct + sumI;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
#endif // LMMS_HILBERT_TRANSFORM_H
|
||||
BIN
plugins/FrequencyShifter/antireflect_off.png
Normal file
|
After Width: | Height: | Size: 745 B |
BIN
plugins/FrequencyShifter/antireflect_on.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
plugins/FrequencyShifter/artwork.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
plugins/FrequencyShifter/help_off.png
Normal file
|
After Width: | Height: | Size: 517 B |
BIN
plugins/FrequencyShifter/help_on.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
plugins/FrequencyShifter/mute_off.png
Normal file
|
After Width: | Height: | Size: 569 B |
BIN
plugins/FrequencyShifter/mute_on.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
plugins/FrequencyShifter/pass_off.png
Normal file
|
After Width: | Height: | Size: 563 B |
BIN
plugins/FrequencyShifter/pass_on.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
plugins/FrequencyShifter/reset_lfo_off.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
plugins/FrequencyShifter/reset_lfo_on.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
plugins/FrequencyShifter/reset_shifter_off.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
plugins/FrequencyShifter/reset_shifter_on.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
plugins/FrequencyShifter/send_off.png
Normal file
|
After Width: | Height: | Size: 573 B |
BIN
plugins/FrequencyShifter/send_on.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |