-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Auto gain control (AGC) effect #14617
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
Changes from 7 commits
dc30194
e259924
d002e6b
d922c7a
b2a0678
c0045de
25a5d22
1991f3b
b3dcc9a
6c86414
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
#include "effects/backends/builtin/autogaincontroleffect.h" | ||
|
||
#include "util/math.h" | ||
|
||
namespace { | ||
constexpr double defaultAttackMs = 1; | ||
constexpr double defaultReleaseMs = 500; | ||
constexpr double defaultThresholdDB = -40; | ||
constexpr double defaultTargetDB = -5; | ||
constexpr double defaultGainDB = 20; | ||
constexpr double defaultKneeDB = 10; | ||
|
||
double calculateBallistics(double paramMs, const mixxx::EngineParameters& engineParameters) { | ||
return exp(-1000.0 / (paramMs * engineParameters.sampleRate())); | ||
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I were pedantic, this should also just take There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason why I didn't use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, it probably wouldn't improve readability, just make it less likely to accidentally assign unrelated quantities/units to each other. |
||
} | ||
} // anonymous namespace | ||
|
||
// static | ||
QString AutoGainControlEffect::getId() { | ||
return "org.mixxx.effects.autogaincontrol"; | ||
} | ||
|
||
// static | ||
EffectManifestPointer AutoGainControlEffect::getManifest() { | ||
auto pManifest = EffectManifestPointer::create(); | ||
pManifest->setId(getId()); | ||
pManifest->setName(QObject::tr("Auto Gain Control")); | ||
pManifest->setShortName(QObject::tr("AGC")); | ||
pManifest->setAuthor("The Mixxx Team"); | ||
Swiftb0y marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
pManifest->setVersion("1.0"); | ||
pManifest->setDescription( | ||
"Auto Gain Control (AGC) automatically adjusts the gain of an " | ||
"audio signal to maintain a consistent output level."); | ||
Swiftb0y marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
pManifest->setEffectRampsFromDry(true); | ||
pManifest->setMetaknobDefault(0.0); | ||
|
||
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 " | ||
"effect starts enhancing the input signal")); | ||
threshold->setValueScaler(EffectManifestParameter::ValueScaler::Linear); | ||
threshold->setUnitsHint(EffectManifestParameter::UnitsHint::Decibel); | ||
threshold->setNeutralPointOnScale(0); | ||
threshold->setRange(-70, defaultThresholdDB, 0); | ||
|
||
EffectManifestParameterPointer target = pManifest->addParameter(); | ||
target->setId("target"); | ||
target->setName(QObject::tr("Target (dBFS)")); | ||
target->setShortName(QObject::tr("Target")); | ||
target->setDescription( | ||
QObject::tr("The Target knob adjusts the desired target level of the output signal")); | ||
target->setValueScaler(EffectManifestParameter::ValueScaler::Linear); | ||
target->setUnitsHint(EffectManifestParameter::UnitsHint::Decibel); | ||
target->setNeutralPointOnScale(0); | ||
target->setRange(-20, defaultTargetDB, 10); | ||
|
||
EffectManifestParameterPointer gain = pManifest->addParameter(); | ||
gain->setId("gain"); | ||
gain->setName(QObject::tr("Gain (dB)")); | ||
gain->setShortName(QObject::tr("Gain")); | ||
gain->setDescription( | ||
QObject::tr("The Gain knob adjusts the maximum amount of gain that " | ||
"the effect will apply")); | ||
gain->setValueScaler(EffectManifestParameter::ValueScaler::Linear); | ||
gain->setUnitsHint(EffectManifestParameter::UnitsHint::Decibel); | ||
gain->setRange(1, defaultGainDB, 40); | ||
|
||
EffectManifestParameterPointer knee = pManifest->addParameter(); | ||
knee->setId("knee"); | ||
knee->setName(QObject::tr("Knee (dB)")); | ||
knee->setShortName(QObject::tr("Knee")); | ||
knee->setDescription(QObject::tr( | ||
"The Knee knob defines the range around the Threshold where gain " | ||
"changes are applied gradually,\nensuring smooth transitions and " | ||
"avoiding abrupt level shifts.")); | ||
knee->setValueScaler(EffectManifestParameter::ValueScaler::Linear); | ||
knee->setUnitsHint(EffectManifestParameter::UnitsHint::Coefficient); | ||
knee->setNeutralPointOnScale(0); | ||
knee->setRange(0.0, defaultKneeDB, 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 " | ||
"auto gain \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 auto gain will recover from the gain\n" | ||
"adjustment 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); | ||
|
||
return pManifest; | ||
} | ||
|
||
void AutoGainControlGroupState::clear(const mixxx::EngineParameters& engineParameters) { | ||
state = CSAMPLE_ONE; | ||
attackCoeff = calculateBallistics(defaultAttackMs, engineParameters); | ||
releaseCoeff = calculateBallistics(defaultReleaseMs, engineParameters); | ||
|
||
previousAttackParamMs = defaultAttackMs; | ||
previousReleaseParamMs = defaultReleaseMs; | ||
previousSampleRate = engineParameters.sampleRate(); | ||
} | ||
|
||
void AutoGainControlGroupState::calculateCoeffsIfChanged( | ||
const mixxx::EngineParameters& engineParameters, | ||
double attackParamMs, | ||
double releaseParamMs) { | ||
if (engineParameters.sampleRate() != previousSampleRate) { | ||
attackCoeff = calculateBallistics(attackParamMs, engineParameters); | ||
previousAttackParamMs = attackParamMs; | ||
|
||
releaseCoeff = calculateBallistics(releaseParamMs, engineParameters); | ||
previousReleaseParamMs = releaseParamMs; | ||
|
||
previousSampleRate = engineParameters.sampleRate(); | ||
} else { | ||
if (attackParamMs != previousAttackParamMs) { | ||
attackCoeff = calculateBallistics(attackParamMs, engineParameters); | ||
previousAttackParamMs = attackParamMs; | ||
} | ||
|
||
if (releaseParamMs != previousReleaseParamMs) { | ||
releaseCoeff = calculateBallistics(releaseParamMs, engineParameters); | ||
previousReleaseParamMs = releaseParamMs; | ||
} | ||
} | ||
} | ||
|
||
void AutoGainControlEffect::loadEngineEffectParameters( | ||
const QMap<QString, EngineEffectParameterPointer>& parameters) { | ||
m_pThreshold = parameters.value("threshold"); | ||
m_pTarget = parameters.value("target"); | ||
m_pGain = parameters.value("gain"); | ||
m_pKnee = parameters.value("knee"); | ||
m_pAttack = parameters.value("attack"); | ||
m_pRelease = parameters.value("release"); | ||
} | ||
|
||
void AutoGainControlEffect::processChannel( | ||
AutoGainControlGroupState* pState, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput, | ||
const mixxx::EngineParameters& engineParameters, | ||
const EffectEnableState enableState, | ||
const GroupFeatureState& groupFeatures) { | ||
Q_UNUSED(groupFeatures); | ||
|
||
if (enableState == EffectEnableState::Enabling) { | ||
pState->clear(engineParameters); | ||
} else { | ||
pState->calculateCoeffsIfChanged(engineParameters, m_pAttack->value(), m_pRelease->value()); | ||
} | ||
|
||
applyAutoGainControl(pState, engineParameters, pInput, pOutput); | ||
} | ||
|
||
void AutoGainControlEffect::applyAutoGainControl(AutoGainControlGroupState* pState, | ||
const mixxx::EngineParameters& engineParameters, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput) { | ||
// Get user-defined parameters | ||
double thresholdDB = m_pThreshold->value(); | ||
double targetLevelDB = m_pTarget->value(); | ||
double maxGainDB = m_pGain->value(); | ||
double kneeDB = m_pKnee->value(); | ||
|
||
// Define the upper and lower boundaries of the knee region | ||
double upperKneeDB = thresholdDB + 0.5 * kneeDB; | ||
double lowerKneeDB = thresholdDB - 0.5 * kneeDB; | ||
|
||
// Initialize the envelope state | ||
double state = pState->state; | ||
|
||
SINT numSamples = engineParameters.samplesPerBuffer(); | ||
int channelCount = engineParameters.channelCount(); | ||
for (SINT i = 0; i < numSamples; i += channelCount) { | ||
// Detect peak level across stereo channels | ||
CSAMPLE maxSample = std::max(fabs(pInput[i]), fabs(pInput[i + 1])); | ||
|
||
// If the input is silent, output silence | ||
if (maxSample == CSAMPLE_ZERO) { | ||
pOutput[i] = CSAMPLE_ZERO; | ||
pOutput[i + 1] = CSAMPLE_ZERO; | ||
JoergAtGithub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
continue; | ||
} | ||
|
||
// Smooth the level detector using attack/release envelope | ||
if (maxSample > state) { | ||
state = pState->attackCoeff * state + (1 - pState->attackCoeff) * maxSample; | ||
} else { | ||
state = pState->releaseCoeff * state + (1 - pState->releaseCoeff) * maxSample; | ||
} | ||
|
||
// Convert current signal level to decibels | ||
double inputLevelDB = ratio2db(state); | ||
|
||
// Determine the appropriate gain based on the input level | ||
double desiredGainDB; | ||
if (inputLevelDB > upperKneeDB) { | ||
// Above the knee range: apply full gain reduction | ||
desiredGainDB = targetLevelDB - inputLevelDB; | ||
} else if (inputLevelDB < lowerKneeDB) { | ||
// Below the knee range: no gain applied | ||
desiredGainDB = 0.0; | ||
} else { | ||
// Within the knee: interpolate gain smoothly | ||
double kneePosition = (inputLevelDB - lowerKneeDB) / kneeDB; | ||
desiredGainDB = (targetLevelDB - upperKneeDB) * kneePosition; | ||
} | ||
|
||
// Limit the gain to the maximum allowed value | ||
desiredGainDB = std::min(desiredGainDB, maxGainDB); | ||
|
||
// Convert gain from decibels to linear amplitude ratio | ||
CSAMPLE_GAIN gain = static_cast<CSAMPLE>(db2ratio(desiredGainDB)); | ||
|
||
pOutput[i] = pInput[i] * gain; | ||
pOutput[i + 1] = pInput[i + 1] * gain; | ||
} | ||
|
||
// Store the envelope state for the next buffer | ||
pState->state = state; | ||
} |
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 AutoGainControlGroupState : public EffectState { | ||
public: | ||
AutoGainControlGroupState(const mixxx::EngineParameters& engineParameters) | ||
: EffectState(engineParameters) { | ||
clear(engineParameters); | ||
} | ||
|
||
void clear(const mixxx::EngineParameters& engineParameters); | ||
|
||
void calculateCoeffsIfChanged( | ||
const mixxx::EngineParameters& engineParameters, | ||
double attackParamMs, | ||
double releaseParamMs); | ||
|
||
double state; | ||
double attackCoeff; | ||
double releaseCoeff; | ||
|
||
double previousAttackParamMs; | ||
double previousReleaseParamMs; | ||
mixxx::audio::SampleRate previousSampleRate; | ||
}; | ||
|
||
class AutoGainControlEffect : public EffectProcessorImpl<AutoGainControlGroupState> { | ||
public: | ||
AutoGainControlEffect() = default; | ||
|
||
static QString getId(); | ||
static EffectManifestPointer getManifest(); | ||
|
||
void loadEngineEffectParameters( | ||
const QMap<QString, EngineEffectParameterPointer>& parameters) override; | ||
|
||
void processChannel( | ||
AutoGainControlGroupState* pState, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput, | ||
const mixxx::EngineParameters& engineParameters, | ||
const EffectEnableState enableState, | ||
const GroupFeatureState& groupFeatures) override; | ||
|
||
private: | ||
QString debugString() const { | ||
return getId(); | ||
} | ||
|
||
EngineEffectParameterPointer m_pThreshold; | ||
EngineEffectParameterPointer m_pTarget; | ||
EngineEffectParameterPointer m_pGain; | ||
EngineEffectParameterPointer m_pKnee; | ||
EngineEffectParameterPointer m_pAttack; | ||
EngineEffectParameterPointer m_pRelease; | ||
|
||
DISALLOW_COPY_AND_ASSIGN(AutoGainControlEffect); | ||
|
||
void applyAutoGainControl(AutoGainControlGroupState* pState, | ||
const mixxx::EngineParameters& engineParameters, | ||
const CSAMPLE* pInput, | ||
CSAMPLE* pOutput); | ||
}; |
Uh oh!
There was an error while loading. Please reload this page.