1
0

Compare commits

...

3 Commits

Author SHA1 Message Date
Geraint
995339d2a2 Add frequency shifter 2025-07-06 12:53:41 +01:00
Geraint
2908a8b56f Stereo energy compensation 2025-07-06 10:08:14 +01:00
Geraint
4782f1ff32 Chorus pans shared delay taps instead of per-channel offset 2025-07-06 09:15:00 +01:00
9 changed files with 200 additions and 40 deletions

View File

@ -1,3 +1,3 @@
# https://geraintluff.github.io/SUPPORT.txt/ # https://geraintluff.github.io/SUPPORT.txt/
2026-01-01 Geraint Luff <geraint@signalsmith-audio.co.uk> 2026-06-01 Geraint Luff <geraint@signalsmith-audio.co.uk>

View File

@ -31,7 +31,7 @@ struct AnalyserSTFX : public BaseEffect {
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
storage.info("[Basics] Analyser", "A Bark-scale spectrum analyser"); storage.info("Analyser", "A Bark-scale spectrum analyser");
storage.version(0); storage.version(0);
storage.range("barkResolution", barkResolution) storage.range("barkResolution", barkResolution)

View File

@ -40,10 +40,11 @@ struct ChorusSTFX : public BaseEffect {
ParamRange mix{0.6}; ParamRange mix{0.6};
ParamRange depthMs{15}; ParamRange depthMs{15};
ParamRange detune{5}; ParamRange detune{5};
ParamRange stereo{1};
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
storage.info("[Basics] Chorus", ""); storage.info("Chorus", "");
storage.version(0); storage.version(0);
stfx::units::rangePercent(storage.range("mix", mix) stfx::units::rangePercent(storage.range("mix", mix)
@ -59,6 +60,10 @@ struct ChorusSTFX : public BaseEffect {
.info("detune", "detuning depth") .info("detune", "detuning depth")
.range(1, 8, 50) .range(1, 8, 50)
.unit(" s.t.", 0); .unit(" s.t.", 0);
stfx::units::rangePercent(storage.range("stereo", stereo)
.info("stereo", "")
.range(0, 0.5, 1.5));
} }
template<class Config> template<class Config>
@ -74,12 +79,16 @@ struct ChorusSTFX : public BaseEffect {
void reset() { void reset() {
delay.reset(); delay.reset();
phase = 0; phase = 0;
stereoPhase = 0;
} }
template<class Io, class Config, class Block> template<class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) { void processSTFX(Io &io, Config &config, Block &block) {
Sample detuneHz = detune*0.45f/depthMs; // 0.45ms oscillation at 1Hz is about 1 semitone Sample detuneHz = detune*0.45f/depthMs; // 0.45ms oscillation at 1Hz is about 1 semitone
Sample phaseStep = detuneHz/config.sampleRate; Sample phaseStep = detuneHz/config.sampleRate;
Sample stereoPhaseStep = phaseStep/Sample(M_PI);
Complex stereoComplexPerOutput = std::polar(Sample(1), Sample(2*M_PI)/config.outputChannels);
Complex stereoComplexPerInternal = std::polar(Sample(1), Sample(1));
bool fading = depthMs.from() != depthMs.to(); bool fading = depthMs.from() != depthMs.to();
Sample depthSamples = depthMs.to()*0.001*config.sampleRate; Sample depthSamples = depthMs.to()*0.001*config.sampleRate;
@ -88,8 +97,16 @@ struct ChorusSTFX : public BaseEffect {
std::array<Sample, 6> multiIn, multiOut; std::array<Sample, 6> multiIn, multiOut;
std::array<Sample, 6> delaySamples; std::array<Sample, 6> delaySamples;
auto dry = block.smooth(mix, [](double m){return 1 - m*m;}); auto wetDryFn = [](double mix, double width, bool isWet){
auto wet = block.smooth(mix, [](double m){return m*(2 - m)/std::sqrt(Sample(6));}); // Equal-power wet/dry fade
double m = isWet ? std::sin(M_PI*mix/2) : std::cos(M_PI*mix/2);
// Compensate for extra energy from stereo
return m/std::sqrt(6*(1 + width*width/2));
};
auto dry = block.smooth(wetDryFn(mix.from(), stereo.from(), false), wetDryFn(mix.to(), stereo.to(), false));
auto wet = block.smooth(wetDryFn(mix.from(), stereo.from(), true), wetDryFn(mix.to(), stereo.to(), true));
bool notMono = (config.outputChannels > 1);
auto width = block.smooth(stereo);
for (size_t i = 0; i < block.length; ++i) { for (size_t i = 0; i < block.length; ++i) {
for (size_t c = 0; c < 6; ++c) { for (size_t c = 0; c < 6; ++c) {
@ -98,34 +115,50 @@ struct ChorusSTFX : public BaseEffect {
} }
delay.write(multiIn); delay.write(multiIn);
for (size_t oc = 0; oc < config.outputChannels; ++oc) { Complex phaseComplex = std::polar(Sample(1), phase*Sample(2*M_PI));
Complex phaseComplex = std::polar(Sample(1), phase*Sample(2*M_PI) + oc*Sample(2.632)); for (size_t c = 0; c < 6; ++c) {
Sample osc = (phaseComplex*oscillatorOffsets[c]).real();
delaySamples[c] = depthSamples*Sample(0.5)*(1 + osc);
}
delay.readMulti(delaySamples, multiOut);
if (fading) {
// read a second set of delay times, and fade between them
std::array<Sample, 6> multiOutFrom;
for (size_t c = 0; c < 6; ++c) { for (size_t c = 0; c < 6; ++c) {
Sample osc = (phaseComplex*oscillatorOffsets[c]).real(); Sample osc = (phaseComplex*oscillatorOffsets[c]).real();
delaySamples[c] = depthSamples*Sample(0.5)*(1 + osc); delaySamples[c] = depthSamplesFrom*Sample(0.5)*(1 + osc);
} }
delay.readMulti(delaySamples, multiOut); delay.readMulti(delaySamples, multiOutFrom);
for (size_t c = 0; c < 6; ++c) {
if (fading) { multiOut[c] = block.fade(i, multiOutFrom[c], multiOut[c]);
// read a second set of delay times, and fade between them
std::array<Sample, 6> multiOutFrom;
for (size_t c = 0; c < 6; ++c) {
Sample osc = (phaseComplex*oscillatorOffsets[c]).real();
delaySamples[c] = depthSamplesFrom*Sample(0.5)*(1 + osc);
}
delay.readMulti(delaySamples, multiOutFrom);
for (size_t c = 0; c < 6; ++c) {
multiOut[c] = block.fade(i, multiOutFrom[c], multiOut[c]);
}
} }
// Arbitrarily chosen gains, 4 positive, 2 negative
Sample sum = multiOut[0] - multiOut[1] + multiOut[2] + multiOut[3] - multiOut[4] + multiOut[5];
io.output[oc][i] = multiIn[oc]*dry.at(i) + sum*wet.at(i);
} }
// Arbitrarily chosen gains, 4 positive, 2 negative
Sample sum = multiOut[0] - multiOut[1] + multiOut[2] + multiOut[3] - multiOut[4] + multiOut[5];
if (notMono) {
Complex stereoComplex = std::polar(Sample(width.at(i)), stereoPhase*Sample(2*M_PI));
for (size_t oc = 0; oc < config.outputChannels; ++oc) {
Sample sumC = sum;
Complex rot = stereoComplex;
for (size_t i = 0; i < 6; ++i) {
sumC += rot.real()*multiOut[i];
rot *= stereoComplexPerInternal;
}
io.output[oc][i] = multiIn[oc]*dry.at(i) + sumC*wet.at(i);
stereoComplex *= stereoComplexPerOutput;
}
stereoPhase += stereoPhaseStep;
} else {
io.output[0][i] = multiIn[0]*dry.at(i) + sum*wet.at(i);
}
phase += phaseStep; phase += phaseStep;
} }
phase -= std::floor(phase); phase -= std::floor(phase);
stereoPhase -= std::floor(stereoPhase);
} }
template<class Storage> template<class Storage>
@ -136,6 +169,7 @@ private:
signalsmith::delay::MultiDelay<Sample, signalsmith::delay::InterpolatorLagrange7> delay; signalsmith::delay::MultiDelay<Sample, signalsmith::delay::InterpolatorLagrange7> delay;
Sample phase = 0; Sample phase = 0;
Sample stereoPhase = 0;
}; };
}} // namespace }} // namespace

View File

@ -6,6 +6,7 @@
#include "signalsmith-basics/analyser.h" #include "signalsmith-basics/analyser.h"
#include "signalsmith-basics/chorus.h" #include "signalsmith-basics/chorus.h"
#include "signalsmith-basics/crunch.h" #include "signalsmith-basics/crunch.h"
#include "signalsmith-basics/freq-shifter.h"
#include "signalsmith-basics/limiter.h" #include "signalsmith-basics/limiter.h"
#include "signalsmith-basics/reverb.h" #include "signalsmith-basics/reverb.h"
@ -25,7 +26,7 @@ bool clap_init(const char *path) {
}, { }, {
CLAP_PLUGIN_FEATURE_ANALYZER, CLAP_PLUGIN_FEATURE_ANALYZER,
}); });
plugins.add<signalsmith::basics::ChorusSTFX>({ plugins.add<signalsmith::basics::ChorusSTFX>({
.clap_version = CLAP_VERSION, .clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.chorus", .id = "uk.co.signalsmith.basics.chorus",
@ -54,6 +55,20 @@ bool clap_init(const char *path) {
CLAP_PLUGIN_FEATURE_DISTORTION, CLAP_PLUGIN_FEATURE_DISTORTION,
}); });
plugins.add<signalsmith::basics::FreqShifterSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.freq-shifter",
.name = "[Basics] Frequency Shifter",
.vendor = "Signalsmith Audio",
.url = "",
.manual_url = "",
.support_url = "",
.version = "1.0.0"
}, {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_FREQUENCY_SHIFTER,
});
plugins.add<signalsmith::basics::LimiterSTFX>({ plugins.add<signalsmith::basics::LimiterSTFX>({
.clap_version = CLAP_VERSION, .clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.limiter", .id = "uk.co.signalsmith.basics.limiter",

View File

@ -1,7 +1,6 @@
/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff /* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
See LICENSE.txt and SUPPORT.txt */ See LICENSE.txt and SUPPORT.txt */
#ifndef SIGNALSMITH_BASICS_CRUNCH_H #pragma once
#define SIGNALSMITH_BASICS_CRUNCH_H
#include "dsp/rates.h" #include "dsp/rates.h"
#include "dsp/filters.h" #include "dsp/filters.h"
@ -40,7 +39,7 @@ struct CrunchSTFX : public BaseEffect {
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
storage.info("[Basics] Crunch", "A simple distortion/saturation"); storage.info("Crunch", "A simple distortion/saturation");
int version = storage.version(0); int version = storage.version(0);
if (version != 0) return; if (version != 0) return;
@ -178,5 +177,3 @@ private:
}; };
}} // namespace }} // namespace
#endif // include guard

118
freq-shifter.h Normal file
View File

@ -0,0 +1,118 @@
/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
See LICENSE.txt and SUPPORT.txt */
#pragma once
#include "modules/hilbert-iir/hilbert.h"
#include "stfx/stfx-library.h"
#include <cmath>
namespace signalsmith { namespace basics {
template<class BaseEffect>
class FreqShifterSTFX;
using FreqShifterFloat = stfx::LibraryEffect<float, FreqShifterSTFX>;
using FreqShifterDouble = stfx::LibraryEffect<double, FreqShifterSTFX>;
template<class BaseEffect>
struct FreqShifterSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using Complex = std::complex<Sample>;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
static double unitToShiftHz(double x) {
return 100*x/(1.1 - x*x);
}
static double shiftHzToUnit(double y) {
return (std::sqrt(2500 + 1.1*y*y) - 50)/y;
}
ParamRange mix{1};
ParamRange shift{shiftHzToUnit(50)};
ParamStepped reflect{0};
template<class Storage>
void state(Storage &storage) {
storage.info("Frequency Shifter", "A Hilbert / Bode single-sideband modulator");
int version = storage.version(0);
if (version != 0) return;
stfx::units::rangePercent(storage.range("mix", mix)
.info("mix", "wet/dry")
.range(0, 0.5, 1));
storage.range("shift", shift)
.info("shift", "pre-distortion input gain")
.range(-1, 0, 1)
.unit("Hz", 1, shiftHzToUnit, unitToShiftHz, shiftHzToUnit(-9.99), shiftHzToUnit(9.99))
.unit("Hz", 0, shiftHzToUnit, unitToShiftHz);
storage.stepped("reflect", reflect)
.info("reflect", "0Hz reflection mode")
.range(0, 3)
.label(0, "no reflect/duplicate", "reflect below 0Hz", "duplicate above 0Hz", "always reflect/duplicate");
}
template<class Config>
void configureSTFX(Config &config) {
auto channels = config.outputChannels = config.inputChannels;
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
hilbert = {Sample(config.sampleRate), channels};
}
void reset() {
shiftPhaseBefore = shiftPhaseAfter = 0;
shiftBefore = shiftAfter = 1;
hilbert.reset();
}
template <class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) {
auto dry = block.smooth(mix, [](double m){return 1 - m*m;});
auto wet = block.smooth(mix, [](double m){return m*(2 - m);});
auto phaseStep = block.smooth(shift, [&](double u){
auto hz = unitToShiftHz(u);
return hz/config.sampleRate;
});
int mode = reflect;
bool noReflectDown = (mode == 0 || mode == 2);
bool duplicateUp = (mode == 2 || mode == 3);
for (size_t i = 0; i < block.length; ++i) {
auto gainDry = dry.at(i), gainWet = wet.at(i);
for (size_t c = 0; c < config.inputChannels; ++c) {
Sample x = io.input[c][i];
// In general, this may alias at the high end when shifting up
// but our Hilbert has a 20kHz lowpass, so that's enough
// room for our maximum +1000Hz shift
Complex y = shiftAfter*hilbert(x*shiftBefore, c);
io.output[c][i] = gainDry*x + gainWet*y.real();
}
auto ps = phaseStep.at(i);
bool shiftInput = (ps < 0) ? noReflectDown : duplicateUp;
if (shiftInput) {
shiftPhaseBefore += ps;
shiftBefore = std::polar(Sample(1), shiftPhaseBefore*Sample(2*M_PI));
} else {
shiftPhaseAfter += ps;
shiftAfter = std::polar(Sample(1), shiftPhaseAfter*Sample(2*M_PI));
}
}
shiftPhaseBefore -= std::floor(shiftPhaseBefore);
shiftPhaseAfter -= std::floor(shiftPhaseAfter);
}
private:
Sample shiftPhaseBefore = 0, shiftPhaseAfter = 0;
Complex shiftBefore = 1, shiftAfter = 1;
signalsmith::hilbert::HilbertIIR<Sample> hilbert;
};
}} // namespace

View File

@ -0,0 +1 @@
#include "../../freq-shifter.h"

View File

@ -1,7 +1,6 @@
/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff /* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
Released under the Boost Software License (see LICENSE.txt) */ Released under the Boost Software License (see LICENSE.txt) */
#ifndef SIGNALSMITH_BASICS_LIMITER_H #pragma once
#define SIGNALSMITH_BASICS_LIMITER_H
#include "dsp/delay.h" #include "dsp/delay.h"
#include "dsp/envelopes.h" #include "dsp/envelopes.h"
@ -36,7 +35,7 @@ struct LimiterSTFX : public BaseEffect {
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
storage.info("[Basics] Limiter", "A simple lookahead limiter"); storage.info("Limiter", "A simple lookahead limiter");
int version = storage.version(4); int version = storage.version(4);
if (version != 4) return; if (version != 4) return;
@ -202,5 +201,3 @@ private:
}; };
}} // namespace }} // namespace
#endif // include guard

View File

@ -1,7 +1,6 @@
/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff /* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
Released under the Boost Software License (see LICENSE.txt) */ Released under the Boost Software License (see LICENSE.txt) */
#ifndef SIGNALSMITH_BASICS_REVERB_H #pragma once
#define SIGNALSMITH_BASICS_REVERB_H
#include "dsp/delay.h" #include "dsp/delay.h"
#include "dsp/mix.h" #include "dsp/mix.h"
@ -43,7 +42,7 @@ struct ReverbSTFX : public BaseEffect {
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
storage.info("[Basics] Reverb", "An FDN reverb"); storage.info("Reverb", "An FDN reverb");
int version = storage.version(5); int version = storage.version(5);
if (version != 5) return; if (version != 5) return;
@ -408,4 +407,3 @@ private:
}; };
}} // namespace }} // namespace
#endif // include guard