diff --git a/build/depends.py b/build/depends.py index b8818beb8674..542feb41bdc5 100644 --- a/build/depends.py +++ b/build/depends.py @@ -709,6 +709,8 @@ def sources(self, build): "effects/native/bessel8lvmixeqeffect.cpp", "effects/native/threebandbiquadeqeffect.cpp", "effects/native/biquadfullkilleqeffect.cpp", + "effects/native/semiparametriceq3knobeffect.cpp", + "effects/native/semiparametriceq4knobeffect.cpp", "effects/native/loudnesscontoureffect.cpp", "effects/native/graphiceqeffect.cpp", "effects/native/flangereffect.cpp", diff --git a/src/effects/effectmanifest.h b/src/effects/effectmanifest.h index d27de0031f71..401c719d25a5 100644 --- a/src/effects/effectmanifest.h +++ b/src/effects/effectmanifest.h @@ -23,6 +23,7 @@ class EffectManifest final { public: EffectManifest() : m_isMixingEQ(false), + m_isWaveformChangingEQ(false), m_isMasterEQ(false), m_effectRampsFromDry(false) { } @@ -82,6 +83,14 @@ class EffectManifest final { m_isMixingEQ = value; } + virtual const bool& isWaveformChangingEQ() const { + return m_isWaveformChangingEQ; + } + + virtual void setIsWaveformChangingEQ(const bool value) { + m_isWaveformChangingEQ = value; + } + virtual const bool& isMasterEQ() const { return m_isMasterEQ; } @@ -127,6 +136,7 @@ class EffectManifest final { QString m_description; // This helps us at DlgPrefEQ's basic selection of Equalizers bool m_isMixingEQ; + bool m_isWaveformChangingEQ; bool m_isMasterEQ; QList m_parameters; bool m_effectRampsFromDry; diff --git a/src/effects/effectmanifestparameter.h b/src/effects/effectmanifestparameter.h index 623b6b2ee12a..77c35660babe 100644 --- a/src/effects/effectmanifestparameter.h +++ b/src/effects/effectmanifestparameter.h @@ -29,6 +29,7 @@ class EffectManifestParameter { HERTZ, SAMPLERATE, // fraction of the samplerate BEATS, // multiples of a beat + DECIBELS, }; enum class LinkType { diff --git a/src/effects/effectsmanager.cpp b/src/effects/effectsmanager.cpp index b66160b887fa..57eed940d3fe 100644 --- a/src/effects/effectsmanager.cpp +++ b/src/effects/effectsmanager.cpp @@ -116,10 +116,6 @@ const QList EffectsManager::getAvailableEffectManifestsFiltered( return list; } -bool EffectsManager::isEQ(const QString& effectId) const { - return getEffectManifest(effectId).isMixingEQ(); -} - QString EffectsManager::getNextEffectId(const QString& effectId) { if (m_availableEffectManifests.isEmpty()) { return QString(); diff --git a/src/effects/effectsmanager.h b/src/effects/effectsmanager.h index a9a5643be14f..5cd1da0b2da6 100644 --- a/src/effects/effectsmanager.h +++ b/src/effects/effectsmanager.h @@ -66,7 +66,6 @@ class EffectsManager : public QObject { }; const QList getAvailableEffectManifestsFiltered( EffectManifestFilterFnc filter) const; - bool isEQ(const QString& effectId) const; QPair getEffectManifestAndBackend( const QString& effectId) const; EffectManifest getEffectManifest(const QString& effectId) const; diff --git a/src/effects/native/equalizer_util.h b/src/effects/native/equalizer_util.h index a1a5e3842d53..c235345a6ca0 100644 --- a/src/effects/native/equalizer_util.h +++ b/src/effects/native/equalizer_util.h @@ -9,6 +9,8 @@ class EqualizerUtil { public: // Creates common EQ parameters like low/mid/high gain and kill buttons. static void createCommonParameters(EffectManifest* manifest) { + manifest->setIsWaveformChangingEQ(true); + EffectManifestParameter* low = manifest->addParameter(); low->setId("low"); low->setName(QObject::tr("Low")); diff --git a/src/effects/native/nativebackend.cpp b/src/effects/native/nativebackend.cpp index 2d830e66654f..582d229e93f5 100644 --- a/src/effects/native/nativebackend.cpp +++ b/src/effects/native/nativebackend.cpp @@ -9,6 +9,8 @@ #include "effects/native/bessel4lvmixeqeffect.h" #include "effects/native/threebandbiquadeqeffect.h" #include "effects/native/biquadfullkilleqeffect.h" +#include "effects/native/semiparametriceq3knobeffect.h" +#include "effects/native/semiparametriceq4knobeffect.h" #include "effects/native/graphiceqeffect.h" #include "effects/native/filtereffect.h" #include "effects/native/moogladder4filtereffect.h" @@ -29,6 +31,8 @@ NativeBackend::NativeBackend(QObject* pParent) registerEffect(); registerEffect(); registerEffect(); + registerEffect(); + registerEffect(); // Compensations EQs registerEffect(); registerEffect(); diff --git a/src/effects/native/semiparametriceq3knobeffect.cpp b/src/effects/native/semiparametriceq3knobeffect.cpp new file mode 100644 index 000000000000..b71871ea43b4 --- /dev/null +++ b/src/effects/native/semiparametriceq3knobeffect.cpp @@ -0,0 +1,125 @@ +#include "effects/native/semiparametriceq3knobeffect.h" + +namespace { +static const int kStartupSamplerate = 44100; +static const double kMinCorner = 13; // Hz +static const double kMaxCorner = 22050; // Hz +static const double kLpfHpfQ = 0.1; +static const double kSemiparametricQ = 0.4; +static const double kSemiparametricMaxBoostDb = 8; +static const double kSemiparametricMaxCutDb = -20; +} // anonymous namespace + +SemiparametricEQEffect3KnobGroupState::SemiparametricEQEffect3KnobGroupState() + : m_lowFilter(1, kMaxCorner / kStartupSamplerate, kLpfHpfQ, true), + m_semiParametricFilter(kStartupSamplerate, 1000, kSemiparametricQ), + m_highFilter(1, kMinCorner / kStartupSamplerate, kLpfHpfQ, true), + m_intermediateBuffer(MAX_BUFFER_LEN), + m_filterBehavior(kMinCorner, kMaxCorner, -40), + m_dCenterOld(0), + m_dGainOld(0), + m_dFilterOld(0) { +} + +// static +QString SemiparametricEQEffect3Knob::getId() { + return "org.mixxx.effects.semiparametriceq3knob"; +} + +EffectManifest SemiparametricEQEffect3Knob::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Semiparametric Equalizer (3 knobs)")); + manifest.setShortName(QObject::tr("Semiparam 3")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription(QObject::tr( + "A semiparametric EQ effect modeled after the PLAYdifferently Model 1 hardware mixer.")); + manifest.setEffectRampsFromDry(true); + manifest.setIsMixingEQ(true); + + EffectManifestParameter* filter = manifest.addParameter(); + filter->setId("filter"); + filter->setName(QObject::tr("Filter")); + filter->setDescription(QObject::tr("Bipolar filter knob. Controls corner frequency ratio of the high pass filter on the left and corner frequency of the low pass filter on the right.")); + filter->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR); + filter->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + filter->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); + filter->setMinimum(0); + filter->setMaximum(1); + filter->setDefault(0.5); + + EffectManifestParameter* gain = manifest.addParameter(); + gain->setId("gain"); + gain->setName(QObject::tr("Gain")); + gain->setDescription(QObject::tr("Gain of the semiparametric EQ")); + gain->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + gain->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + gain->setUnitsHint(EffectManifestParameter::UnitsHint::DECIBELS); + gain->setMinimum(0); + gain->setMaximum(4); + gain->setDefault(1); + + EffectManifestParameter* center = manifest.addParameter(); + center->setId("center"); + center->setName(QObject::tr("Center")); + center->setDescription(QObject::tr("Center frequency of the semiparametric EQ")); + center->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + center->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + center->setUnitsHint(EffectManifestParameter::UnitsHint::HERTZ); + center->setMinimum(70); + center->setMaximum(7000); + center->setDefault(1000); + + return manifest; +} + +SemiparametricEQEffect3Knob::SemiparametricEQEffect3Knob(EngineEffect* pEffect, + const EffectManifest& manifest) + : m_pCenter(pEffect->getParameterById("center")), + m_pGain(pEffect->getParameterById("gain")), + m_pFilter(pEffect->getParameterById("filter")) { + Q_UNUSED(manifest); +} + +void SemiparametricEQEffect3Knob::processChannel(const ChannelHandle& handle, + SemiparametricEQEffect3KnobGroupState* pState, + const CSAMPLE* pInput, CSAMPLE *pOutput, + const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatureState) { + Q_UNUSED(handle); + Q_UNUSED(groupFeatureState); + Q_UNUSED(enableState); + + double center = m_pCenter->value(); + double gain = m_pGain->value(); + double filter = m_pFilter->value(); + + if (center != pState->m_dCenterOld || gain != pState->m_dGainOld) { + double db = gain - 1.0; + if (db >= 0) { + db *= kSemiparametricMaxBoostDb; + } else { + db *= -kSemiparametricMaxCutDb; + } + pState->m_semiParametricFilter.setFrequencyCorners(sampleRate, center, kSemiparametricQ, db); + } + if (filter != pState->m_dFilterOld) { + double lpf = pState->m_filterBehavior.parameterToValue( + std::min((filter * 2.0), 1.0)) / sampleRate; + double hpf = pState->m_filterBehavior.parameterToValue( + std::max((filter - 0.5) * 2.0, 0.0)) / sampleRate; + pState->m_lowFilter.setFrequencyCorners(1, lpf, kLpfHpfQ); + pState->m_highFilter.setFrequencyCorners(1, hpf, kLpfHpfQ); + } + + pState->m_lowFilter.process(pInput, pState->m_intermediateBuffer.data(), numSamples); + pState->m_semiParametricFilter.process(pState->m_intermediateBuffer.data(), pState->m_intermediateBuffer.data(), numSamples); + pState->m_highFilter.process(pState->m_intermediateBuffer.data(), pOutput, numSamples); + + pState->m_dCenterOld = center; + pState->m_dGainOld = gain; + pState->m_dFilterOld = filter; +} diff --git a/src/effects/native/semiparametriceq3knobeffect.h b/src/effects/native/semiparametriceq3knobeffect.h new file mode 100644 index 000000000000..74140827069b --- /dev/null +++ b/src/effects/native/semiparametriceq3knobeffect.h @@ -0,0 +1,65 @@ +#ifndef SEMIPARAMETRICEQ3KNOB_H +#define SEMIPARAMETRICEQ3KNOB_H + +#include "control/controlbehavior.h" +#include "effects/effect.h" +#include "effects/effectprocessor.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "engine/enginefilterbiquad1.h" +#include "util/class.h" +#include "util/defs.h" +#include "util/memory.h" +#include "util/sample.h" +#include "util/samplebuffer.h" +#include "util/types.h" + +struct SemiparametricEQEffect3KnobGroupState { + public: + SemiparametricEQEffect3KnobGroupState(); + inline ~SemiparametricEQEffect3KnobGroupState() {}; + + EngineFilterBiquad1Low m_lowFilter; + EngineFilterBiquad1Peaking m_semiParametricFilter; + EngineFilterBiquad1High m_highFilter; + + SampleBuffer m_intermediateBuffer; + ControlLogPotmeterBehavior m_filterBehavior; + + double m_dCenterOld; + double m_dGainOld; + double m_dFilterOld; +}; + +class SemiparametricEQEffect3Knob : public PerChannelEffectProcessor { + public: + SemiparametricEQEffect3Knob(EngineEffect* pEffect, const EffectManifest& manifest); + inline ~SemiparametricEQEffect3Knob() {}; + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processChannel(const ChannelHandle& handle, + SemiparametricEQEffect3KnobGroupState* pState, + const CSAMPLE* pInput, CSAMPLE *pOutput, + const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatureState); + + private: + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pCenter; + EngineEffectParameter* m_pGain; + EngineEffectParameter* m_pFilter; + + unsigned int m_oldSampleRate; + + DISALLOW_COPY_AND_ASSIGN(SemiparametricEQEffect3Knob); +}; + +#endif // SEMIPARAMETRICEQ3KNOB_H diff --git a/src/effects/native/semiparametriceq4knobeffect.cpp b/src/effects/native/semiparametriceq4knobeffect.cpp new file mode 100644 index 000000000000..6b26c4e87d0f --- /dev/null +++ b/src/effects/native/semiparametriceq4knobeffect.cpp @@ -0,0 +1,139 @@ +#include "effects/native/semiparametriceq4knobeffect.h" + +namespace { +static const int kStartupSamplerate = 44100; +static const double kMinCorner = 13; // Hz +static const double kMaxCorner = 22050; // Hz +static const double kLpfHpfQ = 0.707106781; +static const double kSemiparametricQ = 0.4; +static const double kSemiparametricMaxBoostDb = 8; +static const double kSemiparametricMaxCutDb = -20; +} // anonymous namespace + +SemiparametricEQEffect4KnobGroupState::SemiparametricEQEffect4KnobGroupState() + : m_lowFilter(1, kMaxCorner / kStartupSamplerate, kLpfHpfQ, true), + m_semiParametricFilter(kStartupSamplerate, 1000, kSemiparametricQ), + m_highFilter(1, kMinCorner / kStartupSamplerate, kLpfHpfQ, true), + m_intermediateBuffer(MAX_BUFFER_LEN), + m_dLpfOld(0), + m_dCenterOld(0), + m_dGainOld(0), + m_dHpfOld(0) { +} + +// static +QString SemiparametricEQEffect4Knob::getId() { + return "org.mixxx.effects.semiparametriceq4knob"; +} + +EffectManifest SemiparametricEQEffect4Knob::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Semiparametric Equalizer (4 knobs)")); + manifest.setShortName(QObject::tr("Semiparam 4")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription(QObject::tr( + "A semiparametric EQ effect modeled after the PLAYdifferently Model 1 hardware mixer.")); + manifest.setEffectRampsFromDry(true); + manifest.setIsMixingEQ(true); + + EffectManifestParameter* hpf = manifest.addParameter(); + hpf->setId("hpf"); + hpf->setName(QObject::tr("HPF")); + hpf->setDescription(QObject::tr("Corner frequency ratio of the high pass filter")); + hpf->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + hpf->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + hpf->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); + hpf->setNeutralPointOnScale(0.0); + hpf->setDefault(kMinCorner); + hpf->setMinimum(kMinCorner); + hpf->setMaximum(kMaxCorner); + + EffectManifestParameter* gain = manifest.addParameter(); + gain->setId("gain"); + gain->setName(QObject::tr("Gain")); + gain->setDescription(QObject::tr("Gain of the semiparametric EQ")); + gain->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + gain->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + gain->setUnitsHint(EffectManifestParameter::UnitsHint::DECIBELS); + gain->setMinimum(-20); + gain->setMaximum(8); + gain->setDefault(0); + + EffectManifestParameter* center = manifest.addParameter(); + center->setId("center"); + center->setName(QObject::tr("Center")); + center->setDescription(QObject::tr("Center frequency of the semiparametric EQ")); + center->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + center->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + center->setUnitsHint(EffectManifestParameter::UnitsHint::HERTZ); + center->setMinimum(70); + center->setMaximum(7000); + center->setDefault(1000); + + EffectManifestParameter* lpf = manifest.addParameter(); + lpf->setId("lpf"); + lpf->setName(QObject::tr("LPF")); + lpf->setDescription(QObject::tr("Corner frequency ratio of the low pass filter")); + lpf->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + lpf->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + lpf->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); + lpf->setNeutralPointOnScale(1); + lpf->setMinimum(kMinCorner); + lpf->setMaximum(kMaxCorner); + lpf->setDefault(kMaxCorner); + + return manifest; +} + +SemiparametricEQEffect4Knob::SemiparametricEQEffect4Knob(EngineEffect* pEffect, + const EffectManifest& manifest) + : m_pLPF(pEffect->getParameterById("lpf")), + m_pCenter(pEffect->getParameterById("center")), + m_pGain(pEffect->getParameterById("gain")), + m_pHPF(pEffect->getParameterById("hpf")) { + Q_UNUSED(manifest); +} + +void SemiparametricEQEffect4Knob::processChannel(const ChannelHandle& handle, + SemiparametricEQEffect4KnobGroupState* pState, + const CSAMPLE* pInput, CSAMPLE *pOutput, + const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatureState) { + Q_UNUSED(handle); + Q_UNUSED(groupFeatureState); + Q_UNUSED(enableState); + + double lpf = m_pLPF->value() / sampleRate; + double center = m_pCenter->value(); + double gain = m_pGain->value(); + double hpf = m_pHPF->value() / sampleRate; + + if (lpf != pState->m_dLpfOld) { + pState->m_lowFilter.setFrequencyCorners(1, lpf, kLpfHpfQ); + } + if (center != pState->m_dCenterOld || gain != pState->m_dGainOld) { + double db = gain - 1.0; + if (db >= 0) { + db *= kSemiparametricMaxBoostDb; + } else { + db *= -kSemiparametricMaxCutDb; + } + pState->m_semiParametricFilter.setFrequencyCorners(sampleRate, center, kSemiparametricQ, db); + } + if (hpf != pState->m_dHpfOld) { + pState->m_highFilter.setFrequencyCorners(1, hpf, kLpfHpfQ); + } + + pState->m_lowFilter.process(pInput, pState->m_intermediateBuffer.data(), numSamples); + pState->m_semiParametricFilter.process(pState->m_intermediateBuffer.data(), pState->m_intermediateBuffer.data(), numSamples); + pState->m_highFilter.process(pState->m_intermediateBuffer.data(), pOutput, numSamples); + + pState->m_dLpfOld = lpf; + pState->m_dCenterOld = center; + pState->m_dGainOld = gain; + pState->m_dHpfOld = hpf; +} diff --git a/src/effects/native/semiparametriceq4knobeffect.h b/src/effects/native/semiparametriceq4knobeffect.h new file mode 100644 index 000000000000..77d84dda9caf --- /dev/null +++ b/src/effects/native/semiparametriceq4knobeffect.h @@ -0,0 +1,64 @@ +#ifndef SEMIPARAMETRICEQ4KNOB_H +#define SEMIPARAMETRICEQ4KNOB_H + +#include "effects/effect.h" +#include "effects/effectprocessor.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "engine/enginefilterbiquad1.h" +#include "util/class.h" +#include "util/defs.h" +#include "util/memory.h" +#include "util/sample.h" +#include "util/samplebuffer.h" +#include "util/types.h" + +struct SemiparametricEQEffect4KnobGroupState { + SemiparametricEQEffect4KnobGroupState(); + inline ~SemiparametricEQEffect4KnobGroupState() {}; + + EngineFilterBiquad1Low m_lowFilter; + EngineFilterBiquad1Peaking m_semiParametricFilter; + EngineFilterBiquad1High m_highFilter; + + SampleBuffer m_intermediateBuffer; + + double m_dLpfOld; + double m_dCenterOld; + double m_dGainOld; + double m_dHpfOld; +}; + +class SemiparametricEQEffect4Knob : public PerChannelEffectProcessor { +public: + SemiparametricEQEffect4Knob(EngineEffect* pEffect, const EffectManifest& manifest); + inline ~SemiparametricEQEffect4Knob() {}; + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processChannel(const ChannelHandle& handle, + SemiparametricEQEffect4KnobGroupState* pState, + const CSAMPLE* pInput, CSAMPLE *pOutput, + const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatureState); + +private: + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pLPF; + EngineEffectParameter* m_pCenter; + EngineEffectParameter* m_pGain; + EngineEffectParameter* m_pHPF; + + unsigned int m_oldSampleRate; + + DISALLOW_COPY_AND_ASSIGN(SemiparametricEQEffect4Knob); +}; + +#endif // SEMIPARAMETRICEQ4KNOB_H diff --git a/src/preferences/dialog/dlgprefeq.cpp b/src/preferences/dialog/dlgprefeq.cpp index 1b4edad44dbb..f53d48978a3c 100644 --- a/src/preferences/dialog/dlgprefeq.cpp +++ b/src/preferences/dialog/dlgprefeq.cpp @@ -156,7 +156,9 @@ void DlgPrefEQ::slotNumDecksChanged(double numDecks) { configuredEffect = kDefaultEqId; } m_deckEqEffectSelectors[i]->setCurrentIndex(selectedEffectIndex); - m_filterWaveformEffectLoaded[i] = m_pEffectsManager->isEQ(configuredEffect); + const EffectManifest manifest = + m_pEffectsManager->getEffectManifest(configuredEffect); + m_filterWaveformEffectLoaded[i] = manifest.isWaveformChangingEQ(); m_filterWaveformEnableCOs[i]->set( m_filterWaveformEffectLoaded[i] && !CheckBoxBypass->checkState()); @@ -470,7 +472,10 @@ void DlgPrefEQ::applySelections() { m_pEQEffectRack->loadEffectToGroup(group, pEffect); m_pConfig->set(ConfigKey(kConfigKey, "EffectForGroup_" + group), ConfigValue(effectId)); - m_filterWaveformEnableCOs[deck]->set(m_pEffectsManager->isEQ(effectId)); + + m_filterWaveformEnableCOs[deck]->set( + pEffect->getManifest().isWaveformChangingEQ()); + qDebug() << "DlgPrefEq::applySelections" << pEffect->getManifest().isWaveformChangingEQ() << m_filterWaveformEnableCOs[deck]->get(); // This is required to remove a previous selected effect that does not // fit to the current ShowAllEffects checkbox