-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compressor effect #12523
Merged
Merged
Compressor effect #12523
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
30af769
Add compressor effect
fonsargo bf0bf06
Auto make up refactoring
fonsargo b5e8049
Split compressor logic and make up logic
fonsargo b5fa5bd
Compression refactoring
fonsargo a2b70a4
Add knee param + fixes
fonsargo 75a73b3
Auto make up fixes
fonsargo 1c1adda
Fix attack & release
fonsargo 247624b
Attack & release fix
fonsargo 92f231f
Add descriptions + change Attack knob to logarithmic
fonsargo 6e3c846
Remove unnecessary include
fonsargo c8dba6a
Update src/effects/backends/builtin/compressoreffect.h
fonsargo 3b6f369
Update src/effects/backends/builtin/compressoreffect.h
fonsargo 3c9cebe
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo 503fc79
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo 68cb60a
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo 3494bff
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo fee6e99
AutoMakeUp & Clipping enum fixes
fonsargo 0c337ab
Fix formatting
fonsargo f519762
Add applyClamp function to SampleUtil
fonsargo 97aafcb
Fix formatting 2
fonsargo 5f624ea
Fix compile error - float instead of double
fonsargo 82f4915
Fix formatting 3
fonsargo 923c1ea
Fix compile errors
fonsargo 85f13cc
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo 552d05c
Change makeup description + remove vectorized note
fonsargo 1956ab2
Change variables to double
fonsargo 3530873
Calculate ballistics only if params were changed
fonsargo 86216da
Remove clipping
fonsargo cadd4e3
remove applyMakeUp DB round trip
fonsargo 698201a
Moving namespace to cpp + add defaut constants
fonsargo 2ba0630
Add line breaks to descriptions
fonsargo 638f1a0
Change default values
fonsargo 296f7ce
ratiod2db fix
fonsargo 82ffc6c
Fix reorder error
fonsargo a7cbc08
Fix formatting
fonsargo b74c984
Change auto make up algorithm
fonsargo 0dafd46
Rename output gain to level
fonsargo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
#include "effects/backends/builtin/compressoreffect.h" | ||
|
||
namespace { | ||
constexpr CSAMPLE_GAIN kMakeUpAttackCoeff = 0.03f; | ||
constexpr double defaultAttackMs = 1; | ||
constexpr double defaultReleaseMs = 300; | ||
constexpr CSAMPLE_GAIN defaultThresholdDB = -20; | ||
|
||
double calculateBallistics(double paramMs, const mixxx::EngineParameters& engineParameters) { | ||
return exp(-1000.0 / (paramMs * engineParameters.sampleRate())); | ||
} | ||
|
||
} // anonymous namespace | ||
|
||
// static | ||
QString CompressorEffect::getId() { | ||
return "org.mixxx.effects.compressor"; | ||
} | ||
|
||
// static | ||
EffectManifestPointer CompressorEffect::getManifest() { | ||
auto pManifest = EffectManifestPointer::create(); | ||
pManifest->setId(getId()); | ||
pManifest->setName(QObject::tr("Compressor")); | ||
pManifest->setShortName(QObject::tr("Compressor")); | ||
pManifest->setAuthor("The Mixxx Team"); | ||
pManifest->setVersion("1.0"); | ||
pManifest->setDescription("A single-band compressor effect"); | ||
pManifest->setEffectRampsFromDry(true); | ||
pManifest->setMetaknobDefault(0.0); | ||
|
||
EffectManifestParameterPointer autoMakeUp = pManifest->addParameter(); | ||
autoMakeUp->setId("automakeup"); | ||
autoMakeUp->setName(QObject::tr("Auto Makeup Gain")); | ||
autoMakeUp->setShortName(QObject::tr("Makeup")); | ||
autoMakeUp->setDescription(QObject::tr( | ||
"The Auto Makeup button enables automatic gain adjustment to keep " | ||
"the input signal \nand the processed output signal as close as " | ||
"possible in perceived loudness")); | ||
autoMakeUp->setValueScaler(EffectManifestParameter::ValueScaler::Toggle); | ||
autoMakeUp->setRange(0, 1, 1); | ||
autoMakeUp->appendStep(qMakePair( | ||
QObject::tr("Off"), static_cast<int>(AutoMakeUp::AutoMakeUpOff))); | ||
autoMakeUp->appendStep(qMakePair( | ||
QObject::tr("On"), static_cast<int>(AutoMakeUp::AutoMakeUpOn))); | ||
|
||
EffectManifestParameterPointer threshold = pManifest->addParameter(); | ||
threshold->setId("threshold"); | ||
threshold->setName(QObject::tr("Threshold (dBFS)")); | ||
threshold->setShortName(QObject::tr("Threshold")); | ||
threshold->setDescription( | ||
QObject::tr("The Threshold knob adjusts the level above which the " | ||
"compressor starts attenuating the input signal")); | ||
threshold->setValueScaler(EffectManifestParameter::ValueScaler::Linear); | ||
threshold->setUnitsHint(EffectManifestParameter::UnitsHint::Decibel); | ||
threshold->setNeutralPointOnScale(0); | ||
threshold->setRange(-50, defaultThresholdDB, 0); | ||
|
||
EffectManifestParameterPointer ratio = pManifest->addParameter(); | ||
ratio->setId("ratio"); | ||
ratio->setName(QObject::tr("Ratio (:1)")); | ||
ratio->setShortName(QObject::tr("Ratio")); | ||
ratio->setDescription( | ||
QObject::tr("The Ratio knob determines how much the signal is " | ||
"attenuated above the chosen threshold.\n" | ||
"For a ratio of 4:1, one dB remains for every four dB of " | ||
"input signal above the threshold.\n" | ||
"At a ratio of 1:1 no compression is happening, as the " | ||
"input is exactly the output.")); | ||
ratio->setValueScaler(EffectManifestParameter::ValueScaler::Logarithmic); | ||
ratio->setUnitsHint(EffectManifestParameter::UnitsHint::Coefficient); | ||
ratio->setNeutralPointOnScale(0); | ||
ratio->setRange(1.0, 6.0, 1000); | ||
|
||
EffectManifestParameterPointer knee = pManifest->addParameter(); | ||
knee->setId("knee"); | ||
knee->setName(QObject::tr("Knee (dBFS)")); | ||
knee->setShortName(QObject::tr("Knee")); | ||
knee->setDescription(QObject::tr( | ||
"The Knee knob is used to achieve a rounder compression curve")); | ||
knee->setValueScaler(EffectManifestParameter::ValueScaler::Linear); | ||
knee->setUnitsHint(EffectManifestParameter::UnitsHint::Coefficient); | ||
knee->setNeutralPointOnScale(0); | ||
knee->setRange(0.0, 4.0, 24); | ||
|
||
EffectManifestParameterPointer attack = pManifest->addParameter(); | ||
attack->setId("attack"); | ||
attack->setName(QObject::tr("Attack (ms)")); | ||
attack->setShortName(QObject::tr("Attack")); | ||
attack->setDescription(QObject::tr( | ||
"The Attack knob sets the time that determines how fast the " | ||
"compression \nwill set in once the signal exceeds the threshold")); | ||
attack->setValueScaler(EffectManifestParameter::ValueScaler::Logarithmic); | ||
attack->setUnitsHint(EffectManifestParameter::UnitsHint::Millisecond); | ||
attack->setRange(0, defaultAttackMs, 250); | ||
|
||
EffectManifestParameterPointer release = pManifest->addParameter(); | ||
release->setId("release"); | ||
release->setName(QObject::tr("Release (ms)")); | ||
release->setShortName(QObject::tr("Release")); | ||
release->setDescription( | ||
QObject::tr("The Release knob sets the time that determines how " | ||
"fast the compressor will recover from the gain\n" | ||
"reduction once the signal falls under the threshold. " | ||
"Depending on the input signal, short release times\n" | ||
"may introduce a 'pumping' effect and/or distortion.")); | ||
release->setValueScaler(EffectManifestParameter::ValueScaler::Integral); | ||
release->setUnitsHint(EffectManifestParameter::UnitsHint::Millisecond); | ||
release->setRange(0, defaultReleaseMs, 1500); | ||
|
||
EffectManifestParameterPointer level = pManifest->addParameter(); | ||
level->setId("level"); | ||
level->setName(QObject::tr("Level")); | ||
level->setShortName(QObject::tr("Level")); | ||
level->setDescription( | ||
QObject::tr("The Level knob adjusts the level of the output " | ||
"signal after the compression was applied")); | ||
level->setValueScaler(EffectManifestParameter::ValueScaler::Linear); | ||
level->setUnitsHint(EffectManifestParameter::UnitsHint::Decibel); | ||
level->setRange(-25, 0, 25); | ||
|
||
return pManifest; | ||
} | ||
|
||
CompressorGroupState::CompressorGroupState( | ||
const mixxx::EngineParameters& engineParameters) | ||
: EffectState(engineParameters), | ||
previousStateDB(0), | ||
previousAttackParamMs(defaultAttackMs), | ||
previousAttackCoeff(calculateBallistics(defaultAttackMs, engineParameters)), | ||
previousReleaseParamMs(defaultReleaseMs), | ||
previousReleaseCoeff(calculateBallistics(defaultReleaseMs, engineParameters)), | ||
previousMakeUpGain(1) { | ||
} | ||
|
||
void CompressorEffect::loadEngineEffectParameters( | ||
const QMap<QString, EngineEffectParameterPointer>& parameters) { | ||
m_pThreshold = parameters.value("threshold"); | ||
m_pRatio = parameters.value("ratio"); | ||
m_pKnee = parameters.value("knee"); | ||
m_pAttack = parameters.value("attack"); | ||
m_pRelease = parameters.value("release"); | ||
m_pLevel = parameters.value("level"); | ||
m_pAutoMakeUp = parameters.value("automakeup"); | ||
} | ||
|
||
void CompressorEffect::processChannel( | ||
CompressorGroupState* pState, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput, | ||
const mixxx::EngineParameters& engineParameters, | ||
const EffectEnableState enableState, | ||
const GroupFeatureState& groupFeatures) { | ||
Q_UNUSED(groupFeatures); | ||
Q_UNUSED(enableState); | ||
|
||
SINT numSamples = engineParameters.samplesPerBuffer(); | ||
|
||
// Compression | ||
applyCompression(pState, engineParameters, pInput, pOutput); | ||
|
||
// Auto make up | ||
if (m_pAutoMakeUp->toInt() == static_cast<int>(AutoMakeUp::AutoMakeUpOn)) { | ||
applyAutoMakeUp(pState, pInput, pOutput, numSamples); | ||
} | ||
|
||
// Output gain | ||
CSAMPLE gain = static_cast<CSAMPLE>(db2ratio(m_pLevel->value())); | ||
SampleUtil::applyGain(pOutput, gain, numSamples); | ||
} | ||
|
||
void CompressorEffect::applyAutoMakeUp(CompressorGroupState* pState, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput, | ||
const SINT& numSamples) { | ||
CSAMPLE rmsInput = SampleUtil::rms(pInput, numSamples); | ||
if (rmsInput > CSAMPLE_ZERO) { | ||
CSAMPLE_GAIN makeUpGainState = pState->previousMakeUpGain; | ||
|
||
CSAMPLE rmsOutput = SampleUtil::rms(pOutput, numSamples); | ||
CSAMPLE_GAIN makeUp = rmsInput / rmsOutput; | ||
|
||
// smoothing | ||
makeUpGainState = kMakeUpAttackCoeff * makeUp + (1 - kMakeUpAttackCoeff) * makeUpGainState; | ||
|
||
pState->previousMakeUpGain = makeUpGainState; | ||
SampleUtil::applyGain(pOutput, makeUpGainState, numSamples); | ||
} | ||
} | ||
|
||
void CompressorEffect::applyCompression(CompressorGroupState* pState, | ||
const mixxx::EngineParameters& engineParameters, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput) { | ||
double thresholdParam = m_pThreshold->value(); | ||
double ratioParam = m_pRatio->value(); | ||
double kneeParam = m_pKnee->value(); | ||
double kneeHalf = kneeParam / 2.0f; | ||
|
||
double attackParamMs = m_pAttack->value(); | ||
double attackCoeff = pState->previousAttackCoeff; | ||
if (attackParamMs != pState->previousAttackParamMs) { | ||
attackCoeff = calculateBallistics(attackParamMs, engineParameters); | ||
pState->previousAttackParamMs = attackParamMs; | ||
pState->previousAttackCoeff = attackCoeff; | ||
} | ||
|
||
double releaseParamMs = m_pRelease->value(); | ||
double releaseCoeff = pState->previousReleaseCoeff; | ||
if (releaseParamMs != pState->previousReleaseParamMs) { | ||
releaseCoeff = calculateBallistics(releaseParamMs, engineParameters); | ||
pState->previousReleaseParamMs = releaseParamMs; | ||
pState->previousReleaseCoeff = releaseCoeff; | ||
} | ||
|
||
double stateDB = pState->previousStateDB; | ||
SINT numSamples = engineParameters.samplesPerBuffer(); | ||
int channelCount = engineParameters.channelCount(); | ||
for (SINT i = 0; i < numSamples; i += channelCount) { | ||
CSAMPLE maxSample = std::max(fabs(pInput[i]), fabs(pInput[i + 1])); | ||
if (maxSample == CSAMPLE_ZERO) { | ||
pOutput[i] = CSAMPLE_ZERO; | ||
pOutput[i + 1] = CSAMPLE_ZERO; | ||
continue; | ||
} | ||
|
||
double maxSampleDB = ratio2db(maxSample); | ||
double overDB = maxSampleDB - thresholdParam; | ||
if (overDB <= -kneeHalf) { | ||
overDB = 0.0; | ||
} else if (overDB > -kneeHalf && overDB <= kneeHalf) { | ||
overDB = 0.5 * (overDB + kneeHalf) * (overDB + kneeHalf) / kneeParam; | ||
} | ||
double compressedDB = overDB * (1.0 / ratioParam - 1.0); | ||
|
||
// attack/release | ||
if (compressedDB < stateDB) { | ||
stateDB = compressedDB + attackCoeff * (stateDB - compressedDB); | ||
} else { | ||
stateDB = compressedDB + releaseCoeff * (stateDB - compressedDB); | ||
} | ||
|
||
CSAMPLE gain = static_cast<CSAMPLE>(db2ratio(stateDB)); | ||
pOutput[i] = pInput[i] * gain; | ||
pOutput[i + 1] = pInput[i + 1] * gain; | ||
} | ||
pState->previousStateDB = stateDB; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
#pragma once | ||
|
||
#include "effects/backends/effectprocessor.h" | ||
#include "engine/effects/engineeffect.h" | ||
#include "engine/effects/engineeffectparameter.h" | ||
#include "util/class.h" | ||
#include "util/defs.h" | ||
#include "util/sample.h" | ||
#include "util/types.h" | ||
|
||
class CompressorGroupState : public EffectState { | ||
public: | ||
CompressorGroupState(const mixxx::EngineParameters& engineParameters); | ||
|
||
double previousStateDB; | ||
double previousAttackParamMs; | ||
double previousAttackCoeff; | ||
double previousReleaseParamMs; | ||
double previousReleaseCoeff; | ||
CSAMPLE_GAIN previousMakeUpGain; | ||
}; | ||
|
||
class CompressorEffect : public EffectProcessorImpl<CompressorGroupState> { | ||
public: | ||
CompressorEffect() = default; | ||
|
||
static QString getId(); | ||
static EffectManifestPointer getManifest(); | ||
|
||
void loadEngineEffectParameters( | ||
const QMap<QString, EngineEffectParameterPointer>& parameters) override; | ||
|
||
void processChannel( | ||
CompressorGroupState* pState, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput, | ||
const mixxx::EngineParameters& engineParameters, | ||
const EffectEnableState enableState, | ||
const GroupFeatureState& groupFeatures) override; | ||
|
||
private: | ||
enum class AutoMakeUp { | ||
AutoMakeUpOff = 0, | ||
AutoMakeUpOn = 1, | ||
}; | ||
|
||
QString debugString() const { | ||
return getId(); | ||
} | ||
|
||
EngineEffectParameterPointer m_pAutoMakeUp; | ||
EngineEffectParameterPointer m_pThreshold; | ||
EngineEffectParameterPointer m_pRatio; | ||
EngineEffectParameterPointer m_pKnee; | ||
EngineEffectParameterPointer m_pAttack; | ||
EngineEffectParameterPointer m_pRelease; | ||
EngineEffectParameterPointer m_pLevel; | ||
|
||
DISALLOW_COPY_AND_ASSIGN(CompressorEffect); | ||
|
||
void applyCompression(CompressorGroupState* pState, | ||
const mixxx::EngineParameters& engineParameters, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput); | ||
|
||
void applyAutoMakeUp(CompressorGroupState* pState, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput, | ||
const SINT& numSamples); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ratio2db() is slow. It would be better to do the calculation in ratio and to the db2ratio() conversion of the parameters outside this busy loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I have to do it? I understand your point, but it's not easy to transform such calculations. I'm sure that it's possible in theory, but the final code will be unreadable. All examples of compressor that I saw before were written in DB values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, that was just a suggestion. You may add TODO comment instead, that someone else may pick it up.