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

Distortion effect #10932

Merged
merged 7 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -603,6 +603,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/distortioneffect.cpp
src/effects/backends/builtin/parametriceqeffect.cpp
src/effects/backends/builtin/phasereffect.cpp
src/effects/backends/builtin/pitchshifteffect.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 @@ -19,6 +19,7 @@
#include "effects/backends/builtin/reverbeffect.h"
#endif
#include "effects/backends/builtin/autopaneffect.h"
#include "effects/backends/builtin/distortioneffect.h"
#include "effects/backends/builtin/echoeffect.h"
#include "effects/backends/builtin/loudnesscontoureffect.h"
#include "effects/backends/builtin/metronomeeffect.h"
Expand Down Expand Up @@ -56,6 +57,7 @@ BuiltInBackend::BuiltInBackend() {
registerEffect<MetronomeEffect>();
registerEffect<TremoloEffect>();
registerEffect<PitchShiftEffect>();
registerEffect<DistortionEffect>();
}

std::unique_ptr<EffectProcessor> BuiltInBackend::createProcessor(
Expand Down
125 changes: 125 additions & 0 deletions src/effects/backends/builtin/distortioneffect.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#include "effects/backends/builtin/distortioneffect.h"

namespace {
inline CSAMPLE tanh_approx(CSAMPLE input) {
return input / (1 + input * input / (3 + input * input / 5));
}
} // namespace

// static
QString DistortionEffect::getId() {
return "org.mixxx.effects.distortion";
}

//static
EffectManifestPointer DistortionEffect::getManifest() {
EffectManifestPointer pManifest(new EffectManifest);
pManifest->setId(getId());
pManifest->setName(QObject::tr("Distortion"));
pManifest->setShortName(QObject::tr("Distortion"));
pManifest->setAuthor("The Mixxx Team");
pManifest->setVersion("1.0");
pManifest->setDescription(
"A Distortion effect with several modes ranging from soft to hard "
"clipping.");
pManifest->setEffectRampsFromDry(true);
pManifest->setMetaknobDefault(0.0);

EffectManifestParameterPointer mode = pManifest->addParameter();
mode->setId("mode");
mode->setName(QObject::tr("Hard Clip"));
mode->setShortName(QObject::tr("Hard"));
mode->setDescription(QObject::tr(
"Switches between soft saturation and hard clipping."));
mode->setValueScaler(EffectManifestParameter::ValueScaler::Toggle);
mode->setRange(0, 0, 2);
mode->appendStep(qMakePair(QObject::tr("Soft Clipping"), Mode::SoftClipping));
mode->appendStep(qMakePair(QObject::tr("Hard Clipping"), Mode::HardClipping));

EffectManifestParameterPointer drive = pManifest->addParameter();
drive->setId("drive");
drive->setName(QObject::tr("Drive"));
drive->setShortName(QObject::tr("Drive"));
drive->setDescription(QObject::tr(
"The amount of amplification "
"applied to the audio signal. At higher levels the audio will be more distored."));
drive->setValueScaler(EffectManifestParameter::ValueScaler::Linear);
drive->setDefaultLinkType(EffectManifestParameter::LinkType::Linked);
drive->setNeutralPointOnScale(0);
drive->setRange(0, 0, 1);

return pManifest;
}

DistortionGroupState::DistortionGroupState(
const mixxx::EngineParameters& engineParameters)
: EffectState(engineParameters),
m_driveGain(1),
m_crossfadeParameter(0),
m_samplerate(engineParameters.sampleRate()),
m_previousMakeUpGain(1),
m_previousNormalizationGain(1) {
}
struct DistortionEffect::SoftClippingParameters {
static constexpr const CSAMPLE normalizationLevel = 0.2f;
static constexpr const CSAMPLE crossfadeEndParam = 0.2f;
static constexpr const CSAMPLE_GAIN maxDriveGain = 25.f;

static CSAMPLE process(CSAMPLE sample) {
return tanh_approx(sample);
}
};

struct DistortionEffect::HardClippingParameters {
static constexpr const CSAMPLE normalizationLevel = 0.6f;
static constexpr const CSAMPLE crossfadeEndParam = 0.5f;
static constexpr const CSAMPLE_GAIN maxDriveGain = 30.f;

static CSAMPLE process(CSAMPLE sample) {
return CSAMPLE_clamp(sample);
}
};

void DistortionEffect::loadEngineEffectParameters(
const QMap<QString, EngineEffectParameterPointer>& parameters) {
m_pMode = parameters.value("mode");
m_pDrive = parameters.value("drive");
}

void DistortionEffect::processChannel(
DistortionGroupState* pState,
const CSAMPLE* pInput,
CSAMPLE* pOutput,
const mixxx::EngineParameters& engineParameters,
const EffectEnableState enableState,
const GroupFeatureState& groupFeatures) {
Q_UNUSED(groupFeatures);
Q_UNUSED(enableState);

// TODO: anti-aliasing

SINT numSamples = engineParameters.samplesPerBuffer();
CSAMPLE driveParam = static_cast<CSAMPLE>(m_pDrive->value());

if (driveParam < 0.01) {
SampleUtil::copy(pOutput, pInput, numSamples);
return;
}

switch (m_pMode->toInt()) {
case SoftClipping:
processDistortion<SoftClippingParameters>(
driveParam, pState, pOutput, pInput, engineParameters);
break;

case HardClipping:
processDistortion<HardClippingParameters>(
driveParam, pState, pOutput, pInput, engineParameters);
break;

default:
// We should never enter here, but we act as a noop effect just in case.
SampleUtil::copy(pOutput, pInput, numSamples);
return;
}
}
118 changes: 118 additions & 0 deletions src/effects/backends/builtin/distortioneffect.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#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 DistortionGroupState : public EffectState {
public:
DistortionGroupState(const mixxx::EngineParameters& engineParameters);

CSAMPLE_GAIN m_driveGain;
CSAMPLE m_crossfadeParameter;
double m_samplerate;

CSAMPLE m_previousMakeUpGain;
CSAMPLE m_previousNormalizationGain;
};

class DistortionEffect : public EffectProcessorImpl<DistortionGroupState> {
public:
DistortionEffect() = default;

static QString getId();
static EffectManifestPointer getManifest();

void loadEngineEffectParameters(
const QMap<QString, EngineEffectParameterPointer>& parameters) override;

void processChannel(
DistortionGroupState* pState,
const CSAMPLE* pInput,
CSAMPLE* pOutput,
const mixxx::EngineParameters& engineParameters,
const EffectEnableState enableState,
const GroupFeatureState& groupFeatures) override;

private:
enum Mode {
SoftClipping = 0,
HardClipping = 1,
};

struct SoftClippingParameters;
struct HardClippingParameters;

template<typename ModeParams>
void processDistortion(CSAMPLE driveParam,
DistortionGroupState* pState,
CSAMPLE* pOutput,
const CSAMPLE* pInput,
const mixxx::EngineParameters& engineParameters) {
SINT numSamples = engineParameters.samplesPerBuffer();

// Normalize input
pState->m_previousNormalizationGain =
SampleUtil::copyWithRampingNormalization(pOutput,
pInput,
pState->m_previousNormalizationGain,
ModeParams::normalizationLevel,
numSamples);

// Apply drive gain
CSAMPLE_GAIN driveGain = 1 + driveParam * ModeParams::maxDriveGain;
SampleUtil::copyWithRampingGain(
pOutput, pInput, pState->m_driveGain, driveGain, numSamples);

// Waveshape
for (SINT i = 0;
i < engineParameters.samplesPerBuffer();
i += engineParameters.channelCount()) {
for (int channel = 0; channel < engineParameters.channelCount(); ++channel) {
pOutput[i + channel] = ModeParams::process(pOutput[i + channel]);
}
}

// Volume compensation
CSAMPLE pInputRMS = SampleUtil::rms(pInput, numSamples);
CSAMPLE pOutputRMS = SampleUtil::rms(pOutput, numSamples);
CSAMPLE_GAIN gain = pOutputRMS == CSAMPLE_ZERO
? 1
: pInputRMS / pOutputRMS;

SampleUtil::applyRampingGain(pOutput,
pState->m_previousMakeUpGain,
gain,
engineParameters.samplesPerBuffer());

pState->m_previousMakeUpGain = gain;

// Crossfade
CSAMPLE crossfadeParam = math_min(driveParam / ModeParams::crossfadeEndParam, 1.f);
SampleUtil::applyRampingGain(pOutput,
pState->m_crossfadeParameter,
crossfadeParam,
numSamples);
SampleUtil::addWithRampingGain(pOutput,
pInput,
1 - pState->m_crossfadeParameter,
1 - crossfadeParam,
numSamples);

pState->m_driveGain = driveGain;
pState->m_crossfadeParameter = crossfadeParam;
}

QString debugString() const {
return getId();
}

EngineEffectParameterPointer m_pMode;
EngineEffectParameterPointer m_pDrive;

DISALLOW_COPY_AND_ASSIGN(DistortionEffect);
};
58 changes: 58 additions & 0 deletions src/util/sample.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <cstddef>
#include <cstdlib>

#include "engine/engine.h"
#include "util/math.h"

#ifdef __WINDOWS__
Expand Down Expand Up @@ -156,6 +157,23 @@ void SampleUtil::applyRampingGain(CSAMPLE* pBuffer, CSAMPLE_GAIN old_gain,
}
}

CSAMPLE SampleUtil::copyWithRampingNormalization(CSAMPLE* pDest,
const CSAMPLE* pSrc,
CSAMPLE_GAIN old_gain,
CSAMPLE_GAIN targetAmplitude,
SINT numSamples) {
SINT numMonoSamples = numSamples / mixxx::kEngineChannelCount.value();
mixMultichannelToMono(pDest, pSrc, numSamples);

CSAMPLE maxAmplitude = maxAbsAmplitude(pDest, numMonoSamples);
CSAMPLE_GAIN gain = maxAmplitude == CSAMPLE_ZERO
? 1
: targetAmplitude / maxAmplitude;
copyWithRampingGain(pDest, pSrc, old_gain, gain, numSamples);

return gain;
}

// static
void SampleUtil::applyAlternatingGain(CSAMPLE* pBuffer, CSAMPLE gain1,
CSAMPLE gain2, SINT numSamples) {
Expand Down Expand Up @@ -426,6 +444,33 @@ SampleUtil::CLIP_STATUS SampleUtil::sumAbsPerChannel(CSAMPLE* pfAbsL,
return clipping;
}

// static
CSAMPLE SampleUtil::sumSquared(const CSAMPLE* pBuffer, SINT numSamples) {
CSAMPLE sumSq = CSAMPLE_ZERO;

for (SINT i = 0; i < numSamples; ++i) {
sumSq += pBuffer[i] * pBuffer[i];
}

return sumSq;
}

// static
CSAMPLE SampleUtil::rms(const CSAMPLE* pBuffer, SINT numSamples) {
return sqrtf(sumSquared(pBuffer, numSamples) / numSamples);
}

CSAMPLE SampleUtil::maxAbsAmplitude(const CSAMPLE* pBuffer, SINT numSamples) {
CSAMPLE max = pBuffer[0];
for (SINT i = 1; i < numSamples; ++i) {
CSAMPLE absValue = abs(pBuffer[i]);
if (absValue > max) {
max = absValue;
}
}
return max;
}

// static
void SampleUtil::copyClampBuffer(CSAMPLE* M_RESTRICT pDest,
const CSAMPLE* M_RESTRICT pSrc, SINT iNumSamples) {
Expand Down Expand Up @@ -525,6 +570,19 @@ void SampleUtil::mixStereoToMono(CSAMPLE* pBuffer, SINT numSamples) {
}
}

// static
void SampleUtil::mixMultichannelToMono(CSAMPLE* pDest, const CSAMPLE* pSrc, SINT numSamples) {
auto chCount = mixxx::kEngineChannelCount.value();
const CSAMPLE_GAIN mixScale = CSAMPLE_GAIN_ONE / (CSAMPLE_GAIN_ONE * chCount);
for (SINT i = 0; i < numSamples / chCount; ++i) {
pDest[i] = CSAMPLE_ZERO;
for (auto ch = 0; ch < chCount; ++ch) {
pDest[i] += pSrc[i * chCount + ch];
}
pDest[i] *= mixScale;
}
}

// static
void SampleUtil::doubleMonoToDualMono(CSAMPLE* pBuffer, SINT numFrames) {
// backward loop
Expand Down
Loading