diff --git a/build/depends.py b/build/depends.py index 445ea78b5e5..9fb33d6fa91 100644 --- a/build/depends.py +++ b/build/depends.py @@ -560,6 +560,35 @@ def sources(self, build): "dlghidden.cpp", "dlgmissing.cpp", + "effects/effectmanifest.cpp", + "effects/effectmanifestparameter.cpp", + + "effects/effectchain.cpp", + "effects/effect.cpp", + "effects/effectparameter.cpp", + + "effects/effectrack.cpp", + "effects/effectchainslot.cpp", + "effects/effectslot.cpp", + "effects/effectparameterslot.cpp", + + "effects/effectsmanager.cpp", + "effects/effectchainmanager.cpp", + "effects/effectsbackend.cpp", + + "effects/native/nativebackend.cpp", + "effects/native/bitcrushereffect.cpp", + "effects/native/flangereffect.cpp", + "effects/native/filtereffect.cpp", + "effects/native/reverbeffect.cpp", + "effects/native/echoeffect.cpp", + "effects/native/reverb/Reverb.cc", + + "engine/effects/engineeffectsmanager.cpp", + "engine/effects/engineeffectrack.cpp", + "engine/effects/engineeffectchain.cpp", + "engine/effects/engineeffect.cpp", + "engine/sync/basesyncablelistener.cpp", "engine/sync/enginesync.cpp", "engine/sync/synccontrol.cpp", @@ -580,8 +609,6 @@ def sources(self, build): "engine/enginechannel.cpp", "engine/enginemaster.cpp", "engine/enginedelay.cpp", - "engine/engineflanger.cpp", - "engine/enginefiltereffect.cpp", "engine/enginevumeter.cpp", "engine/enginevinylsoundemu.cpp", "engine/enginesidechaincompressor.cpp", @@ -664,6 +691,9 @@ def sources(self, build): "widget/wimagestore.cpp", "widget/hexspinbox.cpp", "widget/wtrackproperty.cpp", + "widget/weffectchain.cpp", + "widget/weffect.cpp", + "widget/weffectparameter.cpp", "widget/wtime.cpp", "widget/wkey.cpp", "widget/wcombobox.cpp", diff --git a/src/basetrackplayer.cpp b/src/basetrackplayer.cpp index cf0ae7a599c..20feb4225a6 100644 --- a/src/basetrackplayer.cpp +++ b/src/basetrackplayer.cpp @@ -15,10 +15,12 @@ #include "waveform/renderers/waveformwidgetrenderer.h" #include "analyserqueue.h" #include "util/sandbox.h" +#include "effects/effectsmanager.h" BaseTrackPlayer::BaseTrackPlayer(QObject* pParent, ConfigObject* pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group, bool defaultMaster, @@ -26,14 +28,13 @@ BaseTrackPlayer::BaseTrackPlayer(QObject* pParent, : BasePlayer(pParent, group), m_pConfig(pConfig), m_pLoadedTrack() { - // Need to strdup the string because EngineChannel will save the pointer, // but we might get deleted before the EngineChannel. TODO(XXX) // pSafeGroupName is leaked. It's like 5 bytes so whatever. const char* pSafeGroupName = strdup(getGroup().toAscii().constData()); - m_pChannel = new EngineDeck(pSafeGroupName, - pConfig, pMixingEngine, defaultOrientation); + m_pChannel = new EngineDeck(pSafeGroupName, pConfig, pMixingEngine, + pEffectsManager, defaultOrientation); EngineBuffer* pEngineBuffer = m_pChannel->getEngineBuffer(); pMixingEngine->addChannel(m_pChannel); diff --git a/src/basetrackplayer.h b/src/basetrackplayer.h index 42cbb206144..30576560e19 100644 --- a/src/basetrackplayer.h +++ b/src/basetrackplayer.h @@ -12,6 +12,7 @@ class ControlObject; class ControlPotmeter; class ControlObjectThread; class AnalyserQueue; +class EffectsManager; class BaseTrackPlayer : public BasePlayer { Q_OBJECT @@ -19,6 +20,7 @@ class BaseTrackPlayer : public BasePlayer { BaseTrackPlayer(QObject* pParent, ConfigObject* pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group, bool defaultMaster, diff --git a/src/controlvaluedelegate.h b/src/controlvaluedelegate.h index 3f1806df43c..ca6caab804f 100644 --- a/src/controlvaluedelegate.h +++ b/src/controlvaluedelegate.h @@ -41,6 +41,7 @@ class ControlValueDelegate : public QItemDelegate static QStringList getPlaylistControlValues() { return m_playlistControlValues; }; static QStringList getFlangerControlValues() { return m_flangerControlValues; }; static QStringList getMicrophoneControlValues() { return m_microphoneControlValues; }; + private: static QStringList m_channelControlValues; static QStringList m_masterControlValues; diff --git a/src/deck.cpp b/src/deck.cpp index 4815827683e..ffdff906d2d 100644 --- a/src/deck.cpp +++ b/src/deck.cpp @@ -3,12 +3,12 @@ Deck::Deck(QObject* pParent, ConfigObject* pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group) : - BaseTrackPlayer(pParent, pConfig, pMixingEngine, defaultOrientation, - group, true, false) { + BaseTrackPlayer(pParent, pConfig, pMixingEngine, pEffectsManager, + defaultOrientation, group, true, false) { } Deck::~Deck() { } - diff --git a/src/deck.h b/src/deck.h index f60d74ad8e4..c21031ce107 100644 --- a/src/deck.h +++ b/src/deck.h @@ -11,6 +11,7 @@ class Deck : public BaseTrackPlayer { Deck(QObject* pParent, ConfigObject* pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group); virtual ~Deck(); diff --git a/src/defs.h b/src/defs.h index 4c4ef2ccb9c..d41a414a58d 100644 --- a/src/defs.h +++ b/src/defs.h @@ -71,5 +71,8 @@ enum { #define math_min(a,b) (((a) < (b)) ? (a) : (b)) #endif +#ifndef math_clamp +#define math_clamp(v, min, max) (((min) < (max)) ? (math_min((max), math_max((v), (min)))) : (math_min((min), math_max((v), (max))))) #endif +#endif diff --git a/src/effects/effect.cpp b/src/effects/effect.cpp new file mode 100644 index 00000000000..0a7570a9b4b --- /dev/null +++ b/src/effects/effect.cpp @@ -0,0 +1,165 @@ +#include + +#include "effects/effect.h" +#include "effects/effectprocessor.h" +#include "effects/effectsmanager.h" +#include "engine/effects/engineeffectchain.h" +#include "engine/effects/engineeffect.h" +#include "xmlparse.h" + +Effect::Effect(QObject* pParent, EffectsManager* pEffectsManager, + const EffectManifest& manifest, + EffectInstantiatorPointer pInstantiator) + : QObject(pParent), + m_pEffectsManager(pEffectsManager), + m_manifest(manifest), + m_pEngineEffect(new EngineEffect(manifest, + pEffectsManager->registeredGroups(), + pInstantiator)), + m_bAddedToEngine(false), + m_bEnabled(true) { + foreach (const EffectManifestParameter& parameter, m_manifest.parameters()) { + EffectParameter* pParameter = new EffectParameter( + this, pEffectsManager, m_parameters.size(), parameter); + m_parameters.append(pParameter); + if (m_parametersById.contains(parameter.id())) { + qDebug() << debugString() << "WARNING: Loaded EffectManifest that had parameters with duplicate IDs. Dropping one of them."; + } + m_parametersById[parameter.id()] = pParameter; + } +} + +Effect::~Effect() { + qDebug() << debugString() << "destroyed"; + m_parametersById.clear(); + for (int i = 0; i < m_parameters.size(); ++i) { + EffectParameter* pParameter = m_parameters.at(i); + m_parameters[i] = NULL; + delete pParameter; + } +} + +void Effect::addToEngine(EngineEffectChain* pChain, int iIndex) { + EffectsRequest* request = new EffectsRequest(); + request->type = EffectsRequest::ADD_EFFECT_TO_CHAIN; + request->pTargetChain = pChain; + request->AddEffectToChain.pEffect = m_pEngineEffect; + request->AddEffectToChain.iIndex = iIndex; + m_pEffectsManager->writeRequest(request); + m_bAddedToEngine = true; + foreach (EffectParameter* pParameter, m_parameters) { + pParameter->addToEngine(); + } +} + +void Effect::removeFromEngine(EngineEffectChain* pChain, int iIndex) { + EffectsRequest* request = new EffectsRequest(); + request->type = EffectsRequest::REMOVE_EFFECT_FROM_CHAIN; + request->pTargetChain = pChain; + request->RemoveEffectFromChain.pEffect = m_pEngineEffect; + request->RemoveEffectFromChain.iIndex = iIndex; + m_pEffectsManager->writeRequest(request); + m_bAddedToEngine = false; + foreach (EffectParameter* pParameter, m_parameters) { + pParameter->removeFromEngine(); + } +} + +void Effect::updateEngineState() { + if (!m_bAddedToEngine) { + return; + } + sendParameterUpdate(); + foreach (EffectParameter* pParameter, m_parameters) { + pParameter->updateEngineState(); + } +} + +EngineEffect* Effect::getEngineEffect() { + return m_pEngineEffect; +} + +const EffectManifest& Effect::getManifest() const { + return m_manifest; +} + +void Effect::setEnabled(bool enabled) { + if (enabled != m_bEnabled) { + m_bEnabled = enabled; + updateEngineState(); + emit(enabledChanged(m_bEnabled)); + } +} + +bool Effect::enabled() const { + return m_bEnabled; +} + +void Effect::sendParameterUpdate() { + if (!m_bAddedToEngine) { + return; + } + EffectsRequest* pRequest = new EffectsRequest(); + pRequest->type = EffectsRequest::SET_EFFECT_PARAMETERS; + pRequest->pTargetEffect = m_pEngineEffect; + pRequest->SetEffectParameters.enabled = m_bEnabled; + m_pEffectsManager->writeRequest(pRequest); +} + +unsigned int Effect::numParameters() const { + return m_parameters.size(); +} + +void Effect::onChainParameterChanged(double chainParameter) { + foreach (EffectParameter* pParameter, m_parameters) { + pParameter->onChainParameterChanged(chainParameter); + } +} + +EffectParameter* Effect::getParameterById(const QString& id) const { + EffectParameter* pParameter = m_parametersById.value(id, NULL); + if (pParameter == NULL) { + qDebug() << debugString() << "getParameterById" + << "WARNING: parameter for id does not exist:" << id; + } + return pParameter; +} + +EffectParameter* Effect::getParameter(unsigned int parameterNumber) { + EffectParameter* pParameter = m_parameters.value(parameterNumber, NULL); + if (pParameter == NULL) { + qDebug() << debugString() << "WARNING: Invalid parameter index."; + } + return pParameter; +} + +QDomElement Effect::toXML(QDomDocument* doc) const { + QDomElement element = doc->createElement("Effect"); + XmlParse::addElement(*doc, element, "Id", m_manifest.id()); + XmlParse::addElement(*doc, element, "Version", m_manifest.version()); + + QDomElement parameters = doc->createElement("Parameters"); + foreach (EffectParameter* pParameter, m_parameters) { + const EffectManifestParameter& parameterManifest = + pParameter->manifest(); + QDomElement parameter = doc->createElement("Parameter"); + XmlParse::addElement(*doc, parameter, "Id", parameterManifest.id()); + // TODO(rryan): Do smarter QVariant formatting? + XmlParse::addElement(*doc, parameter, "Value", + pParameter->getValue().toString()); + // TODO(rryan): Output link state, etc. + parameters.appendChild(parameter); + } + element.appendChild(parameters); + + return element; +} + +// static +EffectPointer Effect::fromXML(EffectsManager* pEffectsManager, + const QDomElement& element) { + QString effectId = XmlParse::selectNodeQString(element, "Id"); + EffectPointer pEffect = pEffectsManager->instantiateEffect(effectId); + // TODO(rryan): Load parameter values / etc. from element. + return pEffect; +} diff --git a/src/effects/effect.h b/src/effects/effect.h new file mode 100644 index 00000000000..56f2dfec227 --- /dev/null +++ b/src/effects/effect.h @@ -0,0 +1,75 @@ +#ifndef EFFECT_H +#define EFFECT_H + +#include +#include + +#include "defs.h" +#include "util.h" +#include "effects/effectmanifest.h" +#include "effects/effectparameter.h" +#include "effects/effectinstantiator.h" + +class EffectProcessor; +class EngineEffectChain; +class EngineEffect; +class EffectsManager; + +class Effect; +typedef QSharedPointer EffectPointer; + +// The Effect class is the main-thread representation of an instantiation of an +// effect. This class is NOT thread safe and must only be used by the main +// thread. The getEngineEffect() method can be used to get a pointer to the +// Engine-thread representation of the effect. +class Effect : public QObject { + Q_OBJECT + public: + Effect(QObject* pParent, EffectsManager* pEffectsManager, + const EffectManifest& manifest, + EffectInstantiatorPointer pInstantiator); + virtual ~Effect(); + + const EffectManifest& getManifest() const; + + unsigned int numParameters() const; + EffectParameter* getParameter(unsigned int parameterNumber); + EffectParameter* getParameterById(const QString& id) const; + + void setEnabled(bool enabled); + bool enabled() const; + + EngineEffect* getEngineEffect(); + + void onChainParameterChanged(double chainParameter); + + void addToEngine(EngineEffectChain* pChain, int iIndex); + void removeFromEngine(EngineEffectChain* pChain, int iIndex); + void updateEngineState(); + + QDomElement toXML(QDomDocument* doc) const; + static EffectPointer fromXML(EffectsManager* pEffectsManager, + const QDomElement& element); + + signals: + void enabledChanged(bool enabled); + + private: + QString debugString() const { + return QString("Effect(%1)").arg(m_manifest.name()); + } + + void sendParameterUpdate(); + + EffectsManager* m_pEffectsManager; + EffectManifest m_manifest; + EngineEffect* m_pEngineEffect; + bool m_bAddedToEngine; + bool m_bEnabled; + QList m_parameters; + QMap m_parametersById; + + DISALLOW_COPY_AND_ASSIGN(Effect); +}; + +#endif /* EFFECT_H */ diff --git a/src/effects/effectchain.cpp b/src/effects/effectchain.cpp new file mode 100644 index 00000000000..fa15f98e00c --- /dev/null +++ b/src/effects/effectchain.cpp @@ -0,0 +1,373 @@ +#include "effects/effectchain.h" +#include "effects/effectsmanager.h" +#include "effects/effectchainmanager.h" +#include "engine/effects/message.h" +#include "engine/effects/engineeffectrack.h" +#include "engine/effects/engineeffectchain.h" +#include "sampleutil.h" +#include "xmlparse.h" + +EffectChain::EffectChain(EffectsManager* pEffectsManager, const QString& id, + EffectChainPointer pPrototype) + : QObject(pEffectsManager), + m_pEffectsManager(pEffectsManager), + m_pPrototype(pPrototype), + m_bEnabled(true), + m_id(id), + m_name(""), + m_insertionType(EffectChain::INSERT), + m_dMix(0), + m_dParameter(0), + m_pEngineEffectChain(new EngineEffectChain(m_id)), + m_bAddedToEngine(false) { +} + +EffectChain::~EffectChain() { + qDebug() << debugString() << "destroyed"; +} + +void EffectChain::addToEngine(EngineEffectRack* pRack, int iIndex) { + EffectsRequest* pRequest = new EffectsRequest(); + pRequest->type = EffectsRequest::ADD_CHAIN_TO_RACK; + pRequest->pTargetRack = pRack; + pRequest->AddChainToRack.pChain = m_pEngineEffectChain; + pRequest->AddChainToRack.iIndex = iIndex; + m_pEffectsManager->writeRequest(pRequest); + m_bAddedToEngine = true; + + // Add all effects. + for (int i = 0; i < m_effects.size(); ++i) { + // Add the effect to the engine. + EffectPointer pEffect = m_effects[i]; + if (pEffect) { + pEffect->addToEngine(m_pEngineEffectChain, i); + } + } +} + +void EffectChain::removeFromEngine(EngineEffectRack* pRack, int iIndex) { + // Order doesn't matter when removing. + for (int i = 0; i < m_effects.size(); ++i) { + EffectPointer pEffect = m_effects[i]; + if (pEffect) { + pEffect->removeFromEngine(m_pEngineEffectChain, i); + } + } + + EffectsRequest* pRequest = new EffectsRequest(); + pRequest->type = EffectsRequest::REMOVE_CHAIN_FROM_RACK; + pRequest->pTargetRack = pRack; + pRequest->RemoveChainFromRack.pChain = m_pEngineEffectChain; + pRequest->RemoveChainFromRack.iIndex = iIndex; + m_pEffectsManager->writeRequest(pRequest); + m_bAddedToEngine = false; +} + +void EffectChain::updateEngineState() { + if (!m_bAddedToEngine) { + return; + } + // Update chain parameters in the engine. + sendParameterUpdate(); + for (int i = 0; i < m_effects.size(); ++i) { + EffectPointer pEffect = m_effects[i]; + if (pEffect) { + // Update effect parameters in the engine. + pEffect->updateEngineState(); + } + } +} + +// static +EffectChainPointer EffectChain::clone(EffectChainPointer pChain) { + if (!pChain) { + return EffectChainPointer(); + } + + EffectChain* pClone = new EffectChain( + pChain->m_pEffectsManager, pChain->id(), pChain); + pClone->setEnabled(pChain->enabled()); + pClone->setName(pChain->name()); + pClone->setParameter(pChain->parameter()); + pClone->setMix(pChain->mix()); + foreach (const QString& group, pChain->enabledGroups()) { + pClone->enableForGroup(group); + } + foreach (EffectPointer pEffect, pChain->effects()) { + EffectPointer pClonedEffect = pChain->m_pEffectsManager + ->instantiateEffect(pEffect->getManifest().id()); + pClone->addEffect(pClonedEffect); + } + return EffectChainPointer(pClone); +} + +EffectChainPointer EffectChain::prototype() const { + return m_pPrototype; +} + +const QString& EffectChain::id() const { + return m_id; +} + +const QString& EffectChain::name() const { + return m_name; +} + +void EffectChain::setName(const QString& name) { + m_name = name; + emit(nameChanged(name)); +} + +QString EffectChain::description() const { + return m_description; +} + +void EffectChain::setDescription(const QString& description) { + m_description = description; + emit(descriptionChanged(description)); +} + +bool EffectChain::enabled() const { + return m_bEnabled; +} + +void EffectChain::setEnabled(bool enabled) { + m_bEnabled = enabled; + sendParameterUpdate(); + emit(enabledChanged(enabled)); +} + +void EffectChain::enableForGroup(const QString& group) { + if (!m_enabledGroups.contains(group)) { + m_enabledGroups.insert(group); + + EffectsRequest* request = new EffectsRequest(); + request->type = EffectsRequest::ENABLE_EFFECT_CHAIN_FOR_GROUP; + request->pTargetChain = m_pEngineEffectChain; + request->group = group; + m_pEffectsManager->writeRequest(request); + + emit(groupStatusChanged(group, true)); + } +} + +bool EffectChain::enabledForGroup(const QString& group) const { + return m_enabledGroups.contains(group); +} + +const QSet& EffectChain::enabledGroups() const { + return m_enabledGroups; +} + +void EffectChain::disableForGroup(const QString& group) { + if (m_enabledGroups.remove(group)) { + EffectsRequest* request = new EffectsRequest(); + request->type = EffectsRequest::DISABLE_EFFECT_CHAIN_FOR_GROUP; + request->pTargetChain = m_pEngineEffectChain; + request->group = group; + m_pEffectsManager->writeRequest(request); + + emit(groupStatusChanged(group, false)); + } +} + +double EffectChain::parameter() const { + return m_dParameter; +} + +void EffectChain::setParameter(const double& dParameter) { + m_dParameter = dParameter; + sendParameterUpdate(); + + foreach (EffectPointer pEffect, m_effects) { + if (pEffect) { + pEffect->onChainParameterChanged(m_dParameter); + } + } + + emit(parameterChanged(dParameter)); +} + +double EffectChain::mix() const { + return m_dMix; +} + +void EffectChain::setMix(const double& dMix) { + m_dMix = dMix; + sendParameterUpdate(); + emit(mixChanged(dMix)); +} + +EffectChain::InsertionType EffectChain::insertionType() const { + return m_insertionType; +} + +void EffectChain::setInsertionType(InsertionType insertionType) { + m_insertionType = insertionType; + sendParameterUpdate(); + emit(insertionTypeChanged(insertionType)); +} + +void EffectChain::addEffect(EffectPointer pEffect) { + qDebug() << debugString() << "addEffect"; + if (!pEffect) { + return; + } + + if (m_effects.contains(pEffect)) { + qDebug() << debugString() + << "WARNING: EffectChain already contains Effect:" + << pEffect; + return; + } + m_effects.append(pEffect); + pEffect->onChainParameterChanged(m_dParameter); + if (m_bAddedToEngine) { + pEffect->addToEngine(m_pEngineEffectChain, m_effects.size() - 1); + } + emit(effectAdded()); +} + +void EffectChain::replaceEffect(unsigned int iEffectNumber, + EffectPointer pEffect) { + qDebug() << debugString() << "replaceEffect" << iEffectNumber << pEffect; + while (iEffectNumber >= m_effects.size()) { + m_effects.append(EffectPointer()); + } + + EffectPointer pOldEffect = m_effects[iEffectNumber]; + if (pOldEffect) { + if (m_bAddedToEngine) { + pOldEffect->removeFromEngine(m_pEngineEffectChain, iEffectNumber); + } + } + + m_effects.replace(iEffectNumber, pEffect); + if (pEffect) { + pEffect->onChainParameterChanged(m_dParameter); + if (m_bAddedToEngine) { + pEffect->addToEngine(m_pEngineEffectChain, iEffectNumber); + } + } + + // TODO(rryan): Replaced signal? + emit(effectAdded()); +} + +void EffectChain::removeEffect(EffectPointer pEffect) { + qDebug() << debugString() << "removeEffect" << pEffect; + for (int i = 0; i < m_effects.size(); ++i) { + if (m_effects.at(i) == pEffect) { + pEffect->removeFromEngine(m_pEngineEffectChain, i); + m_effects.replace(i, EffectPointer()); + emit(effectRemoved()); + } + } +} + +unsigned int EffectChain::numEffects() const { + return m_effects.size(); +} + +const QList& EffectChain::effects() const { + return m_effects; +} + +EffectPointer EffectChain::getEffect(unsigned int effectNumber) const { + if (effectNumber >= m_effects.size()) { + qDebug() << debugString() << "WARNING: list index out of bounds for getEffect"; + } + return m_effects[effectNumber]; +} + +EngineEffectChain* EffectChain::getEngineEffectChain() { + return m_pEngineEffectChain; +} + +void EffectChain::sendParameterUpdate() { + if (!m_bAddedToEngine) { + return; + } + EffectsRequest* pRequest = new EffectsRequest(); + pRequest->type = EffectsRequest::SET_EFFECT_CHAIN_PARAMETERS; + pRequest->pTargetChain = m_pEngineEffectChain; + pRequest->SetEffectChainParameters.enabled = m_bEnabled; + pRequest->SetEffectChainParameters.insertion_type = m_insertionType; + pRequest->SetEffectChainParameters.mix = m_dMix; + m_pEffectsManager->writeRequest(pRequest); +} + +QDomElement EffectChain::toXML(QDomDocument* doc) const { + QDomElement element = doc->createElement("EffectChain"); + + XmlParse::addElement(*doc, element, "Id", m_id); + XmlParse::addElement(*doc, element, "Name", m_name); + XmlParse::addElement(*doc, element, "Description", m_description); + XmlParse::addElement(*doc, element, "InsertionType", + insertionTypeToString(m_insertionType)); + XmlParse::addElement(*doc, element, "Mix", + QString::number(m_dMix)); + XmlParse::addElement(*doc, element, "Parameter", + QString::number(m_dParameter)); + + QDomElement effectsNode = doc->createElement("Effects"); + foreach (EffectPointer pEffect, m_effects) { + if (pEffect) { + QDomElement effectNode = pEffect->toXML(doc); + effectsNode.appendChild(effectNode); + } + } + element.appendChild(effectsNode); + + return element; +} + +// static +EffectChainPointer EffectChain::fromXML(EffectsManager* pEffectsManager, + const QDomElement& element) { + QString id = XmlParse::selectNodeQString(element, "Id"); + QString name = XmlParse::selectNodeQString(element, "Name"); + QString description = XmlParse::selectNodeQString(element, "Description"); + QString insertionTypeStr = XmlParse::selectNodeQString(element, "InsertionType"); + QString mixStr = XmlParse::selectNodeQString(element, "Mix"); + QString parameterStr = XmlParse::selectNodeQString(element, "ParameterStr"); + + EffectChain* pChain = new EffectChain(pEffectsManager, id); + pChain->setName(name); + pChain->setDescription(description); + InsertionType insertionType = insertionTypeFromString(insertionTypeStr); + if (insertionType != NUM_INSERTION_TYPES) { + pChain->setInsertionType(insertionType); + } + bool ok = false; + double mix = mixStr.toDouble(&ok); + if (ok) { + pChain->setMix(mix); + } + + ok = false; + double parameter = parameterStr.toDouble(&ok); + if (ok) { + pChain->setParameter(parameter); + } + + EffectChainPointer pChainWrapped(pChain); + + pEffectsManager->getEffectChainManager()->addEffectChain(pChainWrapped); + + QDomElement effects = XmlParse::selectElement(element, "Effects"); + QDomNodeList effectChildren = effects.childNodes(); + + for (int i = 0; i < effectChildren.count(); ++i) { + QDomNode effect = effectChildren.at(i); + if (effect.isElement()) { + EffectPointer pEffect = Effect::fromXML( + pEffectsManager, effect.toElement()); + if (pEffect) { + pChain->addEffect(pEffect); + } + } + } + + return pChainWrapped; +} diff --git a/src/effects/effectchain.h b/src/effects/effectchain.h new file mode 100644 index 00000000000..6a3e94a8fab --- /dev/null +++ b/src/effects/effectchain.h @@ -0,0 +1,142 @@ +#ifndef EFFECTCHAIN_H +#define EFFECTCHAIN_H + +#include +#include +#include +#include + +#include "util.h" +#include "effects/effect.h" + +class EffectsManager; +class EngineEffectRack; +class EngineEffectChain; +class EffectChain; +typedef QSharedPointer EffectChainPointer; + +// The main-thread representation of an effect chain. This class is NOT +// thread-safe and must only be used from the main thread. +class EffectChain : public QObject { + Q_OBJECT + public: + EffectChain(EffectsManager* pEffectsManager, const QString& id, + EffectChainPointer prototype=EffectChainPointer()); + virtual ~EffectChain(); + + void addToEngine(EngineEffectRack* pRack, int iIndex); + void removeFromEngine(EngineEffectRack* pRack, int iIndex); + void updateEngineState(); + + // The ID of an EffectChain is a unique ID given to it to help associate it + // with the preset from which it was loaded. + const QString& id() const; + + // Whether the chain is enabled (eligible for processing). + bool enabled() const; + void setEnabled(bool enabled); + + // Activates EffectChain processing for the provided group. + void enableForGroup(const QString& group); + bool enabledForGroup(const QString& group) const; + const QSet& enabledGroups() const; + void disableForGroup(const QString& group); + + EffectChainPointer prototype() const; + + // Get the human-readable name of the EffectChain + const QString& name() const; + void setName(const QString& name); + + // Get the human-readable description of the EffectChain + QString description() const; + void setDescription(const QString& description); + + double parameter() const; + void setParameter(const double& dParameter); + + double mix() const; + void setMix(const double& dMix); + + enum InsertionType { + INSERT = 0, + SEND, + // The number of insertion types. Also used to represent "unknown". + NUM_INSERTION_TYPES + }; + static QString insertionTypeToString(InsertionType type) { + switch (type) { + case INSERT: + return "INSERT"; + case SEND: + return "SEND"; + default: + return "UNKNOWN"; + } + } + static InsertionType insertionTypeFromString(const QString& typeStr) { + if (typeStr == "INSERT") { + return INSERT; + } else if (typeStr == "SEND") { + return SEND; + } else { + return NUM_INSERTION_TYPES; + } + } + + InsertionType insertionType() const; + void setInsertionType(InsertionType type); + + void addEffect(EffectPointer pEffect); + void removeEffect(EffectPointer pEffect); + void replaceEffect(unsigned int iEffectNumber, EffectPointer pEffect); + EffectPointer getEffect(unsigned int i) const; + const QList& effects() const; + unsigned int numEffects() const; + + EngineEffectChain* getEngineEffectChain(); + + QDomElement toXML(QDomDocument* doc) const; + static EffectChainPointer fromXML(EffectsManager* pEffectsManager, + const QDomElement& element); + static EffectChainPointer clone(EffectChainPointer pChain); + + signals: + // Signal that indicates that an effect has been added or removed. + void effectAdded(); + void effectRemoved(); + void nameChanged(const QString& name); + void descriptionChanged(const QString& name); + void enabledChanged(bool enabled); + void mixChanged(double v); + void parameterChanged(double v); + void insertionTypeChanged(EffectChain::InsertionType type); + void groupStatusChanged(const QString& group, bool enabled); + + private: + QString debugString() const { + return QString("EffectChain(%1)").arg(m_id); + } + + void sendParameterUpdate(); + + EffectsManager* m_pEffectsManager; + EffectChainPointer m_pPrototype; + + bool m_bEnabled; + QString m_id; + QString m_name; + QString m_description; + InsertionType m_insertionType; + double m_dMix; + double m_dParameter; + + QSet m_enabledGroups; + QList m_effects; + EngineEffectChain* m_pEngineEffectChain; + bool m_bAddedToEngine; + + DISALLOW_COPY_AND_ASSIGN(EffectChain); +}; + +#endif /* EFFECTCHAIN_H */ diff --git a/src/effects/effectchainmanager.cpp b/src/effects/effectchainmanager.cpp new file mode 100644 index 00000000000..53b6ec69f38 --- /dev/null +++ b/src/effects/effectchainmanager.cpp @@ -0,0 +1,165 @@ +#include "effects/effectchainmanager.h" + +#include +#include +#include +#include + +#include "effects/effectsmanager.h" +#include "xmlparse.h" + +EffectChainManager::EffectChainManager(ConfigObject* pConfig, + EffectsManager* pEffectsManager) + : QObject(pEffectsManager), + m_pConfig(pConfig), + m_pEffectsManager(pEffectsManager) { +} + +EffectChainManager::~EffectChainManager() { + qDebug() << debugString() << "destroyed"; +} + +void EffectChainManager::registerGroup(const QString& group) { + if (m_registeredGroups.contains(group)) { + qDebug() << debugString() << "WARNING: Group already registered:" + << group; + return; + } + m_registeredGroups.insert(group); + + foreach (EffectRackPointer pRack, m_effectRacks) { + pRack->registerGroup(group); + } +} + +EffectRackPointer EffectChainManager::addEffectRack() { + EffectRackPointer pRack = EffectRackPointer(new EffectRack( + m_pEffectsManager, this, m_effectRacks.size())); + m_effectRacks.append(pRack); + return pRack; +} + +EffectRackPointer EffectChainManager::getEffectRack(int i) { + if (i < 0 || i >= m_effectRacks.size()) { + return EffectRackPointer(); + } + return m_effectRacks[i]; +} + +void EffectChainManager::addEffectChain(EffectChainPointer pEffectChain) { + if (pEffectChain) { + m_effectChains.append(pEffectChain); + } +} + +void EffectChainManager::removeEffectChain(EffectChainPointer pEffectChain) { + if (pEffectChain) { + m_effectChains.removeAll(pEffectChain); + } +} + +EffectChainPointer EffectChainManager::getNextEffectChain(EffectChainPointer pEffectChain) { + if (m_effectChains.isEmpty()) + return EffectChainPointer(); + + if (!pEffectChain) { + return m_effectChains[0]; + } + + int indexOf = m_effectChains.lastIndexOf(pEffectChain); + if (indexOf == -1) { + qDebug() << debugString() << "WARNING: getNextEffectChain called for an unmanaged EffectChain"; + return m_effectChains[0]; + } + + return m_effectChains[(indexOf + 1) % m_effectChains.size()]; +} + +EffectChainPointer EffectChainManager::getPrevEffectChain(EffectChainPointer pEffectChain) { + if (m_effectChains.isEmpty()) + return EffectChainPointer(); + + if (!pEffectChain) { + return m_effectChains[m_effectChains.size()-1]; + } + + int indexOf = m_effectChains.lastIndexOf(pEffectChain); + if (indexOf == -1) { + qDebug() << debugString() << "WARNING: getPrevEffectChain called for an unmanaged EffectChain"; + return m_effectChains[m_effectChains.size()-1]; + } + + return m_effectChains[(indexOf - 1 + m_effectChains.size()) % m_effectChains.size()]; +} + +bool EffectChainManager::saveEffectChains() { + qDebug() << debugString() << "saveEffectChains"; + QDomDocument doc("MixxxEffects"); + + QString blank = "\n" + "\n" + "\n"; + doc.setContent(blank); + + QDomElement rootNode = doc.documentElement(); + + QDomElement chains = doc.createElement("EffectChains"); + foreach (EffectChainPointer pChain, m_effectChains) { + QDomElement chain = pChain->toXML(&doc); + chains.appendChild(chain); + } + rootNode.appendChild(chains); + + QDir settingsPath(m_pConfig->getSettingsPath()); + + if (!settingsPath.exists()) { + return false; + } + + QFile file(settingsPath.absoluteFilePath("effects.xml")); + + // TODO(rryan): overwrite the right way. + if (!file.open(QIODevice::Truncate | QIODevice::WriteOnly)) { + return false; + } + + QString effectsXml = doc.toString(); + file.write(effectsXml.toUtf8()); + file.close(); + return true; +} + +bool EffectChainManager::loadEffectChains() { + qDebug() << debugString() << "loadEffectChains"; + + QDir settingsPath(m_pConfig->getSettingsPath()); + QFile file(settingsPath.absoluteFilePath("effects.xml")); + + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + QDomDocument doc; + if (!doc.setContent(&file)) { + file.close(); + return false; + } + file.close(); + + QDomElement root = doc.documentElement(); + + QDomElement effectChains = XmlParse::selectElement(root, "EffectChains"); + QDomNodeList chains = effectChains.childNodes(); + + for (int i = 0; i < chains.count(); ++i) { + QDomNode chainNode = chains.at(i); + + if (chainNode.isElement()) { + EffectChainPointer pChain = EffectChain::fromXML( + m_pEffectsManager, chainNode.toElement()); + + m_effectChains.append(pChain); + } + } + return true; +} diff --git a/src/effects/effectchainmanager.h b/src/effects/effectchainmanager.h new file mode 100644 index 00000000000..bfbe1b73f30 --- /dev/null +++ b/src/effects/effectchainmanager.h @@ -0,0 +1,58 @@ +#ifndef EFFECTCHAINMANAGER_H +#define EFFECTCHAINMANAGER_H + +#include +#include + +#include "configobject.h" +#include "util.h" +#include "effects/effectchain.h" +#include "effects/effectrack.h" + +class EffectsManager; + +// A class for keeping track of all the user's EffectChains. Eventually will +// serialize/deserialize the EffectChains from storage but for Effects v1 we are +// hard-coding the available chains. +class EffectChainManager : public QObject { + Q_OBJECT + public: + EffectChainManager(ConfigObject* pConfig, + EffectsManager* pEffectsManager); + virtual ~EffectChainManager(); + + void registerGroup(const QString& group); + const QSet& registeredGroups() const { + return m_registeredGroups; + } + + EffectRackPointer addEffectRack(); + EffectRackPointer getEffectRack(int i); + + void addEffectChain(EffectChainPointer pEffectChain); + void removeEffectChain(EffectChainPointer pEffectChain); + + // To support cycling through effect chains, there is a global ordering of + // chains. These methods allow you to get the next or previous chain given + // your current chain. + // TODO(rryan): Prevent double-loading of a chain into a slot? + EffectChainPointer getNextEffectChain(EffectChainPointer pEffectChain); + EffectChainPointer getPrevEffectChain(EffectChainPointer pEffectChain); + + bool saveEffectChains(); + bool loadEffectChains(); + + private: + QString debugString() const { + return "EffectChainManager"; + } + + ConfigObject* m_pConfig; + EffectsManager* m_pEffectsManager; + QList m_effectRacks; + QList m_effectChains; + QSet m_registeredGroups; + DISALLOW_COPY_AND_ASSIGN(EffectChainManager); +}; + +#endif /* EFFECTCHAINMANAGER_H */ diff --git a/src/effects/effectchainslot.cpp b/src/effects/effectchainslot.cpp new file mode 100644 index 00000000000..fe8c0ebf1af --- /dev/null +++ b/src/effects/effectchainslot.cpp @@ -0,0 +1,407 @@ +#include "effects/effectchainslot.h" + +#include "effects/effectrack.h" +#include "sampleutil.h" +#include "controlpotmeter.h" +#include "controlpushbutton.h" + +EffectChainSlot::EffectChainSlot(EffectRack* pRack, unsigned int iRackNumber, + unsigned int iChainNumber) + : QObject(pRack), + m_iRackNumber(iRackNumber), + m_iChainNumber(iChainNumber), + // The control group names are 1-indexed while internally everything + // is 0-indexed. + m_group(formatGroupString(iRackNumber, iChainNumber)), + m_pEffectRack(pRack) { + m_pControlClear = new ControlPushButton(ConfigKey(m_group, "clear")); + connect(m_pControlClear, SIGNAL(valueChanged(double)), + this, SLOT(slotControlClear(double))); + + m_pControlNumEffects = new ControlObject(ConfigKey(m_group, "num_effects")); + m_pControlNumEffects->connectValueChangeRequest( + this, SLOT(slotControlNumEffects(double)), Qt::AutoConnection); + + m_pControlNumEffectSlots = new ControlObject(ConfigKey(m_group, "num_effectslots")); + m_pControlNumEffectSlots->connectValueChangeRequest( + this, SLOT(slotControlNumEffectSlots(double)), Qt::AutoConnection); + + m_pControlChainLoaded = new ControlObject(ConfigKey(m_group, "loaded")); + m_pControlChainLoaded->connectValueChangeRequest( + this, SLOT(slotControlChainLoaded(double)), Qt::AutoConnection); + + m_pControlChainEnabled = new ControlPushButton(ConfigKey(m_group, "enabled")); + m_pControlChainEnabled->setButtonMode(ControlPushButton::POWERWINDOW); + // Default to enabled. The skin might not show these buttons. + m_pControlChainEnabled->setDefaultValue(true); + m_pControlChainEnabled->set(true); + connect(m_pControlChainEnabled, SIGNAL(valueChanged(double)), + this, SLOT(slotControlChainEnabled(double))); + + m_pControlChainMix = new ControlPotmeter(ConfigKey(m_group, "mix"), 0.0, 1.0); + connect(m_pControlChainMix, SIGNAL(valueChanged(double)), + this, SLOT(slotControlChainMix(double))); + m_pControlChainMix->set(0.0); + + m_pControlChainParameter = new ControlPotmeter(ConfigKey(m_group, "parameter"), 0.0, 1.0); + connect(m_pControlChainParameter, SIGNAL(valueChanged(double)), + this, SLOT(slotControlChainParameter(double))); + m_pControlChainParameter->set(0.0); + + m_pControlChainInsertionType = new ControlPushButton(ConfigKey(m_group, "insertion_type")); + m_pControlChainInsertionType->setButtonMode(ControlPushButton::TOGGLE); + m_pControlChainInsertionType->setStates(EffectChain::NUM_INSERTION_TYPES); + connect(m_pControlChainInsertionType, SIGNAL(valueChanged(double)), + this, SLOT(slotControlChainInsertionType(double))); + + m_pControlChainNextPreset = new ControlPushButton(ConfigKey(m_group, "next_chain")); + connect(m_pControlChainNextPreset, SIGNAL(valueChanged(double)), + this, SLOT(slotControlChainNextPreset(double))); + + m_pControlChainPrevPreset = new ControlPushButton(ConfigKey(m_group, "prev_chain")); + connect(m_pControlChainPrevPreset, SIGNAL(valueChanged(double)), + this, SLOT(slotControlChainPrevPreset(double))); + + m_pControlChainSelector = new ControlObject(ConfigKey(m_group, "chain_selector")); + connect(m_pControlChainSelector, SIGNAL(valueChanged(double)), + this, SLOT(slotControlChainSelector(double))); + + connect(&m_groupStatusMapper, SIGNAL(mapped(const QString&)), + this, SLOT(slotGroupStatusChanged(const QString&))); +} + +EffectChainSlot::~EffectChainSlot() { + qDebug() << debugString() << "destroyed"; + clear(); + delete m_pControlClear; + delete m_pControlNumEffects; + delete m_pControlNumEffectSlots; + delete m_pControlChainLoaded; + delete m_pControlChainEnabled; + delete m_pControlChainMix; + delete m_pControlChainParameter; + delete m_pControlChainInsertionType; + delete m_pControlChainPrevPreset; + delete m_pControlChainNextPreset; + delete m_pControlChainSelector; + + for (QMap::iterator it = m_groupEnableControls.begin(); + it != m_groupEnableControls.end();) { + delete it.value(); + it = m_groupEnableControls.erase(it); + } + + m_slots.clear(); + m_pEffectChain.clear(); +} + +QString EffectChainSlot::id() const { + if (m_pEffectChain) + return m_pEffectChain->id(); + return ""; +} + +void EffectChainSlot::slotChainNameChanged(const QString&) { + emit(updated()); +} + +void EffectChainSlot::slotChainEnabledChanged(bool bEnabled) { + m_pControlChainEnabled->set(bEnabled); + emit(updated()); +} + +void EffectChainSlot::slotChainMixChanged(double mix) { + m_pControlChainMix->set(mix); + emit(updated()); +} + +void EffectChainSlot::slotChainParameterChanged(double parameter) { + m_pControlChainParameter->set(parameter); + emit(updated()); +} + +void EffectChainSlot::slotChainInsertionTypeChanged(EffectChain::InsertionType type) { + m_pControlChainInsertionType->set(static_cast(type)); + emit(updated()); +} + +void EffectChainSlot::slotChainGroupStatusChanged(const QString& group, + bool enabled) { + ControlObject* pGroupControl = m_groupEnableControls.value(group, NULL); + if (pGroupControl != NULL) { + pGroupControl->set(enabled); + emit(updated()); + } +} + +void EffectChainSlot::slotChainEffectsChanged(bool shouldEmit) { + qDebug() << debugString() << "slotChainEffectsChanged"; + if (m_pEffectChain) { + QList effects = m_pEffectChain->effects(); + while (effects.size() > m_slots.size()) { + addEffectSlot(); + } + + for (int i = 0; i < m_slots.size(); ++i) { + EffectSlotPointer pSlot = m_slots[i]; + EffectPointer pEffect; + if (i < effects.size()) { + pEffect = effects[i]; + } + if (pSlot) + pSlot->loadEffect(pEffect); + } + m_pControlNumEffects->setAndConfirm(m_pEffectChain->numEffects()); + if (shouldEmit) { + emit(updated()); + } + } +} + +void EffectChainSlot::loadEffectChain(EffectChainPointer pEffectChain) { + qDebug() << debugString() << "loadEffectChain" << (pEffectChain ? pEffectChain->id() : "(null)"); + clear(); + + if (pEffectChain) { + m_pEffectChain = pEffectChain; + m_pEffectChain->addToEngine(m_pEffectRack->getEngineEffectRack(), + m_iChainNumber); + m_pEffectChain->updateEngineState(); + + connect(m_pEffectChain.data(), SIGNAL(effectAdded()), + this, SLOT(slotChainEffectsChanged())); + connect(m_pEffectChain.data(), SIGNAL(effectRemoved()), + this, SLOT(slotChainEffectsChanged())); + connect(m_pEffectChain.data(), SIGNAL(nameChanged(const QString&)), + this, SLOT(slotChainNameChanged(const QString&))); + connect(m_pEffectChain.data(), SIGNAL(enabledChanged(bool)), + this, SLOT(slotChainEnabledChanged(bool))); + connect(m_pEffectChain.data(), SIGNAL(parameterChanged(double)), + this, SLOT(slotChainParameterChanged(double))); + connect(m_pEffectChain.data(), SIGNAL(mixChanged(double)), + this, SLOT(slotChainMixChanged(double))); + connect(m_pEffectChain.data(), SIGNAL(insertionTypeChanged(EffectChain::InsertionType)), + this, SLOT(slotChainInsertionTypeChanged(EffectChain::InsertionType))); + connect(m_pEffectChain.data(), SIGNAL(groupStatusChanged(const QString&, bool)), + this, SLOT(slotChainGroupStatusChanged(const QString&, bool))); + + m_pControlChainLoaded->setAndConfirm(true); + m_pControlChainInsertionType->set(m_pEffectChain->insertionType()); + + // Mix, parameter, and enabled channels are persistent properties of the + // chain slot, not of the chain. Propagate the current settings to the + // chain. + m_pEffectChain->setParameter(m_pControlChainParameter->get()); + m_pEffectChain->setMix(m_pControlChainMix->get()); + m_pEffectChain->setEnabled(m_pControlChainEnabled->get() > 0.0); + for (QMap::iterator it = m_groupEnableControls.begin(); + it != m_groupEnableControls.end(); ++it) { + if (it.value()->get() > 0.0) { + m_pEffectChain->enableForGroup(it.key()); + } else { + m_pEffectChain->disableForGroup(it.key()); + } + } + + // Don't emit because we will below. + slotChainEffectsChanged(false); + } + + emit(effectChainLoaded(pEffectChain)); + emit(updated()); +} + +EffectChainPointer EffectChainSlot::getEffectChain() const { + return m_pEffectChain; +} + +void EffectChainSlot::clear() { + // Stop listening to signals from any loaded effect + if (m_pEffectChain) { + m_pEffectChain->removeFromEngine(m_pEffectRack->getEngineEffectRack(), + m_iChainNumber); + m_pEffectChain->disconnect(this); + m_pEffectChain.clear(); + + foreach (EffectSlotPointer pSlot, m_slots) { + pSlot->loadEffect(EffectPointer()); + } + + } + m_pControlNumEffects->setAndConfirm(0.0); + m_pControlChainLoaded->setAndConfirm(0.0); + m_pControlChainInsertionType->set(EffectChain::INSERT); + emit(updated()); +} + +unsigned int EffectChainSlot::numSlots() const { + qDebug() << debugString() << "numSlots"; + return m_slots.size(); +} + +EffectSlotPointer EffectChainSlot::addEffectSlot() { + qDebug() << debugString() << "addEffectSlot"; + + EffectSlot* pEffectSlot = new EffectSlot( + this, m_iRackNumber, m_iChainNumber, m_slots.size()); + // Rebroadcast effectLoaded signals + connect(pEffectSlot, SIGNAL(effectLoaded(EffectPointer, unsigned int)), + this, SLOT(slotEffectLoaded(EffectPointer, unsigned int))); + connect(pEffectSlot, SIGNAL(clearEffect(unsigned int, unsigned int, EffectPointer)), + this, SLOT(slotClearEffect(unsigned int, unsigned int, EffectPointer))); + connect(pEffectSlot, SIGNAL(nextEffect(unsigned int, unsigned int, EffectPointer)), + this, SIGNAL(nextEffect(unsigned int, unsigned int, EffectPointer))); + connect(pEffectSlot, SIGNAL(prevEffect(unsigned int, unsigned int, EffectPointer)), + this, SIGNAL(prevEffect(unsigned int, unsigned int, EffectPointer))); + + EffectSlotPointer pSlot(pEffectSlot); + m_slots.append(pSlot); + m_pControlNumEffectSlots->setAndConfirm(m_pControlNumEffectSlots->get() + 1); + return pSlot; +} + +void EffectChainSlot::registerGroup(const QString& group) { + if (m_groupEnableControls.contains(group)) { + qDebug() << debugString() + << "WARNING: registerGroup already has group registered:" + << group; + return; + } + ControlPushButton* pEnableControl = new ControlPushButton( + ConfigKey(m_group, QString("group_%1_enable").arg(group))); + pEnableControl->setButtonMode(ControlPushButton::POWERWINDOW); + m_groupEnableControls[group] = pEnableControl; + m_groupStatusMapper.setMapping(pEnableControl, group); + connect(pEnableControl, SIGNAL(valueChanged(double)), + &m_groupStatusMapper, SLOT(map())); +} + +void EffectChainSlot::slotEffectLoaded(EffectPointer pEffect, unsigned int slotNumber) { + // const int is a safe read... don't bother locking + emit(effectLoaded(pEffect, m_iChainNumber, slotNumber)); +} + +void EffectChainSlot::slotClearEffect(unsigned int iChainSlotNumber, + unsigned int iEffectSlotNumber, + EffectPointer pEffect) { + if (iEffectSlotNumber >= m_slots.size()) { + return; + } + + if (m_pEffectChain) { + m_pEffectChain->replaceEffect(iEffectSlotNumber, EffectPointer()); + } +} + +EffectSlotPointer EffectChainSlot::getEffectSlot(unsigned int slotNumber) { + qDebug() << debugString() << "getEffectSlot" << slotNumber; + if (slotNumber >= m_slots.size()) { + qDebug() << "WARNING: slotNumber out of range"; + return EffectSlotPointer(); + } + return m_slots[slotNumber]; +} + +void EffectChainSlot::slotControlClear(double v) { + if (v > 0) { + clear(); + } +} + +void EffectChainSlot::slotControlNumEffects(double v) { + // Ignore sets to num_effects. + qDebug() << debugString() << "slotControlNumEffects" << v; + qDebug() << "WARNING: num_effects is a read-only control."; +} + +void EffectChainSlot::slotControlNumEffectSlots(double v) { + // Ignore sets to num_effectslots. + qDebug() << debugString() << "slotControlNumEffectSlots" << v; + qDebug() << "WARNING: num_effectslots is a read-only control."; +} + +void EffectChainSlot::slotControlChainLoaded(double v) { + // Ignore sets to loaded. + qDebug() << debugString() << "slotControlChainLoaded" << v; + qDebug() << "WARNING: loaded is a read-only control."; +} + +void EffectChainSlot::slotControlChainEnabled(double v) { + qDebug() << debugString() << "slotControlChainEnabled" << v; + if (m_pEffectChain) { + m_pEffectChain->setEnabled(v > 0); + } +} + +void EffectChainSlot::slotControlChainMix(double v) { + qDebug() << debugString() << "slotControlChainMix" << v; + + // Clamp to [0.0, 1.0] + if (v < 0.0 || v > 1.0) { + qDebug() << debugString() << "value out of limits"; + v = math_clamp(v, 0.0, 1.0); + m_pControlChainMix->set(v); + } + if (m_pEffectChain) { + m_pEffectChain->setMix(v); + } +} + +void EffectChainSlot::slotControlChainParameter(double v) { + qDebug() << debugString() << "slotControlChainParameter" << v; + + // Clamp to [0.0, 1.0] + if (v < 0.0 || v > 1.0) { + qDebug() << debugString() << "value out of limits"; + v = math_clamp(v, 0.0, 1.0); + m_pControlChainParameter->set(v); + } + if (m_pEffectChain) { + m_pEffectChain->setParameter(v); + } +} + +void EffectChainSlot::slotControlChainInsertionType(double v) { + EffectChain::InsertionType type = static_cast(v); + if (m_pEffectChain && type >= 0 && + type < EffectChain::NUM_INSERTION_TYPES) { + m_pEffectChain->setInsertionType(type); + } +} + +void EffectChainSlot::slotControlChainSelector(double v) { + qDebug() << debugString() << "slotControlChainSelector" << v; + if (v > 0) { + emit(nextChain(m_iChainNumber, m_pEffectChain)); + } else if (v < 0) { + emit(prevChain(m_iChainNumber, m_pEffectChain)); + } +} + +void EffectChainSlot::slotControlChainNextPreset(double v) { + qDebug() << debugString() << "slotControlChainNextPreset" << v; + if (v > 0) { + slotControlChainSelector(1); + } +} + +void EffectChainSlot::slotControlChainPrevPreset(double v) { + qDebug() << debugString() << "slotControlChainPrevPreset" << v; + if (v > 0) { + slotControlChainSelector(-1); + } +} + +void EffectChainSlot::slotGroupStatusChanged(const QString& group) { + if (m_pEffectChain) { + ControlObject* pGroupControl = m_groupEnableControls.value(group, NULL); + if (pGroupControl != NULL) { + bool bEnable = pGroupControl->get() > 0; + if (bEnable) { + m_pEffectChain->enableForGroup(group); + } else { + m_pEffectChain->disableForGroup(group); + } + } + } +} diff --git a/src/effects/effectchainslot.h b/src/effects/effectchainslot.h new file mode 100644 index 00000000000..e386d248ccb --- /dev/null +++ b/src/effects/effectchainslot.h @@ -0,0 +1,150 @@ +#ifndef EFFECTCHAINSLOT_H +#define EFFECTCHAINSLOT_H + +#include +#include +#include +#include + +#include "util.h" +#include "effects/effect.h" +#include "effects/effectslot.h" +#include "effects/effectchain.h" + +class ControlObject; +class ControlPushButton; +class EffectChainSlot; +class EffectRack; +typedef QSharedPointer EffectChainSlotPointer; + +class EffectChainSlot : public QObject { + Q_OBJECT + public: + EffectChainSlot(EffectRack* pRack, + const unsigned int iRackNumber, + const unsigned int iChainNumber); + virtual ~EffectChainSlot(); + + static QString formatGroupString(const unsigned int iRackNumber, + const unsigned int iChainNumber) { + return QString("[EffectRack%1_EffectUnit%2]").arg( + QString::number(iRackNumber+1), QString::number(iChainNumber+1)); + } + + // Get the ID of the loaded EffectChain + QString id() const; + + unsigned int numSlots() const; + EffectSlotPointer addEffectSlot(); + EffectSlotPointer getEffectSlot(unsigned int slotNumber); + + void loadEffectChain(EffectChainPointer pEffectChain); + EffectChainPointer getEffectChain() const; + + void registerGroup(const QString& group); + + // Unload the loaded EffectChain. + void clear(); + + signals: + // Indicates that the effect pEffect has been loaded into slotNumber of + // EffectChainSlot chainNumber. pEffect may be an invalid pointer, which + // indicates that a previously loaded effect was removed from the slot. + void effectLoaded(EffectPointer pEffect, unsigned int chainNumber, + unsigned int slotNumber); + + // Indicates that the given EffectChain was loaded into this + // EffectChainSlot + void effectChainLoaded(EffectChainPointer pEffectChain); + + // Signal that whoever is in charge of this EffectChainSlot should load the + // next EffectChain into it. + void nextChain(unsigned int iChainSlotNumber, + EffectChainPointer pEffectChain); + + // Signal that whoever is in charge of this EffectChainSlot should load the + // previous EffectChain into it. + void prevChain(unsigned int iChainSlotNumber, + EffectChainPointer pEffectChain); + + // Signal that whoever is in charge of this EffectChainSlot should clear + // this EffectChain (by removing the chain from this EffectChainSlot). + void clearChain(unsigned int iChainNumber, EffectChainPointer pEffectChain); + + // Signal that whoever is in charge of this EffectChainSlot should load the + // next Effect into the specified EffectSlot. + void nextEffect(unsigned int iChainSlotNumber, + unsigned int iEffectSlotNumber, + EffectPointer pEffect); + + // Signal that whoever is in charge of this EffectChainSlot should load the + // previous Effect into the specified EffectSlot. + void prevEffect(unsigned int iChainSlotNumber, + unsigned int iEffectSlotNumber, + EffectPointer pEffect); + + // Signal that indicates that the EffectChainSlot has been updated. + void updated(); + + private slots: + void slotChainEffectsChanged(bool shouldEmit=true); + void slotChainNameChanged(const QString& name); + void slotChainParameterChanged(double parameter); + void slotChainEnabledChanged(bool enabled); + void slotChainMixChanged(double mix); + void slotChainInsertionTypeChanged(EffectChain::InsertionType type); + void slotChainGroupStatusChanged(const QString& group, bool enabled); + + void slotEffectLoaded(EffectPointer pEffect, unsigned int slotNumber); + // Clears the effect in the given position in the loaded EffectChain. + void slotClearEffect(unsigned int iChainSlotNumber, + unsigned int iEffectSlotNumber, + EffectPointer pEffect); + + void slotControlClear(double v); + void slotControlNumEffects(double v); + void slotControlNumEffectSlots(double v); + void slotControlChainLoaded(double v); + void slotControlChainEnabled(double v); + void slotControlChainMix(double v); + void slotControlChainParameter(double v); + void slotControlChainInsertionType(double v); + void slotControlChainSelector(double v); + void slotControlChainNextPreset(double v); + void slotControlChainPrevPreset(double v); + void slotGroupStatusChanged(const QString& group); + + private: + QString debugString() const { + return QString("EffectChainSlot(%1)").arg(m_iChainNumber); + } + + const unsigned int m_iRackNumber; + const unsigned int m_iChainNumber; + const QString m_group; + EffectRack* m_pEffectRack; + + EffectChainPointer m_pEffectChain; + + ControlPushButton* m_pControlClear; + ControlObject* m_pControlNumEffects; + ControlObject* m_pControlNumEffectSlots; + ControlObject* m_pControlChainLoaded; + ControlPushButton* m_pControlChainEnabled; + ControlObject* m_pControlChainMix; + ControlObject* m_pControlChainParameter; + ControlPushButton* m_pControlChainInsertionType; + ControlObject* m_pControlChainSelector; + ControlPushButton* m_pControlChainNextPreset; + ControlPushButton* m_pControlChainPrevPreset; + + QMap m_groupEnableControls; + + QList m_slots; + QSignalMapper m_groupStatusMapper; + + DISALLOW_COPY_AND_ASSIGN(EffectChainSlot); +}; + + +#endif /* EFFECTCHAINSLOT_H */ diff --git a/src/effects/effectinstantiator.h b/src/effects/effectinstantiator.h new file mode 100644 index 00000000000..f6813109265 --- /dev/null +++ b/src/effects/effectinstantiator.h @@ -0,0 +1,28 @@ +#ifndef EFFECTINSTANTIATOR_H +#define EFFECTINSTANTIATOR_H + +#include + +#include "effects/effectmanifest.h" +#include "effects/effectprocessor.h" + +class EngineEffect; + +class EffectInstantiator { + public: + virtual ~EffectInstantiator() {} + virtual EffectProcessor* instantiate(EngineEffect* pEngineEffect, + const EffectManifest& manifest) = 0; +}; +typedef QSharedPointer EffectInstantiatorPointer; + +template +class EffectProcessorInstantiator : public EffectInstantiator { + public: + EffectProcessor* instantiate(EngineEffect* pEngineEffect, + const EffectManifest& manifest) { + return new T(pEngineEffect, manifest); + } +}; + +#endif /* EFFECTINSTANTIATOR_H */ diff --git a/src/effects/effectmanifest.cpp b/src/effects/effectmanifest.cpp new file mode 100644 index 00000000000..35326f54cb9 --- /dev/null +++ b/src/effects/effectmanifest.cpp @@ -0,0 +1,5 @@ +#include "effects/effectmanifest.h" + +QDebug operator<<(QDebug dbg, const EffectManifest& manifest) { + return dbg.maybeSpace() << QString("EffectManifest(%1)").arg(manifest.id()); +} diff --git a/src/effects/effectmanifest.h b/src/effects/effectmanifest.h new file mode 100644 index 00000000000..dafc86198c0 --- /dev/null +++ b/src/effects/effectmanifest.h @@ -0,0 +1,86 @@ +#ifndef EFFECTMANIFEST_H +#define EFFECTMANIFEST_H + +#include +#include +#include + +#include "effects/effectmanifestparameter.h" + +// An EffectManifest is a full description of the metadata associated with an +// effect (e.g. name, author, version, description, etc.) and the parameters of +// the effect that are intended to be exposed to the rest of Mixxx for user or +// script control. +// +// EffectManifest is composed purely of simple data types, and when an +// EffectManifest is const, it should be completely immutable. EffectManifest is +// meant to be used in most cases as a reference, and in Qt collections, so it +// is important that the implicit copy and assign constructors work, and that +// the no-argument constructor be non-explicit. All methods are left virtual to +// allow a backend to replace the entire functionality with its own (for +// example, a database-backed manifest) +class EffectManifest { + public: + EffectManifest() { } + virtual ~EffectManifest() { + qDebug() << debugString() << "deleted"; + } + + virtual const QString& id() const { + return m_id; + } + virtual void setId(const QString& id) { + m_id = id; + } + + virtual const QString& name() const { + return m_name; + } + virtual void setName(const QString& name) { + m_name = name; + } + + virtual const QString& author() const { + return m_author; + } + virtual void setAuthor(const QString& author) { + m_author = author; + } + + virtual const QString& version() const { + return m_version; + } + virtual void setVersion(const QString& version) { + m_version = version; + } + + virtual const QString& description() const { + return m_description; + } + virtual void setDescription(const QString& description) { + m_description = description; + } + + virtual const QList& parameters() const { + return m_parameters; + } + + virtual EffectManifestParameter* addParameter() { + m_parameters.append(EffectManifestParameter()); + return &m_parameters.last(); + } + + private: + QString debugString() const { + return QString("EffectManifest(%1)").arg(m_id); + } + + QString m_id; + QString m_name; + QString m_author; + QString m_version; + QString m_description; + QList m_parameters; +}; + +#endif /* EFFECTMANIFEST_H */ diff --git a/src/effects/effectmanifestparameter.cpp b/src/effects/effectmanifestparameter.cpp new file mode 100644 index 00000000000..0094635c148 --- /dev/null +++ b/src/effects/effectmanifestparameter.cpp @@ -0,0 +1,5 @@ +#include "effects/effectmanifestparameter.h" + +QDebug operator<<(QDebug dbg, const EffectManifestParameter& parameter) { + return dbg.maybeSpace() << QString("EffectManifestParameter(%1)").arg(parameter.id()); +} diff --git a/src/effects/effectmanifestparameter.h b/src/effects/effectmanifestparameter.h new file mode 100644 index 00000000000..09c60342f7b --- /dev/null +++ b/src/effects/effectmanifestparameter.h @@ -0,0 +1,176 @@ +#ifndef EFFECTMANIFESTPARAMETER_H +#define EFFECTMANIFESTPARAMETER_H + +#include +#include +#include + +class EffectManifestParameter { + public: + enum ValueHint { + VALUE_UNKNOWN = 0, + VALUE_BOOLEAN, + VALUE_INTEGRAL, + VALUE_FLOAT + }; + + enum ControlHint { + CONTROL_UNKNOWN = 0, + CONTROL_KNOB_LINEAR, + CONTROL_KNOB_LOGARITHMIC, + CONTROL_TOGGLE + }; + + enum SemanticHint { + SEMANTIC_UNKNOWN = 0, + SEMANTIC_SAMPLES, + SEMANTIC_NOTE, + }; + + enum UnitsHint { + UNITS_UNKNOWN = 0, + UNITS_TIME, + UNITS_HERTZ, + UNITS_SAMPLERATE, // fraction of the samplerate + }; + + enum LinkType { + LINK_NONE = 0, + LINK_LINKED, + LINK_INVERSE, + NUM_LINK_TYPES + }; + + EffectManifestParameter() + : m_controlHint(CONTROL_UNKNOWN), + m_valueHint(VALUE_UNKNOWN), + m_semanticHint(SEMANTIC_UNKNOWN), + m_unitsHint(UNITS_UNKNOWN), + m_linkHint(LINK_NONE) { + } + + virtual ~EffectManifestParameter() { + qDebug() << debugString() << "destroyed"; + } + + //////////////////////////////////////////////////////////////////////////////// + // Parameter Information + //////////////////////////////////////////////////////////////////////////////// + + virtual const QString& id() const { + return m_id; + } + virtual void setId(const QString& id) { + m_id = id; + } + + virtual const QString& name() const { + return m_name; + } + virtual void setName(const QString& name) { + m_name = name; + } + + virtual const QString& description() const { + return m_description; + } + virtual void setDescription(const QString& description) { + m_description = description; + } + + //////////////////////////////////////////////////////////////////////////////// + // Usage hints + //////////////////////////////////////////////////////////////////////////////// + + virtual ControlHint controlHint() const { + return m_controlHint; + } + virtual void setControlHint(ControlHint controlHint) { + m_controlHint = controlHint; + } + + virtual ValueHint valueHint() const { + return m_valueHint; + } + virtual void setValueHint(ValueHint valueHint) { + m_valueHint = valueHint; + } + + virtual SemanticHint semanticHint() const { + return m_semanticHint; + } + virtual void setSemanticHint(SemanticHint semanticHint) { + m_semanticHint = semanticHint; + } + + virtual UnitsHint unitsHint() const { + return m_unitsHint; + } + virtual void setUnitsHint(UnitsHint unitsHint) { + m_unitsHint = unitsHint; + } + + virtual LinkType linkHint() const { + return m_linkHint; + } + virtual void setLinkHint(LinkType linkHint) { + m_linkHint = linkHint; + } + + //////////////////////////////////////////////////////////////////////////////// + // Value Settings + //////////////////////////////////////////////////////////////////////////////// + + virtual bool hasDefault() const { + return m_default.isValid(); + } + virtual const QVariant& getDefault() const { + return m_default; + } + virtual void setDefault(const QVariant& defaultValue) { + m_default = defaultValue; + } + + virtual bool hasMinimum() const { + return m_minimum.isValid(); + } + virtual const QVariant& getMinimum() const { + return m_minimum; + } + virtual void setMinimum(const QVariant& minimum) { + m_minimum = minimum; + } + + virtual bool hasMaximum() const { + return m_maximum.isValid(); + } + virtual const QVariant& getMaximum() const { + return m_maximum; + } + virtual void setMaximum(const QVariant& maximum) { + m_maximum = maximum; + } + + private: + QString debugString() const { + return QString("EffectManifestParameter(%1)").arg(m_id); + } + + QString m_id; + QString m_name; + QString m_description; + + ControlHint m_controlHint; + ValueHint m_valueHint; + SemanticHint m_semanticHint; + UnitsHint m_unitsHint; + LinkType m_linkHint; + + QVariant m_default; + QVariant m_minimum; + QVariant m_maximum; +}; + +QDebug operator<<(QDebug dbg, const EffectManifestParameter& parameter); + +#endif /* EFFECTMANIFESTPARAMETER_H */ diff --git a/src/effects/effectparameter.cpp b/src/effects/effectparameter.cpp new file mode 100644 index 00000000000..d6d525308f1 --- /dev/null +++ b/src/effects/effectparameter.cpp @@ -0,0 +1,458 @@ +#include + +#include "effects/effectparameter.h" +#include "effects/effectsmanager.h" +#include "effects/effect.h" + +EffectParameter::EffectParameter(Effect* pEffect, EffectsManager* pEffectsManager, + int iParameterNumber, const EffectManifestParameter& parameter) + : QObject(pEffect), + m_pEffect(pEffect), + m_pEffectsManager(pEffectsManager), + m_iParameterNumber(iParameterNumber), + m_parameter(parameter), + m_linkType(m_parameter.linkHint()), + m_bAddedToEngine(false) { + qDebug() << debugString() << "Constructing new EffectParameter from EffectManifestParameter:" + << m_parameter.id(); + switch (m_parameter.valueHint()) { + case EffectManifestParameter::VALUE_BOOLEAN: + // Minimum and maximum are undefined for a boolean. + m_minimum = QVariant(); + m_maximum = QVariant(); + if (m_parameter.hasDefault() && m_parameter.getDefault().canConvert()) { + m_default = m_parameter.getDefault(); + } else { + // Default to false if no default is given. + m_default = QVariant(false); + } + m_value = m_default; + break; + case EffectManifestParameter::VALUE_INTEGRAL: + m_minimum = m_parameter.hasMinimum() && m_parameter.getMinimum().canConvert() ? + m_parameter.getMinimum() : QVariant(0); + m_maximum = m_parameter.hasMaximum() && m_parameter.getMinimum().canConvert() ? + m_parameter.getMaximum() : QVariant(1); + + // Sanity check the maximum and minimum + if (clampRanges()) { + qDebug() << debugString() << "WARNING: Parameter maximum is less than the minimum."; + } + + // If the parameter specifies a default, set that. Otherwise use the minimum + // value. + if (m_parameter.hasDefault() && m_parameter.getDefault().canConvert()) { + m_default = m_parameter.getDefault(); + if (m_default.toInt() < m_minimum.toInt() || m_default.toInt() > m_maximum.toInt()) { + qDebug() << debugString() << "WARNING: Parameter default is outside of minimum/maximum range."; + m_default = m_minimum; + } + } else { + m_default = m_minimum; + } + + // Finally, set the value to the default. + m_value = m_default; + break; + case EffectManifestParameter::VALUE_UNKNOWN: // Treat unknown like float + case EffectManifestParameter::VALUE_FLOAT: + m_minimum = m_parameter.hasMinimum() && m_parameter.getMinimum().canConvert() ? + m_parameter.getMinimum() : QVariant(0.0f); + m_maximum = m_parameter.hasMaximum() && m_parameter.getMinimum().canConvert() ? + m_parameter.getMaximum() : QVariant(1.0f); + // Sanity check the maximum and minimum + if (m_minimum.toDouble() > m_maximum.toDouble()) { + qDebug() << debugString() << "WARNING: Parameter maximum is less than the minimum."; + m_maximum = m_minimum; + } + + // If the parameter specifies a default, set that. Otherwise use the minimum + // value. + if (m_parameter.hasDefault() && m_parameter.getDefault().canConvert()) { + m_default = m_parameter.getDefault(); + if (m_default.toDouble() < m_minimum.toDouble() || m_default.toDouble() > m_maximum.toDouble()) { + qDebug() << debugString() << "WARNING: Parameter default is outside of minimum/maximum range."; + m_default = m_minimum; + } + } else { + m_default = m_minimum; + } + + // Finally, set the value to the default. + m_value = m_default; + break; + default: + qDebug() << debugString() << "ERROR: Unhandled valueHint"; + break; + } +} + +EffectParameter::~EffectParameter() { + qDebug() << debugString() << "destroyed"; +} + +const EffectManifestParameter& EffectParameter::manifest() const { + return m_parameter; +} + +const QString EffectParameter::id() const { + return m_parameter.id(); +} + +const QString EffectParameter::name() const { + return m_parameter.name(); +} + +const QString EffectParameter::description() const { + return m_parameter.description(); +} + +// static +bool EffectParameter::clampValue(EffectManifestParameter::ValueHint valueHint, QVariant& value, + const QVariant& minimum, const QVariant& maximum) { + switch (valueHint) { + case EffectManifestParameter::VALUE_BOOLEAN: + break; + case EffectManifestParameter::VALUE_INTEGRAL: + if (value.toInt() < minimum.toInt()) { + value = minimum; + return true; + } else if (value.toInt() > maximum.toInt()) { + value = maximum; + return true; + } + break; + case EffectManifestParameter::VALUE_FLOAT: + case EffectManifestParameter::VALUE_UNKNOWN: + if (value.toDouble() < minimum.toDouble()) { + value = minimum; + return true; + } else if (value.toDouble() > maximum.toDouble()) { + value = maximum; + return true; + } + break; + default: + qDebug() << "ERROR: Unhandled valueHint"; + break; + } + return false; +} + +bool EffectParameter::clampValue() { + return clampValue(m_parameter.valueHint(), m_value, m_minimum, m_maximum); +} + +bool EffectParameter::clampDefault() { + return clampValue(m_parameter.valueHint(), m_default, m_minimum, m_maximum); +} + +bool EffectParameter::checkType(const QVariant& value) const { + switch (m_parameter.valueHint()) { + case EffectManifestParameter::VALUE_BOOLEAN: + return value.canConvert(); + case EffectManifestParameter::VALUE_INTEGRAL: + return value.canConvert(); + case EffectManifestParameter::VALUE_FLOAT: + case EffectManifestParameter::VALUE_UNKNOWN: + return value.canConvert(); + default: + qDebug() << debugString() << "ERROR: Unhandled valueHint"; + break; + } + return false; +} + +bool EffectParameter::clampRanges() { + switch (m_parameter.valueHint()) { + case EffectManifestParameter::VALUE_BOOLEAN: + break; + case EffectManifestParameter::VALUE_INTEGRAL: + if (m_minimum.toInt() > m_maximum.toInt()) { + m_maximum = m_minimum; + return true; + } + break; + case EffectManifestParameter::VALUE_FLOAT: + case EffectManifestParameter::VALUE_UNKNOWN: + if (m_minimum.toDouble() > m_maximum.toDouble()) { + m_maximum = m_minimum; + return true; + } + break; + default: + qDebug() << debugString() << "ERROR: Unhandled valueHint"; + break; + } + return false; +} + +EffectManifestParameter::LinkType EffectParameter::getLinkType() const { + return m_linkType; +} + +void EffectParameter::setLinkType(EffectManifestParameter::LinkType linkType) { + m_linkType = linkType; + // TODO(rryan) update value based on link type. +} + +void EffectParameter::onChainParameterChanged(double chainParameter) { + double max; + double min; + switch (m_linkType) { + case EffectManifestParameter::LINK_INVERSE: + chainParameter = 1.0 - chainParameter; + // Intentional fall-through. + case EffectManifestParameter::LINK_LINKED: + if (chainParameter < 0.0 || chainParameter > 1.0) { + return; + } + max = m_maximum.toDouble(); + min = m_minimum.toDouble(); + setValue(min + chainParameter * (max - min)); + break; + case EffectManifestParameter::LINK_NONE: + default: + break; + } +} + +QVariant EffectParameter::getValue() const { + return m_value; +} + +void EffectParameter::setValue(QVariant value) { + if (!checkType(value)) { + qDebug() << debugString() << "WARNING: Value cannot be converted to suitable value, ignoring."; + return; + } + + switch (m_parameter.valueHint()) { + case EffectManifestParameter::VALUE_BOOLEAN: + m_value = value.toBool(); + break; + case EffectManifestParameter::VALUE_INTEGRAL: + m_value = value.toInt(); + break; + case EffectManifestParameter::VALUE_UNKNOWN: // treat unknown as float + case EffectManifestParameter::VALUE_FLOAT: + // TODO(XXX) Handle inf, -inf, and nan + m_value = value.toDouble(); + break; + default: + qDebug() << debugString() << "ERROR: Unhandled valueHint"; + break; + } + + if (clampValue()) { + qDebug() << debugString() << "WARNING: Value was outside of limits, clamped."; + } + updateEngineState(); + emit(valueChanged(m_value)); +} + +QVariant EffectParameter::getDefault() const { + return m_default; +} + +void EffectParameter::setDefault(QVariant dflt) { + if (!checkType(dflt)) { + qDebug() << debugString() << "WARNING: Value for default cannot be converted to suitable value, ignoring."; + return; + } + + switch (m_parameter.valueHint()) { + case EffectManifestParameter::VALUE_BOOLEAN: + m_default = dflt.toBool(); + break; + case EffectManifestParameter::VALUE_INTEGRAL: + m_default = dflt.toInt(); + break; + case EffectManifestParameter::VALUE_UNKNOWN: + case EffectManifestParameter::VALUE_FLOAT: + m_default = dflt.toDouble(); + break; + default: + qDebug() << debugString() << "ERROR: Unhandled valueHint"; + break; + } + + if (clampDefault()) { + qDebug() << debugString() << "WARNING: Default parameter value was outside of range, clamped."; + } + + updateEngineState(); +} + +QVariant EffectParameter::getMinimum() const { + return m_minimum; +} + +void EffectParameter::setMinimum(QVariant minimum) { + if (!checkType(minimum)) { + qDebug() << debugString() << "WARNING: Value for minimum cannot be converted to suitable value, ignoring."; + return; + } + + switch (m_parameter.valueHint()) { + case EffectManifestParameter::VALUE_BOOLEAN: + // Minimum doesn't apply to booleans + break; + case EffectManifestParameter::VALUE_INTEGRAL: + m_minimum = minimum.toInt(); + + if (m_parameter.hasMinimum() && m_minimum.toInt() < m_parameter.getMinimum().toInt()) { + qDebug() << debugString() << "WARNING: Minimum value is less than plugin's absolute minimum, clamping."; + m_minimum = m_parameter.getMinimum(); + } + + if (m_minimum.toInt() > m_maximum.toInt()) { + qDebug() << debugString() << "WARNING: New minimum was above maximum, clamped."; + m_minimum = m_maximum; + } + + // There's a degenerate case here where the maximum could be lower + // than the manifest minimum. If that's the case, then the minimum + // value is currently below the manifest minimum. Since similar + // guards exist in the setMaximum call, this should not be able to + // happen. + if (m_parameter.hasMinimum()) { + Q_ASSERT(m_minimum.toInt() >= m_parameter.getMinimum().toInt()); + } + break; + case EffectManifestParameter::VALUE_UNKNOWN: + case EffectManifestParameter::VALUE_FLOAT: + m_minimum = minimum.toDouble(); + + if (m_parameter.hasMinimum() && m_minimum.toDouble() < m_parameter.getMinimum().toDouble()) { + qDebug() << debugString() << "WARNING: Minimum value is less than plugin's absolute minimum, clamping."; + m_minimum = m_parameter.getMinimum(); + } + + if (m_minimum.toDouble() > m_maximum.toDouble()) { + qDebug() << debugString() << "WARNING: New minimum was above maximum, clamped."; + m_minimum = m_maximum; + } + + // There's a degenerate case here where the maximum could be lower + // than the manifest minimum. If that's the case, then the minimum + // value is currently below the manifest minimum. Since similar + // guards exist in the setMaximum call, this should not be able to + // happen. + if (m_parameter.hasMinimum()) { + Q_ASSERT(m_minimum.toDouble() >= m_parameter.getMinimum().toDouble()); + } + break; + default: + qDebug() << debugString() << "ERROR: Unhandled valueHint"; + break; + } + + if (clampValue()) { + qDebug() << debugString() << "WARNING: Value was outside of new minimum, clamped."; + } + + if (clampDefault()) { + qDebug() << debugString() << "WARNING: Default was outside of new minimum, clamped."; + } + + updateEngineState(); +} + +QVariant EffectParameter::getMaximum() const { + return m_maximum; +} + +void EffectParameter::setMaximum(QVariant maximum) { + if (!checkType(maximum)) { + qDebug() << debugString() << "WARNING: Value for maximum cannot be converted to suitable value, ignoring."; + return; + } + + switch (m_parameter.valueHint()) { + case EffectManifestParameter::VALUE_BOOLEAN: + // Maximum doesn't apply to booleans + break; + case EffectManifestParameter::VALUE_INTEGRAL: + m_maximum = maximum.toInt(); + + if (m_parameter.hasMaximum() && m_maximum.toInt() > m_parameter.getMaximum().toInt()) { + qDebug() << debugString() << "WARNING: Maximum value is less than plugin's absolute maximum, clamping."; + m_maximum = m_parameter.getMaximum(); + } + + if (m_maximum.toInt() < m_minimum.toInt()) { + qDebug() << debugString() << "WARNING: New maximum was below the minimum, clamped."; + m_maximum = m_minimum; + } + + // There's a degenerate case here where the minimum could be larger + // than the manifest maximum. If that's the case, then the maximum + // value is currently above the manifest maximum. Since similar + // guards exist in the setMinimum call, this should not be able to + // happen. + if (m_parameter.hasMaximum()) { + Q_ASSERT(m_maximum.toInt() <= m_parameter.getMaximum().toInt()); + } + break; + case EffectManifestParameter::VALUE_UNKNOWN: + case EffectManifestParameter::VALUE_FLOAT: + m_maximum = maximum.toDouble(); + + if (m_parameter.hasMaximum() && m_maximum.toDouble() > m_parameter.getMaximum().toDouble()) { + qDebug() << debugString() << "WARNING: Maximum value is less than plugin's absolute maximum, clamping."; + m_maximum = m_parameter.getMaximum(); + } + + if (m_maximum.toDouble() < m_minimum.toDouble()) { + qDebug() << debugString() << "WARNING: New maximum was below the minimum, clamped."; + m_maximum = m_minimum; + } + + // There's a degenerate case here where the minimum could be larger + // than the manifest maximum. If that's the case, then the maximum + // value is currently above the manifest maximum. Since similar + // guards exist in the setMinimum call, this should not be able to + // happen. + if (m_parameter.hasMaximum()) { + Q_ASSERT(m_maximum.toDouble() <= m_parameter.getMaximum().toDouble()); + } + break; + default: + qDebug() << debugString() << "ERROR: Unhandled valueHint"; + break; + } + + if (clampValue()) { + qDebug() << debugString() << "WARNING: Value was outside of new maximum, clamped."; + } + + if (clampDefault()) { + qDebug() << debugString() << "WARNING: Default was outside of new maximum, clamped."; + } + + updateEngineState(); +} + +void EffectParameter::addToEngine() { + m_bAddedToEngine = true; +} + +void EffectParameter::removeFromEngine() { + m_bAddedToEngine = false; +} + +void EffectParameter::updateEngineState() { + if (!m_bAddedToEngine) { + return; + } + EffectsRequest* pRequest = new EffectsRequest(); + pRequest->type = EffectsRequest::SET_PARAMETER_PARAMETERS; + pRequest->pTargetEffect = m_pEffect->getEngineEffect(); + pRequest->SetParameterParameters.iParameter = m_iParameterNumber; + pRequest->value = m_value; + pRequest->minimum = m_minimum; + pRequest->maximum = m_maximum; + pRequest->default_value = m_default; + m_pEffectsManager->writeRequest(pRequest); +} diff --git a/src/effects/effectparameter.h b/src/effects/effectparameter.h new file mode 100644 index 00000000000..8dbabf725a2 --- /dev/null +++ b/src/effects/effectparameter.h @@ -0,0 +1,90 @@ +#ifndef EFFECTPARAMETER_H +#define EFFECTPARAMETER_H + +#include +#include + +#include "util.h" +#include "effects/effectmanifestparameter.h" + +class Effect; +class EffectsManager; + +// An EffectParameter is an instance of an EffectManifestParameter, which is in +// charge of keeping track of the instance values for the default, minimum, +// maximum and value for each Effect's parameter, and validating that they are +// always within acceptable ranges. This class is NOT thread-safe and must only +// be used from the main thread. +class EffectParameter : public QObject { + Q_OBJECT + public: + EffectParameter(Effect* pEffect, EffectsManager* pEffectsManager, + int iParameterNumber, const EffectManifestParameter& parameter); + virtual ~EffectParameter(); + + void addToEngine(); + void removeFromEngine(); + + /////////////////////////////////////////////////////////////////////////// + // Parameter Information + /////////////////////////////////////////////////////////////////////////// + + const EffectManifestParameter& manifest() const; + const QString id() const; + const QString name() const; + const QString description() const; + + /////////////////////////////////////////////////////////////////////////// + // Value Settings + /////////////////////////////////////////////////////////////////////////// + + EffectManifestParameter::LinkType getLinkType() const; + void setLinkType(EffectManifestParameter::LinkType linkType); + + QVariant getValue() const; + void setValue(QVariant value); + + QVariant getDefault() const; + void setDefault(QVariant defaultValue); + + QVariant getMinimum() const; + void setMinimum(QVariant minimum); + + QVariant getMaximum() const; + void setMaximum(QVariant maximum); + + void updateEngineState(); + + void onChainParameterChanged(double chainParameter); + + signals: + void valueChanged(QVariant value); + + private: + QString debugString() const { + return QString("EffectParameter(%1)").arg(m_parameter.name()); + } + + static bool clampValue(EffectManifestParameter::ValueHint valueHint, QVariant& value, + const QVariant& minimum, const QVariant& maximum); + bool clampValue(); + bool clampDefault(); + bool clampRanges(); + bool checkType(const QVariant& value) const; + + Effect* m_pEffect; + EffectsManager* m_pEffectsManager; + int m_iParameterNumber; + EffectManifestParameter m_parameter; + EffectManifestParameter::LinkType m_linkType; + QVariant m_minimum; + QVariant m_maximum; + QVariant m_default; + QVariant m_value; + bool m_bAddedToEngine; + + DISALLOW_COPY_AND_ASSIGN(EffectParameter); +}; + + +#endif /* EFFECTPARAMETER_H */ diff --git a/src/effects/effectparameterslot.cpp b/src/effects/effectparameterslot.cpp new file mode 100644 index 00000000000..21cfa28569f --- /dev/null +++ b/src/effects/effectparameterslot.cpp @@ -0,0 +1,281 @@ +#include + +#include "defs.h" +#include "controlpotmeter.h" +#include "effects/effectparameterslot.h" +#include "controlobject.h" +#include "controlpushbutton.h" + +EffectParameterSlot::EffectParameterSlot(QObject* pParent, + const unsigned int iRackNumber, + const unsigned int iChainNumber, + const unsigned int iSlotNumber, + const unsigned int iParameterNumber) + : QObject(), + m_iRackNumber(iRackNumber), + m_iChainNumber(iChainNumber), + m_iSlotNumber(iSlotNumber), + m_iParameterNumber(iParameterNumber), + m_group(formatGroupString(m_iRackNumber, m_iChainNumber, + m_iSlotNumber, m_iParameterNumber)), + m_pEffectParameter(NULL) { + m_pControlLoaded = new ControlObject( + ConfigKey(m_group, QString("loaded"))); + m_pControlLinkType = new ControlPushButton( + ConfigKey(m_group, QString("link_type"))); + m_pControlLinkType->setButtonMode(ControlPushButton::TOGGLE); + m_pControlLinkType->setStates(EffectManifestParameter::NUM_LINK_TYPES); + m_pControlValue = new ControlObject( + ConfigKey(m_group, QString("value"))); + m_pControlValueNormalized = new ControlPotmeter( + ConfigKey(m_group, QString("value_normalized")), 0.0, 1.0); + m_pControlValueType = new ControlObject( + ConfigKey(m_group, QString("value_type"))); + m_pControlValueDefault = new ControlObject( + ConfigKey(m_group, QString("value_default"))); + m_pControlValueMaximum = new ControlObject( + ConfigKey(m_group, QString("value_max"))); + m_pControlValueMaximumLimit = new ControlObject( + ConfigKey(m_group, QString("value_max_limit"))); + m_pControlValueMinimum = new ControlObject( + ConfigKey(m_group, QString("value_min"))); + m_pControlValueMinimumLimit = new ControlObject( + ConfigKey(m_group, QString("value_min_limit"))); + + connect(m_pControlLinkType, SIGNAL(valueChanged(double)), + this, SLOT(slotLinkType(double))); + connect(m_pControlValue, SIGNAL(valueChanged(double)), + this, SLOT(slotValue(double))); + connect(m_pControlValueNormalized, SIGNAL(valueChanged(double)), + this, SLOT(slotValueNormalized(double))); + connect(m_pControlValueMaximum, SIGNAL(valueChanged(double)), + this, SLOT(slotValueMaximum(double))); + connect(m_pControlValueMinimum, SIGNAL(valueChanged(double)), + this, SLOT(slotValueMinimum(double))); + + // Read-only controls. + m_pControlValueType->connectValueChangeRequest( + this, SLOT(slotValueType(double)), Qt::AutoConnection); + m_pControlValueDefault->connectValueChangeRequest( + this, SLOT(slotValueDefault(double)), Qt::AutoConnection); + m_pControlLoaded->connectValueChangeRequest( + this, SLOT(slotLoaded(double)), Qt::AutoConnection); + m_pControlValueMinimumLimit->connectValueChangeRequest( + this, SLOT(slotValueMaximumLimit(double)), Qt::AutoConnection); + m_pControlValueMaximumLimit->connectValueChangeRequest( + this, SLOT(slotValueMinimumLimit(double)), Qt::AutoConnection); + + clear(); +} + +EffectParameterSlot::~EffectParameterSlot() { + //qDebug() << debugString() << "destroyed"; + m_pEffectParameter = NULL; + m_pEffect.clear(); + delete m_pControlLoaded; + delete m_pControlLinkType; + delete m_pControlValue; + delete m_pControlValueNormalized; + delete m_pControlValueType; + delete m_pControlValueDefault; + delete m_pControlValueMaximum; + delete m_pControlValueMaximumLimit; + delete m_pControlValueMinimum; + delete m_pControlValueMinimumLimit; +} + +QString EffectParameterSlot::name() const { + if (m_pEffectParameter) { + return m_pEffectParameter->name(); + } + return QString(); +} + +QString EffectParameterSlot::description() const { + if (m_pEffectParameter) { + return m_pEffectParameter->description(); + } + return tr("No effect loaded."); +} + +void EffectParameterSlot::loadEffect(EffectPointer pEffect) { + //qDebug() << debugString() << "loadEffect" << (pEffect ? pEffect->getManifest().name() : "(null)"); + clear(); + if (pEffect) { + m_pEffect = pEffect; + // Returns null if it doesn't have a parameter for that number + m_pEffectParameter = pEffect->getParameter(m_iParameterNumber); + + if (m_pEffectParameter) { + qDebug() << debugString() << "Loading effect parameter" << m_pEffectParameter->name(); + double dValue = m_pEffectParameter->getValue().toDouble(); + double dMinimum = m_pEffectParameter->getMinimum().toDouble(); + double dMinimumLimit = dMinimum; // TODO(rryan) expose limit from EffectParameter + double dMaximum = m_pEffectParameter->getMaximum().toDouble(); + double dMaximumLimit = dMaximum; // TODO(rryan) expose limit from EffectParameter + double dDefault = m_pEffectParameter->getDefault().toDouble(); + + if (dValue > dMaximum || dValue < dMinimum || + dMinimum < dMinimumLimit || dMaximum > dMaximumLimit || + dDefault > dMaximum || dDefault < dMinimum) { + qDebug() << debugString() << "WARNING: EffectParameter does not satisfy basic sanity checks."; + } + double dNormalized = (dValue - dMinimum) / (dMaximum - dMinimum); + double dDefaultNormalized = (dDefault - dMinimum) / (dMaximum - dMinimum); + + qDebug() << debugString() << QString("Val: %1 Min: %2 MinLimit: %3 Max: %4 MaxLimit: %5 Default: %6 Norm: %7").arg(dValue).arg(dMinimum).arg(dMinimumLimit).arg(dMaximum).arg(dMaximumLimit).arg(dDefault).arg(dNormalized); + + m_pControlValue->set(dValue); + m_pControlValue->setDefaultValue(dDefault); + m_pControlValueNormalized->set(dNormalized); + m_pControlValueNormalized->setDefaultValue(dDefaultNormalized); + m_pControlValueMinimum->set(dMinimum); + m_pControlValueMinimumLimit->setAndConfirm(dMinimumLimit); + m_pControlValueMaximum->set(dMaximum); + m_pControlValueMaximumLimit->setAndConfirm(dMaximumLimit); + // TODO(rryan) expose this from EffectParameter + m_pControlValueType->setAndConfirm(0); + m_pControlValueDefault->setAndConfirm(dDefault); + // Default loaded parameters to loaded and unlinked + m_pControlLoaded->setAndConfirm(1.0); + m_pControlLinkType->set(m_pEffectParameter->getLinkType()); + + connect(m_pEffectParameter, SIGNAL(valueChanged(QVariant)), + this, SLOT(slotParameterValueChanged(QVariant))); + } + } + emit(updated()); +} + +void EffectParameterSlot::clear() { + //qDebug() << debugString() << "clear"; + if (m_pEffectParameter) { + m_pEffectParameter->disconnect(this); + m_pEffectParameter = NULL; + } + + m_pEffect.clear(); + m_pControlLoaded->setAndConfirm(0.0); + m_pControlValue->set(0.0); + m_pControlValue->setDefaultValue(0.0); + m_pControlValueNormalized->set(0.0); + m_pControlValueNormalized->setDefaultValue(0.0); + m_pControlValueType->setAndConfirm(0.0); + m_pControlValueDefault->setAndConfirm(0.0); + m_pControlValueMaximum->set(0.0); + m_pControlValueMaximumLimit->setAndConfirm(0.0); + m_pControlValueMinimum->set(0.0); + m_pControlValueMinimumLimit->setAndConfirm(0.0); + m_pControlLinkType->set(EffectManifestParameter::LINK_NONE); + emit(updated()); +} + +void EffectParameterSlot::slotLoaded(double v) { + qDebug() << debugString() << "slotLoaded" << v; + qDebug() << "WARNING: loaded is a read-only control."; +} + +void EffectParameterSlot::slotLinkType(double v) { + qDebug() << debugString() << "slotLinkType" << v; + if (m_pEffectParameter) { + m_pEffectParameter->setLinkType( + static_cast(v)); + } +} + +void EffectParameterSlot::slotValue(double v) { + qDebug() << debugString() << "slotValue" << v; + + double dMin = m_pControlValueMinimum->get(); + double dMax = m_pControlValueMaximum->get(); + if (v < dMin || v > dMax) { + qDebug() << debugString() << "value out of limits"; + v = math_clamp(v, dMin, dMax); + m_pControlValue->set(v); + } + double dNormalized = (dMax - dMin > 0) ? (v - dMin) / (dMax - dMin) : 0.0f; + m_pControlValueNormalized->set(dNormalized); + + if (m_pEffectParameter) { + m_pEffectParameter->setValue(v); + } +} + +void EffectParameterSlot::slotValueNormalized(double v) { + qDebug() << debugString() << "slotValueNormalized" << v; + + // Set the raw value to match the interpolated equivalent. + double dMin = m_pControlValueMinimum->get(); + double dMax = m_pControlValueMaximum->get(); + // TODO(rryan) implement curve types, just linear for now. + double dRaw = dMin + v * (dMax - dMin); + qDebug() << debugString() << "Normalized set of" << v << "produces raw value of" << dRaw; + m_pControlValue->set(dRaw); + + if (m_pEffectParameter) { + m_pEffectParameter->setValue(dRaw); + } +} + +void EffectParameterSlot::slotValueType(double v) { + qDebug() << debugString() << "slotValueType" << v; + qDebug() << "WARNING: value_type is a read-only control."; +} + +void EffectParameterSlot::slotValueDefault(double v) { + qDebug() << debugString() << "slotValueDefault" << v; + qDebug() << "WARNING: value_default is a read-only control."; +} + +void EffectParameterSlot::slotValueMaximum(double v) { + qDebug() << debugString() << "slotValueMaximum" << v; + double dMaxLimit = m_pControlValueMaximumLimit->get(); + if (v > dMaxLimit) { + qDebug() << "WARNING: Maximum parameter value is out of limits."; + v = dMaxLimit; + m_pControlValueMaximum->set(v); + } + if (m_pEffectParameter) { + m_pEffectParameter->setMaximum(v); + } +} + +void EffectParameterSlot::slotValueMaximumLimit(double v) { + qDebug() << debugString() << "slotValueMaximumLimit" << v; + qDebug() << "WARNING: value_max_limit is a read-only control."; +} + +void EffectParameterSlot::slotValueMinimum(double v) { + qDebug() << debugString() << "slotValueMinimum" << v; + double dMinLimit = m_pControlValueMinimumLimit->get(); + if (v < dMinLimit) { + qDebug() << "WARNING: Minimum parameter value is out of limits."; + v = dMinLimit; + m_pControlValueMinimum->set(v); + } + + if (m_pEffectParameter) { + m_pEffectParameter->setMinimum(v); + } +} + +void EffectParameterSlot::slotValueMinimumLimit(double v) { + qDebug() << debugString() << "slotValueMinimumLimit" << v; + qDebug() << "WARNING: value_min_limit is a read-only control."; +} + +void EffectParameterSlot::slotParameterValueChanged(QVariant value) { + m_pControlValue->set(value.toDouble()); + + if (!m_pEffectParameter) { + return; + } + + double dValue = value.toDouble(); + double dMinimum = m_pEffectParameter->getMinimum().toDouble(); + double dMaximum = m_pEffectParameter->getMaximum().toDouble(); + if (dMaximum - dMinimum != 0.0) { + double dNormalized = (dValue - dMinimum) / (dMaximum - dMinimum); + m_pControlValueNormalized->set(dNormalized); + } +} diff --git a/src/effects/effectparameterslot.h b/src/effects/effectparameterslot.h new file mode 100644 index 00000000000..102ce084cfd --- /dev/null +++ b/src/effects/effectparameterslot.h @@ -0,0 +1,98 @@ +#ifndef EFFECTPARAMETERSLOT_H +#define EFFECTPARAMETERSLOT_H + +#include +#include +#include + +#include "util.h" +#include "controlobject.h" +#include "effects/effect.h" + +class ControlObject; +class ControlPushButton; + +class EffectParameterSlot; +typedef QSharedPointer EffectParameterSlotPointer; + +class EffectParameterSlot : public QObject { + Q_OBJECT + public: + EffectParameterSlot(QObject* pParent, + const unsigned int iRackNumber, + const unsigned int iChainNumber, + const unsigned int iSlotNumber, + const unsigned int iParameterNumber); + virtual ~EffectParameterSlot(); + + static QString formatGroupString(const unsigned int iRackNumber, + const unsigned int iChainNumber, + const unsigned int iSlotNumber, + const unsigned int iParameterNumber) { + return QString("[EffectRack%1_EffectUnit%2_Effect%3_Parameter%4]") + .arg(QString::number(iRackNumber+1), + QString::number(iChainNumber+1), + QString::number(iSlotNumber+1), + QString::number(iParameterNumber+1)); + } + + // Load the parameter of the given effect into this EffectParameterSlot + void loadEffect(EffectPointer pEffect); + + QString name() const; + QString description() const; + + signals: + // Signal that indicates that the EffectParameterSlot has been updated. + void updated(); + + private slots: + // Solely for handling control changes + void slotLoaded(double v); + void slotLinkType(double v); + void slotValue(double v); + void slotValueNormalized(double v); + void slotValueType(double v); + void slotValueDefault(double v); + void slotValueMaximum(double v); + void slotValueMaximumLimit(double v); + void slotValueMinimum(double v); + void slotValueMinimumLimit(double v); + + void slotParameterValueChanged(QVariant value); + + private: + QString debugString() const { + return QString("EffectParameterSlot(%1,%2)").arg(m_group).arg(m_iParameterNumber); + } + + // Clear the currently loaded effect + void clear(); + + const unsigned int m_iRackNumber; + const unsigned int m_iChainNumber; + const unsigned int m_iSlotNumber; + const unsigned int m_iParameterNumber; + const QString m_group; + EffectPointer m_pEffect; + EffectParameter* m_pEffectParameter; + + //////////////////////////////////////////////////////////////////////////////// + // Controls exposed to the rest of Mixxx + //////////////////////////////////////////////////////////////////////////////// + + ControlObject* m_pControlLoaded; + ControlPushButton* m_pControlLinkType; + ControlObject* m_pControlValue; + ControlObject* m_pControlValueNormalized; + ControlObject* m_pControlValueType; + ControlObject* m_pControlValueDefault; + ControlObject* m_pControlValueMaximum; + ControlObject* m_pControlValueMaximumLimit; + ControlObject* m_pControlValueMinimum; + ControlObject* m_pControlValueMinimumLimit; + + DISALLOW_COPY_AND_ASSIGN(EffectParameterSlot); +}; + +#endif /* EFFECTPARAMETERSLOT_H */ diff --git a/src/effects/effectprocessor.h b/src/effects/effectprocessor.h new file mode 100644 index 00000000000..fa83de09526 --- /dev/null +++ b/src/effects/effectprocessor.h @@ -0,0 +1,77 @@ +#ifndef EFFECTPROCESSOR_H +#define EFFECTPROCESSOR_H + +#include +#include + +#include "defs.h" + +class EngineEffect; + +class EffectProcessor { + public: + virtual ~EffectProcessor() { } + + virtual void initialize(const QSet& registeredGroups) = 0; + + // Take a buffer of numSamples samples of audio from group, provided as + // pInput, process the buffer according to Effect-specific logic, and output + // it to the buffer pOutput. If pInput is equal to pOutput, then the + // operation must occur in-place. Both pInput and pOutput are represented as + // stereo interleaved samples. There are numSamples total samples, so + // numSamples/2 left channel samples and numSamples/2 right channel + // samples. The group provided allows the effect to maintain state on a + // per-group basis. This is important because one Effect instance may be + // used to process the audio of multiple channels. + virtual void process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) = 0; +}; + +// Helper class for automatically fetching group state parameters upon receipt +// of a group-specific process call. +template +class GroupEffectProcessor : public EffectProcessor { + public: + GroupEffectProcessor() {} + virtual ~GroupEffectProcessor() { + for (typename QMap::iterator it = m_groupState.begin(); + it != m_groupState.end();) { + T* pState = it.value(); + it = m_groupState.erase(it); + delete pState; + } + } + + virtual void initialize(const QSet& registeredGroups) { + foreach (const QString& group, registeredGroups) { + T* pState = m_groupState.value(group, NULL); + if (pState == NULL) { + pState = new T(); + m_groupState[group] = pState; + } + } + } + + virtual void process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + T* pState = m_groupState.value(group, NULL); + if (pState == NULL) { + pState = new T(); + m_groupState[group] = pState; + qWarning() << "Allocated group state in the engine for" << group; + } + processGroup(group, pState, pInput, pOutput, numSamples); + } + + virtual void processGroup(const QString& group, + T* groupState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) = 0; + + private: + QMap m_groupState; +}; + +#endif /* EFFECTPROCESSOR_H */ diff --git a/src/effects/effectrack.cpp b/src/effects/effectrack.cpp new file mode 100644 index 00000000000..28fe1afe7c7 --- /dev/null +++ b/src/effects/effectrack.cpp @@ -0,0 +1,216 @@ +#include "effects/effectrack.h" + +#include "effects/effectsmanager.h" +#include "effects/effectchainmanager.h" +#include "engine/effects/engineeffectrack.h" + +EffectRack::EffectRack(EffectsManager* pEffectsManager, + EffectChainManager* pEffectChainManager, + const unsigned int iRackNumber) + : m_iRackNumber(iRackNumber), + m_group(formatGroupString(m_iRackNumber)), + m_pEffectsManager(pEffectsManager), + m_pEffectChainManager(pEffectChainManager), + m_controlNumEffectChainSlots(ConfigKey(m_group, "num_effectunits")), + m_controlClearRack(ConfigKey(m_group, "clear")), + m_pEngineEffectRack(new EngineEffectRack(iRackNumber)) { + connect(&m_controlClearRack, SIGNAL(valueChanged(double)), + this, SLOT(slotClearRack(double))); + m_controlNumEffectChainSlots.connectValueChangeRequest( + this, SLOT(slotNumEffectChainSlots(double)), Qt::AutoConnection); + addToEngine(); +} + +EffectRack::~EffectRack() { + removeFromEngine(); +} + +EngineEffectRack* EffectRack::getEngineEffectRack() { + return m_pEngineEffectRack; +} + +void EffectRack::addToEngine() { + EffectsRequest* pRequest = new EffectsRequest(); + pRequest->type = EffectsRequest::ADD_EFFECT_RACK; + pRequest->AddEffectRack.pRack = m_pEngineEffectRack; + m_pEffectsManager->writeRequest(pRequest); + + // Add all effect chains. + for (int i = 0; i < m_effectChainSlots.size(); ++i) { + EffectChainSlotPointer pSlot = m_effectChainSlots[i]; + EffectChainPointer pChain = pSlot->getEffectChain(); + if (pChain) { + // Add the effect to the engine. + pChain->addToEngine(m_pEngineEffectRack, i); + // Update its parameters in the engine. + pChain->updateEngineState(); + } + } +} + +void EffectRack::removeFromEngine() { + // Order doesn't matter when removing. + for (int i = 0; i < m_effectChainSlots.size(); ++i) { + EffectChainSlotPointer pSlot = m_effectChainSlots[i]; + EffectChainPointer pChain = pSlot->getEffectChain(); + if (pChain) { + pChain->removeFromEngine(m_pEngineEffectRack, i); + } + } + + EffectsRequest* pRequest = new EffectsRequest(); + pRequest->type = EffectsRequest::REMOVE_EFFECT_RACK; + pRequest->RemoveEffectRack.pRack = m_pEngineEffectRack; + m_pEffectsManager->writeRequest(pRequest); +} + +void EffectRack::registerGroup(const QString& group) { + foreach (EffectChainSlotPointer pChainSlot, m_effectChainSlots) { + pChainSlot->registerGroup(group); + } +} + +void EffectRack::slotNumEffectChainSlots(double v) { + // Ignore sets to num_effectchain_slots + qDebug() << debugString() << "slotNumEffectChainSlots" << v; + qDebug() << "WARNING: num_effectchain_slots is a read-only control."; +} + +void EffectRack::slotClearRack(double v) { + if (v > 0) { + foreach (EffectChainSlotPointer pChainSlot, m_effectChainSlots) { + pChainSlot->clear(); + } + } +} + +int EffectRack::numEffectChainSlots() const { + return m_effectChainSlots.size(); +} + +EffectChainSlotPointer EffectRack::addEffectChainSlot() { + int iChainSlotNumber = m_effectChainSlots.size(); + EffectChainSlot* pChainSlot = + new EffectChainSlot(this, m_iRackNumber, iChainSlotNumber); + + // TODO(rryan) How many should we make default? They create controls that + // the GUI may rely on, so the choice is important to communicate to skin + // designers. + // TODO(rryan): This should not be done here. + pChainSlot->addEffectSlot(); + pChainSlot->addEffectSlot(); + pChainSlot->addEffectSlot(); + pChainSlot->addEffectSlot(); + + connect(pChainSlot, SIGNAL(nextChain(unsigned int, EffectChainPointer)), + this, SLOT(loadNextChain(unsigned int, EffectChainPointer))); + connect(pChainSlot, SIGNAL(prevChain(unsigned int, EffectChainPointer)), + this, SLOT(loadPrevChain(unsigned int, EffectChainPointer))); + + connect(pChainSlot, SIGNAL(nextEffect(unsigned int, unsigned int, EffectPointer)), + this, SLOT(loadNextEffect(unsigned int, unsigned int, EffectPointer))); + connect(pChainSlot, SIGNAL(prevEffect(unsigned int, unsigned int, EffectPointer)), + this, SLOT(loadPrevEffect(unsigned int, unsigned int, EffectPointer))); + + // Register all the existing channels with the new EffectChain + const QSet& registeredGroups = + m_pEffectChainManager->registeredGroups(); + foreach (const QString& group, registeredGroups) { + pChainSlot->registerGroup(group); + } + + EffectChainSlotPointer pChainSlotPointer = EffectChainSlotPointer(pChainSlot); + m_effectChainSlots.append(pChainSlotPointer); + m_controlNumEffectChainSlots.setAndConfirm( + m_controlNumEffectChainSlots.get() + 1); + + // Now load an empty effect chain into the slot so that users can edit + // effect slots on the fly without having to load a chain. + EffectChainPointer pChain(new EffectChain(m_pEffectsManager, QString(), + EffectChainPointer())); + pChain->setName("Empty Chain"); + pChainSlotPointer->loadEffectChain(pChain); + + return pChainSlotPointer; +} + +EffectChainSlotPointer EffectRack::getEffectChainSlot(int i) { + if (i < 0 || i >= m_effectChainSlots.size()) { + qDebug() << "WARNING: Invalid index for getEffectChainSlot"; + return EffectChainSlotPointer(); + } + return m_effectChainSlots[i]; +} + +void EffectRack::loadNextChain(const unsigned int iChainSlotNumber, + EffectChainPointer pLoadedChain) { + if (pLoadedChain) { + pLoadedChain = pLoadedChain->prototype(); + } + + EffectChainPointer pNextChain = m_pEffectChainManager->getNextEffectChain( + pLoadedChain); + + pNextChain = EffectChain::clone(pNextChain); + m_effectChainSlots[iChainSlotNumber]->loadEffectChain(pNextChain); +} + + +void EffectRack::loadPrevChain(const unsigned int iChainSlotNumber, + EffectChainPointer pLoadedChain) { + if (pLoadedChain) { + pLoadedChain = pLoadedChain->prototype(); + } + + EffectChainPointer pPrevChain = m_pEffectChainManager->getPrevEffectChain( + pLoadedChain); + + pPrevChain = EffectChain::clone(pPrevChain); + m_effectChainSlots[iChainSlotNumber]->loadEffectChain(pPrevChain); +} + +void EffectRack::loadNextEffect(const unsigned int iChainSlotNumber, + const unsigned int iEffectSlotNumber, + EffectPointer pEffect) { + if (iChainSlotNumber >= m_effectChainSlots.size()) { + return; + } + + QString effectId = pEffect ? pEffect->getManifest().id() : QString(); + QString nextEffectId = m_pEffectsManager->getNextEffectId(effectId); + EffectPointer pNextEffect = m_pEffectsManager->instantiateEffect(nextEffectId); + + EffectChainSlotPointer pChainSlot = m_effectChainSlots[iChainSlotNumber]; + EffectChainPointer pChain = pChainSlot->getEffectChain(); + if (!pChain) { + pChain = EffectChainPointer(new EffectChain(m_pEffectsManager, QString(), + EffectChainPointer())); + pChain->setName("Empty Chain"); + pChainSlot->loadEffectChain(pChain); + } + pChain->replaceEffect(iEffectSlotNumber, pNextEffect); +} + + +void EffectRack::loadPrevEffect(const unsigned int iChainSlotNumber, + const unsigned int iEffectSlotNumber, + EffectPointer pEffect) { + if (iChainSlotNumber >= m_effectChainSlots.size()) { + return; + } + + QString effectId = pEffect ? pEffect->getManifest().id() : QString(); + QString prevEffectId = m_pEffectsManager->getPrevEffectId(effectId); + EffectPointer pPrevEffect = m_pEffectsManager->instantiateEffect(prevEffectId); + + EffectChainSlotPointer pChainSlot = m_effectChainSlots[iChainSlotNumber]; + EffectChainPointer pChain = pChainSlot->getEffectChain(); + if (!pChain) { + pChain = EffectChainPointer(new EffectChain(m_pEffectsManager, QString(), + EffectChainPointer())); + pChain->setName("Empty Chain"); + pChainSlot->loadEffectChain(pChain); + } + + pChain->replaceEffect(iEffectSlotNumber, pPrevEffect); +} diff --git a/src/effects/effectrack.h b/src/effects/effectrack.h new file mode 100644 index 00000000000..c2b7565162c --- /dev/null +++ b/src/effects/effectrack.h @@ -0,0 +1,73 @@ +#ifndef EFFECTRACK_H +#define EFFECTRACK_H + +#include +#include +#include + +#include "controlobject.h" +#include "effects/effectchainslot.h" + +class EngineEffectRack; +class EffectsManager; +class EffectChainManager; + +class EffectRack; +typedef QSharedPointer EffectRackPointer; + +class EffectRack : public QObject { + Q_OBJECT + public: + EffectRack(EffectsManager* pEffectsManager, + EffectChainManager* pChainManager, + const unsigned int iRackNumber); + virtual ~EffectRack(); + + static QString formatGroupString(const unsigned int iRackNumber) { + return QString("[EffectRack%1]").arg(iRackNumber); + } + + void addToEngine(); + void removeFromEngine(); + EngineEffectRack* getEngineEffectRack(); + + void registerGroup(const QString& group); + int numEffectChainSlots() const; + EffectChainSlotPointer addEffectChainSlot(); + EffectChainSlotPointer getEffectChainSlot(int i); + + public slots: + void slotClearRack(double v); + void slotNumEffectChainSlots(double v); + + private slots: + void loadNextChain(const unsigned int iChainSlotNumber, + EffectChainPointer pLoadedChain); + void loadPrevChain(const unsigned int iChainSlotNumber, + EffectChainPointer pLoadedChain); + + void loadNextEffect(const unsigned int iChainSlotNumber, + const unsigned int iEffectSlotNumber, + EffectPointer pEffect); + void loadPrevEffect(const unsigned int iChainSlotNumber, + const unsigned int iEffectSlotNumber, + EffectPointer pEffect); + + private: + inline QString debugString() const { + return QString("EffectRack%1").arg(m_iRackNumber); + } + + const unsigned int m_iRackNumber; + const QString m_group; + + EffectsManager* m_pEffectsManager; + EffectChainManager* m_pEffectChainManager; + QList m_effectChainSlots; + ControlObject m_controlNumEffectChainSlots; + ControlObject m_controlClearRack; + EngineEffectRack* m_pEngineEffectRack; +}; + + +#endif /* EFFECTRACK_H */ diff --git a/src/effects/effectsbackend.cpp b/src/effects/effectsbackend.cpp new file mode 100644 index 00000000000..7b4a68a7205 --- /dev/null +++ b/src/effects/effectsbackend.cpp @@ -0,0 +1,59 @@ +#include + +#include "effects/effectsbackend.h" +#include "effects/effectsmanager.h" + +EffectsBackend::EffectsBackend(QObject* pParent, QString name) + : QObject(pParent), + m_name(name) { +} + +EffectsBackend::~EffectsBackend() { + m_registeredEffects.clear(); +} + +const QString EffectsBackend::getName() const { + return m_name; +} + +void EffectsBackend::registerEffect(const QString& id, + const EffectManifest& manifest, + EffectInstantiatorPointer pInstantiator) { + if (m_registeredEffects.contains(id)) { + qDebug() << "WARNING: Effect" << id << "already registered"; + return; + } + + m_registeredEffects[id] = QPair( + manifest, pInstantiator); + emit(effectRegistered()); +} + +const QSet EffectsBackend::getEffectIds() const { + return QSet::fromList(m_registeredEffects.keys()); +} + +EffectManifest EffectsBackend::getManifest(const QString& effectId) const { + if (!m_registeredEffects.contains(effectId)) { + qDebug() << "WARNING: Effect" << effectId << "is not registered."; + return EffectManifest(); + } + return m_registeredEffects[effectId].first; +} + +bool EffectsBackend::canInstantiateEffect(const QString& effectId) const { + return m_registeredEffects.contains(effectId); +} + +EffectPointer EffectsBackend::instantiateEffect(EffectsManager* pEffectsManager, + const QString& effectId) { + if (!m_registeredEffects.contains(effectId)) { + qDebug() << "WARNING: Effect" << effectId << "is not registered."; + return EffectPointer(); + } + QPair& effectInfo = + m_registeredEffects[effectId]; + + return EffectPointer(new Effect(this, pEffectsManager, + effectInfo.first, effectInfo.second)); +} diff --git a/src/effects/effectsbackend.h b/src/effects/effectsbackend.h new file mode 100644 index 00000000000..2c25af1cb8d --- /dev/null +++ b/src/effects/effectsbackend.h @@ -0,0 +1,55 @@ +#ifndef EFFECTSBACKEND_H +#define EFFECTSBACKEND_H + +#include +#include +#include +#include + +#include "effects/effect.h" +#include "effects/effectinstantiator.h" + +class EffectsManager; +class EffectsBackend; +class EffectProcessor; + +// An EffectsBackend is an implementation of a provider of Effect's for use +// within the rest of Mixxx. The job of the EffectsBackend is to both enumerate +// and instantiate effects. +class EffectsBackend : public QObject { + Q_OBJECT + public: + EffectsBackend(QObject* pParent, QString name); + virtual ~EffectsBackend(); + + virtual const QString getName() const; + + virtual const QSet getEffectIds() const; + virtual EffectManifest getManifest(const QString& effectId) const; + virtual bool canInstantiateEffect(const QString& effectId) const; + virtual EffectPointer instantiateEffect( + EffectsManager* pEffectsManager, const QString& effectId); + + signals: + void effectRegistered(); + + protected: + void registerEffect(const QString& id, + const EffectManifest& manifest, + EffectInstantiatorPointer pInstantiator); + + template + void registerEffect() { + registerEffect( + EffectProcessorImpl::getId(), + EffectProcessorImpl::getManifest(), + EffectInstantiatorPointer( + new EffectProcessorInstantiator())); + } + + private: + QString m_name; + QMap > m_registeredEffects; +}; + +#endif /* EFFECTSBACKEND_H */ diff --git a/src/effects/effectslot.cpp b/src/effects/effectslot.cpp new file mode 100644 index 00000000000..b0d6231d35a --- /dev/null +++ b/src/effects/effectslot.cpp @@ -0,0 +1,198 @@ +#include "effects/effectslot.h" + +#include "controlpushbutton.h" + +// The maximum number of effect parameters we're going to support. +const unsigned int kDefaultMaxParameters = 32; + +EffectSlot::EffectSlot(QObject* pParent, const unsigned int iRackNumber, + const unsigned int iChainNumber, + const unsigned int iEffectnumber) + : QObject(), + m_iRackNumber(iRackNumber), + m_iChainNumber(iChainNumber), + m_iEffectNumber(iEffectnumber), + // The control group names are 1-indexed while internally everything + // is 0-indexed. + m_group(formatGroupString(m_iRackNumber, m_iChainNumber, + m_iEffectNumber)) { + m_pControlLoaded = new ControlObject(ConfigKey(m_group, "loaded")); + m_pControlLoaded->connectValueChangeRequest( + this, SLOT(slotLoaded(double)), Qt::AutoConnection); + + m_pControlNumParameters = new ControlObject(ConfigKey(m_group, "num_parameters")); + m_pControlNumParameters->connectValueChangeRequest( + this, SLOT(slotNumParameters(double)), Qt::AutoConnection); + + m_pControlNumParameterSlots = new ControlObject(ConfigKey(m_group, "num_parameterslots")); + m_pControlNumParameterSlots->connectValueChangeRequest( + this, SLOT(slotNumParameterSlots(double)), Qt::AutoConnection); + + m_pControlEnabled = new ControlPushButton(ConfigKey(m_group, "enabled")); + m_pControlEnabled->setButtonMode(ControlPushButton::POWERWINDOW); + // Default to enabled. The skin might not show these buttons. + m_pControlEnabled->setDefaultValue(true); + m_pControlEnabled->set(true); + connect(m_pControlEnabled, SIGNAL(valueChanged(double)), + this, SLOT(slotEnabled(double))); + + m_pControlNextEffect = new ControlPushButton(ConfigKey(m_group, "next_effect")); + connect(m_pControlNextEffect, SIGNAL(valueChanged(double)), + this, SLOT(slotNextEffect(double))); + + m_pControlPrevEffect = new ControlPushButton(ConfigKey(m_group, "prev_effect")); + connect(m_pControlPrevEffect, SIGNAL(valueChanged(double)), + this, SLOT(slotPrevEffect(double))); + + m_pControlEffectSelector = new ControlObject(ConfigKey(m_group, "effect_selector")); + connect(m_pControlEffectSelector, SIGNAL(valueChanged(double)), + this, SLOT(slotEffectSelector(double))); + + m_pControlClear = new ControlPushButton(ConfigKey(m_group, "clear")); + connect(m_pControlClear, SIGNAL(valueChanged(double)), + this, SLOT(slotClear(double))); + + for (unsigned int i = 0; i < kDefaultMaxParameters; ++i) { + addEffectParameterSlot(); + } + + clear(); +} + +EffectSlot::~EffectSlot() { + qDebug() << debugString() << "destroyed"; + clear(); + + delete m_pControlLoaded; + delete m_pControlNumParameters; + delete m_pControlNumParameterSlots; + delete m_pControlNextEffect; + delete m_pControlPrevEffect; + delete m_pControlEffectSelector; + delete m_pControlClear; + delete m_pControlEnabled; +} + +EffectParameterSlotPointer EffectSlot::addEffectParameterSlot() { + EffectParameterSlotPointer pParameter = EffectParameterSlotPointer( + new EffectParameterSlot(this, m_iRackNumber, m_iChainNumber, m_iEffectNumber, + m_parameters.size())); + m_parameters.append(pParameter); + m_pControlNumParameterSlots->setAndConfirm( + m_pControlNumParameterSlots->get() + 1); + return pParameter; +} + +EffectPointer EffectSlot::getEffect() const { + return m_pEffect; +} + +unsigned int EffectSlot::numParameterSlots() const { + return m_parameters.size(); +} + +void EffectSlot::slotLoaded(double v) { + qDebug() << debugString() << "slotLoaded" << v; + qDebug() << "WARNING: loaded is a read-only control."; +} + +void EffectSlot::slotNumParameters(double v) { + qDebug() << debugString() << "slotNumParameters" << v; + qDebug() << "WARNING: num_parameters is a read-only control."; +} + +void EffectSlot::slotNumParameterSlots(double v) { + qDebug() << debugString() << "slotNumParameterSlots" << v; + qDebug() << "WARNING: num_parameterslots is a read-only control."; +} + +void EffectSlot::slotEnabled(double v) { + qDebug() << debugString() << "slotEnabled" << v; + if (m_pEffect) { + m_pEffect->setEnabled(v > 0); + } +} + +void EffectSlot::slotEffectEnabledChanged(bool enabled) { + m_pControlEnabled->set(enabled); +} + +EffectParameterSlotPointer EffectSlot::getEffectParameterSlot(unsigned int slotNumber) { + qDebug() << debugString() << "getEffectParameterSlot" << slotNumber; + if (slotNumber >= m_parameters.size()) { + qDebug() << "WARNING: slotNumber out of range"; + return EffectParameterSlotPointer(); + } + return m_parameters[slotNumber]; +} + +void EffectSlot::loadEffect(EffectPointer pEffect) { + qDebug() << debugString() << "loadEffect" + << (pEffect ? pEffect->getManifest().name() : "(null)"); + if (pEffect) { + m_pEffect = pEffect; + m_pControlLoaded->setAndConfirm(1.0); + m_pControlNumParameters->setAndConfirm(m_pEffect->numParameters()); + + // Enabled is a persistent property of the effect slot, not of the + // effect. Propagate the current setting to the effect. + m_pEffect->setEnabled(m_pControlEnabled->get() > 0.0); + + connect(m_pEffect.data(), SIGNAL(enabledChanged(bool)), + this, SLOT(slotEffectEnabledChanged(bool))); + + while (m_parameters.size() < m_pEffect->numParameters()) { + addEffectParameterSlot(); + } + + foreach (EffectParameterSlotPointer pParameter, m_parameters) { + pParameter->loadEffect(m_pEffect); + } + + emit(effectLoaded(m_pEffect, m_iEffectNumber)); + } else { + clear(); + // Broadcasts a null effect pointer + emit(effectLoaded(m_pEffect, m_iEffectNumber)); + } + emit(updated()); +} + +void EffectSlot::clear() { + if (m_pEffect) { + m_pEffect->disconnect(this); + m_pEffect.clear(); + } + m_pControlLoaded->setAndConfirm(0.0); + m_pControlNumParameters->setAndConfirm(0.0); + foreach (EffectParameterSlotPointer pParameter, m_parameters) { + pParameter->loadEffect(EffectPointer()); + } + emit(updated()); +} + +void EffectSlot::slotPrevEffect(double v) { + if (v > 0) { + slotEffectSelector(-1); + } +} + +void EffectSlot::slotNextEffect(double v) { + if (v > 0) { + slotEffectSelector(1); + } +} + +void EffectSlot::slotEffectSelector(double v) { + if (v > 0) { + emit(nextEffect(m_iChainNumber, m_iEffectNumber, m_pEffect)); + } else if (v < 0) { + emit(prevEffect(m_iChainNumber, m_iEffectNumber, m_pEffect)); + } +} + +void EffectSlot::slotClear(double v) { + if (v > 0) { + emit(clearEffect(m_iChainNumber, m_iEffectNumber, m_pEffect)); + } +} diff --git a/src/effects/effectslot.h b/src/effects/effectslot.h new file mode 100644 index 00000000000..f2b3cde8849 --- /dev/null +++ b/src/effects/effectslot.h @@ -0,0 +1,109 @@ +#ifndef EFFECTSLOT_H +#define EFFECTSLOT_H + +#include +#include +#include + +#include "util.h" +#include "controlobject.h" +#include "controlpushbutton.h" +#include "effects/effect.h" +#include "effects/effectparameterslot.h" + +class EffectSlot; +typedef QSharedPointer EffectSlotPointer; + +class EffectSlot : public QObject { + Q_OBJECT + public: + EffectSlot(QObject* pParent, + const unsigned int iRackNumber, + const unsigned int iChainNumber, + const unsigned int iEffectNumber); + virtual ~EffectSlot(); + + static QString formatGroupString(const unsigned int iRackNumber, + const unsigned int iChainNumber, + const unsigned int iEffectNumber) { + return QString("[EffectRack%1_EffectUnit%2_Effect%3]").arg( + QString::number(iRackNumber+1), + QString::number(iChainNumber+1), + QString::number(iEffectNumber+1)); + + } + + // Return the currently loaded effect, if any. If no effect is loaded, + // returns a null EffectPointer. + EffectPointer getEffect() const; + + unsigned int numParameterSlots() const; + EffectParameterSlotPointer addEffectParameterSlot(); + EffectParameterSlotPointer getEffectParameterSlot(unsigned int slotNumber); + + public slots: + // Request that this EffectSlot load the given Effect + void loadEffect(EffectPointer pEffect); + + void slotLoaded(double v); + void slotNumParameters(double v); + void slotNumParameterSlots(double v); + void slotEnabled(double v); + void slotNextEffect(double v); + void slotPrevEffect(double v); + void slotClear(double v); + void slotEffectSelector(double v); + void slotEffectEnabledChanged(bool enabled); + + signals: + // Indicates that the effect pEffect has been loaded into this + // EffectSlot. The effectSlotNumber is provided for the convenience of + // listeners. pEffect may be an invalid pointer, which indicates that a + // previously loaded effect was removed from the slot. + void effectLoaded(EffectPointer pEffect, unsigned int effectSlotNumber); + + // Signal that whoever is in charge of this EffectSlot should load the next + // Effect into it. + void nextEffect(unsigned int iChainNumber, unsigned int iEffectNumber, + EffectPointer pEffect); + + // Signal that whoever is in charge of this EffectSlot should load the + // previous Effect into it. + void prevEffect(unsigned int iChainNumber, unsigned int iEffectNumber, + EffectPointer pEffect); + + // Signal that whoever is in charge of this EffectSlot should clear this + // EffectSlot (by deleting the effect from the underlying chain). + void clearEffect(unsigned int iChainNumber, unsigned int iEffectNumber, + EffectPointer pEffect); + + void updated(); + + private: + QString debugString() const { + return QString("EffectSlot(%1,%2)").arg(m_iChainNumber).arg(m_iEffectNumber); + } + + // Unload the currently loaded effect + void clear(); + + const unsigned int m_iRackNumber; + const unsigned int m_iChainNumber; + const unsigned int m_iEffectNumber; + const QString m_group; + EffectPointer m_pEffect; + + ControlObject* m_pControlLoaded; + ControlPushButton* m_pControlEnabled; + ControlObject* m_pControlNumParameters; + ControlObject* m_pControlNumParameterSlots; + ControlObject* m_pControlNextEffect; + ControlObject* m_pControlPrevEffect; + ControlObject* m_pControlEffectSelector; + ControlObject* m_pControlClear; + QList m_parameters; + + DISALLOW_COPY_AND_ASSIGN(EffectSlot); +}; + +#endif /* EFFECTSLOT_H */ diff --git a/src/effects/effectsmanager.cpp b/src/effects/effectsmanager.cpp new file mode 100644 index 00000000000..5d24a8e92e4 --- /dev/null +++ b/src/effects/effectsmanager.cpp @@ -0,0 +1,235 @@ +#include "effects/effectsmanager.h" + +#include +#include + +#include "effects/effectchainmanager.h" +#include "engine/effects/engineeffectsmanager.h" + +EffectsManager::EffectsManager(QObject* pParent, ConfigObject* pConfig) + : QObject(pParent), + m_pEffectChainManager(new EffectChainManager(pConfig, this)), + m_nextRequestId(0) { + qRegisterMetaType("EffectChain::InsertionType"); + QPair requestPipes = + TwoWayMessagePipe::makeTwoWayMessagePipe( + 2048, 2048, false, false); + + m_pRequestPipe.reset(requestPipes.first); + m_pEngineEffectsManager = new EngineEffectsManager(requestPipes.second); +} + +EffectsManager::~EffectsManager() { + m_pEffectChainManager->saveEffectChains(); + processEffectsResponses(); + delete m_pEffectChainManager; + while (!m_effectsBackends.isEmpty()) { + EffectsBackend* pBackend = m_effectsBackends.takeLast(); + delete pBackend; + } + for (QHash::iterator it = m_activeRequests.begin(); + it != m_activeRequests.end();) { + delete it.value(); + it = m_activeRequests.erase(it); + } +} + +void EffectsManager::addEffectsBackend(EffectsBackend* pBackend) { + Q_ASSERT(pBackend); + m_effectsBackends.append(pBackend); + connect(pBackend, SIGNAL(effectRegistered()), + this, SIGNAL(availableEffectsUpdated())); +} + +void EffectsManager::registerGroup(const QString& group) { + m_pEffectChainManager->registerGroup(group); +} + +const QSet& EffectsManager::registeredGroups() const { + return m_pEffectChainManager->registeredGroups(); +} + +const QSet EffectsManager::getAvailableEffects() const { + QSet availableEffects; + + foreach (EffectsBackend* pBackend, m_effectsBackends) { + QSet backendEffects = pBackend->getEffectIds(); + foreach (QString effectId, backendEffects) { + if (availableEffects.contains(effectId)) { + qDebug() << "WARNING: Duplicate effect ID" << effectId; + continue; + } + availableEffects.insert(effectId); + } + } + + return availableEffects; +} + +QString EffectsManager::getNextEffectId(const QString& effectId) { + // TODO(rryan): HACK SUPER JANK ALERT. REPLACE THIS WITH SOMETHING NOT + // STUPID + QList effects = getAvailableEffects().toList(); + qSort(effects.begin(), effects.end()); + + if (effects.isEmpty()) { + return QString(); + } + + if (effectId.isNull()) { + return effects.first(); + } + + QList::const_iterator it = + qUpperBound(effects.constBegin(), effects.constEnd(), effectId); + if (it == effects.constEnd()) { + return effects.first(); + } + + return *it; +} + +QString EffectsManager::getPrevEffectId(const QString& effectId) { + // TODO(rryan): HACK SUPER JANK ALERT. REPLACE THIS WITH SOMETHING NOT + // STUPID + QList effects = getAvailableEffects().toList(); + qSort(effects.begin(), effects.end()); + + if (effects.isEmpty()) { + return QString(); + } + + if (effectId.isNull()) { + return effects.last(); + } + + QList::const_iterator it = + qLowerBound(effects.constBegin(), effects.constEnd(), effectId); + if (it == effects.constBegin()) { + return effects.last(); + } + + it--; + return *it; +} + +EffectManifest EffectsManager::getEffectManifest(const QString& effectId) const { + foreach (EffectsBackend* pBackend, m_effectsBackends) { + if (pBackend->canInstantiateEffect(effectId)) { + return pBackend->getManifest(effectId); + } + } + + return EffectManifest(); +} + +EffectPointer EffectsManager::instantiateEffect(const QString& effectId) { + foreach (EffectsBackend* pBackend, m_effectsBackends) { + if (pBackend->canInstantiateEffect(effectId)) { + return pBackend->instantiateEffect(this, effectId); + } + } + return EffectPointer(); +} + +EffectRackPointer EffectsManager::addEffectRack() { + return m_pEffectChainManager->addEffectRack(); +} + +EffectRackPointer EffectsManager::getEffectRack(int i) { + return m_pEffectChainManager->getEffectRack(i); +} + +void EffectsManager::setupDefaults() { + //m_pEffectChainManager->loadEffectChains(); + + EffectRackPointer pRack = addEffectRack(); + pRack->addEffectChainSlot(); + pRack->addEffectChainSlot(); + pRack->addEffectChainSlot(); + pRack->addEffectChainSlot(); + + QSet effects = getAvailableEffects(); + + EffectChainPointer pChain = EffectChainPointer(new EffectChain( + this, "org.mixxx.effectchain.flanger")); + pChain->setName(tr("Flanger")); + pChain->setParameter(0.0f); + EffectPointer pEffect = instantiateEffect( + "org.mixxx.effects.flanger"); + pChain->addEffect(pEffect); + m_pEffectChainManager->addEffectChain(pChain); + + pChain = EffectChainPointer(new EffectChain( + this, "org.mixxx.effectchain.bitcrusher")); + pChain->setName(tr("BitCrusher")); + pChain->setParameter(0.0f); + pEffect = instantiateEffect("org.mixxx.effects.bitcrusher"); + pChain->addEffect(pEffect); + m_pEffectChainManager->addEffectChain(pChain); + + pChain = EffectChainPointer(new EffectChain( + this, "org.mixxx.effectchain.filter")); + pChain->setName(tr("Filter")); + pChain->setParameter(0.0f); + pEffect = instantiateEffect("org.mixxx.effects.filter"); + pChain->addEffect(pEffect); + m_pEffectChainManager->addEffectChain(pChain); + + pChain = EffectChainPointer(new EffectChain( + this, "org.mixxx.effectchain.reverb")); + pChain->setName(tr("Reverb")); + pChain->setParameter(0.0f); + pEffect = instantiateEffect("org.mixxx.effects.reverb"); + pChain->addEffect(pEffect); + m_pEffectChainManager->addEffectChain(pChain); + + pChain = EffectChainPointer(new EffectChain( + this, "org.mixxx.effectchain.echo")); + pChain->setName(tr("Echo")); + pChain->setParameter(0.0f); + pEffect = instantiateEffect("org.mixxx.effects.echo"); + pChain->addEffect(pEffect); + m_pEffectChainManager->addEffectChain(pChain); +} + +bool EffectsManager::writeRequest(EffectsRequest* request) { + // This is effectively only GC at this point so only deal with responses + // when writing new requests. + processEffectsResponses(); + + request->request_id = m_nextRequestId++; + if (m_pRequestPipe->writeMessages(&request, 1) == 1) { + m_activeRequests[request->request_id] = request; + return true; + } + return false; +} + +void EffectsManager::processEffectsResponses() { + EffectsResponse response; + while (m_pRequestPipe->readMessages(&response, 1) == 1) { + QHash::iterator it = + m_activeRequests.find(response.request_id); + + if (it == m_activeRequests.end()) { + qDebug() << debugString() + << "WARNING: EffectsResponse with an inactive request_id:" + << response.request_id; + } + + while (it != m_activeRequests.end() && + it.key() == response.request_id) { + EffectsRequest* pRequest = it.value(); + + if (!response.success) { + qDebug() << debugString() << "WARNING: Failed EffectsRequest" + << "type" << pRequest->type; + } + + delete pRequest; + it = m_activeRequests.erase(it); + } + } + +} diff --git a/src/effects/effectsmanager.h b/src/effects/effectsmanager.h new file mode 100644 index 00000000000..b38bdd41d4b --- /dev/null +++ b/src/effects/effectsmanager.h @@ -0,0 +1,84 @@ +#ifndef EFFECTSMANAGER_H +#define EFFECTSMANAGER_H + +#include +#include +#include +#include +#include + +#include "configobject.h" +#include "util.h" +#include "util/fifo.h" +#include "effects/effect.h" +#include "effects/effectsbackend.h" +#include "effects/effectchainslot.h" +#include "effects/effectchain.h" +#include "effects/effectrack.h" +#include "engine/effects/message.h" + +class EngineEffectsManager; +class EffectChainManager; + +class EffectsManager : public QObject { + Q_OBJECT + public: + EffectsManager(QObject* pParent, ConfigObject* pConfig); + virtual ~EffectsManager(); + + EngineEffectsManager* getEngineEffectsManager() { + return m_pEngineEffectsManager; + } + + EffectChainManager* getEffectChainManager() { + return m_pEffectChainManager; + } + + // Add an effect backend to be managed by EffectsManager. EffectsManager + // takes ownership of the backend, and will delete it when EffectsManager is + // being deleted. Not thread safe -- use only from the GUI thread. + void addEffectsBackend(EffectsBackend* pEffectsBackend); + void registerGroup(const QString& group); + const QSet& registeredGroups() const; + + EffectRackPointer addEffectRack(); + EffectRackPointer getEffectRack(int rack); + + QString getNextEffectId(const QString& effectId); + QString getPrevEffectId(const QString& effectId); + + const QSet getAvailableEffects() const; + EffectManifest getEffectManifest(const QString& effectId) const; + EffectPointer instantiateEffect(const QString& effectId); + + // Temporary, but for setting up all the default EffectChains and EffectRack + void setupDefaults(); + + // Write an EffectsRequest to the EngineEffectsManager. EffectsManager takes + // ownership of request and deletes it once a response is received. + bool writeRequest(EffectsRequest* request); + + signals: + void availableEffectsUpdated(); + + private: + QString debugString() const { + return "EffectsManager"; + } + + void processEffectsResponses(); + + EffectChainManager* m_pEffectChainManager; + QList m_effectsBackends; + + EngineEffectsManager* m_pEngineEffectsManager; + + QScopedPointer m_pRequestPipe; + qint64 m_nextRequestId; + QHash m_activeRequests; + + DISALLOW_COPY_AND_ASSIGN(EffectsManager); +}; + + +#endif /* EFFECTSMANAGER_H */ diff --git a/src/effects/native/bitcrushereffect.cpp b/src/effects/native/bitcrushereffect.cpp new file mode 100644 index 00000000000..5fb228f7f39 --- /dev/null +++ b/src/effects/native/bitcrushereffect.cpp @@ -0,0 +1,85 @@ +#include "effects/native/bitcrushereffect.h" + +// static +QString BitCrusherEffect::getId() { + return "org.mixxx.effects.bitcrusher"; +} + +// static +EffectManifest BitCrusherEffect::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("BitCrusher")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription("TODO"); + + EffectManifestParameter* depth = manifest.addParameter(); + depth->setId("bit_depth"); + depth->setName(QObject::tr("Bit Depth")); + depth->setDescription("TODO"); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setValueHint(EffectManifestParameter::EffectManifestParameter::VALUE_INTEGRAL); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setLinkHint(EffectManifestParameter::LINK_INVERSE); + depth->setDefault(16); + depth->setMinimum(1); + depth->setMaximum(16); + + EffectManifestParameter* frequency = manifest.addParameter(); + frequency->setId("downsample"); + frequency->setName(QObject::tr("Downsampling")); + frequency->setDescription("TODO"); + frequency->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + frequency->setValueHint(EffectManifestParameter::VALUE_FLOAT); + frequency->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + frequency->setUnitsHint(EffectManifestParameter::UNITS_SAMPLERATE); + frequency->setLinkHint(EffectManifestParameter::LINK_LINKED); + frequency->setDefault(0.0); + frequency->setMinimum(0.0); + frequency->setMaximum(0.9999); + + return manifest; +} + +BitCrusherEffect::BitCrusherEffect(EngineEffect* pEffect, + const EffectManifest& manifest) + : m_pBitDepthParameter(pEffect->getParameterById("bit_depth")), + m_pDownsampleParameter(pEffect->getParameterById("downsample")) { +} + +BitCrusherEffect::~BitCrusherEffect() { + qDebug() << debugString() << "destroyed"; +} + +void BitCrusherEffect::processGroup(const QString& group, + BitCrusherGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + // TODO(rryan) this is broken. it needs to take into account the sample + // rate. + const CSAMPLE downsample = m_pDownsampleParameter ? + m_pDownsampleParameter->value().toDouble() : 0.0; + const CSAMPLE accumulate = 1.0 - downsample; + + int bit_depth = m_pBitDepthParameter ? + m_pBitDepthParameter->value().toInt() : 1; + bit_depth = math_max(bit_depth, 1); + + const CSAMPLE scale = 1 << (bit_depth-1); + + const int kChannels = 2; + for (int i = 0; i < numSamples; i += kChannels) { + pState->accumulator += accumulate; + + if (pState->accumulator >= 1.0) { + pState->accumulator -= 1.0; + pState->hold_l = floorf(pInput[i] * scale + 0.5f) / scale; + pState->hold_r = floorf(pInput[i+1] * scale + 0.5f) / scale; + } + + pOutput[i] = pState->hold_l; + pOutput[i+1] = pState->hold_r; + } +} diff --git a/src/effects/native/bitcrushereffect.h b/src/effects/native/bitcrushereffect.h new file mode 100644 index 00000000000..52d8e86e96f --- /dev/null +++ b/src/effects/native/bitcrushereffect.h @@ -0,0 +1,49 @@ +#ifndef BITCRUSHEREFFECT_H +#define BITCRUSHEREFFECT_H + +#include + +#include "effects/effect.h" +#include "effects/effectprocessor.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "util.h" + +struct BitCrusherGroupState { + // Default accumulator to 1 so we immediately pick an input value. + BitCrusherGroupState() + : hold_l(0), + hold_r(0), + accumulator(1) { + } + CSAMPLE hold_l, hold_r; + // Accumulated fractions of a samplerate period. + CSAMPLE accumulator; +}; + +class BitCrusherEffect : public GroupEffectProcessor { + public: + BitCrusherEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~BitCrusherEffect(); + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processGroup(const QString& group, + BitCrusherGroupState* pState, + const CSAMPLE* pInput, CSAMPLE *pOutput, + const unsigned int numSamples); + + private: + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pBitDepthParameter; + EngineEffectParameter* m_pDownsampleParameter; + + DISALLOW_COPY_AND_ASSIGN(BitCrusherEffect); +}; + +#endif /* BITCRUSHEREFFECT_H */ diff --git a/src/effects/native/echoeffect.cpp b/src/effects/native/echoeffect.cpp new file mode 100644 index 00000000000..5fc808cef64 --- /dev/null +++ b/src/effects/native/echoeffect.cpp @@ -0,0 +1,171 @@ +#include + +#include "effects/native/echoeffect.h" + +#include "mathstuff.h" +#include "sampleutil.h" + +#define OFFSET_RING(index, increment, length) (index + increment) % length +#define INCREMENT_RING(index, increment, length) index = (index + increment) % length + +// static +QString EchoEffect::getId() { + return "org.mixxx.effects.echo"; +} + +// static +EffectManifest EchoEffect::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Echo")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription(QObject::tr("Simple Echo. Applies " + "decay and runs a simple low-pass filter to reduce high " + "frequencies")); + + EffectManifestParameter* time = manifest.addParameter(); + time->setId("delay_time"); + time->setName(QObject::tr("Delay")); + time->setDescription(QObject::tr("Delay time (seconds)")); + time->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + time->setValueHint(EffectManifestParameter::VALUE_FLOAT); + time->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + time->setUnitsHint(EffectManifestParameter::UNITS_TIME); + time->setLinkHint(EffectManifestParameter::LINK_LINKED); + time->setMinimum(0.01); + time->setDefault(0.25); + time->setMaximum(2.0); + + time = manifest.addParameter(); + time->setId("decay_amount"); + time->setName(QObject::tr("Decay")); + time->setDescription( + QObject::tr("Amount the echo fades each time it loops")); + time->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + time->setValueHint(EffectManifestParameter::VALUE_FLOAT); + time->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + time->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + time->setMinimum(0.00); + time->setDefault(0.25); + // Allow > 1.0 decay for DANGEROUS TESTING-ONLY feedback! + time->setMaximum(1.2); + + time = manifest.addParameter(); + time->setId("pingpong_amount"); + time->setName(QObject::tr("PingPong")); + time->setDescription( + QObject::tr("As the ping-pong amount increases, increasing amounts " + "of the echoed signal is bounced between the left and " + "right speakers.")); + time->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + time->setValueHint(EffectManifestParameter::VALUE_FLOAT); + time->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + time->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + time->setMinimum(0.0); + time->setDefault(0.0); + time->setMaximum(1.0); + + return manifest; +} + +EchoEffect::EchoEffect(EngineEffect* pEffect, const EffectManifest& manifest) + : m_pDelayParameter(pEffect->getParameterById("delay_time")), + m_pDecayParameter(pEffect->getParameterById("decay_amount")), + m_pPingPongParameter(pEffect->getParameterById("pingpong_amount")) { +} + +EchoEffect::~EchoEffect() { + qDebug() << debugString() << "destroyed"; +} + +int EchoEffect::getDelaySamples(double delay_time) const { + // TODO(owilliams): Use real samplerate. + int delay_samples = delay_time * 44100; + if (delay_samples % 2 == 1) { + --delay_samples; + } + if (delay_samples > MAX_BUFFER_LEN) { + qWarning() << "Delay buffer requested is larger than max buffer!"; + delay_samples = MAX_BUFFER_LEN; + } + return delay_samples; +} + +void EchoEffect::processGroup(const QString& group, EchoGroupState* pGroupState, + const CSAMPLE* pInput, + CSAMPLE* pOutput, const unsigned int numSamples) { + EchoGroupState& gs = *pGroupState; + double delay_time = + m_pDelayParameter ? m_pDelayParameter->value().toDouble() : 1.0f; + double decay_amount = + m_pDecayParameter ? m_pDecayParameter->value().toDouble() : 0.25f; + double pingpong_frac = + m_pPingPongParameter ? m_pPingPongParameter->value().toDouble() + : 0.25f; + + // TODO(owilliams): get actual sample rate from somewhere. + + int delay_samples = gs.prev_delay_samples; + + if (delay_time < gs.prev_delay_time) { + // If the delay time has shrunk, we may need to wrap the write position. + delay_samples = getDelaySamples(delay_time); + gs.write_position = gs.write_position % delay_samples; + } else if (delay_time > gs.prev_delay_time) { + // If the delay time has grown, we need to zero out the new portion + // of the buffer we are using. + SampleUtil::applyGain( + gs.delay_buf + gs.prev_delay_samples, + 0, + MAX_BUFFER_LEN - gs.prev_delay_samples); + delay_samples = getDelaySamples(delay_time); + } + + int read_position = gs.write_position; + gs.prev_delay_time = delay_time; + gs.prev_delay_samples = delay_samples; + + // Lowpass the delay buffer to deaden it a bit. + gs.decay_lowpass->process( + gs.delay_buf, gs.delay_buf, numSamples); + + // Decay the delay buffer and then add the new input. + for (unsigned int i = 0; i < numSamples; ++i) { + gs.delay_buf[gs.write_position] *= decay_amount; + gs.delay_buf[gs.write_position] += pInput[i]; + INCREMENT_RING(gs.write_position, 1, delay_samples); + } + + // TODO(owilliams): delay buffer clipping goes here. + + // Pingpong the output. If the pingpong value is zero, all of the + // math below should result in a simple copy of delay buf to pOutput. + for (unsigned int i = 0; i + 1 < numSamples; i += 2) { + if (gs.ping_pong_left) { + // Left sample plus a fraction of the right sample, normalized + // by 1 + fraction. + pOutput[i] = + (gs.delay_buf[read_position] + + gs.delay_buf[read_position + 1] * pingpong_frac) / + (1 + pingpong_frac); + // Right sample reduced by (1 - fraction) + pOutput[i + 1] = gs.delay_buf[read_position + 1] * (1 - pingpong_frac); + } else { + // Left sample reduced by (1 - fraction) + pOutput[i] = gs.delay_buf[read_position] * (1 - pingpong_frac); + // Right sample plus fraction of left sample, normalized by + // 1 + fraction + pOutput[i + 1] = + (gs.delay_buf[read_position + 1] + + gs.delay_buf[read_position] * pingpong_frac) / + (1 + pingpong_frac); + } + + INCREMENT_RING(read_position, 2, delay_samples); + // If the buffer has looped around, flip-flop the ping-pong. + if (read_position == 0) { + gs.ping_pong_left = !gs.ping_pong_left; + } + } +} diff --git a/src/effects/native/echoeffect.h b/src/effects/native/echoeffect.h new file mode 100644 index 00000000000..2ea5e3e2a7e --- /dev/null +++ b/src/effects/native/echoeffect.h @@ -0,0 +1,66 @@ +#ifndef ECHOEFFECT_H +#define ECHOEFFECT_H + +#include + +#include "defs.h" +#include "util.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "engine/enginefilterbutterworth8.h" +#include "effects/effectprocessor.h" +#include "sampleutil.h" + +struct EchoGroupState { + EchoGroupState() { + delay_buf = SampleUtil::alloc(MAX_BUFFER_LEN); + // TODO(owilliams): use the actual samplerate. + decay_lowpass = + new EngineFilterButterworth8Low(44100, 10000); + SampleUtil::applyGain(delay_buf, 0, MAX_BUFFER_LEN); + prev_delay_time = 0.0; + prev_delay_samples = 0; + write_position = 0; + ping_pong_left = true; + } + ~EchoGroupState() { + SampleUtil::free(delay_buf); + delete decay_lowpass; + } + CSAMPLE* delay_buf; + EngineFilterButterworth8Low* decay_lowpass; + double prev_delay_time; + int prev_delay_samples; + int write_position; + bool ping_pong_left; +}; + +class EchoEffect : public GroupEffectProcessor { + public: + EchoEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~EchoEffect(); + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processGroup(const QString& group, + EchoGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples); + + private: + int getDelaySamples(double delay_time) const; + + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pDelayParameter; + EngineEffectParameter* m_pDecayParameter; + EngineEffectParameter* m_pPingPongParameter; + + DISALLOW_COPY_AND_ASSIGN(EchoEffect); +}; + +#endif /* ECHOEFFECT_H */ diff --git a/src/effects/native/filtereffect.cpp b/src/effects/native/filtereffect.cpp new file mode 100644 index 00000000000..43761fb4bc1 --- /dev/null +++ b/src/effects/native/filtereffect.cpp @@ -0,0 +1,154 @@ +#include "effects/native/filtereffect.h" + +// static +QString FilterEffect::getId() { + return "org.mixxx.effects.filter"; +} + +// static +EffectManifest FilterEffect::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Filter")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription("TODO"); + + EffectManifestParameter* depth = manifest.addParameter(); + depth->setId("depth"); + depth->setName(QObject::tr("Depth")); + depth->setDescription("TODO"); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setValueHint(EffectManifestParameter::EffectManifestParameter::VALUE_FLOAT); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setLinkHint(EffectManifestParameter::LINK_LINKED); + depth->setDefault(0.0); + depth->setMinimum(-1.0); + depth->setMaximum(1.0); + + EffectManifestParameter* bandpass_width = manifest.addParameter(); + bandpass_width->setId("bandpass_width"); + bandpass_width->setName(QObject::tr("Bandpass Width")); + bandpass_width->setDescription("TODO"); + bandpass_width->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + bandpass_width->setValueHint(EffectManifestParameter::VALUE_FLOAT); + bandpass_width->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + bandpass_width->setUnitsHint(EffectManifestParameter::UNITS_SAMPLERATE); + bandpass_width->setDefault(0.01); + bandpass_width->setMinimum(0.001); + bandpass_width->setMaximum(0.01); + + EffectManifestParameter* bandpass_gain = manifest.addParameter(); + bandpass_gain->setId("bandpass_gain"); + bandpass_gain->setName(QObject::tr("Bandpass Gain")); + bandpass_gain->setDescription("TODO"); + bandpass_gain->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + bandpass_gain->setValueHint(EffectManifestParameter::VALUE_FLOAT); + bandpass_gain->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + bandpass_gain->setUnitsHint(EffectManifestParameter::UNITS_SAMPLERATE); + bandpass_gain->setDefault(0.3); + bandpass_gain->setMinimum(0.0); + bandpass_gain->setMaximum(1.0); + + return manifest; +} + +FilterEffect::FilterEffect(EngineEffect* pEffect, + const EffectManifest& manifest) + : m_pDepthParameter(pEffect->getParameterById("depth")), + m_pBandpassWidthParameter( + pEffect->getParameterById("bandpass_width")), + m_pBandpassGainParameter( + pEffect->getParameterById("bandpass_gain")) { +} + +FilterEffect::~FilterEffect() { + qDebug() << debugString() << "destroyed"; +} + +double getLowFrequencyCorner(double depth) { + return pow(2.0, 5.0 + depth * 9.0); +} + +double getHighFrequencyCorner(double depth, double bandpassSize) { + return pow(2.0, 5.0 + (depth + bandpassSize) * 9.0); +} + +void FilterEffect::processGroup(const QString& group, + FilterGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + double depth = m_pDepthParameter ? + m_pDepthParameter->value().toDouble() : 0.0; + double bandpass_width = m_pBandpassWidthParameter ? + m_pBandpassWidthParameter->value().toDouble() : 0.0; + CSAMPLE bandpass_gain = m_pBandpassGainParameter ? + m_pBandpassGainParameter->value().toFloat() : 0.0; + + // TODO(rryan) what if bandpass_gain changes? + bool parametersChanged = depth != pState->oldDepth || + bandpass_width != pState->oldBandpassWidth; + if (parametersChanged) { + if (pState->oldDepth == 0.0) { + SampleUtil::copyWithGain( + pState->crossfadeBuffer, pInput, 1.0, numSamples); + } else if (pState->oldDepth == -1.0 || pState->oldDepth == 1.0) { + SampleUtil::copyWithGain( + pState->crossfadeBuffer, pInput, 0.0, numSamples); + } else { + applyFilters(pState, + pInput, pState->crossfadeBuffer, + pState->bandpassBuffer, + numSamples, pState->oldDepth, + pState->oldBandpassGain); + } + if (depth < 0.0) { + // Lowpass + bandpass + // Freq from 2^5=32Hz to 2^(5+9)=16384 + double freq = getLowFrequencyCorner(depth + 1.0); + double freq2 = getHighFrequencyCorner(depth + 1.0, bandpass_width); + pState->lowFilter.setFrequencyCorners(freq2); + pState->bandpassFilter.setFrequencyCorners(freq, freq2); + } else if (depth > 0.0) { + // Highpass + bandpass + double freq = getLowFrequencyCorner(depth); + double freq2 = getHighFrequencyCorner(depth, bandpass_width); + pState->highFilter.setFrequencyCorners(freq); + pState->bandpassFilter.setFrequencyCorners(freq, freq2); + } + } + + if (depth == 0.0) { + SampleUtil::copyWithGain(pOutput, pInput, 1.0, numSamples); + } else if (depth == -1.0 || depth == 1.0) { + SampleUtil::copyWithGain(pOutput, pInput, 0.0, numSamples); + } else { + applyFilters(pState, pInput, pOutput, pState->bandpassBuffer, + numSamples, depth, bandpass_gain); + } + + if (parametersChanged) { + SampleUtil::linearCrossfadeBuffers(pOutput, pState->crossfadeBuffer, + pOutput, numSamples); + pState->oldDepth = depth; + pState->oldBandpassWidth = bandpass_width; + pState->oldBandpassGain = bandpass_gain; + } +} + +void FilterEffect::applyFilters(FilterGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + CSAMPLE* pTempBuffer, + const int numSamples, + double depth, CSAMPLE bandpassGain) { + if (depth < 0.0) { + pState->lowFilter.process(pInput, pOutput, numSamples); + pState->bandpassFilter.process(pInput, pTempBuffer, numSamples); + } else { + pState->highFilter.process(pInput, pOutput, numSamples); + pState->bandpassFilter.process(pInput, pTempBuffer, numSamples); + + } + SampleUtil::addWithGain(pOutput, pTempBuffer, bandpassGain, numSamples); +} diff --git a/src/effects/native/filtereffect.h b/src/effects/native/filtereffect.h new file mode 100644 index 00000000000..b051e2a9ae9 --- /dev/null +++ b/src/effects/native/filtereffect.h @@ -0,0 +1,69 @@ +#ifndef FILTEREFFECT_H +#define FILTEREFFECT_H + +#include + +#include "effects/effect.h" +#include "effects/effectprocessor.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "engine/enginefilterbutterworth8.h" +#include "sampleutil.h" +#include "util.h" + +struct FilterGroupState { + FilterGroupState() + // TODO(XXX) 44100 should be changed to real sample rate + // https://bugs.launchpad.net/mixxx/+bug/1208816. + : lowFilter(44100, 20), + bandpassFilter(44100, 20, 200), + highFilter(44100, 20), + oldDepth(0), + oldBandpassWidth(0), + oldBandpassGain(0) { + SampleUtil::applyGain(bandpassBuffer, 0, MAX_BUFFER_LEN); + SampleUtil::applyGain(crossfadeBuffer, 0, MAX_BUFFER_LEN); + } + + EngineFilterButterworth8Low lowFilter; + EngineFilterButterworth8Band bandpassFilter; + EngineFilterButterworth8High highFilter; + + CSAMPLE bandpassBuffer[MAX_BUFFER_LEN]; + CSAMPLE crossfadeBuffer[MAX_BUFFER_LEN]; + double oldDepth; + double oldBandpassWidth; + CSAMPLE oldBandpassGain; +}; + +class FilterEffect : public GroupEffectProcessor { + public: + FilterEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~FilterEffect(); + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processGroup(const QString& group, + FilterGroupState* pState, + const CSAMPLE* pInput, CSAMPLE *pOutput, + const unsigned int numSamples); + + private: + QString debugString() const { + return getId(); + } + + void applyFilters(FilterGroupState* pState, + const CSAMPLE* pIn, CSAMPLE* pOut, CSAMPLE* pTempBuffer, + const int numSamples, double depth, CSAMPLE bandpassGain); + + EngineEffectParameter* m_pDepthParameter; + EngineEffectParameter* m_pBandpassWidthParameter; + EngineEffectParameter* m_pBandpassGainParameter; + + DISALLOW_COPY_AND_ASSIGN(FilterEffect); +}; + +#endif /* FILTEREFFECT_H */ diff --git a/src/effects/native/flangereffect.cpp b/src/effects/native/flangereffect.cpp new file mode 100644 index 00000000000..130acf5d94b --- /dev/null +++ b/src/effects/native/flangereffect.cpp @@ -0,0 +1,125 @@ +#include + +#include "effects/native/flangereffect.h" + +#include "mathstuff.h" + +const unsigned int kMaxDelay = 5000; +const unsigned int kLfoAmplitude = 240; +const unsigned int kAverageDelayLength = 250; + +// static +QString FlangerEffect::getId() { + return "org.mixxx.effects.flanger"; +} + +// static +EffectManifest FlangerEffect::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Flanger")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription("TODO"); + + EffectManifestParameter* depth = manifest.addParameter(); + depth->setId("depth"); + depth->setName(QObject::tr("Depth")); + depth->setDescription("TODO"); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setValueHint(EffectManifestParameter::VALUE_FLOAT); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setDefault(0.0); + depth->setMinimum(0.0); + depth->setMaximum(1.0); + + EffectManifestParameter* delay = manifest.addParameter(); + delay->setId("delay"); + delay->setName(QObject::tr("Delay")); + delay->setDescription("TODO"); + delay->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + delay->setValueHint(EffectManifestParameter::VALUE_FLOAT); + delay->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + delay->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + delay->setDefault(50.0); + delay->setMinimum(50.0); + delay->setMaximum(10000.0); + + EffectManifestParameter* period = manifest.addParameter(); + period->setId("period"); + period->setName(QObject::tr("Period")); + period->setDescription("TODO"); + period->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + period->setValueHint(EffectManifestParameter::VALUE_FLOAT); + period->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + period->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + period->setDefault(50000.0); + period->setMinimum(50000.0); + period->setMaximum(2000000.0); + + return manifest; +} + +FlangerEffect::FlangerEffect(EngineEffect* pEffect, + const EffectManifest& manifest) + : m_pPeriodParameter(pEffect->getParameterById("period")), + m_pDepthParameter(pEffect->getParameterById("depth")), + m_pDelayParameter(pEffect->getParameterById("delay")) { +} + +FlangerEffect::~FlangerEffect() { + qDebug() << debugString() << "destroyed"; +} + +void FlangerEffect::processGroup(const QString& group, + FlangerGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + CSAMPLE lfoPeriod = m_pPeriodParameter ? + m_pPeriodParameter->value().toDouble() : 0.0f; + CSAMPLE lfoDepth = m_pDepthParameter ? + m_pDepthParameter->value().toDouble() : 0.0f; + // Unused in EngineFlanger + CSAMPLE lfoDelay = m_pDelayParameter ? + m_pDelayParameter->value().toDouble() : 0.0f; + + // TODO(rryan) check ranges + // period needs to be >=0 + // delay needs to be >=0 + // depth is ??? + + CSAMPLE* delayLeft = pState->delayLeft; + CSAMPLE* delayRight = pState->delayRight; + + const int kChannels = 2; + for (int i = 0; i < numSamples; i += kChannels) { + delayLeft[pState->delayPos] = pInput[i]; + delayRight[pState->delayPos] = pInput[i+1]; + + pState->delayPos = (pState->delayPos + 1) % kMaxDelay; + + pState->time++; + if (pState->time > lfoPeriod) { + pState->time = 0; + } + + CSAMPLE periodFraction = CSAMPLE(pState->time) / lfoPeriod; + CSAMPLE delay = kAverageDelayLength + kLfoAmplitude * sin(two_pi * periodFraction); + + int framePrev = (pState->delayPos - int(delay) + kMaxDelay - 1) % kMaxDelay; + int frameNext = (pState->delayPos - int(delay) + kMaxDelay ) % kMaxDelay; + CSAMPLE prevLeft = delayLeft[framePrev]; + CSAMPLE nextLeft = delayLeft[frameNext]; + + CSAMPLE prevRight = delayRight[framePrev]; + CSAMPLE nextRight = delayRight[frameNext]; + + CSAMPLE frac = delay - floorf(delay); + 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; + } +} diff --git a/src/effects/native/flangereffect.h b/src/effects/native/flangereffect.h new file mode 100644 index 00000000000..a2ea8255b63 --- /dev/null +++ b/src/effects/native/flangereffect.h @@ -0,0 +1,52 @@ +#ifndef FLANGEREFFECT_H +#define FLANGEREFFECT_H + +#include + +#include "defs.h" +#include "util.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "effects/effectprocessor.h" +#include "sampleutil.h" + +struct FlangerGroupState { + FlangerGroupState() + : delayPos(0), + time(0) { + SampleUtil::applyGain(delayLeft, 0, MAX_BUFFER_LEN); + SampleUtil::applyGain(delayRight, 0, MAX_BUFFER_LEN); + } + CSAMPLE delayRight[MAX_BUFFER_LEN]; + CSAMPLE delayLeft[MAX_BUFFER_LEN]; + unsigned int delayPos; + unsigned int time; +}; + +class FlangerEffect : public GroupEffectProcessor { + public: + FlangerEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~FlangerEffect(); + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processGroup(const QString& group, + FlangerGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples); + + private: + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pPeriodParameter; + EngineEffectParameter* m_pDepthParameter; + EngineEffectParameter* m_pDelayParameter; + + DISALLOW_COPY_AND_ASSIGN(FlangerEffect); +}; + +#endif /* FLANGEREFFECT_H */ diff --git a/src/effects/native/nativebackend.cpp b/src/effects/native/nativebackend.cpp new file mode 100644 index 00000000000..e5d26b4f3e3 --- /dev/null +++ b/src/effects/native/nativebackend.cpp @@ -0,0 +1,21 @@ +#include + +#include "effects/native/nativebackend.h" +#include "effects/native/flangereffect.h" +#include "effects/native/bitcrushereffect.h" +#include "effects/native/filtereffect.h" +#include "effects/native/reverbeffect.h" +#include "effects/native/echoeffect.h" + +NativeBackend::NativeBackend(QObject* pParent) + : EffectsBackend(pParent, tr("Native")) { + registerEffect(); + registerEffect(); + registerEffect(); + registerEffect(); + registerEffect(); +} + +NativeBackend::~NativeBackend() { + qDebug() << debugString() << "destroyed"; +} diff --git a/src/effects/native/nativebackend.h b/src/effects/native/nativebackend.h new file mode 100644 index 00000000000..b9ef6411b58 --- /dev/null +++ b/src/effects/native/nativebackend.h @@ -0,0 +1,18 @@ +#ifndef NATIVEBACKEND_H +#define NATIVEBACKEND_H + +#include "effects/effectsbackend.h" + +class NativeBackend : public EffectsBackend { + Q_OBJECT + public: + NativeBackend(QObject* pParent=NULL); + virtual ~NativeBackend(); + + private: + QString debugString() const { + return "NativeBackend"; + } +}; + +#endif /* NATIVEBACKEND_H */ diff --git a/src/effects/native/reverb/Reverb.cc b/src/effects/native/reverb/Reverb.cc new file mode 100644 index 00000000000..787e80a5af1 --- /dev/null +++ b/src/effects/native/reverb/Reverb.cc @@ -0,0 +1,161 @@ +/* + Reverb.cc + + Copyright 2002-13 Tim Goetze + + Port from LADSPA to Mixxx 2014 by Owen Williams , + Mostly just deleting excess code. + + http://quitte.de/dsp/ + + Three reverb units: JVRev, Plate and PlateX2. + + The former is a rewrite of STK's JVRev, a traditional design. + + Original comment: + + This is based on some of the famous + Stanford CCRMA reverbs (NRev, KipRev) + all based on the Chowning/Moorer/ + Schroeder reverberators, which use + networks of simple allpass and comb + delay filters. + + The algorithm is mostly unchanged in this implementation; the delay + line lengths have been fiddled with to make the stereo field more + evenly weighted, denormal protection and a bandwidth control have been + added as well. + + The latter two are based on the circuit discussed in Jon Dattorro's + September 1997 JAES paper on effect design (part 1: reverb & filters). +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ +#include "Reverb.h" + +/* //////////////////////////////////////////////////////////////////////// */ + +void +PlateStub::init() +{ + f_lfo = -1; + // TODO(owilliams): use actual sample rate. + fs = 44100; + +# define L(i) ((int) (l[i] * fs)) + static float l[] = { + 0.004771345048889486, 0.0035953092974026408, + 0.01273478713752898, 0.0093074829474816042, + 0.022579886428547427, 0.030509727495715868, + 0.14962534861059779, 0.060481838647894894, 0.12499579987231611, + 0.14169550754342933, 0.089244313027116023, 0.10628003091293972 + }; + + /* lh */ + input.lattice[0].init (L(0)); + input.lattice[1].init (L(1)); + + /* rh */ + input.lattice[2].init (L(2)); + input.lattice[3].init (L(3)); + + /* modulated, width about 12 samples @ 44.1 */ + tank.mlattice[0].init (L(4), (int) (0.000403221 * fs)); + tank.mlattice[1].init (L(5), (int) (0.000403221 * fs)); + + /* lh */ + tank.delay[0].init (L(6)); + tank.lattice[0].init (L(7)); + tank.delay[1].init (L(8)); + + /* rh */ + tank.delay[2].init (L(9)); + tank.lattice[1].init (L(10)); + tank.delay[3].init (L(11)); +# undef L + +# define T(i) ((int) (t[i] * fs)) + static float t[] = { + 0.0089378717113000241, 0.099929437854910791, 0.064278754074123853, + 0.067067638856221232, 0.066866032727394914, 0.006283391015086859, + 0.01186116057928161, 0.12187090487550822, 0.041262054366452743, + 0.089815530392123921, 0.070931756325392295, 0.011256342192802662 + }; + + for (int i = 0; i < 12; ++i) + tank.taps[i] = T(i); +# undef T + + /* tuned for soft attack, ambience */ + indiff1 = .742; + indiff2 = .712; + + dediff1 = .723; + dediff2 = .729; +} + +void +PlateStub::process (sample_t x, sample_t decay, sample_t * _xl, sample_t * _xr) +{ + x = input.bandwidth.process (x); + + /* lh */ + x = input.lattice[0].process (x, indiff1); + x = input.lattice[1].process (x, indiff1); + + /* rh */ + x = input.lattice[2].process (x, indiff2); + x = input.lattice[3].process (x, indiff2); + + /* summation point */ + register double xl = x + decay * tank.delay[3].get(); + register double xr = x + decay * tank.delay[1].get(); + + /* lh */ + xl = tank.mlattice[0].process (xl, dediff1); + xl = tank.delay[0].putget (xl); + xl = tank.damping[0].process (xl); + xl *= decay; + xl = tank.lattice[0].process (xl, dediff2); + tank.delay[1].put (xl); + + /* rh */ + xr = tank.mlattice[1].process (xr, dediff1); + xr = tank.delay[2].putget (xr); + xr = tank.damping[1].process (xr); + xr *= decay; + xr = tank.lattice[1].process (xr, dediff2); + tank.delay[3].put (xr); + + /* gather output */ + xl = .6 * tank.delay[2] [tank.taps[0]]; + xl += .6 * tank.delay[2] [tank.taps[1]]; + xl -= .6 * tank.lattice[1] [tank.taps[2]]; + xl += .6 * tank.delay[3] [tank.taps[3]]; + xl -= .6 * tank.delay[0] [tank.taps[4]]; + xl += .6 * tank.lattice[0] [tank.taps[5]]; + + xr = .6 * tank.delay[0] [tank.taps[6]]; + xr += .6 * tank.delay[0] [tank.taps[7]]; + xr -= .6 * tank.lattice[0] [tank.taps[8]]; + xr += .6 * tank.delay[1] [tank.taps[9]]; + xr -= .6 * tank.delay[2] [tank.taps[10]]; + xr += .6 * tank.lattice[1] [tank.taps[11]]; + + *_xl = xl; + *_xr = xr; +} diff --git a/src/effects/native/reverb/Reverb.h b/src/effects/native/reverb/Reverb.h new file mode 100644 index 00000000000..b0f0b7e29e8 --- /dev/null +++ b/src/effects/native/reverb/Reverb.h @@ -0,0 +1,175 @@ +/* + Reverb.h + + Copyright 2002-13 Tim Goetze + + http://quitte.de/dsp/ + + two reverb units: JVRev and Plate. + + the former is a rewrite of STK's JVRev, a traditional design. + + original comment: + + This is based on some of the famous + Stanford CCRMA reverbs (NRev, KipRev) + all based on the Chowning/Moorer/ + Schroeder reverberators, which use + networks of simple allpass and comb + delay filters. + + (STK is an effort of Gary Scavone). + + the algorithm is mostly unchanged in this implementation; the delay + line lengths have been fiddled with to make the stereo field more + evenly weighted, and denormal protection has been added. + + the Plate reverb is based on the circuit discussed in Jon Dattorro's + september 1997 JAES paper on effect design (part 1: reverb & filters). +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ + +#ifndef _REVERB_H_ +#define _REVERB_H_ + +#include + +//#include "defs.h" +#include "effects/native/reverb/basics.h" +#include "effects/native/reverb/dsp/Delay.h" +#include "effects/native/reverb/dsp/OnePole.h" +#include "effects/native/reverb/dsp/Sine.h" +#include "effects/native/reverb/dsp/util.h" + +/* both reverbs use this */ +class Lattice +: public DSP::Delay +{ + public: + inline sample_t + process (sample_t x, double d) + { + sample_t y = get(); + x -= d * y; + put (x); + return d * x + y; + } +}; + +/* /////////////////////////////////////////////////////////////////////// */ + +class ModLattice +{ + public: + float n0, width; + + DSP::Delay delay; + DSP::Sine lfo; + + void init (int n, int w) + { + n0 = n; + width = w; + delay.init (n + w); + } + + void reset() + { + delay.reset(); + } + + inline sample_t + process (sample_t x, double d) + { + sample_t y = delay.get_linear (n0 + width * lfo.get()); + x += d * y; + delay.put (x); + return y - d * x; /* note sign */ + } +}; + +class PlateStub +{ + public: + sample_t f_lfo; + + sample_t indiff1, indiff2, dediff1, dediff2; + + struct { + DSP::OnePoleLP bandwidth; + Lattice lattice[4]; + } input; + + struct { + ModLattice mlattice[2]; + Lattice lattice[2]; + DSP::Delay delay[4]; + DSP::OnePoleLP damping[2]; + int taps[12]; + } tank; + + public: + void init(); + void activate() + { + input.bandwidth.reset(); + + for (int i = 0; i < 4; ++i) + { + input.lattice[i].reset(); + tank.delay[i].reset(); + } + + for (int i = 0; i < 2; ++i) + { + tank.mlattice[i].reset(); + tank.lattice[i].reset(); + tank.damping[i].reset(); + } + + tank.mlattice[0].lfo.set_f (1.2, fs, 0); + tank.mlattice[1].lfo.set_f (1.2, fs, .5 * M_PI); + } + + // Process a single mono sample, returning a left and right reverbed + // sample. + void process (sample_t x, sample_t decay, + sample_t * xl, sample_t * xr); + + private: + float fs; // sameple rate; +}; + +class MixxxPlateX2 : public PlateStub { + public: + void setBandwidth(double bandwidth) { + input.bandwidth.set(exp(-M_PI * (1. - bandwidth))); + } + + void setDecay(double decay_control) { + double damp = exp(-M_PI * decay_control); + tank.damping[0].set(damp); + tank.damping[1].set(damp); + } + + void process(sample_t x, sample_t decay, sample_t * xl, sample_t * xr) { + PlateStub::process(x, decay, xl, xr); + } +}; + +#endif /* _REVERB_H_ */ diff --git a/src/effects/native/reverb/basics.h b/src/effects/native/reverb/basics.h new file mode 100644 index 00000000000..d7975ee4fbf --- /dev/null +++ b/src/effects/native/reverb/basics.h @@ -0,0 +1,161 @@ +/* + basics.h + + Copyright 2004-12 Tim Goetze + + http://quitte.de/dsp/ + + Common constants, typedefs, utility functions + and simplified LADSPA #defines. + + Some code removed by Owen Williams for port to Mixxx, mostly ladspa-specific + defines and i386 customizations. + +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ + +#ifndef _BASICS_H_ +#define _BASICS_H_ + +#include +#include + +#include +#include + +#include +#include + +#include "defs.h" +typedef CSAMPLE sample_t; + +typedef __int8_t int8; +typedef __uint8_t uint8; +typedef __int16_t int16; +typedef __uint16_t uint16; +typedef __int32_t int32; +typedef __uint32_t uint32; +typedef __int64_t int64; +typedef __uint64_t uint64; + +#define MIN_GAIN .000001 /* -120 dB */ +/* smallest non-denormal 32 bit IEEE float is 1.18e-38 */ +#define NOISE_FLOOR .00000000000005 /* -266 dB */ + +/* //////////////////////////////////////////////////////////////////////// */ + +typedef unsigned int uint; +typedef unsigned long ulong; + +/* prototype that takes a sample and yields a sample */ +typedef CSAMPLE (*clip_func_t) (CSAMPLE); + +/* flavours for sample store functions run() and run_adding() */ +typedef void (*yield_func_t) (CSAMPLE *, uint, CSAMPLE, CSAMPLE); + +inline void +store_func (CSAMPLE * s, uint i, CSAMPLE x, CSAMPLE gain) +{ + s[i] = x; +} + +inline void +adding_func (CSAMPLE * s, uint i, CSAMPLE x, CSAMPLE gain) +{ + s[i] += gain * x; +} + +#ifndef max + +template +X min (X x, Y y) +{ + return x < y ? x : (X) y; +} + +template +X max (X x, Y y) +{ + return x > y ? x : (X) y; +} + +#endif /* ! max */ + +template +T clamp (T value, T lower, T upper) +{ + if (value < lower) return lower; + if (value > upper) return upper; + return value; +} + +static inline float +frandom() +{ + return (float) random() / (float) RAND_MAX; +} + +/* NB: also true if 0 */ +inline bool +is_denormal (float & f) +{ + int32 i = *((int32 *) &f); + return ((i & 0x7f800000) == 0); +} + +/* not used, check validity before using */ +inline bool +is_denormal (double & f) +{ + int64 i = *((int64 *) &f); + return ((i & 0x7fe0000000000000ll) == 0); +} + +/* lovely algorithm from + http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2Float +*/ +inline uint +next_power_of_2 (uint n) +{ + assert (n <= 0x40000000); + + --n; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + + return ++n; +} + +inline double +db2lin (double db) +{ + return pow(10, db*.05); +} + +inline double +lin2db (double lin) +{ + return 20*log10(lin); +} + +/* //////////////////////////////////////////////////////////////////////// */ + +#endif /* _BASICS_H_ */ diff --git a/src/effects/native/reverb/dsp/Delay.h b/src/effects/native/reverb/dsp/Delay.h new file mode 100644 index 00000000000..53a8a606453 --- /dev/null +++ b/src/effects/native/reverb/dsp/Delay.h @@ -0,0 +1,150 @@ +/* + dsp/Delay.h + + Copyright 2003-13 Tim Goetze + + http://quitte.de/dsp/ + + delay lines with fractional (linear or cubic interpolation) lookup + and an allpass interpolating tap (which needs more work). + + delay line storage is aligned to powers of two for simplified wrapping + checks (no conditional or modulo, binary 'and' suffices instead). + +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ + +#ifndef _DSP_DELAY_H_ +#define _DSP_DELAY_H_ + +#include "effects/native/reverb/dsp/util.h" +#include "effects/native/reverb/dsp/FPTruncateMode.h" + +namespace DSP { + +class Delay +{ + public: + uint size; + sample_t * data; + uint read, write; + + Delay() { read = write = 0; data = 0; } + + ~Delay() { free (data); } + + void init (uint n) + { + size = next_power_of_2 (n); + assert (size <= (1 << 20)); + data = (sample_t *) calloc (sizeof (sample_t), size); + --size; /* used as mask for confining access */ + write = n; + } + + void reset() + { + memset (data, 0, (size + 1) * sizeof (sample_t)); + } + + sample_t & operator [] (int i) { return data [(write - i) & size]; } + + inline void put (sample_t x) + { + data [write] = x; + write = (write + 1) & size; + } + + inline sample_t get() + { + sample_t x = data [read]; + read = (read + 1) & size; + return x; + } + inline sample_t peek() { return data [read]; } + inline sample_t putget (sample_t x) {put(x); return get();} + + /* fractional lookup, linear interpolation */ + inline sample_t get_linear (float f) + { + int n; + fistp (f, n); /* read: i = (int) f; relies on FPTruncateMode */ + f -= n; + + return (1 - f) * (*this) [n] + f * (*this) [n + 1]; + } + + /* fractional lookup, cubic interpolation */ + inline sample_t get_cubic (float f) + { + int n; + fistp (f, n); /* see FPTruncateMode */ + f -= n; + + sample_t x_1 = (*this) [n - 1]; + sample_t x0 = (*this) [n]; + sample_t x1 = (*this) [n + 1]; + sample_t x2 = (*this) [n + 2]; + + /* sample_t (32bit) quicker than double here */ + register sample_t a = + (3 * (x0 - x1) - x_1 + x2) * .5; + register sample_t b = + 2 * x1 + x_1 - (5 * x0 + x2) * .5; + register sample_t c = + (x1 - x_1) * .5; + + return x0 + (((a * f) + b) * f + c) * f; + } +}; + +class MovingAverage +: public Delay +{ + public: + sample_t state, over_n; + + void init (uint n) + { + this->Delay::init (n); + over_n = 1. / n; + /* adjust write pointer so we have a full history of zeros */ + write = (write + size + 1) & size; + state = 0; + } + + void reset() + { + this->Delay::reset(); + state = 0; + } + + void process (sample_t x) + { + x *= over_n; + state -= this->Delay::get(); + state += x; + this->Delay::put (x); + } + + sample_t get() { return state; } +}; + +}; /* namespace DSP */ + +#endif /* _DSP_DELAY_H_ */ diff --git a/src/effects/native/reverb/dsp/FPTruncateMode.h b/src/effects/native/reverb/dsp/FPTruncateMode.h new file mode 100644 index 00000000000..6ffe4e43daa --- /dev/null +++ b/src/effects/native/reverb/dsp/FPTruncateMode.h @@ -0,0 +1,87 @@ +/* + FPTruncateMode.h + + Copyright 2001-11 Tim Goetze + + http://quitte.de/dsp/ + + Sets the FP rounding mode to 'truncate' in the constructor + and loads the previous FP conrol word in the destructor. + + By directly using the machine instruction to convert float to int + we avoid the performance hit that loading the control word twice for + every (int) cast causes on i386. + + On other architectures this is a no-op. + +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ + + +#ifndef _DSP_FP_TRUNCATE_MODE_H_ +#define _DSP_FP_TRUNCATE_MODE_H_ + +#ifdef __i386__ + #define fstcw(i) \ + __asm__ __volatile__ ("fstcw %0" : "=m" (i)) + + #define fldcw(i) \ + __asm__ __volatile__ ("fldcw %0" : : "m" (i)) + + /* gcc chokes on __volatile__ sometimes. */ + #define fistp(f,i) \ + __asm__ ("fistpl %0" : "=m" (i) : "t" (f) : "st") +#else /* ! __i386__ */ + #define fstcw(i) + #define fldcw(i) + + #define fistp(f,i) \ + i = (int) f +#endif + +namespace DSP { + +static inline int +fast_trunc (float f) +{ + int i; + fistp (f, i); + return i; +} + +class FPTruncateMode +{ + public: + int cw0, cw1; /* fp control word */ + + FPTruncateMode() + { + fstcw (cw0); + cw1 = cw0 | 0xC00; + fldcw (cw1); + } + + ~FPTruncateMode() + { + fldcw (cw0); + } +}; + +} /* namespace DSP */ + +#endif /* _DSP_FP_TRUNCATE_MODE_H_ */ diff --git a/src/effects/native/reverb/dsp/OnePole.h b/src/effects/native/reverb/dsp/OnePole.h new file mode 100644 index 00000000000..95262f9dc8f --- /dev/null +++ b/src/effects/native/reverb/dsp/OnePole.h @@ -0,0 +1,118 @@ +/* + dsp/OnePole.h + + Copyright 2003-13 Tim Goetze + + http://quitte.de/dsp/ + + one pole (or one zero, or one zero, one pole) hi- and lo-pass filters. + +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ + +#ifndef _ONE_POLE_H_ +#define _ONE_POLE_H_ + +namespace DSP { + +template +class OnePoleLP +{ + public: + T a0, b1, y1; + + OnePoleLP (double d = 1.) + { + set (d); + y1 = 0.; + } + + inline void reset() + { + y1 = 0.; + } + + inline void set_f (T fc) + { + set (1 - exp(-2*M_PI*fc)); + } + + inline void set (T d) + { + a0 = d; + b1 = 1 - d; + } + + inline T process (T x) + { + return y1 = a0*x + b1*y1; + } + + inline void decay (T d) + { + a0 *= d; + b1 = 1. - a0; + } +}; + +template +class OnePoleHP +{ + public: + T a0, a1, b1, x1, y1; + + OnePoleHP (T d = 1.) + { + set (d); + x1 = y1 = 0.; + } + + void set_f (T f) + { + set (exp (-2*M_PI*f)); + } + + inline void set (T d) + { + a0 = .5*(1. + d); + a1 = -.5*(1. + d); + b1 = d; + } + + inline T process (T x) + { + y1 = a0*x + a1*x1 + b1*y1; + x1 = x; + return y1; + } + + void identity() + { + a0=1; + a1=b1=0; + } + + void reset() + { + x1 = y1 = 0; + } +}; + +} /* namespace DSP */ + +#endif /* _ONE_POLE_H_ */ diff --git a/src/effects/native/reverb/dsp/Sine.h b/src/effects/native/reverb/dsp/Sine.h new file mode 100644 index 00000000000..86c3a47c3be --- /dev/null +++ b/src/effects/native/reverb/dsp/Sine.h @@ -0,0 +1,116 @@ +/* + dsp/Sine.h + + Copyright 2003-13 Tim Goetze + + http://quitte.de/dsp/ + + Direct form I recursive sin() generator. Utilising doubles + for stability. + +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ + +#ifndef _DSP_SINE_H_ +#define _DSP_SINE_H_ + +namespace DSP { + +class Sine +{ + public: + int z; + double y[2]; + double b; + + public: + Sine() + { + b = 0; + y[0] = y[1] = 0; + z = 0; + } + + Sine (double f, double fs, double phase) + { + set_f (f, fs, phase); + } + + Sine (double w, double phase = 0.) + { + set_f (w, phase); + } + + inline void set_f (double f, double fs, double phase) + { + set_f (f*2*M_PI/fs, phase); + } + + inline void set_f (double w, double phase) + { + b = 2 * cos (w); + y[0] = sin (phase - w); + y[1] = sin (phase - w * 2); + z = 0; + } + + /* advance and return 1 sample */ + inline double get() + { + register double s = b * y[z]; + z ^= 1; + s -= y[z]; + return y[z] = s; + } + + double get_phase() + { + double x0 = y[z], x1 = b * y[z] - y[z^1]; + double phi = asin (x0); + + /* slope is falling, we're into the 2nd half. */ + if (x1 < x0) + return M_PI - phi; + + return phi; + } +}; + +/* same as above but including a damping coefficient d */ +class DampedSine +: public Sine +{ + public: + double d; + + public: + DampedSine() + { d = 1; } + + inline double get() + { + register double s = b * y[z]; + z ^= 1; + s -= d * y[z]; + return y[z] = d * s; + } +}; + +} /* namespace DSP */ + +#endif /* _DSP_SINE_H_ */ diff --git a/src/effects/native/reverb/dsp/util.h b/src/effects/native/reverb/dsp/util.h new file mode 100644 index 00000000000..509432f812e --- /dev/null +++ b/src/effects/native/reverb/dsp/util.h @@ -0,0 +1,69 @@ +/* + dsp/util.h + + Copyright 2002-12 Tim Goetze + + http://quitte.de/dsp/ + + Common math utility functions. + +*/ +/* + 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 3 + 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; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA or point your web browser to http://www.gnu.org. +*/ + +#ifndef _DSP_UTIL_H_ +#define _DSP_UTIL_H_ + +namespace DSP { + +inline float pow2 (float x) { return x * x; } +inline float pow3 (float x) { return x * pow2(x); } +inline float pow4 (float x) { return pow2 (pow2(x)); } +inline float pow5 (float x) { return x * pow4(x); } +inline float pow6 (float x) { return pow3 (pow2(x)); } +inline float pow7 (float x) { return x * (pow6 (x)); } +inline float pow8 (float x) { return pow2 (pow4 (x)); } + +inline float +sgn (float x) +{ + union { float f; uint32 i; } u; + u.f = x; + u.i &= 0x80000000; + u.i |= 0x3F800000; + return u.f; +} + +inline bool +isprime (int v) +{ + if (v <= 3) + return true; + + if (!(v & 1)) + return false; + + for (int i = 3; i < (int) sqrt (v) + 1; i += 2) + if ((v % i) == 0) + return false; + + return true; +} + +} /* namespace DSP */ + +#endif /* _DSP_UTIL_H_ */ diff --git a/src/effects/native/reverbeffect.cpp b/src/effects/native/reverbeffect.cpp new file mode 100644 index 00000000000..630f02a0649 --- /dev/null +++ b/src/effects/native/reverbeffect.cpp @@ -0,0 +1,115 @@ +#include + +#include "effects/native/reverbeffect.h" + +#include "mathstuff.h" +#include "sampleutil.h" + +// static +QString ReverbEffect::getId() { + return "org.mixxx.effects.reverb"; +} + +// static +EffectManifest ReverbEffect::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Reverb")); + manifest.setAuthor("The Mixxx Team, CAPS Plugins"); + manifest.setVersion("1.0"); + manifest.setDescription("This is a port of the GPL'ed CAPS Reverb plugin, " + "which has the following description:" + "This is based on some of the famous Stanford CCRMA reverbs " + "(NRev, KipRev) all based on the Chowning/Moorer/Schroeder " + "reverberators, which use networks of simple allpass and comb" + "delay filters."); + + EffectManifestParameter* time = manifest.addParameter(); + time->setId("bandwidth"); + time->setName(QObject::tr("bandwidth")); + time->setDescription(QObject::tr("Higher bandwidth values cause more " + "bright (high-frequency) tones to be included")); + time->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + time->setValueHint(EffectManifestParameter::VALUE_FLOAT); + time->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + time->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + time->setMinimum(0.0005); + time->setDefault(0.5); + time->setMaximum(1.0); + + EffectManifestParameter* damping = manifest.addParameter(); + damping->setId("damping"); + damping->setName(QObject::tr("damping")); + damping->setDescription(QObject::tr("Higher damping values cause " + "reverberations to die out more quickly.")); + damping->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + damping->setValueHint(EffectManifestParameter::VALUE_FLOAT); + damping->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + damping->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + damping->setMinimum(0.005); + damping->setDefault(0.5); + damping->setMaximum(1.0); + + return manifest; +} + +ReverbEffect::ReverbEffect(EngineEffect* pEffect, + const EffectManifest& manifest) + : m_pBandWidthParameter(pEffect->getParameterById("bandwidth")), + m_pDampingParameter(pEffect->getParameterById("damping")) { +} + +ReverbEffect::~ReverbEffect() { + qDebug() << debugString() << "destroyed"; +} + +void ReverbEffect::processGroup(const QString& group, + ReverbGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + CSAMPLE bandwidth = m_pBandWidthParameter ? + m_pBandWidthParameter->value().toDouble() : 1.0f; + CSAMPLE damping = m_pDampingParameter ? + m_pDampingParameter->value().toDouble() : 0.5f; + + // Flip value around. Assumes max allowable is 1.0. + damping = 1.0 - damping; + + bool params_changed = (damping != pState->prev_damping || + bandwidth != pState->prev_bandwidth); + + pState->reverb.setBandwidth(bandwidth); + pState->reverb.setDecay(damping); + + for (uint i = 0; i + 1 < numSamples; i += 2) { + CSAMPLE mono_sample = (pInput[i] + pInput[i + 1]) / 2; + CSAMPLE xl, xr; + + // sample_t is typedefed to be the same as CSAMPLE, so no cast needed. + pState->reverb.process(mono_sample, damping, &xl, &xr); + + pOutput[i] = xl; + pOutput[i + 1] = xr; + } + + if (params_changed) { + pState->reverb.setBandwidth(pState->prev_bandwidth); + pState->reverb.setDecay(pState->prev_damping); + + for (uint i = 0; i + 1 < numSamples; i += 2) { + CSAMPLE mono_sample = (pInput[i] + pInput[i + 1]) / 2; + CSAMPLE xl, xr; + + pState->reverb.process(mono_sample, pState->prev_damping, &xl, &xr); + + pState->crossfade_buffer[i] = xl; + pState->crossfade_buffer[i + 1] = xr; + } + + pState->prev_bandwidth = bandwidth; + pState->prev_damping = damping; + + SampleUtil::linearCrossfadeBuffers( + pOutput, pOutput, pState->crossfade_buffer, numSamples); + } +} diff --git a/src/effects/native/reverbeffect.h b/src/effects/native/reverbeffect.h new file mode 100644 index 00000000000..8e655ca6283 --- /dev/null +++ b/src/effects/native/reverbeffect.h @@ -0,0 +1,62 @@ +// Ported from SWH Plate Reverb 1423. +// This effect is GPL code. + +#ifndef REVERBEFFECT_H +#define REVERBEFFECT_H + +#include + +#include "defs.h" +#include "util.h" +#include "effects/effectprocessor.h" +#include "effects/native/reverb/Reverb.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "sampleutil.h" + +struct ReverbGroupState { + ReverbGroupState() { + // Default damping value. + prev_bandwidth = 0.5; + prev_damping = 0.5; + reverb.init(); + reverb.activate(); + crossfade_buffer = SampleUtil::alloc(MAX_BUFFER_LEN); + } + + ~ReverbGroupState() { + delete crossfade_buffer; + } + + MixxxPlateX2 reverb; + CSAMPLE* crossfade_buffer; + double prev_bandwidth; + double prev_damping; +}; + +class ReverbEffect : public GroupEffectProcessor { + public: + ReverbEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~ReverbEffect(); + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processGroup(const QString& group, + ReverbGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples); + + private: + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pBandWidthParameter; + EngineEffectParameter* m_pDampingParameter; + + DISALLOW_COPY_AND_ASSIGN(ReverbEffect); +}; + +#endif /* REVERBEFFECT_H */ diff --git a/src/engine/effects/engineeffect.cpp b/src/engine/effects/engineeffect.cpp new file mode 100644 index 00000000000..dff88991580 --- /dev/null +++ b/src/engine/effects/engineeffect.cpp @@ -0,0 +1,90 @@ +#include "engine/effects/engineeffect.h" + +EngineEffect::EngineEffect(const EffectManifest& manifest, + const QSet& registeredGroups, + EffectInstantiatorPointer pInstantiator) + : m_manifest(manifest), + m_bEnabled(true), + m_parameters(manifest.parameters().size()) { + const QList& parameters = m_manifest.parameters(); + for (int i = 0; i < parameters.size(); ++i) { + const EffectManifestParameter& parameter = parameters.at(i); + EngineEffectParameter* pParameter = + new EngineEffectParameter(parameter); + m_parameters[i] = pParameter; + m_parametersById[parameter.id()] = pParameter; + } + + // Creating the processor must come last. + m_pProcessor = pInstantiator->instantiate(this, manifest); + m_pProcessor->initialize(registeredGroups); +} + +EngineEffect::~EngineEffect() { + if (kEffectDebugOutput) { + qDebug() << debugString() << "destroyed"; + } + delete m_pProcessor; + m_parametersById.clear(); + for (int i = 0; i < m_parameters.size(); ++i) { + EngineEffectParameter* pParameter = m_parameters.at(i); + m_parameters[i] = NULL; + delete pParameter; + } +} + +bool EngineEffect::processEffectsRequest(const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe) { + EngineEffectParameter* pParameter = NULL; + EffectsResponse response(message); + + switch (message.type) { + case EffectsRequest::SET_EFFECT_PARAMETERS: + if (kEffectDebugOutput) { + qDebug() << debugString() << "SET_EFFECT_PARAMETERS" + << "enabled" << message.SetEffectParameters.enabled; + } + m_bEnabled = message.SetEffectParameters.enabled; + response.success = true; + pResponsePipe->writeMessages(&response, 1); + return true; + break; + case EffectsRequest::SET_PARAMETER_PARAMETERS: + if (kEffectDebugOutput) { + qDebug() << debugString() << "SET_PARAMETER_PARAMETERS" + << "parameter" << message.SetParameterParameters.iParameter + << "minimum" << message.minimum + << "maximum" << message.maximum + << "default_value" << message.default_value + << "value" << message.value; + } + pParameter = m_parameters.value( + message.SetParameterParameters.iParameter, NULL); + if (pParameter) { + pParameter->setMinimum(message.minimum); + pParameter->setMaximum(message.maximum); + pParameter->setDefaultValue(message.default_value); + pParameter->setValue(message.value); + response.success = true; + } else { + response.success = false; + response.status = EffectsResponse::NO_SUCH_PARAMETER; + } + pResponsePipe->writeMessages(&response, 1); + return true; + default: + break; + } + return false; +} + +void EngineEffect::process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + // The EngineEffectChain checks if we are enabled so we don't have to. + if (kEffectDebugOutput && !m_bEnabled) { + qDebug() << debugString() + << "WARNING: EngineEffect::process() called on disabled effect."; + } + m_pProcessor->process(group, pInput, pOutput, numSamples); +} diff --git a/src/engine/effects/engineeffect.h b/src/engine/effects/engineeffect.h new file mode 100644 index 00000000000..6dc5d8ee852 --- /dev/null +++ b/src/engine/effects/engineeffect.h @@ -0,0 +1,57 @@ +#ifndef ENGINEEFFECT_H +#define ENGINEEFFECT_H + +#include +#include +#include +#include +#include +#include + +#include "effects/effectmanifest.h" +#include "effects/effectprocessor.h" +#include "effects/effectinstantiator.h" +#include "engine/effects/engineeffectparameter.h" +#include "engine/effects/message.h" + +class EngineEffect : public EffectsRequestHandler { + public: + EngineEffect(const EffectManifest& manifest, + const QSet& registeredGroups, + EffectInstantiatorPointer pInstantiator); + virtual ~EngineEffect(); + + const QString& name() const { + return m_manifest.name(); + } + + EngineEffectParameter* getParameterById(const QString& id) { + return m_parametersById.value(id, NULL); + } + + bool processEffectsRequest( + const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe); + + void process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples); + + bool enabled() const { + return m_bEnabled; + } + + private: + QString debugString() const { + return QString("EngineEffect(%1)").arg(m_manifest.name()); + } + + EffectManifest m_manifest; + EffectProcessor* m_pProcessor; + bool m_bEnabled; + // Must not be modified after construction. + QVector m_parameters; + QMap m_parametersById; +}; + +#endif /* ENGINEEFFECT_H */ diff --git a/src/engine/effects/engineeffectchain.cpp b/src/engine/effects/engineeffectchain.cpp new file mode 100644 index 00000000000..07de2acc895 --- /dev/null +++ b/src/engine/effects/engineeffectchain.cpp @@ -0,0 +1,300 @@ +#include "engine/effects/engineeffectchain.h" + +#include "engine/effects/engineeffect.h" +#include "sampleutil.h" + +EngineEffectChain::EngineEffectChain(const QString& id) + : m_id(id), + m_bEnabled(true), + m_insertionType(EffectChain::INSERT), + m_dMix(0), + m_pBuffer(SampleUtil::alloc(MAX_BUFFER_LEN)) { + // Try to prevent memory allocation. + m_effects.reserve(256); +} + +EngineEffectChain::~EngineEffectChain() { +} + +bool EngineEffectChain::addEffect(EngineEffect* pEffect, int iIndex) { + if (iIndex < 0) { + if (kEffectDebugOutput) { + qDebug() << debugString() + << "WARNING: ADD_EFFECT_TO_CHAIN message with invalid index:" + << iIndex; + } + return false; + } + if (m_effects.contains(pEffect)) { + if (kEffectDebugOutput) { + qDebug() << debugString() << "WARNING: effect already added to EngineEffectChain:" + << pEffect->name(); + } + return false; + } + + while (iIndex >= m_effects.size()) { + m_effects.append(NULL); + } + m_effects.replace(iIndex, pEffect); + return true; +} + +bool EngineEffectChain::removeEffect(EngineEffect* pEffect, int iIndex) { + if (iIndex < 0) { + if (kEffectDebugOutput) { + qDebug() << debugString() + << "WARNING: REMOVE_EFFECT_FROM_CHAIN message with invalid index:" + << iIndex; + } + return false; + } + if (m_effects.at(iIndex) != pEffect) { + qDebug() << debugString() + << "WARNING: REMOVE_EFFECT_FROM_CHAIN consistency error" + << m_effects.at(iIndex) << "loaded but received request to remove" + << pEffect; + return false; + } + + m_effects.replace(iIndex, NULL); + return true; +} + +bool EngineEffectChain::updateParameters(const EffectsRequest& message) { + // TODO(rryan): Parameter interpolation. + bool wasEnabled = m_bEnabled; + m_bEnabled = message.SetEffectChainParameters.enabled; + m_insertionType = message.SetEffectChainParameters.insertion_type; + m_dMix = message.SetEffectChainParameters.mix; + + // If our enabled state changed then tell each group to ramp in or out. + if (wasEnabled ^ m_bEnabled) { + for (QMap::iterator it = m_groupStatus.begin(); + it != m_groupStatus.end(); it++) { + GroupStatus& status = it.value(); + + if (m_bEnabled) { + // Ramp in. + status.old_gain = 0; + } else { + // Ramp out. + status.ramp_out = true; + } + } + } + return true; +} + +bool EngineEffectChain::processEffectsRequest(const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe) { + EffectsResponse response(message); + switch (message.type) { + case EffectsRequest::ADD_EFFECT_TO_CHAIN: + if (kEffectDebugOutput) { + qDebug() << debugString() << "ADD_EFFECT_TO_CHAIN" + << message.AddEffectToChain.pEffect + << message.AddEffectToChain.iIndex; + } + response.success = addEffect(message.AddEffectToChain.pEffect, + message.AddEffectToChain.iIndex); + break; + case EffectsRequest::REMOVE_EFFECT_FROM_CHAIN: + if (kEffectDebugOutput) { + qDebug() << debugString() << "REMOVE_EFFECT_FROM_CHAIN" + << message.RemoveEffectFromChain.pEffect + << message.RemoveEffectFromChain.iIndex; + } + response.success = removeEffect(message.RemoveEffectFromChain.pEffect, + message.RemoveEffectFromChain.iIndex); + break; + case EffectsRequest::SET_EFFECT_CHAIN_PARAMETERS: + if (kEffectDebugOutput) { + qDebug() << debugString() << "SET_EFFECT_CHAIN_PARAMETERS" + << "enabled" << message.SetEffectChainParameters.enabled + << "mix" << message.SetEffectChainParameters.mix; + } + response.success = updateParameters(message); + break; + case EffectsRequest::ENABLE_EFFECT_CHAIN_FOR_GROUP: + if (kEffectDebugOutput) { + qDebug() << debugString() << "ENABLE_EFFECT_CHAIN_FOR_GROUP" + << message.group; + } + response.success = enableForGroup(message.group); + break; + case EffectsRequest::DISABLE_EFFECT_CHAIN_FOR_GROUP: + if (kEffectDebugOutput) { + qDebug() << debugString() << "DISABLE_EFFECT_CHAIN_FOR_GROUP" + << message.group; + } + response.success = disableForGroup(message.group); + break; + default: + return false; + } + pResponsePipe->writeMessages(&response, 1); + return true; +} + +bool EngineEffectChain::enabledForGroup(const QString& group) const { + const GroupStatus& status = m_groupStatus[group]; + return status.enabled; +} + +bool EngineEffectChain::enableForGroup(const QString& group) { + GroupStatus& status = m_groupStatus[group]; + status.enabled = true; + // Ramp in to prevent clicking. + status.old_gain = 0; + status.ramp_out = false; + return true; +} + +bool EngineEffectChain::disableForGroup(const QString& group) { + GroupStatus& status = m_groupStatus[group]; + status.enabled = false; + // Ramp out to prevent clicking. + status.ramp_out = true; + return true; +} + +void EngineEffectChain::process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + GroupStatus& group_info = m_groupStatus[group]; + bool bEnabled = m_bEnabled && group_info.enabled; + + // If the chain is not enabled and the group is not enabled and we are not + // ramping out then do nothing. + if (!bEnabled && !group_info.ramp_out) { + // If not in-place then copy. This is slow because every + // EngineEffectChain does this. We should pull the processing decision + // out into a predicate that EngineEffectsManager calls but that would + // result in two QMap lookups. + if (pInput != pOutput) { + SampleUtil::copyWithGain(pOutput, pInput, 1.0, numSamples); + if (kEffectDebugOutput) { + qDebug() << "WARNING: EngineEffectChain took the slow path!" + << "If you want to do this talk to rryan."; + } + } + return; + } + + // At this point either the chain and group are enabled or we are ramping + // out. If we are ramping out then ramp to 0 instead of m_dMix. + CSAMPLE wet_gain = group_info.ramp_out ? 0 : m_dMix; + CSAMPLE wet_gain_old = group_info.old_gain; + + // INSERT mode: output = input * (1-wet) + effect(input) * wet + if (m_insertionType == EffectChain::INSERT) { + if (wet_gain_old == 1.0 && wet_gain == 1.0) { + bool anyProcessed = false; + // Fully wet, no ramp, insert optimization. No temporary buffer needed. + for (int i = 0; i < m_effects.size(); ++i) { + EngineEffect* pEffect = m_effects[i]; + if (pEffect == NULL || !pEffect->enabled()) { + continue; + } + const CSAMPLE* pIntermediateInput = (i == 0) ? pInput : pOutput; + CSAMPLE* pIntermediateOutput = pOutput; + pEffect->process(group, pIntermediateInput, pIntermediateOutput, numSamples); + anyProcessed = true; + } + // If no effects were active then we have to copy input to output if + // they are not the same. + if (!anyProcessed && pInput != pOutput) { + SampleUtil::copyWithGain(pOutput, pInput, 1.0, numSamples); + if (kEffectDebugOutput) { + qDebug() << "WARNING: EngineEffectChain took the slow path!" + << "If you want to do this talk to rryan."; + } + } + } else if (wet_gain_old == 0.0 && wet_gain == 0.0) { + // Fully dry, no ramp, insert optimization. No action is needed + // unless we are not processing in-place. + if (pInput != pOutput) { + SampleUtil::copyWithGain(pOutput, pInput, 1.0, numSamples); + if (kEffectDebugOutput) { + qDebug() << "WARNING: EngineEffectChain took the slow path!" + << "If you want to do this talk to rryan."; + } + } + } else { + // Clear scratch buffer. + SampleUtil::applyGain(m_pBuffer, 0.0, numSamples); + + // Chain each effect + bool anyProcessed = false; + for (int i = 0; i < m_effects.size(); ++i) { + EngineEffect* pEffect = m_effects[i]; + if (pEffect == NULL || !pEffect->enabled()) { + continue; + } + const CSAMPLE* pIntermediateInput = (i == 0) ? pInput : m_pBuffer; + CSAMPLE* pIntermediateOutput = m_pBuffer; + pEffect->process(group, pIntermediateInput, pIntermediateOutput, numSamples); + anyProcessed = true; + } + + if (anyProcessed) { + // m_pBuffer now contains the fully wet output. + // TODO(rryan): benchmark applyGain followed by addWithGain versus + // copy2WithGain. + SampleUtil::copy2WithRampingGain( + pOutput, pInput, 1.0 - wet_gain_old, 1.0 - wet_gain, + m_pBuffer, wet_gain_old, wet_gain, numSamples); + } else if (pInput != pOutput) { + // If no effects processed then we have to copy input to output + // if they are not the same. + SampleUtil::copyWithGain(pOutput, pInput, 1.0, numSamples); + if (kEffectDebugOutput) { + qDebug() << "WARNING: EngineEffectChain took the slow path!" + << "If you want to do this talk to rryan."; + } + } + } + } else { // SEND mode: output = input + effect(input) * wet + // Clear scratch buffer. + SampleUtil::applyGain(m_pBuffer, 0.0, numSamples); + + // Chain each effect + bool anyProcessed = false; + for (int i = 0; i < m_effects.size(); ++i) { + EngineEffect* pEffect = m_effects[i]; + if (pEffect == NULL || !pEffect->enabled()) { + continue; + } + const CSAMPLE* pIntermediateInput = (i == 0) ? pInput : m_pBuffer; + CSAMPLE* pIntermediateOutput = m_pBuffer; + pEffect->process(group, pIntermediateInput, + pIntermediateOutput, numSamples); + anyProcessed = true; + } + + if (anyProcessed) { + // m_pBuffer now contains the fully wet output. + if (pInput == pOutput) { + SampleUtil::addWithRampingGain(pOutput, m_pBuffer, + wet_gain_old, wet_gain, numSamples); + } else { + SampleUtil::copy2WithRampingGain(pOutput, pInput, 1.0, 1.0, + m_pBuffer, wet_gain_old, wet_gain, + numSamples); + } + } else if (pInput != pOutput) { + // If no effects processed then we have to copy input to output + // if they are not the same. + SampleUtil::copyWithGain(pOutput, pInput, 1.0, numSamples); + if (kEffectDebugOutput) { + qDebug() << "WARNING: EngineEffectChain took the slow path!" + << "If you want to do this talk to rryan."; + } + } + } + + // Update GroupStatus with the latest values. + group_info.old_gain = wet_gain; + group_info.ramp_out = false; +} diff --git a/src/engine/effects/engineeffectchain.h b/src/engine/effects/engineeffectchain.h new file mode 100644 index 00000000000..386c151cf7e --- /dev/null +++ b/src/engine/effects/engineeffectchain.h @@ -0,0 +1,69 @@ +#ifndef ENGINEEFFECTCHAIN_H +#define ENGINEEFFECTCHAIN_H + +#include +#include +#include + +#include "defs.h" +#include "util.h" +#include "engine/effects/message.h" +#include "effects/effectchain.h" + +class EngineEffect; + +class EngineEffectChain : public EffectsRequestHandler { + public: + EngineEffectChain(const QString& id); + virtual ~EngineEffectChain(); + + bool processEffectsRequest( + const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe); + + void process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples); + + const QString& id() const { + return m_id; + } + + bool enabled() const { + return m_bEnabled; + } + + bool enabledForGroup(const QString& group) const; + + private: + QString debugString() const { + return QString("EngineEffectChain(%1)").arg(m_id); + } + + bool updateParameters(const EffectsRequest& message); + bool addEffect(EngineEffect* pEffect, int iIndex); + bool removeEffect(EngineEffect* pEffect, int iIndex); + bool enableForGroup(const QString& group); + bool disableForGroup(const QString& group); + + QString m_id; + bool m_bEnabled; + EffectChain::InsertionType m_insertionType; + CSAMPLE m_dMix; + QList m_effects; + CSAMPLE* m_pBuffer; + struct GroupStatus { + GroupStatus() : enabled(false), + old_gain(0), + ramp_out(false) { + } + bool enabled; + CSAMPLE old_gain; + bool ramp_out; + }; + QMap m_groupStatus; + + DISALLOW_COPY_AND_ASSIGN(EngineEffectChain); +}; + +#endif /* ENGINEEFFECTCHAIN_H */ diff --git a/src/engine/effects/engineeffectparameter.h b/src/engine/effects/engineeffectparameter.h new file mode 100644 index 00000000000..bc4abcaf89c --- /dev/null +++ b/src/engine/effects/engineeffectparameter.h @@ -0,0 +1,89 @@ +#ifndef ENGINEEFFECTPARAMETER_H +#define ENGINEEFFECTPARAMETER_H + +#include +#include + +#include "util.h" +#include "effects/effectmanifestparameter.h" + +class EngineEffectParameter { + public: + EngineEffectParameter(const EffectManifestParameter& parameter) + : m_parameter(parameter) { + // NOTE(rryan): This is just to set the parameter values to sane + // defaults. When an effect is loaded into the engine it is supposed to + // immediately send a parameter update. Some effects will go crazy if + // their parameters are not within the manifest's minimum/maximum bounds + // so just to be safe we read the min/max/default from the manifest + // here. + if (m_parameter.hasMinimum()) { + m_minimum = m_parameter.getMinimum(); + } + if (m_parameter.hasMaximum()) { + m_maximum = m_parameter.getMaximum(); + } + if (m_parameter.hasDefault()) { + m_default_value = m_parameter.getDefault(); + } + m_value = m_default_value; + } + virtual ~EngineEffectParameter() { } + + /////////////////////////////////////////////////////////////////////////// + // Parameter Information + /////////////////////////////////////////////////////////////////////////// + + const QString& id() const { + return m_parameter.id(); + } + const QString& name() const { + return m_parameter.name(); + } + const QString& description() const { + return m_parameter.description(); + } + + /////////////////////////////////////////////////////////////////////////// + // Value Settings + /////////////////////////////////////////////////////////////////////////// + + const QVariant& value() const { + return m_value; + } + void setValue(const QVariant& value) { + m_value = value; + } + + const QVariant& defaultValue() const { + return m_default_value; + } + void setDefaultValue(const QVariant& default_value) { + m_default_value = default_value; + } + + const QVariant& minimum() const { + return m_minimum; + } + void setMinimum(const QVariant& minimum) { + m_minimum = minimum; + } + + const QVariant& maximum() const { + return m_maximum; + } + void setMaximum(const QVariant& maximum) { + m_maximum = maximum; + } + + private: + EffectManifestParameter m_parameter; + QVariant m_value; + QVariant m_default_value; + QVariant m_minimum; + QVariant m_maximum; + + DISALLOW_COPY_AND_ASSIGN(EngineEffectParameter); +}; + +#endif /* ENGINEEFFECTPARAMETER_H */ diff --git a/src/engine/effects/engineeffectrack.cpp b/src/engine/effects/engineeffectrack.cpp new file mode 100644 index 00000000000..b39ec4de31d --- /dev/null +++ b/src/engine/effects/engineeffectrack.cpp @@ -0,0 +1,106 @@ +#include "engine/effects/engineeffectrack.h" + +#include "engine/effects/engineeffectchain.h" +#include "sampleutil.h" + +EngineEffectRack::EngineEffectRack(int iRackNumber) + : m_iRackNumber(iRackNumber) { + // Try to prevent memory allocation. + m_chains.reserve(256); +} + +EngineEffectRack::~EngineEffectRack() { +} + +bool EngineEffectRack::processEffectsRequest(const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe) { + EffectsResponse response(message); + switch (message.type) { + case EffectsRequest::ADD_CHAIN_TO_RACK: + if (kEffectDebugOutput) { + qDebug() << debugString() << "ADD_CHAIN_TO_RACK" + << message.AddChainToRack.pChain + << message.AddChainToRack.iIndex; + } + response.success = addEffectChain(message.AddChainToRack.pChain, + message.AddChainToRack.iIndex); + break; + case EffectsRequest::REMOVE_CHAIN_FROM_RACK: + if (kEffectDebugOutput) { + qDebug() << debugString() << "REMOVE_CHAIN_FROM_RACK" + << message.RemoveChainFromRack.pChain + << message.RemoveChainFromRack.iIndex; + } + response.success = removeEffectChain(message.RemoveChainFromRack.pChain, + message.RemoveChainFromRack.iIndex); + break; + default: + return false; + } + pResponsePipe->writeMessages(&response, 1); + return true; +} + +void EngineEffectRack::process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + bool anyProcessed = false; + foreach (EngineEffectChain* pChain, m_chains) { + if (pChain != NULL) { + pChain->process(group, pInput, pOutput, numSamples); + anyProcessed = true; + } + } + if (!anyProcessed && pInput != pOutput) { + SampleUtil::copyWithGain(pOutput, pInput, 1.0, numSamples); + if (kEffectDebugOutput) { + qDebug() << "WARNING: EngineEffectRack took the slow path!" + << "If you want to do this talk to rryan."; + } + } +} + +bool EngineEffectRack::addEffectChain(EngineEffectChain* pChain, int iIndex) { + if (iIndex < 0) { + if (kEffectDebugOutput) { + qDebug() << debugString() + << "WARNING: ADD_CHAIN_TO_RACK message with invalid index:" + << iIndex; + } + return false; + } + if (m_chains.contains(pChain)) { + if (kEffectDebugOutput) { + qDebug() << debugString() << "WARNING: chain already added to EngineEffectRack:" + << pChain->id(); + } + return false; + } + while (iIndex >= m_chains.size()) { + m_chains.append(NULL); + } + m_chains.replace(iIndex, pChain); + return true; +} + +bool EngineEffectRack::removeEffectChain(EngineEffectChain* pChain, int iIndex) { + if (iIndex < 0) { + if (kEffectDebugOutput) { + qDebug() << debugString() + << "WARNING: REMOVE_CHAIN_FROM_RACK message with invalid index:" + << iIndex; + } + return false; + } + + if (m_chains.at(iIndex) != pChain) { + qDebug() << debugString() + << "WARNING: REMOVE_CHAIN_FROM_RACK consistency error" + << m_chains.at(iIndex) << "loaded but received request to remove" + << pChain; + return false; + } + + m_chains.replace(iIndex, NULL); + return true; +} diff --git a/src/engine/effects/engineeffectrack.h b/src/engine/effects/engineeffectrack.h new file mode 100644 index 00000000000..2b4b0433519 --- /dev/null +++ b/src/engine/effects/engineeffectrack.h @@ -0,0 +1,41 @@ +#ifndef ENGINEEFFECTRACK_H +#define ENGINEEFFECTRACK_H + +#include + +#include "engine/effects/message.h" + +class EngineEffectChain; + +class EngineEffectRack : public EffectsRequestHandler { + public: + EngineEffectRack(int iRackNumber); + virtual ~EngineEffectRack(); + + bool processEffectsRequest( + const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe); + + void process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples); + + int number() const { + return m_iRackNumber; + } + + private: + bool addEffectChain(EngineEffectChain* pChain, int iIndex); + bool removeEffectChain(EngineEffectChain* pChain, int iIndex); + + QString debugString() const { + return QString("EngineEffectRack%1").arg(m_iRackNumber); + } + + int m_iRackNumber; + QList m_chains; + + DISALLOW_COPY_AND_ASSIGN(EngineEffectRack); +}; + +#endif /* ENGINEEFFECTRACK_H */ diff --git a/src/engine/effects/engineeffectsmanager.cpp b/src/engine/effects/engineeffectsmanager.cpp new file mode 100644 index 00000000000..e6b518b0e58 --- /dev/null +++ b/src/engine/effects/engineeffectsmanager.cpp @@ -0,0 +1,180 @@ +#include "engine/effects/engineeffectsmanager.h" + +#include "engine/effects/engineeffectrack.h" +#include "engine/effects/engineeffectchain.h" +#include "engine/effects/engineeffect.h" + +EngineEffectsManager::EngineEffectsManager(EffectsResponsePipe* pResponsePipe) + : m_pResponsePipe(pResponsePipe) { + // Try to prevent memory allocation. + m_racks.reserve(256); + m_chains.reserve(256); + m_effects.reserve(256); +} + +EngineEffectsManager::~EngineEffectsManager() { +} + +void EngineEffectsManager::onCallbackStart() { + EffectsRequest* request = NULL; + while (m_pResponsePipe->readMessages(&request, 1) > 0) { + EffectsResponse response(*request); + bool processed = false; + switch (request->type) { + case EffectsRequest::ADD_EFFECT_RACK: + case EffectsRequest::REMOVE_EFFECT_RACK: + if (processEffectsRequest(*request, m_pResponsePipe.data())) { + processed = true; + } + break; + case EffectsRequest::ADD_CHAIN_TO_RACK: + case EffectsRequest::REMOVE_CHAIN_FROM_RACK: + if (!m_racks.contains(request->pTargetRack)) { + if (kEffectDebugOutput) { + qDebug() << debugString() + << "WARNING: message for unloaded rack" + << request->pTargetRack; + } + response.success = false; + response.status = EffectsResponse::NO_SUCH_RACK; + } else { + processed = request->pTargetRack->processEffectsRequest( + *request, m_pResponsePipe.data()); + + if (processed) { + // When an effect-chain becomes active (part of a rack), keep + // it in our master list so that we can respond to + // requests about it. + if (request->type == EffectsRequest::ADD_CHAIN_TO_RACK) { + m_chains.append(request->AddChainToRack.pChain); + } else if (request->type == EffectsRequest::REMOVE_CHAIN_FROM_RACK) { + m_chains.removeAll(request->RemoveChainFromRack.pChain); + } + } else { + if (!processed) { + // If we got here, the message was not handled for + // an unknown reason. + response.success = false; + response.status = EffectsResponse::INVALID_REQUEST; + } + } + } + break; + case EffectsRequest::ADD_EFFECT_TO_CHAIN: + case EffectsRequest::REMOVE_EFFECT_FROM_CHAIN: + case EffectsRequest::SET_EFFECT_CHAIN_PARAMETERS: + case EffectsRequest::ENABLE_EFFECT_CHAIN_FOR_GROUP: + case EffectsRequest::DISABLE_EFFECT_CHAIN_FOR_GROUP: + if (!m_chains.contains(request->pTargetChain)) { + if (kEffectDebugOutput) { + qDebug() << debugString() + << "WARNING: message for unloaded chain" + << request->pTargetChain; + } + response.success = false; + response.status = EffectsResponse::NO_SUCH_CHAIN; + } else { + processed = request->pTargetChain->processEffectsRequest( + *request, m_pResponsePipe.data()); + + if (processed) { + // When an effect becomes active (part of a chain), keep + // it in our master list so that we can respond to + // requests about it. + if (request->type == EffectsRequest::ADD_EFFECT_TO_CHAIN) { + m_effects.append(request->AddEffectToChain.pEffect); + } else if (request->type == EffectsRequest::REMOVE_EFFECT_FROM_CHAIN) { + m_effects.removeAll(request->RemoveEffectFromChain.pEffect); + } + } else { + if (!processed) { + // If we got here, the message was not handled for + // an unknown reason. + response.success = false; + response.status = EffectsResponse::INVALID_REQUEST; + } + } + } + break; + case EffectsRequest::SET_EFFECT_PARAMETERS: + case EffectsRequest::SET_PARAMETER_PARAMETERS: + if (!m_effects.contains(request->pTargetEffect)) { + if (kEffectDebugOutput) { + qDebug() << debugString() + << "WARNING: message for unloaded effect" + << request->pTargetEffect; + } + response.success = false; + response.status = EffectsResponse::NO_SUCH_EFFECT; + } else { + processed = request->pTargetEffect + ->processEffectsRequest(*request, m_pResponsePipe.data()); + + if (!processed) { + // If we got here, the message was not handled for an + // unknown reason. + response.success = false; + response.status = EffectsResponse::INVALID_REQUEST; + } + } + break; + default: + response.success = false; + response.status = EffectsResponse::UNHANDLED_MESSAGE_TYPE; + break; + } + + if (!processed) { + m_pResponsePipe->writeMessages(&response, 1); + } + } +} + +void EngineEffectsManager::process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples) { + foreach (EngineEffectRack* pRack, m_racks) { + pRack->process(group, pInput, pOutput, numSamples); + } +} + +bool EngineEffectsManager::addEffectRack(EngineEffectRack* pRack) { + if (m_racks.contains(pRack)) { + if (kEffectDebugOutput) { + qDebug() << debugString() << "WARNING: EffectRack already added to EngineEffectsManager:" + << pRack->number(); + } + return false; + } + m_racks.append(pRack); + return true; +} + +bool EngineEffectsManager::removeEffectRack(EngineEffectRack* pRack) { + return m_racks.removeAll(pRack) > 0; +} + +bool EngineEffectsManager::processEffectsRequest(const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe) { + EffectsResponse response(message); + switch (message.type) { + case EffectsRequest::ADD_EFFECT_RACK: + if (kEffectDebugOutput) { + qDebug() << debugString() << "ADD_EFFECT_RACK" + << message.AddEffectRack.pRack; + } + response.success = addEffectRack(message.AddEffectRack.pRack); + break; + case EffectsRequest::REMOVE_EFFECT_RACK: + if (kEffectDebugOutput) { + qDebug() << debugString() << "REMOVE_EFFECT_RACK" + << message.RemoveEffectRack.pRack; + } + response.success = removeEffectRack(message.RemoveEffectRack.pRack); + break; + default: + return false; + } + pResponsePipe->writeMessages(&response, 1); + return true; +} diff --git a/src/engine/effects/engineeffectsmanager.h b/src/engine/effects/engineeffectsmanager.h new file mode 100644 index 00000000000..cbae4462897 --- /dev/null +++ b/src/engine/effects/engineeffectsmanager.h @@ -0,0 +1,51 @@ +#ifndef ENGINEEFFECTSMANAGER_H +#define ENGINEEFFECTSMANAGER_H + +#include + +#include "defs.h" +#include "util/fifo.h" +#include "engine/effects/message.h" + +class EngineEffectRack; +class EngineEffectChain; +class EngineEffect; + +class EngineEffectsManager : public EffectsRequestHandler { + public: + EngineEffectsManager(EffectsResponsePipe* pResponsePipe); + virtual ~EngineEffectsManager(); + + void onCallbackStart(); + + // Take a buffer of numSamples samples of audio from group, provided as + // pInput, and apply each EffectChain enabled for this group to it, + // putting the resulting output in pOutput. If pInput is equal to pOutput, + // then the operation must occur in-place. Both pInput and pOutput are + // represented as stereo interleaved samples. There are numSamples total + // samples, so numSamples/2 left channel samples and numSamples/2 right + // channel samples. + virtual void process(const QString& group, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples); + + bool processEffectsRequest( + const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe); + + private: + QString debugString() const { + return QString("EngineEffectsManager"); + } + + bool addEffectRack(EngineEffectRack* pRack); + bool removeEffectRack(EngineEffectRack* pRack); + + QScopedPointer m_pResponsePipe; + QList m_racks; + QList m_chains; + QList m_effects; +}; + + +#endif /* ENGINEEFFECTSMANAGER_H */ diff --git a/src/engine/effects/message.h b/src/engine/effects/message.h new file mode 100644 index 00000000000..e0a018c86b1 --- /dev/null +++ b/src/engine/effects/message.h @@ -0,0 +1,177 @@ +#ifndef MESSAGE_H +#define MESSAGE_H + +#include +#include +#include + +#include "util/fifo.h" +#include "effects/effectchain.h" + +const bool kEffectDebugOutput = true; + +class EngineEffectRack; +class EngineEffectChain; +class EngineEffect; + +struct EffectsRequest { + enum MessageType { + // Messages for EngineEffectsManager + ADD_EFFECT_RACK = 0, + REMOVE_EFFECT_RACK, + + // Messages for EngineEffectRack + ADD_CHAIN_TO_RACK, + REMOVE_CHAIN_FROM_RACK, + + // Messages for EngineEffectChain + SET_EFFECT_CHAIN_PARAMETERS, + ADD_EFFECT_TO_CHAIN, + REMOVE_EFFECT_FROM_CHAIN, + ENABLE_EFFECT_CHAIN_FOR_GROUP, + DISABLE_EFFECT_CHAIN_FOR_GROUP, + + // Messages for EngineEffect + SET_EFFECT_PARAMETERS, + SET_PARAMETER_PARAMETERS, + + // Must come last. + NUM_REQUEST_TYPES + }; + + EffectsRequest() + : type(NUM_REQUEST_TYPES), + request_id(-1) { + pTargetRack = NULL; + pTargetChain = NULL; + pTargetEffect = NULL; +#define CLEAR_STRUCT(x) memset(&x, 0, sizeof(x)); + CLEAR_STRUCT(AddEffectRack); + CLEAR_STRUCT(RemoveEffectRack); + CLEAR_STRUCT(AddChainToRack); + CLEAR_STRUCT(RemoveChainFromRack); + CLEAR_STRUCT(AddEffectToChain); + CLEAR_STRUCT(RemoveEffectFromChain); + CLEAR_STRUCT(SetEffectChainParameters); + CLEAR_STRUCT(SetEffectParameters); + CLEAR_STRUCT(SetParameterParameters); +#undef CLEAR_STRUCT + } + + MessageType type; + qint64 request_id; + + // Target of the message. + union { + // Used by: + // - ADD_CHAIN_TO_RACK + // - REMOVE_CHAIN_FROM_RACK + EngineEffectRack* pTargetRack; + // Used by: + // - ADD_EFFECT_TO_CHAIN + // - REMOVE_EFFECT_FROM_CHAIN + // - SET_EFFECT_CHAIN_PARAMETERS + // - ENABLE_EFFECT_CHAIN_FOR_GROUP + // - DISABLE_EFFECT_CHAIN_FOR_GROUP + EngineEffectChain* pTargetChain; + // Used by: + // - SET_EFFECT_PARAMETER + EngineEffect* pTargetEffect; + }; + + // Message-specific data. + union { + struct { + EngineEffectRack* pRack; + } AddEffectRack; + struct { + EngineEffectRack* pRack; + } RemoveEffectRack; + struct { + EngineEffectChain* pChain; + int iIndex; + } AddChainToRack; + struct { + EngineEffectChain* pChain; + int iIndex; + } RemoveChainFromRack; + struct { + EngineEffect* pEffect; + int iIndex; + } AddEffectToChain; + struct { + EngineEffect* pEffect; + int iIndex; + } RemoveEffectFromChain; + struct { + bool enabled; + EffectChain::InsertionType insertion_type; + double mix; + } SetEffectChainParameters; + struct { + bool enabled; + } SetEffectParameters; + struct { + int iParameter; + } SetParameterParameters; + }; + + //////////////////////////////////////////////////////////////////////////// + // Message-specific, non-POD values that can't be part of the above union. + //////////////////////////////////////////////////////////////////////////// + + // Used by ENABLE_EFFECT_CHAIN_FOR_GROUP and DISABLE_EFFECT_CHAIN_FOR_GROUP. + QString group; + + // Used by SET_EFFECT_PARAMETER. + QVariant minimum; + QVariant maximum; + QVariant default_value; + QVariant value; +}; + +struct EffectsResponse { + enum StatusCode { + OK, + UNHANDLED_MESSAGE_TYPE, + NO_SUCH_RACK, + NO_SUCH_CHAIN, + NO_SUCH_EFFECT, + NO_SUCH_PARAMETER, + INVALID_REQUEST, + + // Must come last. + NUM_STATUS_CODES + }; + + EffectsResponse() + : request_id(-1), + success(false), + status(NUM_STATUS_CODES) { + } + + EffectsResponse(const EffectsRequest& request, bool succeeded=false) + : request_id(request.request_id), + success(succeeded), + status(NUM_STATUS_CODES) { + } + + qint64 request_id; + bool success; + StatusCode status; +}; + +// For communicating from the main thread to the EngineEffectsManager. +typedef MessagePipe EffectsRequestPipe; + +// For communicating from the EngineEffectsManager to the main thread. +typedef MessagePipe EffectsResponsePipe; + +class EffectsRequestHandler { + public: + virtual bool processEffectsRequest( + const EffectsRequest& message, + EffectsResponsePipe* pResponsePipe) = 0; +}; + +#endif /* MESSAGE_H */ diff --git a/src/engine/enginechannel.h b/src/engine/enginechannel.h index 8baa2897ffe..e29e32cb5fb 100644 --- a/src/engine/enginechannel.h +++ b/src/engine/enginechannel.h @@ -26,11 +26,9 @@ class EngineBuffer; class EnginePregain; class EngineFilterBlock; class EngineClipping; -class EngineFlanger; class EngineVuMeter; class EngineVinylSoundEmu; class ControlPushButton; -class ControlObject; class EngineChannel : public EngineObject { Q_OBJECT diff --git a/src/engine/enginedeck.cpp b/src/engine/enginedeck.cpp index 83370e79862..bd83478e356 100644 --- a/src/engine/enginedeck.cpp +++ b/src/engine/enginedeck.cpp @@ -16,13 +16,13 @@ ***************************************************************************/ #include "controlpushbutton.h" +#include "effects/effectsmanager.h" +#include "engine/effects/engineeffectsmanager.h" #include "engine/enginebuffer.h" #include "engine/enginevinylsoundemu.h" #include "engine/enginedeck.h" #include "engine/engineclipping.h" #include "engine/enginepregain.h" -#include "engine/engineflanger.h" -#include "engine/enginefiltereffect.h" #include "engine/enginefilterblock.h" #include "engine/enginevumeter.h" #include "engine/enginefilteriir.h" @@ -32,13 +32,16 @@ EngineDeck::EngineDeck(const char* group, ConfigObject* pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation) : EngineChannel(group, defaultOrientation), m_pConfig(pConfig), + m_pEngineEffectsManager(pEffectsManager->getEngineEffectsManager()), m_pPassing(new ControlPushButton(ConfigKey(group, "passthrough"))), // Need a +1 here because the CircularBuffer only allows its size-1 // items to be held at once (it keeps a blank spot open persistently) m_sampleBuffer(MAX_BUFFER_LEN+1) { + pEffectsManager->registerGroup(getGroup()); // Set up passthrough utilities and fields m_pPassing->setButtonMode(ControlPushButton::POWERWINDOW); @@ -54,8 +57,6 @@ EngineDeck::EngineDeck(const char* group, // Set up additional engines m_pPregain = new EnginePregain(group); m_pFilter = new EngineFilterBlock(group); - m_pFlanger = new EngineFlanger(group); - m_pFilterEffect = new EngineFilterEffect(group); m_pClipping = new EngineClipping(group); m_pBuffer = new EngineBuffer(group, pConfig, this, pMixingEngine); m_pVinylSoundEmu = new EngineVinylSoundEmu(pConfig, group); @@ -69,8 +70,6 @@ EngineDeck::~EngineDeck() { delete m_pBuffer; delete m_pClipping; delete m_pFilter; - delete m_pFlanger; - delete m_pFilterEffect; delete m_pPregain; delete m_pVinylSoundEmu; delete m_pVUMeter; @@ -108,8 +107,8 @@ void EngineDeck::process(const CSAMPLE*, CSAMPLE* pOut, const int iBufferSize) { m_pPregain->process(pOut, pOut, iBufferSize); // Filter the channel with EQs m_pFilter->process(pOut, pOut, iBufferSize); - m_pFlanger->process(pOut, pOut, iBufferSize); - m_pFilterEffect->process(pOut, pOut, iBufferSize); + // Process effects enabled for this channel + m_pEngineEffectsManager->process(getGroup(), pOut, pOut, iBufferSize); // Apply clipping m_pClipping->process(pOut, pOut, iBufferSize); // Update VU meter diff --git a/src/engine/enginedeck.h b/src/engine/enginedeck.h index 0ddf1f18fd7..dd6e79c6548 100644 --- a/src/engine/enginedeck.h +++ b/src/engine/enginedeck.h @@ -31,17 +31,18 @@ class EnginePregain; class EngineBuffer; class EngineFilterBlock; class EngineClipping; -class EngineFlanger; -class EngineFilterEffect; class EngineMaster; class EngineVuMeter; class EngineVinylSoundEmu; +class EffectsManager; +class EngineEffectsManager; class ControlPushButton; class EngineDeck : public EngineChannel, public AudioDestination { Q_OBJECT public: - EngineDeck(const char* group, ConfigObject* pConfig, EngineMaster* pMixingEngine, + EngineDeck(const char* group, ConfigObject* pConfig, + EngineMaster* pMixingEngine, EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation = CENTER); virtual ~EngineDeck(); @@ -79,11 +80,10 @@ class EngineDeck : public EngineChannel, public AudioDestination { EngineBuffer* m_pBuffer; EngineClipping* m_pClipping; EngineFilterBlock* m_pFilter; - EngineFlanger* m_pFlanger; - EngineFilterEffect* m_pFilterEffect; EnginePregain* m_pPregain; EngineVinylSoundEmu* m_pVinylSoundEmu; EngineVuMeter* m_pVUMeter; + EngineEffectsManager* m_pEngineEffectsManager; // Begin vinyl passthrough fields ControlPushButton* m_pPassing; diff --git a/src/engine/enginefilterbutterworth8.h b/src/engine/enginefilterbutterworth8.h index afa5a2508c2..e32aeeffc4c 100644 --- a/src/engine/enginefilterbutterworth8.h +++ b/src/engine/enginefilterbutterworth8.h @@ -1,3 +1,6 @@ +#ifndef ENGINE_ENGINEFILTERBUTTERWORTH8_H_ +#define ENGINE_ENGINEFILTERBUTTERWORTH8_H_ + #define MAX_COEFS 17 #define MAX_INTERNAL_BUF 16 @@ -54,3 +57,5 @@ class EngineFilterButterworth8High : public EngineFilterButterworth8 { void setFrequencyCorners(double freqCorner1); void process(const CSAMPLE* pIn, CSAMPLE* pOut, const int iBufferSize); }; + +#endif // ENGINE_ENGINEFILTERBUTTERWORTH8_H_ diff --git a/src/engine/enginefiltereffect.cpp b/src/engine/enginefiltereffect.cpp deleted file mode 100644 index 486b51f83de..00000000000 --- a/src/engine/enginefiltereffect.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "engine/enginefiltereffect.h" - -#include "sampleutil.h" -#include "controlobject.h" -#include "engine/enginefilterbutterworth8.h" -#include "controlpushbutton.h" -#include "controlpotmeter.h" - -EngineFilterEffect::EngineFilterEffect(const char* group) { - m_pPotmeterDepth = new ControlPotmeter(ConfigKey(group, "filterDepth"), -1., 1.); - m_pFilterEnable = new ControlPushButton(ConfigKey(group, "filter")); - m_pFilterEnable->setButtonMode(ControlPushButton::TOGGLE); - - // TODO(XXX) 44100 should be changed to real sample rate - // https://bugs.launchpad.net/mixxx/+bug/1208816 - m_pLowFilter = new EngineFilterButterworth8Low(44100, 20); - m_pBandpassFilter = new EngineFilterButterworth8Band(44100, 20, 200); - m_pHighFilter = new EngineFilterButterworth8High(44100, 20); - - m_pCrossfade_buffer = SampleUtil::alloc(MAX_BUFFER_LEN); - m_pBandpass_buffer = SampleUtil::alloc(MAX_BUFFER_LEN); - - m_old_depth = 0.0; -} - -EngineFilterEffect::~EngineFilterEffect() { - delete m_pLowFilter; - delete m_pBandpassFilter; - delete m_pHighFilter; - - delete m_pPotmeterDepth; - delete m_pFilterEnable; - - SampleUtil::free(m_pCrossfade_buffer); - SampleUtil::free(m_pBandpass_buffer); -} - -void EngineFilterEffect::applyFilters(const CSAMPLE* pIn, CSAMPLE* pOut, - const int iBufferSize, double depth) { - // Gain of bandpass filter - double bandpass_gain = 0.3; - - if (depth < 0.0) { - m_pLowFilter->process(pIn, pOut, iBufferSize); - m_pBandpassFilter->process(pIn, m_pBandpass_buffer, iBufferSize); - } else { - m_pHighFilter->process(pIn, pOut, iBufferSize); - m_pBandpassFilter->process(pIn, m_pBandpass_buffer, iBufferSize); - } - - SampleUtil::addWithGain(pOut, m_pBandpass_buffer, bandpass_gain, iBufferSize); -} - -void EngineFilterEffect::process(const CSAMPLE* pIn, CSAMPLE* pOutput, - const int iBufferSize) { - double depth = m_pPotmeterDepth->get(); - - if (m_pFilterEnable->get() == 0.0) { - depth = 0.0; - } - - double freq, freq2; - // Length of bandpass filter - double bandpass_size = 0.01; - - if (depth != m_old_depth) { - if (m_old_depth == 0.0) { - SampleUtil::copyWithGain(m_pCrossfade_buffer, pIn, 1.0, iBufferSize); - } else if (m_old_depth == -1.0 || m_old_depth == 1.0) { - SampleUtil::copyWithGain(m_pCrossfade_buffer, pIn, 0.0, iBufferSize); - } else { - applyFilters(pIn, m_pCrossfade_buffer, iBufferSize, m_old_depth); - } - if (depth < 0.0) { - // Lowpass + bandpass - // Freq from 2^5=32Hz to 2^(5+9)=16384 - freq = pow(2.0, 5.0 + (depth + 1.0) * 9.0); - freq2 = pow(2.0, 5.0 + (depth + 1.0 + bandpass_size) * 9.0); - m_pLowFilter->setFrequencyCorners(freq2); - m_pBandpassFilter->setFrequencyCorners(freq, freq2); - } else if (depth > 0.0) { - // Highpass + bandpass - freq = pow(2.0, 5.0 + depth * 9.0); - freq2 = pow(2.0, 5.0 + (depth + bandpass_size) * 9.0); - m_pHighFilter->setFrequencyCorners(freq); - m_pBandpassFilter->setFrequencyCorners(freq, freq2); - } - } - - if (depth == 0.0) { - SampleUtil::copyWithGain(pOutput, pIn, 1.0, iBufferSize); - } else if (depth == -1.0 || depth == 1.0) { - SampleUtil::copyWithGain(pOutput, pIn, 0.0, iBufferSize); - } else { - applyFilters(pIn, pOutput, iBufferSize, depth); - } - - if (depth != m_old_depth) { - SampleUtil::linearCrossfadeBuffers(pOutput, m_pCrossfade_buffer, - pOutput, iBufferSize); - } - - m_old_depth = depth; -} diff --git a/src/engine/enginefiltereffect.h b/src/engine/enginefiltereffect.h deleted file mode 100644 index 3a1a23689ae..00000000000 --- a/src/engine/enginefiltereffect.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef ENGINEFILTEREFFECT_H -#define ENGINEFILTEREFFECT_H - -#include "engine/engineobject.h" - -class ControlObject; -class ControlPushButton; -class EngineFilterButterworth8Low; -class EngineFilterButterworth8Band; -class EngineFilterButterworth8High; - -class EngineFilterEffect : public EngineObject { - public: - EngineFilterEffect(const char* group); - virtual ~EngineFilterEffect(); - - void process(const CSAMPLE* pIn, CSAMPLE* pOut, const int iBufferSize); - - private: - void applyFilters(const CSAMPLE* pIn, CSAMPLE* pOut, const int iBufferSize, - double depth); - - // Buffers for old filter's value and for bandpass filter - CSAMPLE* m_pCrossfade_buffer; - CSAMPLE* m_pBandpass_buffer; - EngineFilterButterworth8Low* m_pLowFilter; - EngineFilterButterworth8Band* m_pBandpassFilter; - EngineFilterButterworth8High* m_pHighFilter; - - ControlObject* m_pPotmeterDepth; - ControlPushButton* m_pFilterEnable; - - double m_old_depth; -}; - -#endif // ENGINEFILTEREFFECT_H diff --git a/src/engine/engineflanger.cpp b/src/engine/engineflanger.cpp deleted file mode 100644 index cc89b78ae16..00000000000 --- a/src/engine/engineflanger.cpp +++ /dev/null @@ -1,143 +0,0 @@ -/*************************************************************************** - engineflanger.cpp - description - ------------------- - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : -***************************************************************************/ - -/*************************************************************************** -* * -* 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. * -* * -***************************************************************************/ - -#include - -#include "controlpushbutton.h" -#include "controlpotmeter.h" -#include "engine/engineflanger.h" -#include "mathstuff.h" -#include "sampleutil.h" - -class EngineFlangerControls { - public: - static QSharedPointer instance() { - if (!m_pInstance) { - QSharedPointer ptr(new EngineFlangerControls()); - m_pInstance = ptr; - return ptr; - } - return m_pInstance; - } - - ~EngineFlangerControls() { - qDebug() << "~EngineFlangerControls"; - delete m_pPotmeterDepth; - delete m_pPotmeterDelay; - delete m_pPotmeterLFOperiod; - } - - ControlObject* m_pPotmeterDepth; - ControlObject* m_pPotmeterDelay; - ControlObject* m_pPotmeterLFOperiod; - - private: - EngineFlangerControls() { - m_pPotmeterDepth = new ControlPotmeter( - ConfigKey("[Flanger]", "lfoDepth"), 0., 1.); - m_pPotmeterDelay = new ControlPotmeter( - ConfigKey("[Flanger]", "lfoDelay"), 50., 10000.); - m_pPotmeterLFOperiod = new ControlPotmeter( - ConfigKey("[Flanger]", "lfoPeriod"), 50000., 2000000.); - } - static QWeakPointer m_pInstance; -}; - -// static -QWeakPointer EngineFlangerControls::m_pInstance; - -/*---------------------------------------------------------------- - A flanger effect. - The flanger is controlled by the following variables: - average_delay_length - The average length of the delay, which is modulated by the LFO. - LFOperiod - the period of LFO given in samples. - LFOamplitude - the amplitude of the modulation of the delay length. - depth - the depth of the flanger, controlled by a ControlPotmeter. - ----------------------------------------------------------------*/ -EngineFlanger::EngineFlanger(const char* group) { - // Init. buffers: - m_pDelay_buffer = SampleUtil::alloc(max_delay + 1); - SampleUtil::clear(m_pDelay_buffer, max_delay+1); - - // Init. potmeters - - // rryan 6/2010 This is gross. The flanger was originally written as this - // hack that hard-coded the two channels, and while pulling it apart, I have - // to keep these global [Flanger]-group controls, except there is one - // EngineFlanger per deck - m_pControls = EngineFlangerControls::instance(); - - // Create an enable key on a per-deck basis. - m_pFlangerEnable = new ControlPushButton(ConfigKey(group, "flanger")); - m_pFlangerEnable->setButtonMode(ControlPushButton::TOGGLE); - - // Fixed values of controls: - m_LFOamplitude = 240; - m_average_delay_length = 250; - - // Set initial values for vars - m_delay_pos = 0; - m_time = 0; -} - -EngineFlanger::~EngineFlanger() { - // Don't delete the controls anymore since we don't know if we created them. - // delete m_pPotmeterDepth; - // delete m_pPotmeterDelay; - // delete m_pPotmeterLFOperiod; - - delete m_pFlangerEnable; - - SampleUtil::free(m_pDelay_buffer); -} - -void EngineFlanger::process(const CSAMPLE* pIn, - CSAMPLE* pOutput, const int iBufferSize) { - CSAMPLE delayed_sample,prev,next; - FLOAT_TYPE frac; - - if (m_pFlangerEnable->get() == 0.0) { - // SampleUtil handles shortcuts when aliased, and gains of 1.0, etc. - return SampleUtil::copyWithGain(pOutput, pIn, 1.0f, iBufferSize); - } - - for (int i=0; i= max_delay) { - m_delay_pos = 0; - } - - // Update the LFO to find the current delay: - m_time++; - if (m_time == m_pControls->m_pPotmeterLFOperiod->get()) { - m_time = 0; - } - FLOAT_TYPE delay = m_average_delay_length + m_LFOamplitude * - sin(two_pi * ((FLOAT_TYPE)m_time) / - ((FLOAT_TYPE)m_pControls->m_pPotmeterLFOperiod->get())); - - // Make a linear interpolation to find the delayed sample: - prev = m_pDelay_buffer[(m_delay_pos-(int)delay + max_delay - 1) % max_delay]; - next = m_pDelay_buffer[(m_delay_pos-(int)delay + max_delay) % max_delay]; - frac = delay - floor(delay); - delayed_sample = prev + frac * (next - prev); - - // Take the sample from the delay buffer and mix it with the source buffer: - pOutput[i] = pIn[i] + m_pControls->m_pPotmeterDepth->get() * delayed_sample; - } -} diff --git a/src/engine/engineflanger.h b/src/engine/engineflanger.h deleted file mode 100644 index 2129358fa66..00000000000 --- a/src/engine/engineflanger.h +++ /dev/null @@ -1,49 +0,0 @@ -/*************************************************************************** - engineflanger.h - description - ------------------- - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * 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. * - * * - ***************************************************************************/ - -#ifndef ENGINEFLANGER_H -#define ENGINEFLANGER_H - -#include -#include - -#include "engine/engineobject.h" - -class ControlPotmeter; -class ControlPushButton; -class EngineFlangerControls; - -const int max_delay = 5000; - -class EngineFlanger : public EngineObject { - Q_OBJECT - public: - EngineFlanger(const char* group); - virtual ~EngineFlanger(); - - void process(const CSAMPLE* pIn, CSAMPLE* pOut, const int iBufferSize); - - private: - ControlPushButton* m_pFlangerEnable; - CSAMPLE* m_pDelay_buffer; - int m_LFOamplitude; - int m_average_delay_length; - int m_time; - int m_delay_pos; - QSharedPointer m_pControls; -}; - -#endif diff --git a/src/engine/enginemaster.cpp b/src/engine/enginemaster.cpp index e130375b7c0..9f0c23a330c 100644 --- a/src/engine/enginemaster.cpp +++ b/src/engine/enginemaster.cpp @@ -26,6 +26,7 @@ #include "engine/enginebuffer.h" #include "engine/enginemaster.h" #include "engine/engineworkerscheduler.h" +#include "engine/enginedeck.h" #include "engine/enginebuffer.h" #include "engine/enginechannel.h" #include "engine/engineclipping.h" @@ -36,6 +37,8 @@ #include "engine/sidechain/enginesidechain.h" #include "engine/sync/enginesync.h" #include "sampleutil.h" +#include "engine/effects/engineeffectsmanager.h" +#include "effects/effectsmanager.h" #include "util/timer.h" #include "util/trace.h" #include "playermanager.h" @@ -43,9 +46,12 @@ EngineMaster::EngineMaster(ConfigObject* _config, const char* group, + EffectsManager* pEffectsManager, bool bEnableSidechain, bool bRampingGain) - : m_bRampingGain(bRampingGain), + : m_pEngineEffectsManager(pEffectsManager ? pEffectsManager->getEngineEffectsManager() : NULL), + m_bRampingGain(bRampingGain), + m_masterVolumeOld(0.0), m_headphoneMasterGainOld(0.0), m_headphoneVolumeOld(1.0), m_bMasterOutputConnected(false), @@ -56,6 +62,11 @@ EngineMaster::EngineMaster(ConfigObject* _config, m_pWorkerScheduler = new EngineWorkerScheduler(this); m_pWorkerScheduler->start(QThread::HighPriority); + if (pEffectsManager) { + pEffectsManager->registerGroup(getMasterGroup()); + pEffectsManager->registerGroup(getHeadphoneGroup()); + } + // Master sample rate m_pMasterSampleRate = new ControlObject(ConfigKey(group, "samplerate"), true, true); m_pMasterSampleRate->set(44100.); @@ -301,6 +312,9 @@ void EngineMaster::process(const int iBufferSize) { int iSampleRate = static_cast(m_pMasterSampleRate->get()); // Update internal master sync. m_pMasterSync->onCallbackStart(iSampleRate, iBufferSize); + if (m_pEngineEffectsManager) { + m_pEngineEffectsManager->onCallbackStart(); + } // Bitvector of enabled channels const unsigned int maxChannels = 32; @@ -341,13 +355,10 @@ void EngineMaster::process(const int iBufferSize) { m_pXFaderReverse->get() == 1.0, &c1_gain, &c2_gain); - // And mix the 3 buses into the master. - CSAMPLE master_gain = m_pMasterVolume->get(); - // Channels with the talkover flag should be mixed with the master signal at // full master volume. All other channels should be adjusted by ducking gain. - m_masterGain.setGains(master_gain * m_pTalkoverDucking->getGain(iBufferSize / 2), - c1_gain, 1.0, c2_gain, master_gain); + m_masterGain.setGains(m_pTalkoverDucking->getGain(iBufferSize / 2), + c1_gain, 1.0, c2_gain, 1.0); // Make the mix for each output bus. m_masterGain takes care of applying the // master volume, the channel volume, and the orientation gain. @@ -375,6 +386,21 @@ void EngineMaster::process(const int iBufferSize) { m_pOutputBusBuffers[EngineChannel::RIGHT], 1.0, iBufferSize); + // Process master channel effects + if (m_pEngineEffectsManager) { + m_pEngineEffectsManager->process(getMasterGroup(), m_pMaster, m_pMaster, iBufferSize); + } + + // Apply master volume after effects. + CSAMPLE master_volume = m_pMasterVolume->get(); + if (m_bRampingGain) { + SampleUtil::applyRampingGain(m_pMaster, m_masterVolumeOld, + master_volume, iBufferSize); + } else { + SampleUtil::applyGain(m_pHead, master_volume, iBufferSize); + } + m_masterVolumeOld = master_volume; + // Clipping m_pClipping->process(m_pMaster, m_pMaster, iBufferSize); @@ -412,6 +438,11 @@ void EngineMaster::process(const int iBufferSize) { } m_headphoneMasterGainOld = cmaster_gain; + // Process headphone channel effects + if (m_pEngineEffectsManager) { + m_pEngineEffectsManager->process(getHeadphoneGroup(), m_pHead, m_pHead, iBufferSize); + } + // Head volume and clipping CSAMPLE headphoneVolume = m_pHeadVolume->get(); if (m_bRampingGain) { diff --git a/src/engine/enginemaster.h b/src/engine/enginemaster.h index 38e9069ec89..9394c5477c5 100644 --- a/src/engine/enginemaster.h +++ b/src/engine/enginemaster.h @@ -30,6 +30,7 @@ class EngineWorkerScheduler; class EngineBuffer; class EngineChannel; +class EngineDeck; class EngineClipping; class EngineFlanger; class EngineVuMeter; @@ -37,6 +38,8 @@ class ControlPotmeter; class ControlPushButton; class EngineVinylSoundEmu; class EngineSideChain; +class EffectsManager; +class EngineEffectsManager; class SyncWorker; class GuiTick; class EngineSync; @@ -48,14 +51,23 @@ class EngineMaster : public QObject, public AudioSource { public: EngineMaster(ConfigObject* pConfig, const char* pGroup, + EffectsManager* pEffectsManager, bool bEnableSidechain, - bool bRampingGain=true); + bool bRampingGain); virtual ~EngineMaster(); // Get access to the sample buffers. None of these are thread safe. Only to // be called by SoundManager. const CSAMPLE* buffer(AudioOutput output) const; + const QString getMasterGroup() const { + return QString("[Master]"); + } + + const QString getHeadphoneGroup() const { + return QString("[Headphone]"); + } + // WARNING: These methods are called by the main thread. They should only // touch the volatile bool connected indicators (see below). However, when // these methods are called the callback is guaranteed to be inactive @@ -178,6 +190,7 @@ class EngineMaster : public QObject, public AudioSource { unsigned int* headphoneOutput, int iBufferSize); + EngineEffectsManager* m_pEngineEffectsManager; bool m_bRampingGain; QList m_channels; QList m_channelMasterGainCache; @@ -217,6 +230,7 @@ class EngineMaster : public QObject, public AudioSource { ConstantGainCalculator m_headphoneGain; OrientationVolumeGainCalculator m_masterGain; + CSAMPLE m_masterVolumeOld; CSAMPLE m_headphoneMasterGainOld; CSAMPLE m_headphoneVolumeOld; diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 0bae27e05e6..7de1a73faf2 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -34,6 +34,8 @@ #include "dlgpreferences.h" #include "engine/enginemaster.h" #include "engine/enginemicrophone.h" +#include "effects/effectsmanager.h" +#include "effects/native/nativebackend.h" #include "engine/engineaux.h" #include "library/library.h" #include "library/library_preferences.h" @@ -124,8 +126,19 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) setAttribute(Qt::WA_AcceptTouchEvents); m_pTouchShift = new ControlPushButton(ConfigKey("[Controls]", "touch_shift")); + // Create the Effects subsystem. + m_pEffectsManager = new EffectsManager(this, m_pConfig); + // Starting the master (mixing of the channels and effects): - m_pEngine = new EngineMaster(m_pConfig, "[Master]", true); + m_pEngine = new EngineMaster(m_pConfig, "[Master]", m_pEffectsManager, true, true); + + // Create effect backends. We do this after creating EngineMaster to allow + // effect backends to refer to controls that are produced by the engine. + NativeBackend* pNativeBackend = new NativeBackend(m_pEffectsManager); + m_pEffectsManager->addEffectsBackend(pNativeBackend); + + // Sets up the default EffectChains and EffectRack. + m_pEffectsManager->setupDefaults(); m_pRecordingManager = new RecordingManager(m_pConfig, m_pEngine); #ifdef __SHOUTCAST__ @@ -186,7 +199,8 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) #endif // Create the player manager. - m_pPlayerManager = new PlayerManager(m_pConfig, m_pSoundManager, m_pEngine); + m_pPlayerManager = new PlayerManager(m_pConfig, m_pSoundManager, + m_pEffectsManager, m_pEngine); // Add the same number of decks that were last used. This ensures that when // audio inputs and outputs are set up, connections for decks > 2 will @@ -338,7 +352,8 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) m_pPlayerManager, m_pControllerManager, m_pLibrary, - m_pVCManager))) { + m_pVCManager, + m_pEffectsManager))) { reportCriticalErrorAndQuit( "default skin cannot be loaded see mixxx trace for more information."); @@ -477,6 +492,9 @@ MixxxMainWindow::~MixxxMainWindow() { qDebug() << "delete m_pEngine " << qTime.elapsed(); delete m_pEngine; + qDebug() << "deleting effects manager, " << qTime.elapsed(); + delete m_pEffectsManager; + // HACK: Save config again. We saved it once before doing some dangerous // stuff. We only really want to save it here, but the first one was just // a precaution. The earlier one can be removed when stuff is more stable @@ -1564,7 +1582,8 @@ void MixxxMainWindow::rebootMixxxView() { m_pPlayerManager, m_pControllerManager, m_pLibrary, - m_pVCManager))) { + m_pVCManager, + m_pEffectsManager))) { QMessageBox::critical(this, tr("Error in skin file"), @@ -1757,4 +1776,3 @@ bool MixxxMainWindow::confirmExit() { } return true; } - diff --git a/src/mixxx.h b/src/mixxx.h index 9616570410c..d5f6f569ae3 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -37,9 +37,9 @@ class PlayerManager; class RecordingManager; class ShoutcastManager; class SkinLoader; +class EffectsManager; class VinylControlManager; class GuiTick; - class DlgPreferences; class SoundManager; class ControlPushButton; @@ -147,6 +147,9 @@ class MixxxMainWindow : public QMainWindow { // Pointer to the root GUI widget QWidget* m_pWidgetParent; + // The effects processing system + EffectsManager* m_pEffectsManager; + // The mixing engine. EngineMaster* m_pEngine; diff --git a/src/playermanager.cpp b/src/playermanager.cpp index ba7651c586d..aa4fad902b3 100644 --- a/src/playermanager.cpp +++ b/src/playermanager.cpp @@ -16,15 +16,18 @@ #include "library/trackcollection.h" #include "engine/enginemaster.h" #include "soundmanager.h" +#include "effects/effectsmanager.h" #include "util/stat.h" #include "engine/enginedeck.h" PlayerManager::PlayerManager(ConfigObject* pConfig, SoundManager* pSoundManager, + EffectsManager* pEffectsManager, EngineMaster* pEngine) : m_mutex(QMutex::Recursive), m_pConfig(pConfig), m_pSoundManager(pSoundManager), + m_pEffectsManager(pEffectsManager), m_pEngine(pEngine), // NOTE(XXX) LegacySkinParser relies on these controls being COs and // not COTMs listening to a CO. @@ -227,7 +230,8 @@ void PlayerManager::addDeckInner() { orientation = EngineChannel::RIGHT; } - Deck* pDeck = new Deck(this, m_pConfig, m_pEngine, orientation, group); + Deck* pDeck = new Deck(this, m_pConfig, m_pEngine, m_pEffectsManager, + orientation, group); if (m_pAnalyserQueue) { connect(pDeck, SIGNAL(newTrackLoaded(TrackPointer)), m_pAnalyserQueue, SLOT(slotAnalyseTrack(TrackPointer))); @@ -260,7 +264,8 @@ void PlayerManager::addSamplerInner() { // All samplers are in the center EngineChannel::ChannelOrientation orientation = EngineChannel::CENTER; - Sampler* pSampler = new Sampler(this, m_pConfig, m_pEngine, orientation, group); + Sampler* pSampler = new Sampler(this, m_pConfig, m_pEngine, + m_pEffectsManager, orientation, group); if (m_pAnalyserQueue) { connect(pSampler, SIGNAL(newTrackLoaded(TrackPointer)), m_pAnalyserQueue, SLOT(slotAnalyseTrack(TrackPointer))); @@ -284,7 +289,9 @@ void PlayerManager::addPreviewDeckInner() { // All preview decks are in the center EngineChannel::ChannelOrientation orientation = EngineChannel::CENTER; - PreviewDeck* pPreviewDeck = new PreviewDeck(this, m_pConfig, m_pEngine, orientation, group); + PreviewDeck* pPreviewDeck = new PreviewDeck(this, m_pConfig, m_pEngine, + m_pEffectsManager, orientation, + group); if (m_pAnalyserQueue) { connect(pPreviewDeck, SIGNAL(newTrackLoaded(TrackPointer)), m_pAnalyserQueue, SLOT(slotAnalyseTrack(TrackPointer))); diff --git a/src/playermanager.h b/src/playermanager.h index 7fa44d3955d..cb1b5222d15 100644 --- a/src/playermanager.h +++ b/src/playermanager.h @@ -20,6 +20,7 @@ class Library; class EngineMaster; class AnalyserQueue; class SoundManager; +class EffectsManager; class TrackCollection; class PlayerManager : public QObject { @@ -27,6 +28,7 @@ class PlayerManager : public QObject { public: PlayerManager(ConfigObject* pConfig, SoundManager* pSoundManager, + EffectsManager* pEffectsManager, EngineMaster* pEngine); virtual ~PlayerManager(); @@ -126,6 +128,7 @@ class PlayerManager : public QObject { ConfigObject* m_pConfig; SoundManager* m_pSoundManager; + EffectsManager* m_pEffectsManager; EngineMaster* m_pEngine; AnalyserQueue* m_pAnalyserQueue; ControlObject* m_pCONumDecks; diff --git a/src/previewdeck.cpp b/src/previewdeck.cpp index 0ae829d8d66..13145142e4a 100644 --- a/src/previewdeck.cpp +++ b/src/previewdeck.cpp @@ -3,10 +3,11 @@ PreviewDeck::PreviewDeck(QObject* pParent, ConfigObject *pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group) : - BaseTrackPlayer(pParent, pConfig, pMixingEngine, defaultOrientation, - group, false, true) { + BaseTrackPlayer(pParent, pConfig, pMixingEngine, pEffectsManager, + defaultOrientation, group, false, true) { } PreviewDeck::~PreviewDeck() { diff --git a/src/previewdeck.h b/src/previewdeck.h index 975a37240b5..9be809d1ff6 100644 --- a/src/previewdeck.h +++ b/src/previewdeck.h @@ -9,6 +9,7 @@ class PreviewDeck : public BaseTrackPlayer { PreviewDeck(QObject* pParent, ConfigObject *pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group); virtual ~PreviewDeck(); diff --git a/src/sampler.cpp b/src/sampler.cpp index a8f60648afe..c749ad051e5 100644 --- a/src/sampler.cpp +++ b/src/sampler.cpp @@ -5,10 +5,11 @@ Sampler::Sampler(QObject* pParent, ConfigObject* pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group) : - BaseTrackPlayer(pParent, pConfig, pMixingEngine, defaultOrientation, - group, true, false) { + BaseTrackPlayer(pParent, pConfig, pMixingEngine, pEffectsManager, + defaultOrientation, group, true, false) { } Sampler::~Sampler() { diff --git a/src/sampler.h b/src/sampler.h index f9d96d67da8..68b4d86de63 100644 --- a/src/sampler.h +++ b/src/sampler.h @@ -9,6 +9,7 @@ class Sampler : public BaseTrackPlayer { Sampler(QObject* pParent, ConfigObject *pConfig, EngineMaster* pMixingEngine, + EffectsManager* pEffectsManager, EngineChannel::ChannelOrientation defaultOrientation, QString group); virtual ~Sampler(); diff --git a/src/skin/legacyskinparser.cpp b/src/skin/legacyskinparser.cpp index 305b4b9c486..6949b796a49 100644 --- a/src/skin/legacyskinparser.cpp +++ b/src/skin/legacyskinparser.cpp @@ -27,6 +27,8 @@ #include "skin/colorschemeparser.h" #include "skin/skincontext.h" +#include "effects/effectsmanager.h" + #include "widget/controlwidgetconnection.h" #include "widget/wbasewidget.h" #include "widget/wwidget.h" @@ -44,6 +46,9 @@ #include "widget/wnumber.h" #include "widget/wnumberpos.h" #include "widget/wnumberrate.h" +#include "widget/weffectchain.h" +#include "widget/weffect.h" +#include "widget/weffectparameter.h" #include "widget/woverviewlmh.h" #include "widget/woverviewhsv.h" #include "widget/wspinny.h" @@ -112,13 +117,15 @@ LegacySkinParser::LegacySkinParser(ConfigObject* pConfig, PlayerManager* pPlayerManager, ControllerManager* pControllerManager, Library* pLibrary, - VinylControlManager* pVCMan) + VinylControlManager* pVCMan, + EffectsManager* pEffectsManager) : m_pConfig(pConfig), m_pKeyboard(pKeyboard), m_pPlayerManager(pPlayerManager), m_pControllerManager(pControllerManager), m_pLibrary(pLibrary), m_pVCManager(pVCMan), + m_pEffectsManager(pEffectsManager), m_pParent(NULL), m_pContext(NULL) { } @@ -429,6 +436,12 @@ QList LegacySkinParser::parseNode(QDomElement node) { result = wrapWidget(parseWidgetGroup(node)); } else if (nodeName == "WidgetStack") { result = wrapWidget(parseWidgetStack(node)); + } else if (nodeName == "EffectChainName") { + result = wrapWidget(parseEffectChainName(node)); + } else if (nodeName == "EffectName") { + result = wrapWidget(parseEffectName(node)); + } else if (nodeName == "EffectParameterName") { + result = wrapWidget(parseEffectParameterName(node)); } else if (nodeName == "Spinny") { result = wrapWidget(parseSpinny(node)); } else if (nodeName == "Time") { @@ -1233,6 +1246,24 @@ const char* LegacySkinParser::safeChannelString(QString channelStr) { return safe; } +QWidget* LegacySkinParser::parseEffectChainName(QDomElement node) { + WEffectChain* pEffectChain = new WEffectChain(m_pParent, m_pEffectsManager); + setupLabelWidget(node, pEffectChain); + return pEffectChain; +} + +QWidget* LegacySkinParser::parseEffectName(QDomElement node) { + WEffect* pEffect = new WEffect(m_pParent, m_pEffectsManager); + setupLabelWidget(node, pEffect); + return pEffect; +} + +QWidget* LegacySkinParser::parseEffectParameterName(QDomElement node) { + WEffectParameter* pEffectParameter = new WEffectParameter(m_pParent, m_pEffectsManager); + setupLabelWidget(node, pEffectParameter); + return pEffectParameter; +} + void LegacySkinParser::setupPosition(QDomNode node, QWidget* pWidget) { if (m_pContext->hasNode(node, "Pos")) { QString pos = m_pContext->selectString(node, "Pos"); diff --git a/src/skin/legacyskinparser.h b/src/skin/legacyskinparser.h index 766fd81975c..38fc6dcb5d9 100644 --- a/src/skin/legacyskinparser.h +++ b/src/skin/legacyskinparser.h @@ -17,6 +17,7 @@ class WBaseWidget; class Library; class MixxxKeyboard; class PlayerManager; +class EffectsManager; class ControllerManager; class SkinContext; class WLabel; @@ -28,7 +29,8 @@ class LegacySkinParser : public QObject, public SkinParser { LegacySkinParser(ConfigObject* pConfig, MixxxKeyboard* pKeyboard, PlayerManager* pPlayerManager, ControllerManager* pControllerManager, - Library* pLibrary, VinylControlManager* pVCMan); + Library* pLibrary, VinylControlManager* pVCMan, + EffectsManager* pEffectsManager); virtual ~LegacySkinParser(); virtual bool canParse(QString skinPath); @@ -69,6 +71,9 @@ class LegacySkinParser : public QObject, public SkinParser { QWidget* parseTrackProperty(QDomElement node); QWidget* parseNumberRate(QDomElement node); QWidget* parseNumberPos(QDomElement node); + QWidget* parseEffectChainName(QDomElement node); + QWidget* parseEffectName(QDomElement node); + QWidget* parseEffectParameterName(QDomElement node); // Legacy pre-1.12.0 skin support. QWidget* parseBackground(QDomElement node, QWidget* pOuterWidget, QWidget* pInnerWidget); @@ -114,6 +119,7 @@ class LegacySkinParser : public QObject, public SkinParser { ControllerManager* m_pControllerManager; Library* m_pLibrary; VinylControlManager* m_pVCManager; + EffectsManager* m_pEffectsManager; QWidget* m_pParent; SkinContext* m_pContext; Tooltips m_tooltips; diff --git a/src/skin/skincontext.cpp b/src/skin/skincontext.cpp index 8abd0c507e3..efe5828a019 100644 --- a/src/skin/skincontext.cpp +++ b/src/skin/skincontext.cpp @@ -103,9 +103,13 @@ double SkinContext::selectDouble(const QDomNode& node, } int SkinContext::selectInt(const QDomNode& node, - const QString& nodeName) const { + const QString& nodeName, + bool* pOk) const { bool ok = false; int conv = nodeToString(selectElement(node, nodeName)).toInt(&ok); + if (pOk != NULL) { + *pOk = ok; + } return ok ? conv : 0; } diff --git a/src/skin/skincontext.h b/src/skin/skincontext.h index 72b68a3b4bb..58b820f1ea5 100644 --- a/src/skin/skincontext.h +++ b/src/skin/skincontext.h @@ -49,7 +49,7 @@ class SkinContext { QString selectString(const QDomNode& node, const QString& nodeName) const; float selectFloat(const QDomNode& node, const QString& nodeName) const; double selectDouble(const QDomNode& node, const QString& nodeName) const; - int selectInt(const QDomNode& node, const QString& nodeName) const; + int selectInt(const QDomNode& node, const QString& nodeName, bool* pOk=NULL) const; bool selectBool(const QDomNode& node, const QString& nodeName, bool defaultValue) const; bool hasNodeSelectBool(const QDomNode& node, const QString& nodeName, bool *value) const; bool selectAttributeBool(const QDomElement& element, diff --git a/src/skin/skinloader.cpp b/src/skin/skinloader.cpp index 134f465b158..6e353904dfd 100644 --- a/src/skin/skinloader.cpp +++ b/src/skin/skinloader.cpp @@ -13,6 +13,7 @@ #include "skin/legacyskinparser.h" #include "controllers/controllermanager.h" #include "library/library.h" +#include "effects/effectsmanager.h" #include "playermanager.h" #include "util/debug.h" @@ -77,9 +78,11 @@ QWidget* SkinLoader::loadDefaultSkin(QWidget* pParent, PlayerManager* pPlayerManager, ControllerManager* pControllerManager, Library* pLibrary, - VinylControlManager* pVCMan) { + VinylControlManager* pVCMan, + EffectsManager* pEffectsManager) { QString skinPath = getConfiguredSkinPath(); - - LegacySkinParser legacy(m_pConfig, pKeyboard, pPlayerManager, pControllerManager, pLibrary, pVCMan); + LegacySkinParser legacy(m_pConfig, pKeyboard, pPlayerManager, + pControllerManager, pLibrary, pVCMan, + pEffectsManager); return legacy.parseSkin(skinPath, pParent); } diff --git a/src/skin/skinloader.h b/src/skin/skinloader.h index a56bfc9942d..162a063d732 100644 --- a/src/skin/skinloader.h +++ b/src/skin/skinloader.h @@ -10,6 +10,7 @@ class PlayerManager; class ControllerManager; class Library; class VinylControlManager; +class EffectsManager; class SkinLoader { public: @@ -20,7 +21,8 @@ class SkinLoader { PlayerManager* pPlayerManager, ControllerManager* pControllerManager, Library* pLibrary, - VinylControlManager* pVCMan); + VinylControlManager* pVCMan, + EffectsManager* pEffectsManager); QString getConfiguredSkinPath(); diff --git a/src/skin/tooltips.cpp b/src/skin/tooltips.cpp index 8f25e4bb0f3..f002346134c 100644 --- a/src/skin/tooltips.cpp +++ b/src/skin/tooltips.cpp @@ -444,33 +444,6 @@ void Tooltips::addStandardTooltips() { << trackTags << dropTracksHere; - add("flanger") - << tr("Flanger") - << tr("Toggles the flange effect. Use the depth/delay/lfo knobs to adjust."); - - add("lfoDelay") - << tr("Flanger Delay") - << tr("Adjusts the phase delay of the flange effect (when active).") - << QString("%1: %2").arg(rightClick, resetToDefault); - - add("lfoDepth") - << tr("Flanger Depth") - << tr("Adjusts the intensity of the flange effect (when active).") - << QString("%1: %2").arg(rightClick, resetToDefault); - - add("lfoPeriod") - << tr("Flanger LFO Period") - << tr("Adjusts the wavelength of the flange effect (when active).") - << QString("%1: %2").arg(rightClick, resetToDefault); - - add("filter") - << tr("Filter") - << tr("Toggles the filter effect. Use the depth knobs to adjust."); - - add("filterDepth") - << tr("Filter Depth") - << tr("Adjusts the intensity of the filter effect (when active)."); - add("time") << tr("Clock") << tr("Displays the current time."); diff --git a/src/test/baseeffecttest.cpp b/src/test/baseeffecttest.cpp new file mode 100644 index 00000000000..0042842747e --- /dev/null +++ b/src/test/baseeffecttest.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include "test/baseeffecttest.h" + +using ::testing::Return; +using ::testing::Invoke; +using ::testing::_; + +void BaseEffectTest::registerTestEffect(const EffectManifest& manifest) { + MockEffectProcessor* pProcessor = new MockEffectProcessor(); + MockEffectInstantiator* pInstantiator = new MockEffectInstantiator(); + + EXPECT_CALL(*pInstantiator, instantiate(_, _)) + .Times(1) + .WillOnce(Return(pProcessor)); + + m_pTestBackend->registerEffect(manifest.id(), manifest, + EffectInstantiatorPointer(pInstantiator)); +} diff --git a/src/test/baseeffecttest.h b/src/test/baseeffecttest.h new file mode 100644 index 00000000000..7fdb0f3e755 --- /dev/null +++ b/src/test/baseeffecttest.h @@ -0,0 +1,71 @@ +#ifndef BASEEFFECTTEST_H +#define BASEEFFECTTEST_H + +#include +#include + +#include + +#include "effects/effectchain.h" +#include "effects/effect.h" +#include "effects/effectsmanager.h" +#include "effects/effectmanifest.h" +#include "effects/effectsbackend.h" +#include "effects/effectinstantiator.h" +#include "effects/effectprocessor.h" + + +#include "test/mixxxtest.h" + +class TestEffectBackend : public EffectsBackend { + public: + TestEffectBackend() : EffectsBackend(NULL, "TestBackend") { + } + + // Expose as public + void registerEffect(const QString& id, + const EffectManifest& manifest, + EffectInstantiatorPointer pInstantiator) { + EffectsBackend::registerEffect(id, manifest, pInstantiator); + } +}; + +class MockEffectProcessor : public EffectProcessor { + public: + MockEffectProcessor() {} + + MOCK_METHOD4(process, void(const QString& group, const CSAMPLE* pInput, + CSAMPLE* pOutput, + const unsigned int numSamples)); + + MOCK_METHOD1(initialize, void(const QSet& registeredGroups)); +}; + +class MockEffectInstantiator : public EffectInstantiator { + public: + MockEffectInstantiator() {} + MOCK_METHOD2(instantiate, EffectProcessor*(EngineEffect* pEngineEffect, + const EffectManifest& manifest)); +}; + + +class BaseEffectTest : public MixxxTest { + protected: + BaseEffectTest() : m_pTestBackend(NULL), + m_pEffectsManager(new EffectsManager(NULL, config())) { + } + + void registerTestBackend() { + m_pTestBackend = new TestEffectBackend(); + m_pEffectsManager->addEffectsBackend(m_pTestBackend); + } + + void registerTestEffect(const EffectManifest& manifest); + + // Deleted by EffectsManager. Do not delete. + TestEffectBackend* m_pTestBackend; + QScopedPointer m_pEffectsManager; +}; + + +#endif /* BASEEFFECTTEST_H */ diff --git a/src/test/effectchainslottest.cpp b/src/test/effectchainslottest.cpp new file mode 100644 index 00000000000..738c0fc3cc4 --- /dev/null +++ b/src/test/effectchainslottest.cpp @@ -0,0 +1,101 @@ +#include +#include + +#include +#include + +#include "mixxxtest.h" +#include "controlobject.h" +#include "effects/effectchain.h" +#include "effects/effectchainslot.h" +#include "effects/effectsmanager.h" +#include "test/baseeffecttest.h" + +using ::testing::Return; +using ::testing::_; + +class EffectChainSlotTest : public BaseEffectTest { + protected: + virtual void SetUp() { + m_pEffectsManager->registerGroup("[Master]"); + m_pEffectsManager->registerGroup("[Headphone]"); + } +}; + +TEST_F(EffectChainSlotTest, ChainSlotMirrorsLoadedChain) { + EffectChainPointer pChain(new EffectChain(m_pEffectsManager.data(), + "org.mixxx.test.chain1")); + int iRackNumber = 0; + int iChainNumber = 0; + + EffectRackPointer pRack = m_pEffectsManager->addEffectRack(); + EffectChainSlotPointer pSlot = pRack->addEffectChainSlot(); + + QString group = EffectChainSlot::formatGroupString(iRackNumber, + iChainNumber); + pSlot->loadEffectChain(pChain); + + pChain->setEnabled(true); + EXPECT_LT(0.0, ControlObject::get(ConfigKey(group, "enabled"))); + + pChain->setEnabled(false); + EXPECT_DOUBLE_EQ(0.0, ControlObject::get(ConfigKey(group, "enabled"))); + + // Enabled is read-only. Sets to it should not do anything. + ControlObject::set(ConfigKey(group, "enabled"), 1); + EXPECT_FALSE(pChain->enabled()); + + // numEffects is read-only. Sets to it should not do anything. + ControlObject::set(ConfigKey(group, "num_effects"), 1); + EXPECT_EQ(0, pChain->numEffects()); + + ControlObject::set(ConfigKey(group, "parameter"), 0.5); + EXPECT_DOUBLE_EQ(0.5, pChain->parameter()); + + pChain->setParameter(1.0); + EXPECT_DOUBLE_EQ(pChain->parameter(), + ControlObject::get(ConfigKey(group, "parameter"))); + + ControlObject::set(ConfigKey(group, "parameter"), 0.5); + EXPECT_DOUBLE_EQ(0.5, pChain->parameter()); + + pChain->setMix(1.0); + EXPECT_DOUBLE_EQ(pChain->mix(), + ControlObject::get(ConfigKey(group, "mix"))); + + ControlObject::set(ConfigKey(group, "mix"), 0.5); + EXPECT_DOUBLE_EQ(0.5, pChain->mix()); + + pChain->setInsertionType(EffectChain::SEND); + EXPECT_DOUBLE_EQ(pChain->insertionType(), + ControlObject::get(ConfigKey(group, "insertion_type"))); + + ControlObject::set(ConfigKey(group, "insertion_type"), EffectChain::INSERT); + EXPECT_DOUBLE_EQ(EffectChain::INSERT, pChain->insertionType()); + + EXPECT_FALSE(pChain->enabledForGroup("[Master]")); + pChain->enableForGroup("[Master]"); + EXPECT_LT(0.0, ControlObject::get(ConfigKey(group, "group_[Master]_enable"))); + + ControlObject::set(ConfigKey(group, "group_[Master]_enable"), 0); + EXPECT_FALSE(pChain->enabledForGroup("[Master]")); +} + +TEST_F(EffectChainSlotTest, ChainSlotMirrorsLoadedChain_Clear) { + EffectChainPointer pChain(new EffectChain(m_pEffectsManager.data(), + "org.mixxx.test.chain1")); + + int iRackNumber = 0; + int iChainNumber = 0; + + EffectRackPointer pRack = m_pEffectsManager->addEffectRack(); + EffectChainSlotPointer pSlot = pRack->addEffectChainSlot(); + + QString group = EffectChainSlot::formatGroupString(iRackNumber, + iChainNumber); + EXPECT_FALSE(pChain->enabled()); + pSlot->loadEffectChain(pChain); + EXPECT_TRUE(pChain->enabled()); + ControlObject::set(ConfigKey(group, "clear"), 1.0); + EXPECT_FALSE(pChain->enabled()); +} diff --git a/src/test/effectslottest.cpp b/src/test/effectslottest.cpp new file mode 100644 index 00000000000..e0b183508df --- /dev/null +++ b/src/test/effectslottest.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include +#include + +#include "mixxxtest.h" +#include "controlobject.h" +#include "effects/effectchain.h" +#include "effects/effectchainslot.h" +#include "effects/effectsmanager.h" +#include "effects/effectmanifest.h" + +#include "test/baseeffecttest.h" + +using ::testing::Return; +using ::testing::_; + +class EffectSlotTest : public BaseEffectTest { + protected: + virtual void SetUp() { + m_pEffectsManager->registerGroup("[Master]"); + m_pEffectsManager->registerGroup("[Headphone]"); + registerTestBackend(); + } +}; + +TEST_F(EffectSlotTest, ControlsReflectSlotState) { + EffectChainPointer pChain(new EffectChain(m_pEffectsManager.data(), + "org.mixxx.test.chain1")); + int iRackNumber = 0; + int iChainNumber = 0; + int iEffectNumber = 0; + + EffectRackPointer pRack = m_pEffectsManager->addEffectRack(); + EffectChainSlotPointer pChainSlot = pRack->addEffectChainSlot(); + // EffectRack::addEffectChainSlot automatically adds 4 effect slots. In the + // future we will probably remove this so this will just start segfaulting. + EffectSlotPointer pEffectSlot = pChainSlot->getEffectSlot(0); + + QString group = EffectSlot::formatGroupString(iRackNumber, + iChainNumber, + iEffectNumber); + + EffectManifest manifest; + manifest.setId("org.mixxx.test.effect"); + manifest.setName("Test Effect"); + EffectManifestParameter* pParameter = manifest.addParameter(); + registerTestEffect(manifest); + + // Check the controls reflect the state of their loaded effect. + EffectPointer pEffect = m_pEffectsManager->instantiateEffect(manifest.id()); + EXPECT_DOUBLE_EQ(0, ControlObject::get(ConfigKey(group, "enabled"))); + EXPECT_DOUBLE_EQ(0, ControlObject::get(ConfigKey(group, "num_parameters"))); + pEffectSlot->loadEffect(pEffect); + EXPECT_LE(0, ControlObject::get(ConfigKey(group, "enabled"))); + EXPECT_DOUBLE_EQ(1, ControlObject::get(ConfigKey(group, "num_parameters"))); + + // Enabled is read-only. + ControlObject::set(ConfigKey(group, "enabled"), 0.0); + EXPECT_LE(0, ControlObject::get(ConfigKey(group, "enabled"))); + + // num_parameters is read-only. + ControlObject::set(ConfigKey(group, "num_parameters"), 2.0); + EXPECT_DOUBLE_EQ(1, ControlObject::get(ConfigKey(group, "num_parameters"))); +} diff --git a/src/test/effectsmanagertest.cpp b/src/test/effectsmanagertest.cpp new file mode 100644 index 00000000000..de7739585c8 --- /dev/null +++ b/src/test/effectsmanagertest.cpp @@ -0,0 +1,38 @@ +#include +#include + +#include +#include + +#include "test/mixxxtest.h" +#include "effects/effectchain.h" +#include "effects/effectchainslot.h" +#include "effects/effectsmanager.h" +#include "effects/effectmanifest.h" + +#include "test/baseeffecttest.h" + +using ::testing::Return; +using ::testing::_; + +class EffectsManagerTest : public BaseEffectTest { + protected: + virtual void SetUp() { + registerTestBackend(); + } +}; + +TEST_F(EffectsManagerTest, CanInstantiateEffectsFromBackend) { + EffectManifest manifest; + manifest.setId("org.mixxx.test.effect"); + manifest.setName("Test Effect"); + registerTestEffect(manifest); + + // Check we can get the same manifest that we registered back. + EffectManifest effect_to_load = m_pEffectsManager->getEffectManifest(manifest.id()); + EXPECT_QSTRING_EQ(effect_to_load.name(), manifest.name()); + + // Check we can instantiate the effect. + EffectPointer pEffect = m_pEffectsManager->instantiateEffect(manifest.id()); + EXPECT_FALSE(pEffect.isNull()); +} diff --git a/src/test/enginemastertest.cpp b/src/test/enginemastertest.cpp index cd8e3e917fc..310730a6937 100644 --- a/src/test/enginemastertest.cpp +++ b/src/test/enginemastertest.cpp @@ -33,7 +33,7 @@ class EngineChannelMock : public EngineChannel { class EngineMasterTest : public MixxxTest { protected: virtual void SetUp() { - m_pMaster = new EngineMaster(config(), "[Master]", false, false); + m_pMaster = new EngineMaster(config(), "[Master]", NULL, false, false); } virtual void TearDown() { diff --git a/src/test/mockedenginebackendtest.h b/src/test/mockedenginebackendtest.h index b6635b2dcea..4c362d4d113 100644 --- a/src/test/mockedenginebackendtest.h +++ b/src/test/mockedenginebackendtest.h @@ -10,6 +10,7 @@ #include "configobject.h" #include "controlobject.h" #include "deck.h" +#include "effects/effectsmanager.h" #include "engine/enginebuffer.h" #include "engine/enginebufferscale.h" #include "engine/enginechannel.h" @@ -41,14 +42,19 @@ class MockedEngineBackendTest : public MixxxTest { protected: virtual void SetUp() { m_pNumDecks = new ControlObject(ConfigKey("[Master]", "num_decks")); - m_pEngineMaster = new EngineMaster(m_pConfig.data(), "[Master]", false, false); + m_pEffectsManager = new EffectsManager(NULL, config()); + m_pEngineMaster = new EngineMaster(m_pConfig.data(), "[Master]", + m_pEffectsManager, false, false); m_pChannel1 = new EngineDeck(m_sGroup1, m_pConfig.data(), - m_pEngineMaster, EngineChannel::CENTER); + m_pEngineMaster, m_pEffectsManager, + EngineChannel::CENTER); m_pChannel2 = new EngineDeck(m_sGroup2, m_pConfig.data(), - m_pEngineMaster, EngineChannel::CENTER); + m_pEngineMaster, m_pEffectsManager, + EngineChannel::CENTER); m_pChannel3 = new EngineDeck(m_sGroup3, m_pConfig.data(), - m_pEngineMaster, EngineChannel::CENTER); + m_pEngineMaster, m_pEffectsManager, + EngineChannel::CENTER); addDeck(m_pChannel1); addDeck(m_pChannel2); @@ -86,6 +92,7 @@ class MockedEngineBackendTest : public MixxxTest { // Deletes all EngineChannels added to it. delete m_pEngineMaster; + delete m_pEffectsManager; delete m_pMockScaler1; delete m_pMockScaler2; delete m_pMockScaler3; @@ -102,6 +109,7 @@ class MockedEngineBackendTest : public MixxxTest { ControlObject* m_pNumDecks; + EffectsManager* m_pEffectsManager; EngineSync* m_pEngineSync; EngineMaster* m_pEngineMaster; EngineDeck *m_pChannel1, *m_pChannel2, *m_pChannel3; diff --git a/src/util.h b/src/util.h index bbc436eac78..d667ea914be 100644 --- a/src/util.h +++ b/src/util.h @@ -1,6 +1,8 @@ #ifndef UTIL_H #define UTIL_H +// A macro to disallow the copy constructor and operator= functions +// This should be used in the private: declarations for a class #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ TypeName(const TypeName&); \ void operator=(const TypeName&) diff --git a/src/util/fifo.h b/src/util/fifo.h index e41cef5e067..df8cb61552e 100644 --- a/src/util/fifo.h +++ b/src/util/fifo.h @@ -2,8 +2,12 @@ #define FIFO_H #include +#include +#include +#include #include "util/pa_ringbuffer.h" +#include "util/reference.h" #include "util.h" template @@ -53,79 +57,116 @@ class FIFO { DISALLOW_COPY_AND_ASSIGN(FIFO); }; -// TwoWayMessagePipe is a bare-bones wrapper around the above FIFO class that -// facilitates non-blocking two-way communication. To keep terminology clear, -// there are two sides to the message pipe, the sender side and the target side. -// The non-blocking aspect of the underlying FIFO class requires that the sender -// methods and target methods each only be called from a single thread, or -// alternatively guarded with a mutex. The most common use-case of this class is -// sending and receiving messages with the callback thread without the callback -// thread blocking. -template -class TwoWayMessagePipe { +// MessagePipe represents one side of a TwoWayMessagePipe. The direction of the +// pipe is with respect to the owner so sender and receiver are +// perspective-dependent. If serializeWrites is true then calls to writeMessages +// will be serialized with a mutex. +template +class MessagePipe { public: - TwoWayMessagePipe(int sender_fifo_size, int target_fifo_size) - : m_target_messages(target_fifo_size), - m_sender_messages(sender_fifo_size) { + MessagePipe(FIFO& receiver_messages, + FIFO& sender_messages, + BaseReferenceHolder* pTwoWayMessagePipeReference, + bool serialize_writes) + : m_receiver_messages(receiver_messages), + m_sender_messages(sender_messages), + m_pTwoWayMessagePipeReference(pTwoWayMessagePipeReference), + m_bSerializeWrites(serialize_writes) { } - //////////////////////////////////////////////////////////////////////////// - // Target methods. These should only be called from the target thread. Wrap - // with a mutex to make these methods callable from any thread. - //////////////////////////////////////////////////////////////////////////// - - // Returns the number of SenderMessageType messages waiting to be read by - // the target. - inline int targetMessageCount() const { - return m_target_messages.readAvailable(); + // Returns the number of ReceiverMessageType messages waiting to be read by + // the receiver. Non-blocking. + inline int messageCount() const { + return m_sender_messages.readAvailable(); } - // Read SenderMessageType messages from the sender up to a maximum of - // 'count'. Returns the number of messages written to 'messages'. - inline int targetReadMessages(SenderMessageType* messages, int count) { - return m_target_messages.read(messages, count); + // Read a ReceiverMessageType written by the receiver addressed to the + // sender. Non-blocking. + inline int readMessages(ReceiverMessageType* messages, int count) { + return m_sender_messages.read(messages, count); } - // Writes up to 'count' messages from the 'message' array to the sender and - // returns the number of successfully written messages. - inline int targetWriteMessage(const TargetMessageType* messages, int count) { - return m_sender_messages.write(messages, count); + // Writes up to 'count' messages from the 'message' array to the receiver + // and returns the number of successfully written messages. If + // serializeWrites is active, this method is blocking. + inline int writeMessages(const SenderMessageType* messages, int count) { + if (m_bSerializeWrites) { + m_serializationMutex.lock(); + } + return m_receiver_messages.write(messages, count); + if (m_bSerializeWrites) { + m_serializationMutex.unlock(); + } } + private: + QMutex m_serializationMutex; + FIFO& m_receiver_messages; + FIFO& m_sender_messages; + QScopedPointer m_pTwoWayMessagePipeReference; + bool m_bSerializeWrites; - //////////////////////////////////////////////////////////////////////////// - // Sender methods. These should only be called by the sender thread. Wrap - // with a mutex to make these methods callable from any thread. - //////////////////////////////////////////////////////////////////////////// - - // Returns the number of TargetMessageType messages waiting to be read by - // the sender. - inline int senderMessageCount() const { - return m_sender_messages.readAvailable(); - } +#define COMMA , + DISALLOW_COPY_AND_ASSIGN(MessagePipe); +#undef COMMA +}; - // Read a TargetMessageType written by the target addressed to the - // sender. - inline int senderReadMessages(TargetMessageType* messages, int count) { - return m_sender_messages.read(messages, count); +// TwoWayMessagePipe is a bare-bones wrapper around the above FIFO class that +// facilitates non-blocking two-way communication. To keep terminology clear, +// there are two sides to the message pipe, the sender side and the receiver +// side. The non-blocking aspect of the underlying FIFO class requires that the +// sender methods and target methods each only be called from a single thread, +// or alternatively guarded with a mutex. The most common use-case of this class +// is sending and receiving messages with the callback thread without the +// callback thread blocking. +// +// This class is an implementation detail and cannot be instantiated +// directly. Use makeTwoWayMessagePipe(...) to create a two-way pipe. +template +class TwoWayMessagePipe { + public: + // Creates a TwoWayMessagePipe with SenderMessageType and + // ReceiverMessageType as the message types. Returns a pair of MessagePipes, + // the first is the sender's pipe (sends SenderMessageType and receives + // ReceiverMessageType messages) and the second is the receiver's pipe + // (sends ReceiverMessageType and receives SenderMessageType messages). + static QPair*, + MessagePipe*> makeTwoWayMessagePipe( + int sender_fifo_size, + int receiver_fifo_size, + bool serialize_sender_writes, + bool serialize_receiver_writes) { + QSharedPointer > pipe( + new TwoWayMessagePipe( + sender_fifo_size, receiver_fifo_size)); + + return QPair*, + MessagePipe*>( + new MessagePipe( + pipe->m_receiver_messages, pipe->m_sender_messages, + new ReferenceHolder >(pipe), + serialize_sender_writes), + new MessagePipe( + pipe->m_sender_messages, pipe->m_receiver_messages, + new ReferenceHolder >(pipe), + serialize_receiver_writes)); } - // Writes up to 'count' messages from the 'message' array to the target and - // returns the number of successfully written messages. - inline int senderWriteMessage(const SenderMessageType* messages, int count) { - return m_target_messages.write(messages, count); + private: + TwoWayMessagePipe(int sender_fifo_size, int receiver_fifo_size) + : m_receiver_messages(receiver_fifo_size), + m_sender_messages(sender_fifo_size) { } - private: - // Messages waiting to be delivered to the target. - FIFO m_target_messages; + // Messages waiting to be delivered to the receiver. + FIFO m_receiver_messages; // Messages waiting to be delivered to the sender. - FIFO m_sender_messages; + FIFO m_sender_messages; // This #define is because the macro gets confused by the template // parameters. #define COMMA , - DISALLOW_COPY_AND_ASSIGN(TwoWayMessagePipe); + DISALLOW_COPY_AND_ASSIGN(TwoWayMessagePipe); #undef COMMA }; diff --git a/src/util/reference.h b/src/util/reference.h new file mode 100644 index 00000000000..85f34b7e604 --- /dev/null +++ b/src/util/reference.h @@ -0,0 +1,25 @@ +#ifndef REFERENCE_H +#define REFERENCE_H + +// General tool for removing concrete dependencies while still incrementing a +// reference count. +class BaseReferenceHolder { + public: + BaseReferenceHolder() { } + virtual ~BaseReferenceHolder() { } +}; + +template +class ReferenceHolder : public BaseReferenceHolder { + public: + ReferenceHolder(QSharedPointer& reference) + : m_reference(reference) { + } + virtual ~ReferenceHolder() {} + + private: + QSharedPointer m_reference; +}; + + +#endif /* REFERENCE_H */ diff --git a/src/widget/weffect.cpp b/src/widget/weffect.cpp new file mode 100644 index 00000000000..ba2b3818538 --- /dev/null +++ b/src/widget/weffect.cpp @@ -0,0 +1,77 @@ +#include + +#include "widget/weffect.h" + +#include "effects/effectsmanager.h" + +WEffect::WEffect(QWidget* pParent, EffectsManager* pEffectsManager) + : WLabel(pParent), + m_pEffectsManager(pEffectsManager) { + effectUpdated(); +} + +WEffect::~WEffect() { +} + +void WEffect::setup(QDomNode node, const SkinContext& context) { + bool rackOk = false; + int rackNumber = context.selectInt(node, "EffectRack", &rackOk) - 1; + bool chainOk = false; + int chainNumber = context.selectInt(node, "EffectUnit", &chainOk) - 1; + bool effectOk = false; + int effectNumber = context.selectInt(node, "Effect", &effectOk) - 1; + + // Tolerate no . Use the default one. + if (!rackOk) { + rackNumber = 0; + } + + if (!chainOk) { + qDebug() << "EffectName node had invalid EffectUnit number:" << chainNumber; + } + + if (!effectOk) { + qDebug() << "EffectName node had invalid Effect number:" << effectNumber; + } + + EffectRackPointer pRack = m_pEffectsManager->getEffectRack(rackNumber); + if (pRack) { + EffectChainSlotPointer pChainSlot = pRack->getEffectChainSlot(chainNumber); + if (pChainSlot) { + EffectSlotPointer pEffectSlot = pChainSlot->getEffectSlot(effectNumber); + if (pEffectSlot) { + setEffectSlot(pEffectSlot); + } else { + qDebug() << "EffectName node had invalid Effect number:" << effectNumber; + } + } else { + qDebug() << "EffectName node had invalid EffectUnit number:" << chainNumber; + } + } else { + qDebug() << "EffectName node had invalid EffectRack number:" << rackNumber; + } +} + +void WEffect::setEffectSlot(EffectSlotPointer pEffectSlot) { + if (pEffectSlot) { + m_pEffectSlot = pEffectSlot; + connect(pEffectSlot.data(), SIGNAL(updated()), + this, SLOT(effectUpdated())); + effectUpdated(); + } +} + +void WEffect::effectUpdated() { + QString name = tr("None"); + QString description = tr("No effect loaded."); + if (m_pEffectSlot) { + EffectPointer pEffect = m_pEffectSlot->getEffect(); + if (pEffect) { + const EffectManifest& manifest = pEffect->getManifest(); + name = manifest.name(); + description = manifest.description(); + } + } + setText(name); + setBaseTooltip(description); +} diff --git a/src/widget/weffect.h b/src/widget/weffect.h new file mode 100644 index 00000000000..e3916941b54 --- /dev/null +++ b/src/widget/weffect.h @@ -0,0 +1,32 @@ +#ifndef WEFFECT_H +#define WEFFECT_H + +#include + +#include "widget/wlabel.h" +#include "effects/effectslot.h" +#include "skin/skincontext.h" + +class EffectsManager; + +class WEffect : public WLabel { + Q_OBJECT + public: + WEffect(QWidget* pParent, EffectsManager* pEffectsManager); + virtual ~WEffect(); + + void setup(QDomNode node, const SkinContext& context); + + private slots: + void effectUpdated(); + + private: + // Set the EffectSlot that should be monitored by this WEffect. + void setEffectSlot(EffectSlotPointer pEffectSlot); + + EffectsManager* m_pEffectsManager; + EffectSlotPointer m_pEffectSlot; +}; + + +#endif /* WEFFECT_H */ diff --git a/src/widget/weffectchain.cpp b/src/widget/weffectchain.cpp new file mode 100644 index 00000000000..54b9f856701 --- /dev/null +++ b/src/widget/weffectchain.cpp @@ -0,0 +1,64 @@ +#include + +#include "widget/weffectchain.h" +#include "effects/effectsmanager.h" + +WEffectChain::WEffectChain(QWidget* pParent, EffectsManager* pEffectsManager) + : WLabel(pParent), + m_pEffectsManager(pEffectsManager) { + chainUpdated(); +} + +WEffectChain::~WEffectChain() { +} + +void WEffectChain::setup(QDomNode node, const SkinContext& context) { + bool rackOk = false; + int rackNumber = context.selectInt(node, "EffectRack", &rackOk) - 1; + bool chainOk = false; + int chainNumber = context.selectInt(node, "EffectUnit", &chainOk) - 1; + + // Tolerate no . Use the default one. + if (!rackOk) { + rackNumber = 0; + } + + if (!chainOk) { + qDebug() << "EffectChainName node had invalid EffectChain number:" << chainNumber; + } + + EffectRackPointer pRack = m_pEffectsManager->getEffectRack(rackNumber); + if (pRack) { + EffectChainSlotPointer pChainSlot = pRack->getEffectChainSlot(chainNumber); + if (pChainSlot) { + setEffectChainSlot(pChainSlot); + } else { + qDebug() << "EffectChainName node had invalid EffectChain number:" << chainNumber; + } + } else { + qDebug() << "EffectChainName node had invalid EffectRack number:" << rackNumber; + } +} + +void WEffectChain::setEffectChainSlot(EffectChainSlotPointer pEffectChainSlot) { + if (pEffectChainSlot) { + m_pEffectChainSlot = pEffectChainSlot; + connect(pEffectChainSlot.data(), SIGNAL(updated()), + this, SLOT(chainUpdated())); + chainUpdated(); + } +} + +void WEffectChain::chainUpdated() { + QString name = tr("None"); + QString description = tr("No effect chain loaded."); + if (m_pEffectChainSlot) { + EffectChainPointer pChain = m_pEffectChainSlot->getEffectChain(); + if (pChain) { + name = pChain->name(); + description = pChain->description(); + } + } + setText(name); + setBaseTooltip(description); +} diff --git a/src/widget/weffectchain.h b/src/widget/weffectchain.h new file mode 100644 index 00000000000..ffc0eb9ced9 --- /dev/null +++ b/src/widget/weffectchain.h @@ -0,0 +1,33 @@ +#ifndef WEFFECTCHAIN_H +#define WEFFECTCHAIN_H + +#include +#include +#include + +#include "effects/effectchainslot.h" +#include "widget/wlabel.h" +#include "skin/skincontext.h" + +class EffectsManager; + +class WEffectChain : public WLabel { + Q_OBJECT + public: + WEffectChain(QWidget* pParent, EffectsManager* pEffectsManager); + virtual ~WEffectChain(); + + void setup(QDomNode node, const SkinContext& context); + + private slots: + void chainUpdated(); + + private: + // Set the EffectChain that should be monitored by this WEffectChain + void setEffectChainSlot(EffectChainSlotPointer pEffectChainSlot); + + EffectsManager* m_pEffectsManager; + EffectChainSlotPointer m_pEffectChainSlot; +}; + +#endif /* WEFFECTCHAIN_H */ diff --git a/src/widget/weffectparameter.cpp b/src/widget/weffectparameter.cpp new file mode 100644 index 00000000000..35eba5d5c73 --- /dev/null +++ b/src/widget/weffectparameter.cpp @@ -0,0 +1,83 @@ +#include + +#include "widget/weffectparameter.h" +#include "effects/effectsmanager.h" + +WEffectParameter::WEffectParameter(QWidget* pParent, EffectsManager* pEffectsManager) + : WLabel(pParent), + m_pEffectsManager(pEffectsManager) { + parameterUpdated(); +} + +WEffectParameter::~WEffectParameter() { +} + +void WEffectParameter::setup(QDomNode node, const SkinContext& context) { + bool rackOk = false; + int rackNumber = context.selectInt(node, "EffectRack", &rackOk) - 1; + bool chainOk = false; + int chainNumber = context.selectInt(node, "EffectUnit", &chainOk) - 1; + bool effectOk = false; + int effectNumber = context.selectInt(node, "Effect", &effectOk) - 1; + bool parameterOk = false; + int parameterNumber = context.selectInt(node, "EffectParameter", ¶meterOk) - 1; + + // Tolerate no . Use the default one. + if (!rackOk) { + rackNumber = 0; + } + + if (!chainOk) { + qDebug() << "EffectParameterName node had invalid EffectUnit number:" << chainNumber; + } + + if (!effectOk) { + qDebug() << "EffectParameterName node had invalid Effect number:" << effectNumber; + } + + if (!parameterOk) { + qDebug() << "EffectParameterName node had invalid Parameter number:" << parameterNumber; + } + + EffectRackPointer pRack = m_pEffectsManager->getEffectRack(rackNumber); + if (pRack) { + EffectChainSlotPointer pChainSlot = pRack->getEffectChainSlot(chainNumber); + if (pChainSlot) { + EffectSlotPointer pEffectSlot = pChainSlot->getEffectSlot(effectNumber); + if (pEffectSlot) { + EffectParameterSlotPointer pParameterSlot = + pEffectSlot->getEffectParameterSlot(parameterNumber); + if (pParameterSlot) { + setEffectParameterSlot(pParameterSlot); + } else { + qDebug() << "EffectParameterName node had invalid Parameter number:" << parameterNumber; + } + } else { + qDebug() << "EffectParameterName node had invalid Effect number:" << effectNumber; + } + } else { + qDebug() << "EffectParameterName node had invalid EffectUnit number:" << chainNumber; + } + } else { + qDebug() << "EffectParameterName node had invalid EffectRack number:" << rackNumber; + } +} + +void WEffectParameter::setEffectParameterSlot(EffectParameterSlotPointer pEffectParameterSlot) { + if (pEffectParameterSlot) { + m_pEffectParameterSlot = pEffectParameterSlot; + connect(pEffectParameterSlot.data(), SIGNAL(updated()), + this, SLOT(parameterUpdated())); + parameterUpdated(); + } +} + +void WEffectParameter::parameterUpdated() { + if (m_pEffectParameterSlot) { + setText(m_pEffectParameterSlot->name()); + setBaseTooltip(m_pEffectParameterSlot->description()); + } else { + setText(tr("None")); + setBaseTooltip(tr("No effect loaded.")); + } +} diff --git a/src/widget/weffectparameter.h b/src/widget/weffectparameter.h new file mode 100644 index 00000000000..272bdb0da0f --- /dev/null +++ b/src/widget/weffectparameter.h @@ -0,0 +1,33 @@ +#ifndef WEFFECTPARAMETER_H +#define WEFFECTPARAMETER_H + +#include + +#include "widget/wlabel.h" +#include "effects/effectparameterslot.h" +#include "skin/skincontext.h" + +class EffectsManager; + +class WEffectParameter : public WLabel { + Q_OBJECT + public: + WEffectParameter(QWidget* pParent, EffectsManager* pEffectsManager); + virtual ~WEffectParameter(); + + void setup(QDomNode node, const SkinContext& context); + + private slots: + void parameterUpdated(); + + private: + // Set the EffectParameterSlot that should be monitored by this + // WEffectParameter. + void setEffectParameterSlot(EffectParameterSlotPointer pEffectParameterSlot); + + EffectsManager* m_pEffectsManager; + EffectParameterSlotPointer m_pEffectParameterSlot; +}; + + +#endif /* WEFFECTPARAMETER_H */ diff --git a/src/widget/wlabel.h b/src/widget/wlabel.h index 994168f4717..d3647af9cac 100644 --- a/src/widget/wlabel.h +++ b/src/widget/wlabel.h @@ -35,7 +35,6 @@ class WLabel : public QLabel, public WBaseWidget { protected: bool event(QEvent* pEvent); void fillDebugTooltip(QStringList* debug); - QString m_qsText; // Foreground and background colors. QColor m_qFgColor;