Add Frequency Shifter effect (not a pitch shifter) (#8140)

This commit is contained in:
Lost Robot
2026-03-01 09:04:51 -06:00
committed by GitHub
parent 5d5f319942
commit f5688e9bad
26 changed files with 1171 additions and 0 deletions

View 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)

View 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 &quot;frequency&quot; refers to Hz, &quot;pitch&quot; 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 &quot;barberpole phaser&quot;. 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 &quot;anti-reflect&quot; 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 &quot;Pass&quot; 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB