diff --git a/src/control/control.cpp b/src/control/control.cpp index 178fa922c812..0e757ed43ecf 100644 --- a/src/control/control.cpp +++ b/src/control/control.cpp @@ -233,30 +233,31 @@ double ControlDoublePrivate::getParameterForValue(double value) const { return value; } -double ControlDoublePrivate::getParameterForMidiValue(double midiValue) const { +double ControlDoublePrivate::getParameterForMidi(double midiParam) const { QSharedPointer pBehavior = m_pBehavior; - if (!pBehavior.isNull()) { - return pBehavior->midiValueToParameter(midiValue); + VERIFY_OR_DEBUG_ASSERT(pBehavior) { + qWarning() << "Cannot set" << m_key << "by Midi"; + return 0; } - return midiValue; + return pBehavior->midiToParameter(midiParam); } -void ControlDoublePrivate::setMidiParameter(MidiOpCode opcode, double dParam) { +void ControlDoublePrivate::setValueFromMidi(MidiOpCode opcode, double midiParam) { QSharedPointer pBehavior = m_pBehavior; - if (!pBehavior.isNull()) { - pBehavior->setValueFromMidiParameter(opcode, dParam, this); - } else { - set(dParam, NULL); + VERIFY_OR_DEBUG_ASSERT(pBehavior) { + qWarning() << "Cannot set" << m_key << "by Midi"; + return; } + pBehavior->setValueFromMidi(opcode, midiParam, this); } double ControlDoublePrivate::getMidiParameter() const { QSharedPointer pBehavior = m_pBehavior; - double value = get(); - if (!pBehavior.isNull()) { - value = pBehavior->valueToMidiParameter(value); + VERIFY_OR_DEBUG_ASSERT(pBehavior) { + qWarning() << "Cannot get" << m_key << "by Midi"; + return 0; } - return value; + return pBehavior->valueToMidiParameter(get()); } bool ControlDoublePrivate::connectValueChangeRequest(const QObject* receiver, diff --git a/src/control/control.h b/src/control/control.h index edc876f5eac2..ea3fded2f73e 100644 --- a/src/control/control.h +++ b/src/control/control.h @@ -80,9 +80,9 @@ class ControlDoublePrivate : public QObject { void setParameter(double dParam, QObject* pSender); double getParameter() const; double getParameterForValue(double value) const; - double getParameterForMidiValue(double midiValue) const; + double getParameterForMidi(double midiValue) const; - void setMidiParameter(MidiOpCode opcode, double dParam); + void setValueFromMidi(MidiOpCode opcode, double dParam); double getMidiParameter() const; inline bool ignoreNops() const { diff --git a/src/control/controlbehavior.cpp b/src/control/controlbehavior.cpp index a435bf7d973f..9d39efdfbac0 100644 --- a/src/control/controlbehavior.cpp +++ b/src/control/controlbehavior.cpp @@ -11,8 +11,8 @@ double ControlNumericBehavior::valueToParameter(double dValue) { return dValue; } -double ControlNumericBehavior::midiValueToParameter(double midiValue) { - return midiValue; +double ControlNumericBehavior::midiToParameter(double midiValue) { + return midiValue / 127.0; } double ControlNumericBehavior::parameterToValue(double dParam) { @@ -20,13 +20,14 @@ double ControlNumericBehavior::parameterToValue(double dParam) { } double ControlNumericBehavior::valueToMidiParameter(double dValue) { - return dValue; + double dParam = valueToParameter(dValue); + return dParam * 127.0; } -void ControlNumericBehavior::setValueFromMidiParameter(MidiOpCode o, double dParam, - ControlDoublePrivate* pControl) { +void ControlNumericBehavior::setValueFromMidi( + MidiOpCode o, double dParam, ControlDoublePrivate* pControl) { Q_UNUSED(o); - double dNorm = midiValueToParameter(dParam); + double dNorm = midiToParameter(dParam); pControl->set(parameterToValue(dNorm), NULL); } @@ -38,9 +39,6 @@ ControlPotmeterBehavior::ControlPotmeterBehavior(double dMinValue, double dMaxVa m_bAllowOutOfBounds(allowOutOfBounds) { } -ControlPotmeterBehavior::~ControlPotmeterBehavior() { -} - bool ControlPotmeterBehavior::setFilter(double* dValue) { if (!m_bAllowOutOfBounds) { if (*dValue > m_dMaxValue) { @@ -64,7 +62,7 @@ double ControlPotmeterBehavior::valueToParameter(double dValue) { return (dValue - m_dMinValue) / m_dValueRange; } -double ControlPotmeterBehavior::midiValueToParameter(double midiValue) { +double ControlPotmeterBehavior::midiToParameter(double midiValue) { double parameter; if (midiValue > 64) { parameter = (midiValue - 1) / 126.0; @@ -98,7 +96,8 @@ double ControlPotmeterBehavior::valueToMidiParameter(double dValue) { #define middlePosition ((maxPosition - minPosition) / 2.0) #define positionrange (maxPosition - minPosition) -ControlLogPotmeterBehavior::ControlLogPotmeterBehavior(double dMinValue, double dMaxValue, double minDB) +ControlLogPotmeterBehavior::ControlLogPotmeterBehavior(double dMinValue, + double dMaxValue, double minDB) : ControlPotmeterBehavior(dMinValue, dMaxValue, false) { if (minDB >= 0) { qWarning() << "ControlLogPotmeterBehavior::ControlLogPotmeterBehavior() minDB must be negative"; @@ -109,9 +108,6 @@ ControlLogPotmeterBehavior::ControlLogPotmeterBehavior(double dMinValue, double m_minOffset = db2ratio(m_minDB); } -ControlLogPotmeterBehavior::~ControlLogPotmeterBehavior() { -} - double ControlLogPotmeterBehavior::valueToParameter(double dValue) { if (m_dValueRange == 0.0) { return 0; @@ -132,12 +128,35 @@ double ControlLogPotmeterBehavior::parameterToValue(double dParam) { return m_dMinValue + (linPrameter * m_dValueRange); } -ControlLinPotmeterBehavior::ControlLinPotmeterBehavior(double dMinValue, double dMaxValue, - bool allowOutOfBounds) +ControlLogInvPotmeterBehavior::ControlLogInvPotmeterBehavior( + double dMinValue, double dMaxValue, double minDB) + : ControlLogPotmeterBehavior(dMinValue, dMaxValue, minDB) { +} + +double ControlLogInvPotmeterBehavior::valueToParameter(double dValue) { + return 1 - ControlLogPotmeterBehavior::valueToParameter(dValue); +} + +double ControlLogInvPotmeterBehavior::parameterToValue(double dParam) { + return ControlLogPotmeterBehavior::parameterToValue(1 - dParam); +} + +ControlLinPotmeterBehavior::ControlLinPotmeterBehavior( + double dMinValue, double dMaxValue, bool allowOutOfBounds) + : ControlPotmeterBehavior(dMinValue, dMaxValue, allowOutOfBounds) { +} + +ControlLinInvPotmeterBehavior::ControlLinInvPotmeterBehavior( + double dMinValue, double dMaxValue, bool allowOutOfBounds) : ControlPotmeterBehavior(dMinValue, dMaxValue, allowOutOfBounds) { } -ControlLinPotmeterBehavior::~ControlLinPotmeterBehavior() { +double ControlLinInvPotmeterBehavior::valueToParameter(double dValue) { + return 1 - ControlPotmeterBehavior::valueToParameter(dValue); +} + +double ControlLinInvPotmeterBehavior::parameterToValue(double dParam) { + return ControlPotmeterBehavior::parameterToValue(1 - dParam); } ControlAudioTaperPotBehavior::ControlAudioTaperPotBehavior( @@ -151,9 +170,6 @@ ControlAudioTaperPotBehavior::ControlAudioTaperPotBehavior( m_midiCorrection = ceil(m_neutralParameter * 127) - (m_neutralParameter * 127); } -ControlAudioTaperPotBehavior::~ControlAudioTaperPotBehavior() { -} - double ControlAudioTaperPotBehavior::valueToParameter(double dValue) { double dParam = 1.0; if (dValue <= 0.0) { @@ -206,7 +222,7 @@ double ControlAudioTaperPotBehavior::parameterToValue(double dParam) { return dValue; } -double ControlAudioTaperPotBehavior::midiValueToParameter(double midiValue) { +double ControlAudioTaperPotBehavior::midiToParameter(double midiValue) { double dParam; if (m_neutralParameter && m_neutralParameter != 1.0) { double neutralTest = (midiValue - m_midiCorrection) / 127.0; @@ -241,14 +257,13 @@ double ControlAudioTaperPotBehavior::valueToMidiParameter(double dValue) { return dMidiParam; } -void ControlAudioTaperPotBehavior::setValueFromMidiParameter(MidiOpCode o, double dMidiParam, - ControlDoublePrivate* pControl) { +void ControlAudioTaperPotBehavior::setValueFromMidi( + MidiOpCode o, double dMidiParam, ControlDoublePrivate* pControl) { Q_UNUSED(o); - double dParam = midiValueToParameter(dMidiParam); + double dParam = midiToParameter(dMidiParam); pControl->set(parameterToValue(dParam), NULL); } - double ControlTTRotaryBehavior::valueToParameter(double dValue) { return (dValue * 200.0 + 64) / 127.0; } @@ -273,7 +288,7 @@ ControlPushButtonBehavior::ControlPushButtonBehavior(ButtonMode buttonMode, m_iNumStates(iNumStates) { } -void ControlPushButtonBehavior::setValueFromMidiParameter( +void ControlPushButtonBehavior::setValueFromMidi( MidiOpCode o, double dParam, ControlDoublePrivate* pControl) { // Calculate pressed State of the midi Button // Some controller like the RMX2 are sending always MIDI_NOTE_ON diff --git a/src/control/controlbehavior.h b/src/control/controlbehavior.h index 7185d62a5085..9686794022f7 100644 --- a/src/control/controlbehavior.h +++ b/src/control/controlbehavior.h @@ -8,33 +8,38 @@ class ControlDoublePrivate; +// A linear 0 .. 1 control without Midi representation class ControlNumericBehavior { public: virtual ~ControlNumericBehavior() { }; - // Returns true if the set should occur. Mutates dValue if the value should - // be changed. + // this may change the dValue in place before it is adopted + // Returns false to reject the new value entirely virtual bool setFilter(double* dValue); + // returns the normalized parameter range 0..1 virtual double valueToParameter(double dValue); - virtual double midiValueToParameter(double midiValue); + // returns the normalized parameter range 0..1 + virtual double midiToParameter(double midiValue); + // returns the scaled user visible value virtual double parameterToValue(double dParam); + // returns the midi range parameter 0..127 virtual double valueToMidiParameter(double dValue); - virtual void setValueFromMidiParameter(MidiOpCode o, double dParam, - ControlDoublePrivate* pControl); + + virtual void setValueFromMidi( + MidiOpCode o, double dParam, ControlDoublePrivate* pControl); }; class ControlPotmeterBehavior : public ControlNumericBehavior { public: ControlPotmeterBehavior(double dMinValue, double dMaxValue, bool allowOutOfBounds); - virtual ~ControlPotmeterBehavior(); - virtual bool setFilter(double* dValue); - virtual double valueToParameter(double dValue); - virtual double midiValueToParameter(double midiValue); - virtual double parameterToValue(double dParam); - virtual double valueToMidiParameter(double dValue); + bool setFilter(double* dValue) override; + double valueToParameter(double dValue) override; + double midiToParameter(double midiValue) override; + double parameterToValue(double dParam) override; + double valueToMidiParameter(double dValue) override; protected: double m_dMinValue; @@ -46,35 +51,49 @@ class ControlPotmeterBehavior : public ControlNumericBehavior { class ControlLogPotmeterBehavior : public ControlPotmeterBehavior { public: ControlLogPotmeterBehavior(double dMinValue, double dMaxValue, double minDB); - virtual ~ControlLogPotmeterBehavior(); - virtual double valueToParameter(double dValue); - virtual double parameterToValue(double dParam); + double valueToParameter(double dValue) override; + double parameterToValue(double dParam) override; protected: double m_minDB; double m_minOffset; }; +class ControlLogInvPotmeterBehavior : public ControlLogPotmeterBehavior { + public: + ControlLogInvPotmeterBehavior(double dMinValue, double dMaxValue, double minDB); + + double valueToParameter(double dValue) override; + double parameterToValue(double dParam) override; +}; + class ControlLinPotmeterBehavior : public ControlPotmeterBehavior { public: - ControlLinPotmeterBehavior(double dMinValue, double dMaxValue, - bool allowOutOfBounds); - virtual ~ControlLinPotmeterBehavior(); + ControlLinPotmeterBehavior( + double dMinValue, double dMaxValue, bool allowOutOfBounds); +}; + +class ControlLinInvPotmeterBehavior : public ControlPotmeterBehavior { + public: + ControlLinInvPotmeterBehavior( + double dMinValue, double dMaxValue, bool allowOutOfBounds); + double valueToParameter(double dValue) override; + double parameterToValue(double dParam) override; }; class ControlAudioTaperPotBehavior : public ControlPotmeterBehavior { public: ControlAudioTaperPotBehavior(double minDB, double maxDB, double neutralParameter); - virtual ~ControlAudioTaperPotBehavior(); - virtual double valueToParameter(double dValue); - virtual double parameterToValue(double dParam); - virtual double midiValueToParameter(double midiValue); - virtual double valueToMidiParameter(double dValue); - virtual void setValueFromMidiParameter(MidiOpCode o, double dParam, - ControlDoublePrivate* pControl); + double valueToParameter(double dValue) override; + double parameterToValue(double dParam) override; + double midiToParameter(double midiValue) override; + double valueToMidiParameter(double dValue) override; + void setValueFromMidi( + MidiOpCode o, double dParam, ControlDoublePrivate* pControl) + override; protected: // a knob position between 0 and 1 where the gain is 1 (0dB) @@ -94,8 +113,8 @@ class ControlAudioTaperPotBehavior : public ControlPotmeterBehavior { class ControlTTRotaryBehavior : public ControlNumericBehavior { public: - virtual double valueToParameter(double dValue); - virtual double parameterToValue(double dParam); + double valueToParameter(double dValue) override; + double parameterToValue(double dParam) override; }; class ControlPushButtonBehavior : public ControlNumericBehavior { @@ -114,8 +133,9 @@ class ControlPushButtonBehavior : public ControlNumericBehavior { }; ControlPushButtonBehavior(ButtonMode buttonMode, int iNumStates); - virtual void setValueFromMidiParameter(MidiOpCode o, double dParam, - ControlDoublePrivate* pControl); + void setValueFromMidi( + MidiOpCode o, double dParam, ControlDoublePrivate* pControl) + override; private: // We create many hundreds of push buttons at Mixxx startup and most of them diff --git a/src/control/controleffectknob.cpp b/src/control/controleffectknob.cpp index b42d1ed83634..169cade841d2 100644 --- a/src/control/controleffectknob.cpp +++ b/src/control/controleffectknob.cpp @@ -16,6 +16,9 @@ void ControlEffectKnob::setBehaviour(EffectManifestParameter::ControlHint type, if (type == EffectManifestParameter::ControlHint::KNOB_LINEAR) { m_pControl->setBehavior(new ControlLinPotmeterBehavior( dMinValue, dMaxValue, false)); + } else if (type == EffectManifestParameter::ControlHint::KNOB_LINEAR_INVERSE) { + m_pControl->setBehavior(new ControlLinInvPotmeterBehavior( + dMinValue, dMaxValue, false)); } else if (type == EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC) { if (dMinValue == 0) { if (dMaxValue == 1.0) { @@ -34,5 +37,8 @@ void ControlEffectKnob::setBehaviour(EffectManifestParameter::ControlHint type, m_pControl->setBehavior( new ControlLogPotmeterBehavior(dMinValue, dMaxValue, -40)); } + } else if (type == EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC_INVERSE) { + m_pControl->setBehavior( + new ControlLogInvPotmeterBehavior(dMinValue, dMaxValue, -40)); } } diff --git a/src/control/controlobject.cpp b/src/control/controlobject.cpp index a4a29f5cddca..31c8c2b51022 100644 --- a/src/control/controlobject.cpp +++ b/src/control/controlobject.cpp @@ -80,7 +80,7 @@ ControlObject* ControlObject::getControl(const ConfigKey& key, bool warn) { void ControlObject::setValueFromMidi(MidiOpCode o, double v) { if (m_pControl) { - m_pControl->setMidiParameter(o, v); + m_pControl->setValueFromMidi(o, v); } } @@ -102,8 +102,8 @@ double ControlObject::getParameterForValue(double value) const { return m_pControl ? m_pControl->getParameterForValue(value) : 0.0; } -double ControlObject::getParameterForMidiValue(double midiValue) const { - return m_pControl ? m_pControl->getParameterForMidiValue(midiValue) : 0.0; +double ControlObject::getParameterForMidi(double midiParameter) const { + return m_pControl ? m_pControl->getParameterForMidi(midiParameter) : 0.0; } void ControlObject::setParameter(double v) { diff --git a/src/control/controlobject.h b/src/control/controlobject.h index f5e6396fc173..96de336d8018 100644 --- a/src/control/controlobject.h +++ b/src/control/controlobject.h @@ -136,7 +136,7 @@ class ControlObject : public QObject { virtual double getParameterForValue(double value) const; // Returns the parameterized value of the object. Thread safe, non-blocking. - virtual double getParameterForMidiValue(double midiValue) const; + virtual double getParameterForMidi(double midiValue) const; // Sets the control parameterized value to v. Thread safe, non-blocking. virtual void setParameter(double v); diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp index c2848d302023..8ebf383c6119 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -348,28 +348,29 @@ void MidiController::processInputMapping(const MidiInputMapping& mapping, if (mapping.options.soft_takeover) { // This is the only place to enable it if it isn't already. m_st.enable(pCO); - if (m_st.ignore(pCO, pCO->getParameterForMidiValue(newValue))) { + if (m_st.ignore(pCO, pCO->getParameterForMidi(newValue))) { return; } } pCO->setValueFromMidi(static_cast(opCode), newValue); } -double MidiController::computeValue(MidiOptions options, double _prevmidivalue, double _newmidivalue) { +double MidiController::computeValue( + MidiOptions options, double prevmidivalue, double newmidivalue) { double tempval = 0.; double diff = 0.; if (options.all == 0) { - return _newmidivalue; + return newmidivalue; } if (options.invert) { - return 127. - _newmidivalue; + return 127. - newmidivalue; } if (options.rot64 || options.rot64_inv) { - tempval = _prevmidivalue; - diff = _newmidivalue - 64.; + tempval = prevmidivalue; + diff = newmidivalue - 64.; if (diff == -1 || diff == 1) diff /= 16; else @@ -382,8 +383,8 @@ double MidiController::computeValue(MidiOptions options, double _prevmidivalue, } if (options.rot64_fast) { - tempval = _prevmidivalue; - diff = _newmidivalue - 64.; + tempval = prevmidivalue; + diff = newmidivalue - 64.; diff *= 1.5; tempval += diff; return (tempval < 0. ? 0. : (tempval > 127. ? 127.0 : tempval)); @@ -391,19 +392,19 @@ double MidiController::computeValue(MidiOptions options, double _prevmidivalue, if (options.diff) { //Interpret 7-bit signed value using two's compliment. - if (_newmidivalue >= 64.) - _newmidivalue = _newmidivalue - 128.; + if (newmidivalue >= 64.) + newmidivalue = newmidivalue - 128.; //Apply sensitivity to signed value. FIXME // if(sensitivity > 0) // _newmidivalue = _newmidivalue * ((double)sensitivity / 50.); //Apply new value to current value. - _newmidivalue = _prevmidivalue + _newmidivalue; + newmidivalue = prevmidivalue + newmidivalue; } if (options.selectknob) { //Interpret 7-bit signed value using two's compliment. - if (_newmidivalue >= 64.) - _newmidivalue = _newmidivalue - 128.; + if (newmidivalue >= 64.) + newmidivalue = newmidivalue - 128.; //Apply sensitivity to signed value. FIXME //if(sensitivity > 0) // _newmidivalue = _newmidivalue * ((double)sensitivity / 50.); @@ -411,11 +412,11 @@ double MidiController::computeValue(MidiOptions options, double _prevmidivalue, } if (options.button) { - _newmidivalue = _newmidivalue != 0; + newmidivalue = newmidivalue != 0; } if (options.sw) { - _newmidivalue = 1; + newmidivalue = 1; } if (options.spread64) { @@ -424,32 +425,32 @@ double MidiController::computeValue(MidiOptions options, double _prevmidivalue, // Uses a similar non-linear scaling formula as ControlTTRotary::getValueFromWidget() // but with added sensitivity adjustment. This formula is still experimental. - _newmidivalue = _newmidivalue - 64.; + newmidivalue = newmidivalue - 64.; //FIXME //double distance = _newmidivalue - 64.; // _newmidivalue = distance * distance * sensitivity / 50000.; //if (distance < 0.) - // _newmidivalue = -_newmidivalue; + // _newmidivalue = -newmidivalue; - //qDebug() << "Spread64: in " << distance << " out " << _newmidivalue; + //qDebug() << "Spread64: in " << distance << " out " << newmidivalue; } if (options.herc_jog) { - if (_newmidivalue > 64.) { - _newmidivalue -= 128.; + if (newmidivalue > 64.) { + newmidivalue -= 128.; } - _newmidivalue += _prevmidivalue; - //if (_prevmidivalue != 0.0) { qDebug() << "AAAAAAAAAAAA" << _prevmidivalue; } + newmidivalue += prevmidivalue; + //if (_prevmidivalue != 0.0) { qDebug() << "AAAAAAAAAAAA" << prevmidivalue; } } if (options.herc_jog_fast) { - if (_newmidivalue > 64.) { - _newmidivalue -= 128.; + if (newmidivalue > 64.) { + newmidivalue -= 128.; } - _newmidivalue = _prevmidivalue + (_newmidivalue * 3); + newmidivalue = prevmidivalue + (newmidivalue * 3); } - return _newmidivalue; + return newmidivalue; } void MidiController::receive(QByteArray data, mixxx::Duration timestamp) { diff --git a/src/effects/effectmanifestparameter.h b/src/effects/effectmanifestparameter.h index 623b6b2ee12a..0e059a52bbf6 100644 --- a/src/effects/effectmanifestparameter.h +++ b/src/effects/effectmanifestparameter.h @@ -10,7 +10,9 @@ class EffectManifestParameter { enum class ControlHint { UNKNOWN = 0, KNOB_LINEAR, + KNOB_LINEAR_INVERSE, KNOB_LOGARITHMIC, + KNOB_LOGARITHMIC_INVERSE, KNOB_STEPPING, // A step rotary, steps given by m_steps // are arranged with equal distance on scale TOGGLE_STEPPING // For button and enum controls, not accessible diff --git a/src/effects/native/balanceeffect.h b/src/effects/native/balanceeffect.h index 33453d7b841e..925ffbcad92c 100644 --- a/src/effects/native/balanceeffect.h +++ b/src/effects/native/balanceeffect.h @@ -1,4 +1,4 @@ -#ifndef PANEFFECT_H +#ifndef BALANCEEFFECT_H #define BALANCEEFFECT_H #include "effects/effectprocessor.h" diff --git a/src/effects/native/flangereffect.cpp b/src/effects/native/flangereffect.cpp index 11d19eaace1f..82ba0399f6b1 100644 --- a/src/effects/native/flangereffect.cpp +++ b/src/effects/native/flangereffect.cpp @@ -4,9 +4,16 @@ #include "util/math.h" -const unsigned int kMaxDelay = 5000; -const unsigned int kLfoAmplitude = 240; -const unsigned int kAverageDelayLength = 250; +namespace{ + +// Gain correction was verified with replay gain and default parameters +const double kGainCorrection = 1.4125375446227544; // 3 dB + +inline CSAMPLE tanh_approx(CSAMPLE input) { + // return tanhf(input); // 142ns for process; + return input / (1 + input * input / (3 + input * input / 5)); // 119ns for process +} +} // static QString FlangerEffect::getId() { @@ -24,30 +31,64 @@ EffectManifest FlangerEffect::getManifest() { "A simple modulation effect, created by taking the input signal " "and mixing it with a delayed, pitch modulated copy of itself.")); - EffectManifestParameter* period = manifest.addParameter(); - period->setId("period"); - period->setName(QObject::tr("Period")); - period->setDescription(QObject::tr("Controls the period of the LFO (low frequency oscillator)\n" - "1/4 - 4 beats rounded to 1/2 beat if tempo is detected (decks and samplers) \n" - "0.05 - 4 seconds if no tempo is detected (mic & aux inputs, master mix)")); - period->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR); - period->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); - period->setUnitsHint(EffectManifestParameter::UnitsHint::BEATS); - period->setMinimum(0.00); - period->setMaximum(4.00); - period->setDefault(1.00); - - EffectManifestParameter* depth = manifest.addParameter(); - depth->setId("depth"); - depth->setName(QObject::tr("Depth")); - depth->setDescription(QObject::tr("Controls the intensity of the effect.")); - depth->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR); - depth->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); - depth->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); - depth->setDefaultLinkType(EffectManifestParameter::LinkType::LINKED); - depth->setDefault(1.0); - depth->setMinimum(0.0); - depth->setMaximum(1.0); + EffectManifestParameter* speed = manifest.addParameter(); + speed->setId("speed"); + speed->setName(QObject::tr("Speed")); + speed->setDescription(QObject::tr("Controls the speed of the LFO (low frequency oscillator)\n" + "32 - 1/4 beats rounded to 1/2 beat per lfo cycle if tempo is detected (decks and samplers) \n" + "1/32 - 4 Hz if no tempo is detected (mic & aux inputs, master mix)")); + speed->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC_INVERSE); + speed->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + speed->setMinimum(kMinLfoBeats); + speed->setMaximum(kMaxLfoBeats); + speed->setDefault(8); + + EffectManifestParameter* width = manifest.addParameter(); + width->setId("width"); + width->setName(QObject::tr("Width")); + width->setDescription(QObject::tr("Controls the delay amplitude of the LFO (low frequency oscillator).")); + width->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + width->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + width->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); + width->setDefault(kMaxLfoWidthMs / 2); + width->setMinimum(0.0); + width->setMaximum(kMaxLfoWidthMs); + + EffectManifestParameter* manual = manifest.addParameter(); + manual->setId("manual"); + manual->setName(QObject::tr("Manual")); + manual->setDescription(QObject::tr("Controls the delay offset of the LFO (low frequency oscillator).\n" + "With width at zero, it allows to manual sweep over the entire delay range.")); + manual->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC); + manual->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + manual->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); + manual->setDefault(kCenterDelayMs); + manual->setMinimum(kMinDelayMs); + manual->setMaximum(kMaxDelayMs); + + EffectManifestParameter* regen = manifest.addParameter(); + regen->setId("regen"); + regen->setName(QObject::tr("Regeneration")); + regen->setShortName(QObject::tr("Regen")); + regen->setDescription(QObject::tr("Controls how much of the delay output is feed back into the input.")); + regen->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR); + regen->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + regen->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); + regen->setDefault(0.25); + regen->setMinimum(0.0); + regen->setMaximum(1.0); + + EffectManifestParameter* mix = manifest.addParameter(); + mix->setId("mix"); + mix->setName(QObject::tr("Mix")); + mix->setDescription(QObject::tr("Controls the intensity of the effect.")); + mix->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR); + mix->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN); + mix->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN); + mix->setDefaultLinkType(EffectManifestParameter::LinkType::LINKED); + mix->setDefault(1.0); + mix->setMinimum(0.0); + mix->setMaximum(1.0); EffectManifestParameter* triplet = manifest.addParameter(); triplet->setId("triplet"); @@ -65,8 +106,11 @@ EffectManifest FlangerEffect::getManifest() { FlangerEffect::FlangerEffect(EngineEffect* pEffect, const EffectManifest& manifest) - : m_pPeriodParameter(pEffect->getParameterById("period")), - m_pDepthParameter(pEffect->getParameterById("depth")), + : m_pSpeedParameter(pEffect->getParameterById("speed")), + m_pWidthParameter(pEffect->getParameterById("width")), + m_pManualParameter(pEffect->getParameterById("manual")), + m_pRegenParameter(pEffect->getParameterById("regen")), + m_pMixParameter(pEffect->getParameterById("mix")), m_pTripletParameter(pEffect->getParameterById("triplet")) { Q_UNUSED(manifest); } @@ -87,61 +131,106 @@ void FlangerEffect::processChannel(const ChannelHandle& handle, // TODO: remove assumption of stereo signal const int kChannels = 2; - // The parameter minimum is zero so the exact center of the knob is 2 beats. - double lfoPeriodParameter = m_pPeriodParameter->value(); - double lfoPeriodSamples; + double lfoPeriodParameter = m_pSpeedParameter->value(); + double lfoPeriodFrames; if (groupFeatures.has_beat_length_sec) { // lfoPeriodParameter is a number of beats - lfoPeriodParameter = std::max(roundToFraction(lfoPeriodParameter, 2.0), 1/4.0); + lfoPeriodParameter = std::max(roundToFraction(lfoPeriodParameter, 2.0), kMinLfoBeats); if (m_pTripletParameter->toBool()) { lfoPeriodParameter /= 3.0; } - lfoPeriodSamples = lfoPeriodParameter * groupFeatures.beat_length_sec * sampleRate; + lfoPeriodFrames = lfoPeriodParameter * groupFeatures.beat_length_sec * sampleRate; } else { // lfoPeriodParameter is a number of seconds - lfoPeriodSamples = std::max(lfoPeriodParameter, 1/4.0) * sampleRate; + lfoPeriodFrames = std::max(lfoPeriodParameter, kMinLfoBeats) * sampleRate; } + + // When the period is changed, the position of the sound shouldn't + // so time need to be recalculated + if (pState->previousPeriodFrames != -1.0) { + pState->lfoFrames *= lfoPeriodFrames / pState->previousPeriodFrames; + } + pState->previousPeriodFrames = lfoPeriodFrames; + + // lfoPeriodSamples is used to calculate the delay for each channel // independently in the loop below, so do not multiply lfoPeriodSamples by // the number of channels. - CSAMPLE lfoDepth = m_pDepthParameter->value(); + CSAMPLE_GAIN mix = m_pMixParameter->value(); + RampingValue mixRamped( + pState->prev_mix, mix, numSamples / kChannels); + pState->prev_mix = mix; + + CSAMPLE_GAIN regen = m_pRegenParameter->value(); + RampingValue regenRamped( + pState->prev_regen, regen, numSamples / kChannels); + pState->prev_regen = regen; + + // With and Manual is limited by amount of amplitude that remains from width + // to kMaxDelayMs + double width = m_pWidthParameter->value(); + double manual = m_pManualParameter->value(); + double maxManual = kCenterDelayMs + (kMaxLfoWidthMs - width) / 2; + double minManual = kCenterDelayMs - (kMaxLfoWidthMs - width) / 2; + manual = math_clamp(manual, minManual, maxManual); + + RampingValue widthRamped( + pState->prev_width, width, numSamples / kChannels); + pState->prev_width = width; + + RampingValue manualRamped( + pState->prev_manual, manual, numSamples / kChannels); + pState->prev_manual = manual; CSAMPLE* delayLeft = pState->delayLeft; CSAMPLE* delayRight = pState->delayRight; - for (unsigned int i = 0; i < numSamples; i += kChannels) { - delayLeft[pState->delayPos] = pInput[i]; - delayRight[pState->delayPos] = pInput[i+1]; + for (unsigned int i = 0; i < numSamples; i += kChannels) { - pState->delayPos = (pState->delayPos + 1) % kMaxDelay; + CSAMPLE_GAIN mix_ramped = mixRamped.getNext(); + CSAMPLE_GAIN regen_ramped = regenRamped.getNext(); + double width_ramped = widthRamped.getNext(); + double manual_ramped = manualRamped.getNext(); - pState->time++; - if (pState->time > lfoPeriodSamples) { - pState->time = 0; + pState->lfoFrames++; + if (pState->lfoFrames >= lfoPeriodFrames) { + pState->lfoFrames = 0; } - CSAMPLE periodFraction = CSAMPLE(pState->time) / lfoPeriodSamples; - CSAMPLE delay = kAverageDelayLength + kLfoAmplitude * sin(M_PI * 2.0f * periodFraction); + float periodFraction = static_cast(pState->lfoFrames) / lfoPeriodFrames; + double delayMs = manual_ramped + width_ramped / 2 * sin(M_PI * 2.0f * periodFraction); + double delayFrames = delayMs * sampleRate / 1000; - int framePrev = (pState->delayPos - int(delay) + kMaxDelay - 1) % kMaxDelay; - int frameNext = (pState->delayPos - int(delay) + kMaxDelay ) % kMaxDelay; + SINT framePrev = (pState->delayPos - static_cast(floor(delayFrames)) + + kBufferLenth) % kBufferLenth; + SINT frameNext = (pState->delayPos - static_cast(ceil(delayFrames)) + + kBufferLenth) % kBufferLenth; CSAMPLE prevLeft = delayLeft[framePrev]; CSAMPLE nextLeft = delayLeft[frameNext]; CSAMPLE prevRight = delayRight[framePrev]; CSAMPLE nextRight = delayRight[frameNext]; - CSAMPLE frac = delay - floorf(delay); + CSAMPLE frac = delayFrames - floorf(delayFrames); CSAMPLE delayedSampleLeft = prevLeft + frac * (nextLeft - prevLeft); CSAMPLE delayedSampleRight = prevRight + frac * (nextRight - prevRight); - pOutput[i] = pInput[i] + lfoDepth * delayedSampleLeft; - pOutput[i+1] = pInput[i+1] + lfoDepth * delayedSampleRight; + delayLeft[pState->delayPos] = tanh_approx(pInput[i] + regen_ramped * delayedSampleLeft); + delayRight[pState->delayPos] = tanh_approx(pInput[i + 1] + regen_ramped * delayedSampleRight); + + pState->delayPos = (pState->delayPos + 1) % kBufferLenth; + + double gain = (1 - mix_ramped + kGainCorrection * mix_ramped); + pOutput[i] = (pInput[i] + mix_ramped * delayedSampleLeft) / gain; + pOutput[i + 1] = (pInput[i + 1] + mix_ramped * delayedSampleRight) / gain; } if (enableState == EffectProcessor::DISABLING) { - SampleUtil::clear(delayLeft, numSamples); - SampleUtil::clear(delayRight, numSamples); + SampleUtil::clear(delayLeft, kBufferLenth); + SampleUtil::clear(delayRight, kBufferLenth); + pState->previousPeriodFrames = -1; + pState->prev_regen = 0; + pState->prev_mix = 0; } } diff --git a/src/effects/native/flangereffect.h b/src/effects/native/flangereffect.h index f9c626f8cb1a..642a5bc62c11 100644 --- a/src/effects/native/flangereffect.h +++ b/src/effects/native/flangereffect.h @@ -10,18 +10,40 @@ #include "util/defs.h" #include "util/sample.h" #include "util/types.h" +#include "util/rampingvalue.h" + +namespace { +constexpr double kMaxDelayMs = 13.0; +constexpr double kMinDelayMs = 0.22; +constexpr double kCenterDelayMs = (kMaxDelayMs - kMinDelayMs) / 2 + kMinDelayMs; +constexpr double kMaxLfoWidthMs = kMaxDelayMs - kMinDelayMs; +// using + 1.0 instead of ceil() for Mac OS +constexpr SINT kBufferLenth = static_cast(kMaxDelayMs + 1.0) * 96; // for 96 kHz +constexpr double kMinLfoBeats = 1/4.0; +constexpr double kMaxLfoBeats = 32.0; +} // anonymous namespace struct FlangerGroupState { FlangerGroupState() : delayPos(0), - time(0) { - SampleUtil::applyGain(delayLeft, 0, MAX_BUFFER_LEN); - SampleUtil::applyGain(delayRight, 0, MAX_BUFFER_LEN); + lfoFrames(0), + previousPeriodFrames(-1), + prev_regen(0), + prev_mix(0), + prev_width(0), + prev_manual(kCenterDelayMs) { + SampleUtil::clear(delayLeft, kBufferLenth); + SampleUtil::clear(delayRight, kBufferLenth); } - CSAMPLE delayRight[MAX_BUFFER_LEN]; - CSAMPLE delayLeft[MAX_BUFFER_LEN]; + CSAMPLE delayLeft[kBufferLenth]; + CSAMPLE delayRight[kBufferLenth]; unsigned int delayPos; - unsigned int time; + unsigned int lfoFrames; + double previousPeriodFrames; + CSAMPLE_GAIN prev_regen; + CSAMPLE_GAIN prev_mix; + CSAMPLE_GAIN prev_width; + CSAMPLE_GAIN prev_manual; }; class FlangerEffect : public PerChannelEffectProcessor { @@ -46,8 +68,11 @@ class FlangerEffect : public PerChannelEffectProcessor { return getId(); } - EngineEffectParameter* m_pPeriodParameter; - EngineEffectParameter* m_pDepthParameter; + EngineEffectParameter* m_pSpeedParameter; + EngineEffectParameter* m_pWidthParameter; + EngineEffectParameter* m_pManualParameter; + EngineEffectParameter* m_pRegenParameter; + EngineEffectParameter* m_pMixParameter; EngineEffectParameter* m_pTripletParameter; DISALLOW_COPY_AND_ASSIGN(FlangerEffect); diff --git a/src/test/audiotaperpot_test.cpp b/src/test/audiotaperpot_test.cpp index cd312f320ded..3b3e611a687f 100644 --- a/src/test/audiotaperpot_test.cpp +++ b/src/test/audiotaperpot_test.cpp @@ -25,10 +25,10 @@ TEST_F(AudioTaperPotTest, ScaleTest) { double neutralMidi = catpb.valueToMidiParameter(1); ASSERT_EQ(0.0, fmod(neutralMidi, 1)); // Midi value 64 should result in 0,5 - ASSERT_EQ(neutralParameter, catpb.midiValueToParameter(neutralMidi)); + ASSERT_EQ(neutralParameter, catpb.midiToParameter(neutralMidi)); // roundtrip check - ASSERT_DOUBLE_EQ(0.25, catpb.parameterToValue(catpb.midiValueToParameter(catpb.valueToMidiParameter(0.25)))); - ASSERT_DOUBLE_EQ(0.75, catpb.parameterToValue(catpb.midiValueToParameter(catpb.valueToMidiParameter(0.75)))); + ASSERT_DOUBLE_EQ(0.25, catpb.parameterToValue(catpb.midiToParameter(catpb.valueToMidiParameter(0.25)))); + ASSERT_DOUBLE_EQ(0.75, catpb.parameterToValue(catpb.midiToParameter(catpb.valueToMidiParameter(0.75)))); } { @@ -46,10 +46,10 @@ TEST_F(AudioTaperPotTest, ScaleTest) { double neutralMidi = catpb.valueToMidiParameter(1); ASSERT_EQ(0.0, fmod(neutralMidi, 1)); // Midi value 64 should result in 0,5 - ASSERT_EQ(neutralParameter, catpb.midiValueToParameter(neutralMidi)); + ASSERT_EQ(neutralParameter, catpb.midiToParameter(neutralMidi)); // roundtrip check - ASSERT_DOUBLE_EQ(0.25, catpb.parameterToValue(catpb.midiValueToParameter(catpb.valueToMidiParameter(0.25)))); - ASSERT_DOUBLE_EQ(0.75, catpb.parameterToValue(catpb.midiValueToParameter(catpb.valueToMidiParameter(0.75)))); + ASSERT_DOUBLE_EQ(0.25, catpb.parameterToValue(catpb.midiToParameter(catpb.valueToMidiParameter(0.25)))); + ASSERT_DOUBLE_EQ(0.75, catpb.parameterToValue(catpb.midiToParameter(catpb.valueToMidiParameter(0.75)))); } { @@ -67,9 +67,9 @@ TEST_F(AudioTaperPotTest, ScaleTest) { double neutralMidi = catpb.valueToMidiParameter(1); ASSERT_EQ(0.0, fmod(neutralMidi, 1)); // Midi value 64 should result in 0,5 - ASSERT_EQ(neutralParameter, catpb.midiValueToParameter(neutralMidi)); + ASSERT_EQ(neutralParameter, catpb.midiToParameter(neutralMidi)); // roundtrip checkx - ASSERT_DOUBLE_EQ(0.25, catpb.parameterToValue(catpb.midiValueToParameter(catpb.valueToMidiParameter(0.25)))); - ASSERT_DOUBLE_EQ(0.75, catpb.parameterToValue(catpb.midiValueToParameter(catpb.valueToMidiParameter(0.75)))); + ASSERT_DOUBLE_EQ(0.25, catpb.parameterToValue(catpb.midiToParameter(catpb.valueToMidiParameter(0.25)))); + ASSERT_DOUBLE_EQ(0.75, catpb.parameterToValue(catpb.midiToParameter(catpb.valueToMidiParameter(0.75)))); } } diff --git a/src/util/rampingvalue.h b/src/util/rampingvalue.h new file mode 100644 index 000000000000..51fa17f31437 --- /dev/null +++ b/src/util/rampingvalue.h @@ -0,0 +1,22 @@ +#ifndef MIXXX_UTIL_RAMPINGVALUE_H +#define MIXXX_UTIL_RAMPINGVALUE_H + + +template +class RampingValue { + public: + RampingValue(const T& initial, const T& final, int steps) { + m_value = initial; + m_increment = (final - initial) / steps; + } + + T getNext() { + return m_value += m_increment; + } + + private: + T m_value; + T m_increment; +}; + +#endif // MIXXX_UTIL_RAMPINGVALUE_H