/* * 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