diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 7a6b266cf..8dc1f72f6 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -38,6 +38,7 @@ SET(LMMS_PLUGIN_LIST DynamicsProcessor Eq Flanger + FrequencyShifter GranularPitchShifter HydrogenImport LadspaBrowser diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index baaaab6ec..a8503970b 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -1105,6 +1105,17 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob { + color: #ffffff; + qproperty-outerColor: #ffffff; + qproperty-lineWidth: 3; +} +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob#fs_glide, +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob#fs_dglide, +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob#fs_phase { + qproperty-lineWidth: 2; +} + lmms--gui--VectorView { qproperty-colorTrace: rgba(255, 170, 33, 255); qproperty-colorGrid: rgba(76, 80, 84, 128); diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 583ef8ec7..1a511a4dc 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -1165,6 +1165,17 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob { + color: #ffffff; + qproperty-outerColor: #ffffff; + qproperty-lineWidth: 3; +} +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob#fs_glide, +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob#fs_dglide, +lmms--gui--FrequencyShifterControlDialog lmms--gui--Knob#fs_phase { + qproperty-lineWidth: 2; +} + lmms--gui--VectorView { qproperty-colorTrace: rgba(60, 255, 130, 255); qproperty-colorGrid: rgba(76, 80, 84, 128); diff --git a/plugins/FrequencyShifter/CMakeLists.txt b/plugins/FrequencyShifter/CMakeLists.txt new file mode 100755 index 000000000..41fc694db --- /dev/null +++ b/plugins/FrequencyShifter/CMakeLists.txt @@ -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) diff --git a/plugins/FrequencyShifter/FrequencyShifterControlDialog.cpp b/plugins/FrequencyShifter/FrequencyShifterControlDialog.cpp new file mode 100755 index 000000000..6c7edf2b6 --- /dev/null +++ b/plugins/FrequencyShifter/FrequencyShifterControlDialog.cpp @@ -0,0 +1,259 @@ +/* + * FrequencyShifterControlDialog.cpp + * + * Copyright (c) 2025 Lost Robot + * + * 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 + +#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( +"
" +"Frequency Shifter

" +"Plugin by Lost Robot
" +"GUI by Haeleon
" +"
" +"

Overview:

" +"Frequency Shifter is not a pitch shifter.

" +"While "frequency" refers to Hz, "pitch" refers to octaves, semitones, cents, etc.
" +"So, pitch shifting impacts all partials in the audio multiplicatively, while frequency shifting impacts it additively.
" +"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.
" +"Notice that a pitch shifter preserves the harmonic relationships between these frequencies, while frequency shifting destroys them entirely, " +"resulting in an inharmonic timbre.

" +"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.
" +"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.
" +"You may also achieve this by simply increasing the delay feedback, and keeping the delay length very low.

" +"This frequency shifter sports a unique "anti-reflect" algorithm which eliminates all frequencies aliasing through Nyquist and 0 Hz.

" +"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.
" +"

Shifter:

" +"Mix - Blends between the wet and dry signals.
" +"Frequency Shift - The amount of frequency shifting, in Hz.
" +"Spread - Offsets the frequency shift amount in opposite directions for the left and right channels.
" +"Even very small amounts will add a lot of stereo width to the signal.
" +"Phase - Gives you manual control over the phase of the frequency shifter's internal oscillators.
" +"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.
" +"Ring - Blends in ring modulation, instead of just frequency shifting.
" +"Harm - Distorts the frequency shifter's internal sine oscillators. This brings them much closer to a smoothed square shape.
" +"Tone - A basic 1-pole lowpass on the frequency shifter's output, helpful for taming harsh high frequencies.
" +"Glide - Lowpass filters any frequency shift and phase parameter movements, so they move slowly over time rather than snapping " +"to their target value instantly.
" +"Reset - Instantly resets the phases of the frequency shifter's internal oscillators. This is automatable.
" +"Anti-reflect - Magic.
" +"It removes all aliased frequencies through Nyquist and through 0 Hz. " +"This is done via clean and CPU-efficient math tricks, not oversampling.
" +"

LFO:

" +"This modulates the frequency shift amount. Audio-rate modulation is fully supported.

" +"Amount - The amplitude of the LFO.
" +"Rate - LFO rate, in Hz.
" +"Stereo Phase - Offsets the phase of the LFO's right channel, making things stereo.
" +"Reset - Instantly resets the phases of the LFO's oscillators. This is automatable.
" +"

Routing:

" +"Send - Sends the frequency shifter output into the delay.
" +"Pass - 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.
" +"Mute - Like "Pass" routing, except the input signal isn't sent to the output, " +"so all you hear is the output from the delay line.
" +"

Delay:

" +"Length - Delay time in milliseconds.
" +"Fine - 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.
" +"Feedback - Feeds the output of the delay back into the input of the frequency shifter.
" +"The delay's feedback path has very gentle saturation at high amplitudes, so the plugin can't break from high feedback values.
" +"Damping - A 1-pole lowpass filter in the feedback loop, so high frequencies fade out sooner than low frequencies.
" +"Glide - Lowpass filters any delay length changes, so they move slowly over time rather than snapping to their target value instantly.
" +"Help - Instantly spawns a kiwano in a randomized location on the planet. 30 second cooldown.
" +); + +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 + diff --git a/plugins/FrequencyShifter/FrequencyShifterControlDialog.h b/plugins/FrequencyShifter/FrequencyShifterControlDialog.h new file mode 100755 index 000000000..09a177967 --- /dev/null +++ b/plugins/FrequencyShifter/FrequencyShifterControlDialog.h @@ -0,0 +1,70 @@ +/* + * FrequencyShifterControlDialog.h + * + * Copyright (c) 2025 Lost Robot + * + * 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 + +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 diff --git a/plugins/FrequencyShifter/FrequencyShifterControls.cpp b/plugins/FrequencyShifter/FrequencyShifterControls.cpp new file mode 100755 index 000000000..e9988ae73 --- /dev/null +++ b/plugins/FrequencyShifter/FrequencyShifterControls.cpp @@ -0,0 +1,112 @@ +/* + * FrequencyShifterControls.cpp + * + * Copyright (c) 2025 Lost Robot + * + * 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 + +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 + diff --git a/plugins/FrequencyShifter/FrequencyShifterControls.h b/plugins/FrequencyShifter/FrequencyShifterControls.h new file mode 100755 index 000000000..bcdb86f36 --- /dev/null +++ b/plugins/FrequencyShifter/FrequencyShifterControls.h @@ -0,0 +1,86 @@ +/* + * FrequencyShifterControls.h + * + * Copyright (c) 2025 Lost Robot + * + * 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 diff --git a/plugins/FrequencyShifter/FrequencyShifterEffect.cpp b/plugins/FrequencyShifter/FrequencyShifterEffect.cpp new file mode 100755 index 000000000..5ae17c77a --- /dev/null +++ b/plugins/FrequencyShifter/FrequencyShifterEffect.cpp @@ -0,0 +1,278 @@ +/* + * FrequencyShifter.cpp + * + * Copyright (c) 2025 Lost Robot + * + * 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 +#include +#include + +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 ", + 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(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 * 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(m_writeIndex) - m_trueDelay; + if (readIndex < 0.f) { readIndex += static_cast(m_ringBufSize); } + const int indexFloor = static_cast(readIndex); + const float frac = readIndex - static_cast(indexFloor); + const std::array 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 * 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 + diff --git a/plugins/FrequencyShifter/FrequencyShifterEffect.h b/plugins/FrequencyShifter/FrequencyShifterEffect.h new file mode 100755 index 000000000..e28f7c31a --- /dev/null +++ b/plugins/FrequencyShifter/FrequencyShifterEffect.h @@ -0,0 +1,115 @@ +/* + * FrequencyShifter.h + * + * Copyright (c) 2025 Lost Robot + * + * 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 +#include +#include +#include + +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 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 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> m_ringBuf; + + std::array m_phase{}; + std::array m_trueShift{}; + std::array m_dampState{}; + std::array 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 + diff --git a/plugins/FrequencyShifter/HilbertTransform.h b/plugins/FrequencyShifter/HilbertTransform.h new file mode 100644 index 000000000..812128d0e --- /dev/null +++ b/plugins/FrequencyShifter/HilbertTransform.h @@ -0,0 +1,225 @@ +/* + * HilbertTransform.h + * + * Copyright (c) 2025 Lost Robot + * + * 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 + +#ifdef __SSE2__ + #include +#endif + +namespace lmms +{ + +template +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 diff --git a/plugins/FrequencyShifter/antireflect_off.png b/plugins/FrequencyShifter/antireflect_off.png new file mode 100644 index 000000000..1afa8f289 Binary files /dev/null and b/plugins/FrequencyShifter/antireflect_off.png differ diff --git a/plugins/FrequencyShifter/antireflect_on.png b/plugins/FrequencyShifter/antireflect_on.png new file mode 100644 index 000000000..7d40cf69b Binary files /dev/null and b/plugins/FrequencyShifter/antireflect_on.png differ diff --git a/plugins/FrequencyShifter/artwork.png b/plugins/FrequencyShifter/artwork.png new file mode 100644 index 000000000..70ee0e68a Binary files /dev/null and b/plugins/FrequencyShifter/artwork.png differ diff --git a/plugins/FrequencyShifter/help_off.png b/plugins/FrequencyShifter/help_off.png new file mode 100644 index 000000000..9da329096 Binary files /dev/null and b/plugins/FrequencyShifter/help_off.png differ diff --git a/plugins/FrequencyShifter/help_on.png b/plugins/FrequencyShifter/help_on.png new file mode 100644 index 000000000..f6d783928 Binary files /dev/null and b/plugins/FrequencyShifter/help_on.png differ diff --git a/plugins/FrequencyShifter/mute_off.png b/plugins/FrequencyShifter/mute_off.png new file mode 100644 index 000000000..551042b4e Binary files /dev/null and b/plugins/FrequencyShifter/mute_off.png differ diff --git a/plugins/FrequencyShifter/mute_on.png b/plugins/FrequencyShifter/mute_on.png new file mode 100644 index 000000000..98f3cc8fa Binary files /dev/null and b/plugins/FrequencyShifter/mute_on.png differ diff --git a/plugins/FrequencyShifter/pass_off.png b/plugins/FrequencyShifter/pass_off.png new file mode 100644 index 000000000..b6c0c446b Binary files /dev/null and b/plugins/FrequencyShifter/pass_off.png differ diff --git a/plugins/FrequencyShifter/pass_on.png b/plugins/FrequencyShifter/pass_on.png new file mode 100644 index 000000000..fb9fcb2d8 Binary files /dev/null and b/plugins/FrequencyShifter/pass_on.png differ diff --git a/plugins/FrequencyShifter/reset_lfo_off.png b/plugins/FrequencyShifter/reset_lfo_off.png new file mode 100644 index 000000000..c16e9fa4c Binary files /dev/null and b/plugins/FrequencyShifter/reset_lfo_off.png differ diff --git a/plugins/FrequencyShifter/reset_lfo_on.png b/plugins/FrequencyShifter/reset_lfo_on.png new file mode 100644 index 000000000..a792ec2ed Binary files /dev/null and b/plugins/FrequencyShifter/reset_lfo_on.png differ diff --git a/plugins/FrequencyShifter/reset_shifter_off.png b/plugins/FrequencyShifter/reset_shifter_off.png new file mode 100644 index 000000000..53c66179c Binary files /dev/null and b/plugins/FrequencyShifter/reset_shifter_off.png differ diff --git a/plugins/FrequencyShifter/reset_shifter_on.png b/plugins/FrequencyShifter/reset_shifter_on.png new file mode 100644 index 000000000..432da66ce Binary files /dev/null and b/plugins/FrequencyShifter/reset_shifter_on.png differ diff --git a/plugins/FrequencyShifter/send_off.png b/plugins/FrequencyShifter/send_off.png new file mode 100644 index 000000000..d5715ed3b Binary files /dev/null and b/plugins/FrequencyShifter/send_off.png differ diff --git a/plugins/FrequencyShifter/send_on.png b/plugins/FrequencyShifter/send_on.png new file mode 100644 index 000000000..0c0646a44 Binary files /dev/null and b/plugins/FrequencyShifter/send_on.png differ