Skip to content
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 37 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
30af769
Add compressor effect
fonsargo Nov 7, 2023
bf0bf06
Auto make up refactoring
fonsargo Nov 7, 2023
b5e8049
Split compressor logic and make up logic
fonsargo Nov 7, 2023
b5fa5bd
Compression refactoring
fonsargo Nov 13, 2023
a2b70a4
Add knee param + fixes
fonsargo Nov 13, 2023
75a73b3
Auto make up fixes
fonsargo Nov 26, 2023
1c1adda
Fix attack & release
fonsargo Dec 18, 2023
247624b
Attack & release fix
fonsargo Dec 20, 2023
92f231f
Add descriptions + change Attack knob to logarithmic
fonsargo Jan 6, 2024
6e3c846
Remove unnecessary include
fonsargo Jan 6, 2024
c8dba6a
Update src/effects/backends/builtin/compressoreffect.h
fonsargo Jan 13, 2024
3b6f369
Update src/effects/backends/builtin/compressoreffect.h
fonsargo Jan 13, 2024
3c9cebe
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo Jan 13, 2024
503fc79
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo Jan 13, 2024
68cb60a
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo Jan 13, 2024
3494bff
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo Jan 13, 2024
fee6e99
AutoMakeUp & Clipping enum fixes
fonsargo Jan 13, 2024
0c337ab
Fix formatting
fonsargo Jan 13, 2024
f519762
Add applyClamp function to SampleUtil
fonsargo Jan 13, 2024
97aafcb
Fix formatting 2
fonsargo Jan 13, 2024
5f624ea
Fix compile error - float instead of double
fonsargo Jan 13, 2024
82f4915
Fix formatting 3
fonsargo Jan 14, 2024
923c1ea
Fix compile errors
fonsargo Jan 14, 2024
85f13cc
Update src/effects/backends/builtin/compressoreffect.cpp
fonsargo Feb 3, 2024
552d05c
Change makeup description + remove vectorized note
fonsargo Feb 3, 2024
1956ab2
Change variables to double
fonsargo Feb 3, 2024
3530873
Calculate ballistics only if params were changed
fonsargo Feb 3, 2024
86216da
Remove clipping
fonsargo Feb 3, 2024
cadd4e3
remove applyMakeUp DB round trip
fonsargo Feb 19, 2024
698201a
Moving namespace to cpp + add defaut constants
fonsargo Feb 19, 2024
2ba0630
Add line breaks to descriptions
fonsargo Feb 19, 2024
638f1a0
Change default values
fonsargo Feb 19, 2024
296f7ce
ratiod2db fix
fonsargo Feb 19, 2024
82ffc6c
Fix reorder error
fonsargo Mar 13, 2024
a7cbc08
Fix formatting
fonsargo Mar 13, 2024
b74c984
Change auto make up algorithm
fonsargo Mar 27, 2024
0dafd46
Rename output gain to level
fonsargo Mar 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL
src/effects/backends/builtin/loudnesscontoureffect.cpp
src/effects/backends/builtin/metronomeeffect.cpp
src/effects/backends/builtin/moogladder4filtereffect.cpp
src/effects/backends/builtin/compressoreffect.cpp
src/effects/backends/builtin/parametriceqeffect.cpp
src/effects/backends/builtin/phasereffect.cpp
src/effects/backends/builtin/reverbeffect.cpp
Expand Down
2 changes: 2 additions & 0 deletions src/effects/backends/builtin/builtinbackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "effects/backends/builtin/reverbeffect.h"
#endif
#include "effects/backends/builtin/autopaneffect.h"
#include "effects/backends/builtin/compressoreffect.h"
#include "effects/backends/builtin/distortioneffect.h"
#include "effects/backends/builtin/echoeffect.h"
#include "effects/backends/builtin/glitcheffect.h"
Expand Down Expand Up @@ -62,6 +63,7 @@ BuiltInBackend::BuiltInBackend() {
#endif
registerEffect<DistortionEffect>();
registerEffect<GlitchEffect>();
registerEffect<CompressorEffect>();
}

std::unique_ptr<EffectProcessor> BuiltInBackend::createProcessor(
Expand Down
248 changes: 248 additions & 0 deletions src/effects/backends/builtin/compressoreffect.cpp
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);
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

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;
}
70 changes: 70 additions & 0 deletions src/effects/backends/builtin/compressoreffect.h
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);
};
2 changes: 1 addition & 1 deletion src/util/math.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ roundToFraction(double value, int denominator) {
template<typename T>
requires std::is_floating_point_v<T>
CMATH_CONSTEXPR T ratio2db(T a) {
return log10(a) * 20;
return static_cast<T>(log10(a) * 20);
}

template<typename T>
Expand Down