diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index fe98a64b442..ed9e3b2514e 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -53,6 +53,7 @@ SET(LMMS_PLUGIN_LIST FreeBoy Patman PeakControllerEffect + Phaser GigPlayer ReverbSC Sf2Player diff --git a/plugins/Phaser/CMakeLists.txt b/plugins/Phaser/CMakeLists.txt new file mode 100644 index 00000000000..e72528461df --- /dev/null +++ b/plugins/Phaser/CMakeLists.txt @@ -0,0 +1,4 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(phaser PhaserEffect.cpp PhaserControls.cpp PhaserControlDialog.cpp MOCFILES PhaserEffect.h PhaserControls.h PhaserControlDialog.h ../Eq/EqFader.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") +TARGET_LINK_LIBRARIES(phaser hiir) diff --git a/plugins/Phaser/PhaserControlDialog.cpp b/plugins/Phaser/PhaserControlDialog.cpp new file mode 100644 index 00000000000..53b41a1260d --- /dev/null +++ b/plugins/Phaser/PhaserControlDialog.cpp @@ -0,0 +1,242 @@ +/* + * PhaserControlDialog.cpp + * + * Copyright (c) 2019 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 + +#include "PhaserControlDialog.h" +#include "PhaserControls.h" +#include "PhaserEffect.h" + +#include "../Eq/EqFader.h" +#include "embed.h" +#include "gui_templates.h" +#include "LedCheckBox.h" +#include "lmms_math.h" +#include "PixmapButton.h" +#include "TempoSyncKnob.h" + +namespace lmms::gui +{ + +constexpr int PHA_DOT_SLIDER_LENGTH = 338; +constexpr float PHA_MIN_FREQ = 20; +constexpr float PHA_MAX_FREQ = 20000; + + +PhaserControlDialog::PhaserControlDialog(PhaserControls* controls) : + EffectControlDialog(controls), + m_controls(controls) +{ + setAutoFillBackground(true); + QPalette pal; + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); + setPalette(pal); + setFixedSize(370, 200); + + Knob * cutoffKnob = new Knob(knobBright_26, this); + cutoffKnob -> move(64, 50); + cutoffKnob->setModel(&controls->m_cutoffModel); + cutoffKnob->setHintText(tr("Cutoff:"), " Hz"); + cutoffKnob->setToolTip(tr("Center frequency of allpass filters")); + + Knob * resonanceKnob = new Knob(knobBright_26, this); + resonanceKnob -> move(102, 50); + resonanceKnob->setModel(&controls->m_resonanceModel); + resonanceKnob->setHintText(tr("Resonance:"), ""); + resonanceKnob->setToolTip(tr("Resonance of allpass filters")); + + Knob * feedbackKnob = new Knob(knobBright_26, this); + feedbackKnob -> move(158, 50); + feedbackKnob->setModel(&controls->m_feedbackModel); + feedbackKnob->setHintText(tr("Feedback:"), "%"); + feedbackKnob->setToolTip(tr("Feedback amount for allpass filters")); + + LcdSpinBox * m_orderBox = new LcdSpinBox(2, this, "Order"); + m_orderBox->setModel(&controls->m_orderModel); + m_orderBox->move(247, 61); + m_orderBox->setToolTip(tr("Number of allpass filters")); + + Knob * delayKnob = new Knob(knobBright_26, this); + delayKnob -> move(196, 50); + delayKnob->setModel(&controls->m_delayModel); + delayKnob->setHintText(tr("Delay:"), " ms"); + delayKnob->setToolTip(tr("Delay length of allpass filter feedback")); + + TempoSyncKnob * rateKnob = new TempoSyncKnob(knobBright_26, this); + rateKnob -> move(103, 126); + rateKnob->setModel(&controls->m_rateModel); + rateKnob->setHintText(tr("Rate:"), " Sec"); + rateKnob->setToolTip(tr("LFO frequency")); + + Knob * amountKnob = new Knob(knobBright_26, this); + amountKnob -> move(65, 126); + amountKnob->setModel(&controls->m_amountModel); + amountKnob->setHintText(tr("Amount:"), " octaves"); + amountKnob->setToolTip(tr("LFO amplitude")); + + Knob * phaseKnob = new Knob(knobBright_26, this); + phaseKnob -> move(141, 126); + phaseKnob->setModel(&controls->m_phaseModel); + phaseKnob->setHintText(tr("Phase:"), " degrees"); + phaseKnob->setToolTip(tr("LFO stereo phase")); + + Knob * inFollowKnob = new Knob(knobBright_26, this); + inFollowKnob -> move(202, 126); + inFollowKnob->setModel(&controls->m_inFollowModel); + inFollowKnob->setHintText(tr("Input Following:"), " octaves"); + inFollowKnob->setToolTip(tr("Input follower amplitude")); + + Knob * attackKnob = new Knob(knobBright_26, this); + attackKnob -> move(240, 126); + attackKnob->setModel(&controls->m_attackModel); + attackKnob->setHintText(tr("Attack:"), " ms"); + attackKnob->setToolTip(tr("Input follower attack")); + + Knob * releaseKnob = new Knob(knobBright_26, this); + releaseKnob -> move(278, 126); + releaseKnob->setModel(&controls->m_releaseModel); + releaseKnob->setHintText(tr("Release:"), " ms"); + releaseKnob->setToolTip(tr("Input follower release")); + + Knob * distortionKnob = new Knob(knobSmall_17, this); + distortionKnob -> move(292, 63); + distortionKnob->setModel(&controls->m_distortionModel); + distortionKnob->setHintText(tr("Distortion:"), "%"); + distortionKnob->setToolTip(tr("Feedback rectifier")); + + Knob * analogDistKnob = new Knob(knobSmall_17, this); + analogDistKnob -> move(243, 8); + analogDistKnob->setModel(&controls->m_analogDistModel); + analogDistKnob->setHintText(tr("Analog Distortion:"), "x"); + analogDistKnob->setToolTip(tr("Analog distortion amount")); + + Knob * delayControlKnob = new Knob(knobSmall_17, this); + delayControlKnob -> move(344, 8); + delayControlKnob->setModel(&controls->m_delayControlModel); + delayControlKnob->setHintText(tr("Delay Control:"), "x"); + delayControlKnob->setToolTip(tr("Delay control amount")); + + Knob * cutoffControlKnob = new Knob(knobSmall_17, this); + cutoffControlKnob -> move(300, 8); + cutoffControlKnob->setModel(&controls->m_cutoffControlModel); + cutoffControlKnob->setHintText(tr("Cutoff Control:"), "x"); + cutoffControlKnob->setToolTip(tr("Cutoff control amount")); + + ComboBox * m_modeBox = new ComboBox(this); + m_modeBox->setGeometry(6, 6, 74, 22); + m_modeBox->setFont(pointSize<8>(m_modeBox->font())); + m_modeBox->setModel(&m_controls->m_modeModel); + m_modeBox->setToolTip(tr("Change Phaser feedback circuit")); + + QPixmap cutoffDotLeftImg = PLUGIN_NAME::getIconPixmap("cutoffDotLeft"); + m_cutoffDotLeftLabel = new QLabel(this); + m_cutoffDotLeftLabel->setPixmap(cutoffDotLeftImg); + m_cutoffDotLeftLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + m_cutoffDotLeftLabel->move(182, 183); + + QPixmap cutoffDotRightImg = PLUGIN_NAME::getIconPixmap("cutoffDotRight"); + m_cutoffDotRightLabel = new QLabel(this); + m_cutoffDotRightLabel->setPixmap(cutoffDotRightImg); + m_cutoffDotRightLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + m_cutoffDotRightLabel->move(182, 183); + + LedCheckBox * enableLFO = new LedCheckBox("", this, tr("Enable LFO"), LedCheckBox::Green); + enableLFO->setModel(&controls->m_enableLFOModel); + enableLFO->move(159, 104); + enableLFO->setToolTip(tr("Enable LFO")); + + PixmapButton * invertButton = new PixmapButton(this, tr("Invert wet signal")); + invertButton->move(241, 42); + invertButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("invert_active")); + invertButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("invert_inactive")); + invertButton->setCheckable(true); + invertButton->setModel(&controls->m_invertModel); + invertButton->setToolTip(tr("Invert wet signal")); + + PixmapButton * wetButton = new PixmapButton(this, tr("Wet")); + wetButton->move(287, 42); + wetButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("wet_active")); + wetButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("wet_inactive")); + wetButton->setCheckable(true); + wetButton->setModel(&controls->m_wetModel); + wetButton->setToolTip(tr("Isolate wet signal")); + + PixmapButton * analogButton = new PixmapButton(this, tr("Analog")); + analogButton->move(188, 9); + analogButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("analog_active")); + analogButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("analog_inactive")); + analogButton->setCheckable(true); + analogButton->setModel(&controls->m_analogModel); + analogButton->setToolTip(tr("Saturate feedback signal")); + + PixmapButton * doubleButton = new PixmapButton(this, tr("Double")); + doubleButton->move(88, 9); + doubleButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("double_active")); + doubleButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("double_inactive")); + doubleButton->setCheckable(true); + doubleButton->setModel(&controls->m_doubleModel); + doubleButton->setToolTip(tr("Send through allpass filters twice")); + + PixmapButton * aliasButton = new PixmapButton(this, tr("Alias")); + aliasButton->move(143, 9); + aliasButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap("alias_active")); + aliasButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap("alias_inactive")); + aliasButton->setCheckable(true); + aliasButton->setModel(&controls->m_aliasModel); + aliasButton->setToolTip(tr("Linearly invert spectrum before/after allpass filters")); + + EqFader * outFader = new EqFader(&controls->m_outGainModel,tr("Output gain"), + this, &controls->m_outPeakL, &controls->m_outPeakR); + outFader->setMinimumHeight(84); + outFader->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + outFader->resize(23, 94); + outFader->move(333, 67); + outFader->setDisplayConversion(false); + outFader->setHintText(tr("Gain"), "dBFS"); + outFader->setToolTip(tr("Output gain")); + + EqFader * inFader = new EqFader(&controls->m_inGainModel,tr("Input gain"), + this, &controls->m_inPeakL, &controls->m_inPeakR); + inFader->setMinimumHeight(84); + inFader->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + inFader->resize(23, 94); + inFader->move(14, 67); + inFader->setDisplayConversion(false); + inFader->setHintText(tr("Gain"), "dBFS"); + inFader->setToolTip(tr("Input gain")); + + connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(updateSliders())); +} + + +void PhaserControlDialog::updateSliders() +{ + // Magic. Do not touch. + m_cutoffDotLeftLabel->move((int((log2(m_controls->m_effect->getCutoff(0)) - log2(PHA_MIN_FREQ)) / + log2(PHA_MAX_FREQ / PHA_MIN_FREQ) * PHA_DOT_SLIDER_LENGTH)) + 12, 183); + m_cutoffDotRightLabel->move((int((log2(m_controls->m_effect->getCutoff(1)) - log2(PHA_MIN_FREQ)) / + log2(PHA_MAX_FREQ / PHA_MIN_FREQ) * PHA_DOT_SLIDER_LENGTH)) + 12, 183); +} + +} // namespace lmms::gui diff --git a/plugins/Phaser/PhaserControlDialog.h b/plugins/Phaser/PhaserControlDialog.h new file mode 100644 index 00000000000..b3e508f4300 --- /dev/null +++ b/plugins/Phaser/PhaserControlDialog.h @@ -0,0 +1,67 @@ +/* + * PhaserControlDialog.h + * + * Copyright (c) 2019 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 PHASER_CONTROL_DIALOG_H +#define PHASER_CONTROL_DIALOG_H + +#include + +#include "ComboBox.h" +#include "EffectControlDialog.h" +#include "Fader.h" + + +namespace lmms +{ + +class PhaserControls; + +namespace gui +{ + +class PhaserControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + PhaserControlDialog(PhaserControls* controls); + ~PhaserControlDialog() override + { + } + + QLabel * m_cutoffDotLeftLabel; + QLabel * m_cutoffDotRightLabel; + +private slots: + void updateSliders(); + +private: + PhaserControls * m_controls; +} ; + +} // namespace gui + +} // namespace lmms + +#endif + diff --git a/plugins/Phaser/PhaserControls.cpp b/plugins/Phaser/PhaserControls.cpp new file mode 100644 index 00000000000..32529c163a1 --- /dev/null +++ b/plugins/Phaser/PhaserControls.cpp @@ -0,0 +1,147 @@ +/* + * PhaserControls.cpp + * + * Copyright (c) 2019 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 + +#include "PhaserControls.h" +#include "PhaserEffect.h" + +#include "Engine.h" +#include "GuiApplication.h" +#include "MainWindow.h" +#include "Song.h" + +namespace lmms +{ + +constexpr int PHA_DOT_SLIDER_LENGTH = 338; +constexpr float PHA_MIN_FREQ = 20; +constexpr float PHA_MAX_FREQ = 20000; + + +PhaserControls::PhaserControls(PhaserEffect* effect) : + EffectControls(effect), + m_effect(effect), + m_cutoffModel(640.0f, 20.0f, 20000.0f, 0.01f, this, tr("Cutoff")), + m_resonanceModel(0.05f, 0.05f, 2.0f, 0.001f, this, tr("Resonance")), + m_feedbackModel(0.0f, -100.0f, 100.0f, 0.01f, this, tr("Feedback")), + m_orderModel(8, 0, 32, this, tr("Order")), + m_delayModel(0.0f, 0.0f, 20.0f, 0.001f, this, tr("Delay")), + m_rateModel(9.0f, 0.01f, 60.0f, 0.001f, 60000.0, this, tr("Rate")), + m_amountModel(4.5f, 0.f, 5.0f, 0.01f, this, tr("Amount")), + m_enableLFOModel(true , this, tr("Enable LFO")), + m_phaseModel(180.0f, 0.f, 360.0f, 0.01f, this, tr("Phase")), + m_inFollowModel(0.f, -30.f, 30.0f, 0.01f, this, tr("Input Follow")), + m_attackModel(200.0f, 0.0f, 2000.0f, 1.0f, this, tr("Attack")), + m_releaseModel(500.0f, 0.0f, 2000.0f, 1.0f, this, tr("Release")), + m_distortionModel(0.0f, 0.0f, 100.0f, 0.001f, this, tr("Distortion")), + m_analogDistModel(1.0f, 0.125f, 8.0f, 0.01f, this, tr("Analog Distortion")), + m_cutoffControlModel(1.0f, -4.0f, 4.0f, 0.01f, this, tr("Delay Control")), + m_delayControlModel(0.0f, -4.0f, 4.0f, 0.01f, this, tr("Delay Control")), + m_outGainModel(0.0, -60.0, 20.0, 0.01, this, tr("Output gain")), + m_inGainModel(0.0, -60.0, 20.0, 0.01, this, tr("Input gain")), + m_invertModel(false, this, tr("Invert")), + m_wetModel(false, this, tr("Wet")), + m_analogModel(false, this, tr("Analog")), + m_doubleModel(false, this, tr("Double")), + m_aliasModel(false, this, tr("Alias")), + m_modeModel(this, tr("Feedback Circuit Mode")) +{ + m_cutoffModel.setScaleLogarithmic(true); + m_resonanceModel.setScaleLogarithmic(true); + m_delayModel.setScaleLogarithmic(true); + m_rateModel.setScaleLogarithmic(true); + m_analogDistModel.setScaleLogarithmic(true); + m_cutoffControlModel.setScaleLogarithmic(true); + m_delayControlModel.setScaleLogarithmic(true); + + m_modeModel.addItem(tr("Default")); + m_modeModel.addItem(tr("Standard")); + m_modeModel.addItem(tr("Nested")); + m_modeModel.addItem(tr("Yeet Mode")); +} + + +void PhaserControls::saveSettings(QDomDocument& doc, QDomElement& _this) +{ + m_cutoffModel.saveSettings(doc, _this, "cutoff"); + m_resonanceModel.saveSettings(doc, _this, "resonance"); + m_feedbackModel.saveSettings(doc, _this, "feedback"); + m_orderModel.saveSettings(doc, _this, "order"); + m_delayModel.saveSettings(doc, _this, "delay"); + m_rateModel.saveSettings(doc, _this, "rate"); + m_enableLFOModel.saveSettings(doc, _this, "enableLFO"); + m_amountModel.saveSettings(doc, _this, "amount"); + m_phaseModel.saveSettings(doc, _this, "phase"); + m_inFollowModel.saveSettings(doc, _this, "inFollow"); + m_attackModel.saveSettings(doc, _this, "attack"); + m_releaseModel.saveSettings(doc, _this, "release"); + m_distortionModel.saveSettings(doc, _this, "distortion"); + m_outGainModel.saveSettings(doc, _this, "outGain"); + m_inGainModel.saveSettings(doc, _this, "inGain"); + m_invertModel.saveSettings(doc, _this, "invert"); + m_wetModel.saveSettings(doc, _this, "wet"); + m_analogModel.saveSettings(doc, _this, "analog"); + m_doubleModel.saveSettings(doc, _this, "double"); + m_aliasModel.saveSettings(doc, _this, "alias"); + m_modeModel.saveSettings(doc, _this, "mode"); + m_analogDistModel.saveSettings(doc, _this, "analogDist"); + m_cutoffControlModel.saveSettings(doc, _this, "cutoffControl"); + m_delayControlModel.saveSettings(doc, _this, "delayControl"); +} + + + +void PhaserControls::loadSettings(const QDomElement& _this) +{ + m_cutoffModel.loadSettings(_this, "cutoff"); + m_resonanceModel.loadSettings(_this, "resonance"); + m_feedbackModel.loadSettings(_this, "feedback"); + m_orderModel.loadSettings(_this, "order"); + m_delayModel.loadSettings(_this, "delay"); + m_rateModel.loadSettings(_this, "rate"); + m_amountModel.loadSettings(_this, "amount"); + m_enableLFOModel.loadSettings(_this, "enableLFO"); + m_phaseModel.loadSettings(_this, "phase"); + m_inFollowModel.loadSettings(_this, "inFollow"); + m_attackModel.loadSettings(_this, "attack"); + m_releaseModel.loadSettings(_this, "release"); + m_distortionModel.loadSettings(_this, "distortion"); + m_outGainModel.loadSettings(_this, "outGain"); + m_inGainModel.loadSettings(_this, "inGain"); + m_invertModel.loadSettings(_this, "invert"); + m_wetModel.loadSettings(_this, "wet"); + m_analogModel.loadSettings(_this, "analog"); + m_doubleModel.loadSettings(_this, "double"); + m_aliasModel.loadSettings(_this, "alias"); + m_modeModel.loadSettings(_this, "mode"); + m_analogDistModel.loadSettings(_this, "analogDist"); + m_cutoffControlModel.loadSettings(_this, "cutoffControl"); + m_delayControlModel.loadSettings(_this, "delayControl"); +} + + +} // namespace lmms + diff --git a/plugins/Phaser/PhaserControls.h b/plugins/Phaser/PhaserControls.h new file mode 100644 index 00000000000..6a4f56301f1 --- /dev/null +++ b/plugins/Phaser/PhaserControls.h @@ -0,0 +1,105 @@ +/* + * PhaserControls.h + * + * Copyright (c) 2019 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 PHASER_CONTROLS_H +#define PHASER_CONTROLS_H + +#include "PhaserControlDialog.h" + +#include "ComboBox.h" +#include "EffectControls.h" +#include "Knob.h" +#include "LcdSpinBox.h" + +namespace lmms +{ + +class PhaserEffect; + + +class PhaserControls : public EffectControls +{ + Q_OBJECT +public: + PhaserControls(PhaserEffect* effect); + + void saveSettings(QDomDocument & _doc, QDomElement & _parent) override; + void loadSettings(const QDomElement & _this) override; + inline QString nodeName() const override + { + return "PhaserControls"; + } + + int controlCount() override + { + return 24; + } + + gui::EffectControlDialog * createView() override + { + return new gui::PhaserControlDialog(this); + } + + float m_inPeakL = 0; + float m_inPeakR = 0; + float m_outPeakL = 0; + float m_outPeakR = 0; + +private: + PhaserEffect* m_effect; + + FloatModel m_cutoffModel; + FloatModel m_resonanceModel; + FloatModel m_feedbackModel; + IntModel m_orderModel; + FloatModel m_delayModel; + TempoSyncKnobModel m_rateModel; + FloatModel m_amountModel; + BoolModel m_enableLFOModel; + FloatModel m_phaseModel; + FloatModel m_inFollowModel; + FloatModel m_attackModel; + FloatModel m_releaseModel; + FloatModel m_distortionModel; + FloatModel m_analogDistModel; + FloatModel m_cutoffControlModel; + FloatModel m_delayControlModel; + FloatModel m_outGainModel; + FloatModel m_inGainModel; + BoolModel m_invertModel; + BoolModel m_wetModel; + BoolModel m_analogModel; + BoolModel m_doubleModel; + BoolModel m_aliasModel; + ComboBoxModel m_modeModel; + + friend class gui::PhaserControlDialog; + friend class PhaserEffect; + +} ; + +} // namespace lmms + +#endif + diff --git a/plugins/Phaser/PhaserEffect.cpp b/plugins/Phaser/PhaserEffect.cpp new file mode 100644 index 00000000000..0a9da33a3f5 --- /dev/null +++ b/plugins/Phaser/PhaserEffect.cpp @@ -0,0 +1,631 @@ +/* + * PhaserEffect.cpp + * + * Copyright (c) 2019 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 "PhaserEffect.h" + +#include "embed.h" +#include "Engine.h" +#include "GuiApplication.h" +#include "lmms_math.h" +#include "MainWindow.h" +#include "plugin_export.h" +#include "Song.h" + +namespace lmms +{ + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT phaser_plugin_descriptor = +{ + LMMS_STRINGIFY(PLUGIN_NAME), + "Phaser", + QT_TRANSLATE_NOOP("PluginBrowser", "A versatile phaser plugin"), + "Lost Robot ", + 0x0100, + Plugin::Effect, + new PluginPixmapLoader("logo"), + NULL, + NULL +} ; + +} + + +constexpr float PHA_NOISE_FLOOR = 0.00001f; +constexpr double PHA_LOG = 2.2; + +PhaserEffect::PhaserEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) : + Effect(&phaser_plugin_descriptor, parent, key), + m_phaserControls(this) +{ + m_currentPeak[0] = m_currentPeak[1] = PHA_NOISE_FLOOR; + + m_lfo = new QuadratureLfo(Engine::audioEngine()->processingSampleRate()); + m_lfo->setFrequency(1.0 / m_phaserControls.m_rateModel.value()); + + // Prepare the oversampling filters + double coefs[PHASER_OVERSAMPLE_COEFS]; + hiir::PolyphaseIir2Designer::compute_coefs_spec_order_tbw(coefs, PHASER_OVERSAMPLE_COEFS, 0.04); + for (int i = 0; i < 2; ++i) + { + m_oversampleAnalogIn1[i].set_coefs(coefs); + m_oversampleAnalogIn2[i].set_coefs(coefs); + m_oversampleAnalogOut[i].set_coefs(coefs); + m_oversampleFeedbackIn[i].set_coefs(coefs); + m_oversampleFeedbackOut[i].set_coefs(coefs); + } + + changeSampleRate(); + + connect(&m_phaserControls.m_attackModel, SIGNAL(dataChanged()), this, SLOT(calcAttack()), Qt::DirectConnection); + connect(&m_phaserControls.m_releaseModel, SIGNAL(dataChanged()), this, SLOT(calcRelease()), Qt::DirectConnection); + connect(&m_phaserControls.m_outGainModel, SIGNAL(dataChanged()), this, SLOT(calcOutGain()), Qt::DirectConnection); + connect(&m_phaserControls.m_inGainModel, SIGNAL(dataChanged()), this, SLOT(calcInGain()), Qt::DirectConnection); + connect(&m_phaserControls.m_phaseModel, SIGNAL(dataChanged()), this, SLOT(calcPhase()), Qt::DirectConnection); + + calcAttack(); + calcRelease(); + calcOutGain(); + calcInGain(); + calcPhase(); + + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(changeSampleRate())); + connect(Engine::getSong(), SIGNAL(playbackStateChanged()), this, SLOT(restartLFO())); +} + + +PhaserEffect::~PhaserEffect() +{ + delete m_lfo; +} + + +void PhaserEffect::calcAttack() +{ + m_attCoeff = pow(10.f, (PHA_LOG / (m_phaserControls.m_attackModel.value() * 0.001)) / Engine::audioEngine()->processingSampleRate()); +} + +void PhaserEffect::calcRelease() +{ + m_relCoeff = pow(10.f, (-PHA_LOG / (m_phaserControls.m_releaseModel.value() * 0.001)) / Engine::audioEngine()->processingSampleRate()); +} + +void PhaserEffect::calcOutGain() +{ + m_outGain = dbfsToAmp(m_phaserControls.m_outGainModel.value()); +} + +void PhaserEffect::calcInGain() +{ + m_inGain = dbfsToAmp(m_phaserControls.m_inGainModel.value()); +} + +void PhaserEffect::calcPhase() +{ + m_lfo->setOffset(m_phaserControls.m_phaseModel.value() / 180 * D_PI); +} + + +bool PhaserEffect::processAudioBuffer(sampleFrame* buf, const fpp_t frames) +{ + if (!isEnabled() || !isRunning ()) + { + m_currentPeak[0] = m_currentPeak[1] = PHA_NOISE_FLOOR; + return false; + } + + sample_rate_t sample_rate = Engine::audioEngine()->processingSampleRate(); + + double outSum = 0.0; + const float d = dryLevel(); + const float w = wetLevel(); + + // The following are sample-exact. + const ValueBuffer * cutoffBuf = m_phaserControls.m_cutoffModel.valueBuffer(); + const ValueBuffer * resonanceBuf = m_phaserControls.m_resonanceModel.valueBuffer(); + const ValueBuffer * orderBuf = m_phaserControls.m_orderModel.valueBuffer(); + const ValueBuffer * enableLFOBuf = m_phaserControls.m_enableLFOModel.valueBuffer(); + const ValueBuffer * amountBuf = m_phaserControls.m_amountModel.valueBuffer(); + const ValueBuffer * inFollowBuf = m_phaserControls.m_inFollowModel.valueBuffer(); + const ValueBuffer * invertBuf = m_phaserControls.m_invertModel.valueBuffer(); + const ValueBuffer * wetBuf = m_phaserControls.m_wetModel.valueBuffer(); + const ValueBuffer * modeBuf = m_phaserControls.m_modeModel.valueBuffer(); + const ValueBuffer * analogBuf = m_phaserControls.m_analogModel.valueBuffer(); + const ValueBuffer * analogDistBuf = m_phaserControls.m_analogDistModel.valueBuffer(); + const ValueBuffer * delayBuf = m_phaserControls.m_delayModel.valueBuffer(); + const ValueBuffer * cutoffControlBuf = m_phaserControls.m_cutoffControlModel.valueBuffer(); + const ValueBuffer * delayControlBuf = m_phaserControls.m_delayControlModel.valueBuffer(); + const ValueBuffer * doubleBuf = m_phaserControls.m_doubleModel.valueBuffer(); + const ValueBuffer * aliasBuf = m_phaserControls.m_aliasModel.valueBuffer(); + const ValueBuffer * distortionBuf = m_phaserControls.m_distortionModel.valueBuffer(); + const ValueBuffer * feedbackBuf = m_phaserControls.m_feedbackModel.valueBuffer(); + + float lOutPeak = 0.0; + float rOutPeak = 0.0; + float lInPeak = 0.0; + float rInPeak = 0.0; + + if (m_phaserControls.m_inFollowModel.isValueChanged() && m_phaserControls.m_inFollowModel.value() == 0) + { + m_currentPeak[0] = m_currentPeak[1] = PHA_NOISE_FLOOR; + } + + m_lfo->setFrequency(1.0 / m_phaserControls.m_rateModel.value()); + + + for (fpp_t f = 0; f < frames; ++f) + { + /* + Feedback diagrams + + + Default + + ------------------------- + | | + | V + x(t)-->(+)-->(+)-->[allpass]-->(+)-->(+)-->(*0.5)-->y(t) + ^ | + | | + --{delay}<---(*k)<------- + + A regular phaser signal flow, as suggested in Vadim + Zavalishin's "The Art of VA Filter Design" book. + + + Standard + + ------------------------------- + | | + | V + x(t)-->(+)-->(+)-->[allpass]-->(+)-->(+)-->(*0.5)-->y(t) + ^ | + | | + --{delay}<--(*k)<-- + + A more generic signal flow. + + + Nested + + ------------>(*-k)----------------- + | | + | V + x(t)-->(+)-->(+)-->{delay}-->[allpass]-->(+)-->(+)-->(*0.5)-->y(t) + ^ | + | | + -------------------(*k)<----------- + + A nested allpass filter. + + + Yeet Mode + + ------------------------------- + | | + | V + x(t)-->(+)-->(+)----->[allpass]----->(+)-->(+)-->(*0.5)-->y(t) + ^ | + | | + --[allpass]<--{delay}<---(*k)<- + + Straight insanity. Two allpass filters are shown, + but only one filter buffer is used, so every allpass + calculation borrows the buffer left over from the other. + + --------------------------------------------------------- + + Usually the delays shown in the above diagrams never + show up in any phasers, but I added them in because + they sound really cool (adjustable via delay knob). + + The extra delay can turn this into an interesting + allpass/comb hybrid. + */ + + float drySignal[2] = {buf[f][0], buf[f][1]}; + + drySignal[0] *= m_inGain; + drySignal[1] *= m_inGain; + + sample_t s[2] = {drySignal[0], drySignal[1]}; + + /* + You can reflect an entire sound's frequencies + around Nyquist by flipping every other sample. + This Phaser's Alias button does this both before + and after the allpass filters, linearly reflecting + the resulting phase response around Nyquist. + */ + m_aliasFlip = -m_aliasFlip; + + const float cutoff = cutoffBuf ? cutoffBuf->value(f) : m_phaserControls.m_cutoffModel.value(); + const float resonance = resonanceBuf ? resonanceBuf->value(f) : m_phaserControls.m_resonanceModel.value(); + const float order = orderBuf ? orderBuf->value(f) : m_phaserControls.m_orderModel.value(); + const bool enableLFO = enableLFOBuf ? enableLFOBuf->value(f) : m_phaserControls.m_enableLFOModel.value(); + const float amount = amountBuf ? amountBuf->value(f) : m_phaserControls.m_amountModel.value(); + const float inFollow = inFollowBuf ? inFollowBuf->value(f) : m_phaserControls.m_inFollowModel.value(); + const bool invert = invertBuf ? invertBuf->value(f) : m_phaserControls.m_invertModel.value(); + const bool wetIsolate = wetBuf ? wetBuf->value(f) : m_phaserControls.m_wetModel.value(); + const int mode = modeBuf ? modeBuf->value(f) : m_phaserControls.m_modeModel.value(); + const bool analog = analogBuf ? analogBuf->value(f) : m_phaserControls.m_analogModel.value(); + const float analogDist = analogDistBuf ? analogDistBuf->value(f) : m_phaserControls.m_analogDistModel.value(); + const bool doubleVal = doubleBuf ? doubleBuf->value(f) : m_phaserControls.m_doubleModel.value(); + const bool alias = aliasBuf ? aliasBuf->value(f) : m_phaserControls.m_aliasModel.value(); + const float delay = delayBuf ? delayBuf->value(f) : m_phaserControls.m_delayModel.value(); + const float cutoffControl = cutoffControlBuf ? cutoffControlBuf->value(f) : m_phaserControls.m_cutoffControlModel.value(); + const float delayControl = delayControlBuf ? delayControlBuf->value(f) : m_phaserControls.m_delayControlModel.value(); + const float distortion = (distortionBuf ? distortionBuf->value(f) : m_phaserControls.m_distortionModel.value()) * 0.01f; + const float feedback = (feedbackBuf ? feedbackBuf->value(f) : m_phaserControls.m_feedbackModel.value()) * 0.01f; + + // Calculate input follower values + const double sAbs[2] = {abs(s[0]), abs(s[1])}; + for (int i = 0; i < 2; i++) + { + if (sAbs[i] > m_currentPeak[i]) + { + m_currentPeak[i] = qMin(m_currentPeak[i] * m_attCoeff, sAbs[i]); + } + else + if (sAbs[i] < m_currentPeak[i]) + { + m_currentPeak[i] = qMax(m_currentPeak[i] * m_relCoeff, sAbs[i]); + } + + m_currentPeak[i] = qBound(PHA_NOISE_FLOOR, m_currentPeak[i], 10.0f); + } + + // Calculate real allpass filter frequencies + float lfoResults[2] = {0, 0}; + if (enableLFO) + { + m_lfo->tick(&lfoResults[0], &lfoResults[1]); + } + + const float realLfo[2] = {lfoResults[0] * amount, lfoResults[1] * amount}; + const float realInFollow[2] = {m_currentPeak[0] * inFollow, m_currentPeak[1] * inFollow}; + for (int i = 0; i < 2; i++) + { + m_displayCutoff[i] = qBound(20.f, detuneWithOctaves(cutoff, realLfo[i] + realInFollow[i]), 20000.f); + m_realCutoff[i] = qBound(20.f, detuneWithOctaves(cutoff, (realLfo[i] + realInFollow[i]) * cutoffControl), 20000.f); + } + + //Precalculate the allpass filter coefficients. + float apCoeff1[2]; + float apCoeff2[2]; + for (int b = 0; b < 2; ++b) + { + const float w0 = m_twoPiOverSr * m_realCutoff[b]; + const float a0 = 1 + (sin(w0) / (resonance * 2.f)); + apCoeff1[b] = (1 - (a0 - 1)) / a0; + apCoeff2[b] = (-2*cos(w0)) / a0; + } + + float firstAdd[2] = {0, 0}; + float secondAdd[2] = {0, 0}; + float delayOut[2] = {0, 0}; + + for (int i = 0; i < 2; ++i) + { + // Calculate delay amount to find read location + float readLoc = m_filtFeedbackLoc - + qMin(abs(delay + ((realLfo[i] + realInFollow[i]) * delayControl)), 20.f) * + 0.001f * Engine::audioEngine()->processingSampleRate(); + + if (readLoc < 0) {readLoc += m_delayBufSize;} + float readLocFrac = fraction(readLoc); + + // Read value from delay buffer + if (readLoc < m_delayBufSize - 1) + { + delayOut[i] = m_filtDelayBuf[i][floor(readLoc)] * (1 - readLocFrac) + m_filtDelayBuf[i][ceil(readLoc)] * readLocFrac; + } + else// For when the interpolation wraps around to the beginning of the buffer + { + delayOut[i] = m_filtDelayBuf[i][m_delayBufSize - 1] * (1 - readLocFrac) + m_filtDelayBuf[i][0] * readLocFrac; + } + } + + float tempAdd[2] = {0, 0}; + switch (mode) + { + case 0: case 1: + { + tempAdd[0] = delayOut[0] * feedback; + tempAdd[1] = delayOut[1] * feedback; + break; + } + case 2: + { + tempAdd[0] = m_lastSecondAdd[0] * feedback; + tempAdd[1] = m_lastSecondAdd[1] * feedback; + break; + } + case 3: + { + tempAdd[0] = delayOut[0] * feedback; + tempAdd[1] = delayOut[1] * feedback; + for (int a = 0; a < order; ++a) + { + for (int b = 0; b < 2; ++b) + { + for (int c = 0; c < 1 + doubleVal; ++c)// Runs twice if doubleVal is true + { + tempAdd[b] = calcAllpassFilter(tempAdd[b], sample_rate, a, b, apCoeff1[b], apCoeff2[b]); + } + } + } + } + } + if (analog) + { + for (int i = 0; i < 2; ++i) + { + // Upsample + float oversampledS1; + float oversampledS2; + m_oversampleAnalogIn1[i].process_sample(oversampledS1, oversampledS2, s[i]); + + float oversampledAdd1; + float oversampledAdd2; + m_oversampleAnalogIn2[i].process_sample(oversampledAdd1, oversampledAdd2, tempAdd[i]); + + // Waveshaping + float analogOut1 = tanh(oversampledS1 * analogDist) / analogDist + tanh(oversampledAdd1 * analogDist) / analogDist; + float analogOut2 = tanh(oversampledS2 * analogDist) / analogDist + tanh(oversampledAdd2 * analogDist) / analogDist; + + // Downsample + float downsampleIn[2] = {analogOut1, analogOut2}; + firstAdd[i] = m_oversampleFeedbackOut[i].process_sample(&downsampleIn[0]); + } + } + else + { + firstAdd[0] = s[0] + tempAdd[0]; + firstAdd[1] = s[1] + tempAdd[1]; + } + + + + // Unleash the allpass filters + switch (mode) + { + case 0: case 1: case 3: + { + secondAdd[0] = firstAdd[0]; + secondAdd[1] = firstAdd[1]; + break; + } + case 2: + { + secondAdd[0] = delayOut[0]; + secondAdd[1] = delayOut[1]; + break; + } + } + + if (alias) + { + secondAdd[0] *= m_aliasFlip; + secondAdd[1] *= m_aliasFlip; + } + for (int a = 0; a < order; ++a) + { + for (int b = 0; b < 2; ++b) + { + for (int c = 0; c < 1 + doubleVal; ++c)// Runs twice if doubleVal is true + { + secondAdd[b] = calcAllpassFilter(secondAdd[b], sample_rate, a, b, apCoeff1[b], apCoeff2[b]); + } + } + } + if (alias) + { + secondAdd[0] *= m_aliasFlip; + secondAdd[1] *= m_aliasFlip; + } + + // Get final result + switch (mode) + { + case 0: case 3: + { + s[0] = (secondAdd[0] * (invert ? -1 : 1) + firstAdd[0] * !wetIsolate) / 2.f; + s[1] = (secondAdd[1] * (invert ? -1 : 1) + firstAdd[1] * !wetIsolate) / 2.f; + break; + } + case 1: + { + s[0] = (secondAdd[0] * (invert ? -1 : 1) + s[0] * !wetIsolate) / 2.f; + s[1] = (secondAdd[1] * (invert ? -1 : 1) + s[1] * !wetIsolate) / 2.f; + break; + } + case 2: + { + s[0] = (secondAdd[0] * (invert ? -1 : 1) + firstAdd[0] * !wetIsolate * -feedback) / 2.f; + s[1] = (secondAdd[1] * (invert ? -1 : 1) + firstAdd[1] * !wetIsolate * -feedback) / 2.f; + break; + } + } + + m_lastSecondAdd[0] = secondAdd[0]; + m_lastSecondAdd[1] = secondAdd[1]; + + tempAdd[0] = tempAdd[1] = 0; + switch (mode) + { + case 0: case 1: case 3: + { + tempAdd[0] = secondAdd[0]; + tempAdd[1] = secondAdd[1]; + break; + } + case 2: + { + tempAdd[0] = firstAdd[0]; + tempAdd[1] = firstAdd[1]; + break; + } + } + + const float trueDistVal = distortion * 1.5; + for (int i = 0; i < 2; ++i) + { + // Upsample + float tempAdd1; + float tempAdd2; + m_oversampleFeedbackIn[i].process_sample(tempAdd1, tempAdd2, tempAdd[i]); + + + //Some gentle feedback distortion to prevent infinite loops. + tempAdd1 = tanh(tempAdd1 * 0.25) * 4; + tempAdd2 = tanh(tempAdd2 * 0.25) * 4; + + if (distortion) + { + /* + This behaves as a full-wave rectifier at 100% distortion. + + Why would I put a full-wave rectifier in a feedback loop? + 1. I needed a distortion type that made a noticeable change + to the sound that wouldn't result in infinite feedback loops. + An overdriven symmetrical distortion wouldn't work for that + reason. + 2. I just tried this strange idea as an experiment, and, + surprisingly, it sounded fantastic. + + Note that the DC offset added here by this distortion is + removed later on. + */ + + tempAdd1 = abs(tempAdd1 + 1.5 - trueDistVal) - 1.5 + trueDistVal; + tempAdd2 = abs(tempAdd2 + 1.5 - trueDistVal) - 1.5 + trueDistVal; + } + + // Downsample + float downsampleIn[2] = {tempAdd1, tempAdd2}; + tempAdd[i] = m_oversampleFeedbackOut[i].process_sample(&downsampleIn[0]); + } + + // Remove DC Offset + for (int i = 0; i < 2; ++i) + { + // Just subtract the approximate average of the latest many + // audio samples. + m_sampAvg[i] = m_sampAvg[i] * (1.f - m_dcTimeConst) + tempAdd[i] * m_dcTimeConst; + tempAdd[i] -= m_sampAvg[i]; + } + + + // Increment delay ring buffer index + ++m_filtFeedbackLoc; + if (m_filtFeedbackLoc >= m_delayBufSize) + { + m_filtFeedbackLoc -= m_delayBufSize; + } + + // Send new value to delay line + m_filtDelayBuf[0][m_filtFeedbackLoc] = tempAdd[0]; + m_filtDelayBuf[1][m_filtFeedbackLoc] = tempAdd[1]; + + s[0] *= m_outGain; + s[1] *= m_outGain; + + // Calculate values for displaying volume in the two volume meters + lInPeak = drySignal[0] > lInPeak ? drySignal[0] : lInPeak; + rInPeak = drySignal[1] > rInPeak ? drySignal[1] : rInPeak; + lOutPeak = s[0] > lOutPeak ? s[0] : lOutPeak; + rOutPeak = s[1] > rOutPeak ? s[1] : rOutPeak; + + buf[f][0] = d * buf[f][0] + w * s[0]; + buf[f][1] = d * buf[f][1] + w * s[1]; + + outSum += buf[f][0] + buf[f][1]; + } + + checkGate(outSum / frames); + + m_phaserControls.m_outPeakL = lOutPeak; + m_phaserControls.m_outPeakR = rOutPeak; + m_phaserControls.m_inPeakL = lInPeak; + m_phaserControls.m_inPeakR = rInPeak; + + return isRunning(); +} + + +sample_t PhaserEffect::calcAllpassFilter(sample_t inSamp, sample_rate_t Fs, int filtNum, int channel, float apCoeff1, float apCoeff2) +{ + // The original formula can be found here: https://pastebin.com/AyMH6k36 + // Much effort was put into CPU optimization, so now the filters + // only require a very small number of operations. + + float filterOutput = apCoeff1 * (inSamp - m_filtY[filtNum][channel][1]) + + apCoeff2 * (m_filtX[filtNum][channel][0] - m_filtY[filtNum][channel][0]) + + m_filtX[filtNum][channel][1]; + + m_filtX[filtNum][channel][1] = m_filtX[filtNum][channel][0]; + m_filtX[filtNum][channel][0] = inSamp; + m_filtY[filtNum][channel][1] = m_filtY[filtNum][channel][0]; + m_filtY[filtNum][channel][0] = filterOutput; + + return filterOutput; +} + + +void PhaserEffect::changeSampleRate() +{ + m_lfo->setSampleRate(Engine::audioEngine()->processingSampleRate()); + m_twoPiOverSr = F_2PI / Engine::audioEngine()->processingSampleRate(); + m_dcTimeConst = 44.1f / Engine::audioEngine()->processingSampleRate(); + calcAttack(); + calcRelease(); + + for (int b = 0; b < 2; ++b) + { + m_delayBufSize = Engine::audioEngine()->processingSampleRate() * 0.03 + 1; + m_filtDelayBuf[b].resize(m_delayBufSize); + } +} + + +void PhaserEffect::restartLFO() +{ + m_lfo->restart(); +} + + +extern "C" +{ + +// necessary for getting instance out of shared lib +PLUGIN_EXPORT Plugin * lmms_plugin_main(Model* parent, void* data) +{ + return new PhaserEffect(parent, static_cast(data)); +} +} + +} // namespace lmms diff --git a/plugins/Phaser/PhaserEffect.h b/plugins/Phaser/PhaserEffect.h new file mode 100644 index 00000000000..cf28c178a6a --- /dev/null +++ b/plugins/Phaser/PhaserEffect.h @@ -0,0 +1,125 @@ +/* + * PhaserEffect.h + * + * Copyright (c) 2019 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 PHASER_H +#define PHASER_H + +#include "PhaserControls.h" +#include "Effect.h" + +#include "hiir/PolyphaseIir2Designer.h" +#include "hiir/Downsampler2xFpu.h" +#include "hiir/Upsampler2xFpu.h" + +#include "BasicFilters.h" +#include "QuadratureLfo.h" +#include "ValueBuffer.h" + +constexpr int PHASER_OVERSAMPLE_COEFS = 8; + +namespace lmms +{ + +class PhaserEffect : public Effect +{ + Q_OBJECT +public: + PhaserEffect(Model * parent, const Descriptor::SubPluginFeatures::Key * key); + ~PhaserEffect() override; + bool processAudioBuffer(sampleFrame * buf, const fpp_t frames) override; + + EffectControls* controls() override + { + return &m_phaserControls; + } + + float getCutoff(int channel) + { + return m_displayCutoff[channel]; + } + + sample_t calcAllpassFilter(sample_t inSamp, sample_rate_t Fs, int filtNum, int channel, float apCoeff1, float apCoeff2); + inline float detuneWithOctaves(float pitchValue, float detuneValue) + { + return pitchValue * std::exp2(detuneValue); + } + +private slots: + void calcAttack(); + void calcRelease(); + void calcOutGain(); + void calcInGain(); + void calcPhase(); + void changeSampleRate(); + void restartLFO(); + +private: + PhaserControls m_phaserControls; + + float m_filtX[32][2][2] = {{{0}}};// [filter number][channel][samples back in time] + float m_filtY[32][2][2] = {{{0}}};// [filter number][channel][samples back in time] + std::vector m_filtDelayBuf[2]; + int m_delayBufSize; + int m_filtFeedbackLoc = 0; + + QuadratureLfo * m_lfo; + + float m_twoPiOverSr; + + float m_currentPeak[2]; + + double m_attCoeff; + double m_relCoeff; + + float m_outGain; + float m_inGain; + + float m_sampAvg[2] = {0}; + float m_oscillateTracker1[2] = {0}; + float m_oscillateTracker2[2] = {0}; + + float m_displayCutoff[2] = {0, 0}; + float m_realCutoff[2] = {0, 0}; + + float m_lastSecondAdd[2] = {0}; + + float m_dcTimeConst; + + float m_aliasFlip = 1; + + hiir::Upsampler2xFpu m_oversampleAnalogIn1[2]; + hiir::Upsampler2xFpu m_oversampleAnalogIn2[2]; + hiir::Downsampler2xFpu m_oversampleAnalogOut[2]; + hiir::Upsampler2xFpu m_oversampleFeedbackIn[2]; + hiir::Downsampler2xFpu m_oversampleFeedbackOut[2]; + + friend class PhaserControls; + +} ; + +} // namespace lmms + +#endif + diff --git a/plugins/Phaser/alias_active.png b/plugins/Phaser/alias_active.png new file mode 100644 index 00000000000..2711c3f7e88 Binary files /dev/null and b/plugins/Phaser/alias_active.png differ diff --git a/plugins/Phaser/alias_inactive.png b/plugins/Phaser/alias_inactive.png new file mode 100644 index 00000000000..59d9d654d67 Binary files /dev/null and b/plugins/Phaser/alias_inactive.png differ diff --git a/plugins/Phaser/analog_active.png b/plugins/Phaser/analog_active.png new file mode 100644 index 00000000000..68b2df39674 Binary files /dev/null and b/plugins/Phaser/analog_active.png differ diff --git a/plugins/Phaser/analog_inactive.png b/plugins/Phaser/analog_inactive.png new file mode 100644 index 00000000000..24c7fd2b6fd Binary files /dev/null and b/plugins/Phaser/analog_inactive.png differ diff --git a/plugins/Phaser/artwork.png b/plugins/Phaser/artwork.png new file mode 100644 index 00000000000..24b4ca29d14 Binary files /dev/null and b/plugins/Phaser/artwork.png differ diff --git a/plugins/Phaser/cutoffDotLeft.png b/plugins/Phaser/cutoffDotLeft.png new file mode 100644 index 00000000000..086addb283b Binary files /dev/null and b/plugins/Phaser/cutoffDotLeft.png differ diff --git a/plugins/Phaser/cutoffDotRight.png b/plugins/Phaser/cutoffDotRight.png new file mode 100644 index 00000000000..86808a73420 Binary files /dev/null and b/plugins/Phaser/cutoffDotRight.png differ diff --git a/plugins/Phaser/double_active.png b/plugins/Phaser/double_active.png new file mode 100644 index 00000000000..482fb065120 Binary files /dev/null and b/plugins/Phaser/double_active.png differ diff --git a/plugins/Phaser/double_inactive.png b/plugins/Phaser/double_inactive.png new file mode 100644 index 00000000000..f18019ea69b Binary files /dev/null and b/plugins/Phaser/double_inactive.png differ diff --git a/plugins/Phaser/invert_active.png b/plugins/Phaser/invert_active.png new file mode 100644 index 00000000000..d23fb5be6fb Binary files /dev/null and b/plugins/Phaser/invert_active.png differ diff --git a/plugins/Phaser/invert_inactive.png b/plugins/Phaser/invert_inactive.png new file mode 100644 index 00000000000..b6cf8b8fbc7 Binary files /dev/null and b/plugins/Phaser/invert_inactive.png differ diff --git a/plugins/Phaser/logo.png b/plugins/Phaser/logo.png new file mode 100644 index 00000000000..9340da708dd Binary files /dev/null and b/plugins/Phaser/logo.png differ diff --git a/plugins/Phaser/wet_active.png b/plugins/Phaser/wet_active.png new file mode 100644 index 00000000000..7d7f5e1c85f Binary files /dev/null and b/plugins/Phaser/wet_active.png differ diff --git a/plugins/Phaser/wet_inactive.png b/plugins/Phaser/wet_inactive.png new file mode 100644 index 00000000000..c2ea6a8190a Binary files /dev/null and b/plugins/Phaser/wet_inactive.png differ