1
0
basics/crunch.h
2025-07-06 12:53:41 +01:00

180 lines
5.5 KiB
C++

/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
See LICENSE.txt and SUPPORT.txt */
#pragma once
#include "dsp/rates.h"
#include "dsp/filters.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 4, 1)
#include "stfx/stfx-library.h"
#include <cmath>
namespace signalsmith { namespace basics {
template<class BaseEffect>
class CrunchSTFX;
using CrunchFloat = stfx::LibraryEffect<float, CrunchSTFX>;
using CrunchDouble = stfx::LibraryEffect<double, CrunchSTFX>;
template<class BaseEffect>
struct CrunchSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
static constexpr int oversampleHalfLatency = 16;
static constexpr Sample autoGainLevel = 0.1;
const bool autoGain;
ParamRange drive{stfx::units::dbToGain(autoGain ? 24 : 12)};
ParamRange fuzz{0};
ParamRange toneHz{2000};
ParamRange cutHz{50};
ParamRange outGain{1};
CrunchSTFX(bool autoGain=true) : autoGain(autoGain) {}
template<class Storage>
void state(Storage &storage) {
storage.info("Crunch", "A simple distortion/saturation");
int version = storage.version(0);
if (version != 0) return;
using stfx::units::dbToGain;
stfx::units::rangeGain(storage.range("drive", drive)
.info("drive", "pre-distortion input gain")
.range(dbToGain(-12), 4, dbToGain(40)));
stfx::units::rangePercent(storage.range("fuzz", fuzz)
.info("fuzz", "amplitude-independent distortion")
.range(0, 0.5, 1));
stfx::units::rangeHz(storage.range("toneHz", toneHz)
.info("tone", "limits the brightness of the distortion")
.range(100, 4000, 20000));
stfx::units::rangeHz(storage.range("cutHz", cutHz)
.info("cut", "prevents low frequencies from driving the distortion")
.range(20, 100, 500));
stfx::units::rangeGain(storage.range("outGain", outGain)
.info("out", "output gain")
.range(dbToGain(-12), 1, dbToGain(24)));
}
template<class Config>
void configureSTFX(Config &config) {
channels = config.outputChannels = config.inputChannels;
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
oversampler.resize(channels, config.maxBlockSize, oversampleHalfLatency, std::min(0.45, 21000/config.sampleRate));
gainshapers.resize(channels);
cutFilters.resize(channels);
toneFilters.resize(channels);
outputFilters.resize(channels);
}
void reset() {
oversampler.reset();
for (auto &g : gainshapers) g.reset();
for (auto &f : cutFilters) f.reset();
for (auto &f : toneFilters) f.reset();
for (auto &f : outputFilters) f.reset();
}
int latencySamples() const {
return oversampleHalfLatency*2;
}
template <class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) {
auto inputGain = block.smooth(drive);
double outputGainFrom = outGain.from();
double outputGainTo = outGain.to();
if (autoGain) {
Sample cutRatioFrom = 1 - cutHz.from()/(cutHz.from() + 200);
Sample averageGainFrom = gainshapers[0].averageGain(autoGainLevel*cutRatioFrom*drive.from());
outputGainFrom /= drive.from()*averageGainFrom;
Sample cutRatioTo = 1 - cutHz.to()/(cutHz.to() + 200);
Sample averageGainTo = gainshapers[0].averageGain(autoGainLevel*cutRatioTo*drive.to());
outputGainTo /= drive.to()*averageGainTo;
}
auto outputGain = block.smooth(outputGainFrom, outputGainTo);
for (int c = 0; c < channels; ++c) {
auto &cutFilter = cutFilters[c];
cutFilter.highpass(cutHz/(config.sampleRate*2));
auto &gainshaper = gainshapers[c];
gainshaper.setFuzzFactor(fuzz);
auto &toneFilter = toneFilters[c];
toneFilter.lowpass(toneHz/(config.sampleRate*2));
auto &outputFilter = outputFilters[c];
outputFilter.highpass((10 + 40*fuzz)/(config.sampleRate*2)); // more aggressive when fuzz is enabled, since it's very asymmetrical
oversampler.upChannel(c, io.input[c], block.length);
Sample *samples = oversampler[c];
for (int i = 0; i < block.length*2; ++i) {
double hi = i*0.5;
Sample x = samples[i]*inputGain.at(hi);
Sample gain = gainshaper(cutFilter(x))*outputGain.at(hi);
Sample y = x*toneFilter(gain);
samples[i] = outputFilter(y);
}
oversampler.downChannel(c, io.output[c], block.length);
}
}
private:
int channels = 0;
signalsmith::rates::Oversampler2xFIR<Sample> oversampler;
struct GainshapeADAA {
Sample prevX = 0, prevIntegral = 0;
Sample fuzzPositive = 1, fuzzNegative = 1;
void setFuzzFactor(Sample k) {
fuzzPositive = 1 + k - k*k;
fuzzNegative = 1 - k - k*k;
prevIntegral = integralGain(prevX);
}
Sample gain(Sample x) const {
Sample fuzzGain = (x >= 0 ? fuzzPositive : fuzzNegative);
return fuzzGain/std::sqrt(1 + x*x);
}
Sample integralGain(Sample x) const {
if (x >= 0) {
return fuzzPositive*std::log(std::sqrt(1 + x*x) + x);
} else { // more accurate if we flip it
return -fuzzNegative*std::log(std::sqrt(1 + x*x) - x);
}
}
Sample averageGain(Sample range) const {
// Average gain from 0-range, ignoring fuzz
return std::log(std::sqrt(1 + range*range) + range)/range;
}
static constexpr Sample minDiffX = 1e-4;
void reset() {
prevX = 0;
prevIntegral = integralGain(prevX);
}
Sample operator()(Sample x) {
Sample diffX = x - prevX;
Sample integral = integralGain(x);
Sample diffIntegral = integral - prevIntegral;
prevX = x;
prevIntegral = integral;
if (std::abs(diffX) < minDiffX) return gain(x);
return diffIntegral/diffX;
}
};
std::vector<GainshapeADAA> gainshapers;
using Filter = signalsmith::filters::BiquadStatic<Sample>;
std::vector<Filter> cutFilters, toneFilters, outputFilters;
};
}} // namespace