diff --git a/SUPPORT.txt b/SUPPORT.txt index a45773d..4bda5d1 100644 --- a/SUPPORT.txt +++ b/SUPPORT.txt @@ -1,3 +1,3 @@ # https://geraintluff.github.io/SUPPORT.txt/ -2026-01-01 Geraint Luff +2026-06-01 Geraint Luff diff --git a/analyser.h b/analyser.h index 2c57335..877ea09 100644 --- a/analyser.h +++ b/analyser.h @@ -31,7 +31,7 @@ struct AnalyserSTFX : public BaseEffect { template 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.range("barkResolution", barkResolution) diff --git a/chorus.h b/chorus.h index 6cbf5d1..750a784 100644 --- a/chorus.h +++ b/chorus.h @@ -44,7 +44,7 @@ struct ChorusSTFX : public BaseEffect { template void state(Storage &storage) { - storage.info("[Basics] Chorus", ""); + storage.info("Chorus", ""); storage.version(0); stfx::units::rangePercent(storage.range("mix", mix) diff --git a/clap/source/basics.cpp b/clap/source/basics.cpp index 2883f8a..bd38146 100644 --- a/clap/source/basics.cpp +++ b/clap/source/basics.cpp @@ -6,6 +6,7 @@ #include "signalsmith-basics/analyser.h" #include "signalsmith-basics/chorus.h" #include "signalsmith-basics/crunch.h" +#include "signalsmith-basics/freq-shifter.h" #include "signalsmith-basics/limiter.h" #include "signalsmith-basics/reverb.h" @@ -25,7 +26,7 @@ bool clap_init(const char *path) { }, { CLAP_PLUGIN_FEATURE_ANALYZER, }); - + plugins.add({ .clap_version = CLAP_VERSION, .id = "uk.co.signalsmith.basics.chorus", @@ -54,6 +55,20 @@ bool clap_init(const char *path) { CLAP_PLUGIN_FEATURE_DISTORTION, }); + plugins.add({ + .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({ .clap_version = CLAP_VERSION, .id = "uk.co.signalsmith.basics.limiter", diff --git a/crunch.h b/crunch.h index 4d2217c..31ca24b 100644 --- a/crunch.h +++ b/crunch.h @@ -1,7 +1,6 @@ /* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff See LICENSE.txt and SUPPORT.txt */ -#ifndef SIGNALSMITH_BASICS_CRUNCH_H -#define SIGNALSMITH_BASICS_CRUNCH_H +#pragma once #include "dsp/rates.h" #include "dsp/filters.h" @@ -40,7 +39,7 @@ struct CrunchSTFX : public BaseEffect { template void state(Storage &storage) { - storage.info("[Basics] Crunch", "A simple distortion/saturation"); + storage.info("Crunch", "A simple distortion/saturation"); int version = storage.version(0); if (version != 0) return; @@ -178,5 +177,3 @@ private: }; }} // namespace - -#endif // include guard diff --git a/freq-shifter.h b/freq-shifter.h new file mode 100644 index 0000000..757b476 --- /dev/null +++ b/freq-shifter.h @@ -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 + +namespace signalsmith { namespace basics { + +template +class FreqShifterSTFX; + +using FreqShifterFloat = stfx::LibraryEffect; +using FreqShifterDouble = stfx::LibraryEffect; + +template +struct FreqShifterSTFX : public BaseEffect { + using typename BaseEffect::Sample; + using Complex = std::complex; + 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 + 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 + 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 + 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 hilbert; +}; + +}} // namespace diff --git a/include/signalsmith-basics/freq-shifter.h b/include/signalsmith-basics/freq-shifter.h new file mode 100644 index 0000000..dbf2206 --- /dev/null +++ b/include/signalsmith-basics/freq-shifter.h @@ -0,0 +1 @@ +#include "../../freq-shifter.h" diff --git a/limiter.h b/limiter.h index 6dba9c3..470a201 100644 --- a/limiter.h +++ b/limiter.h @@ -1,7 +1,6 @@ /* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff Released under the Boost Software License (see LICENSE.txt) */ -#ifndef SIGNALSMITH_BASICS_LIMITER_H -#define SIGNALSMITH_BASICS_LIMITER_H +#pragma once #include "dsp/delay.h" #include "dsp/envelopes.h" @@ -36,7 +35,7 @@ struct LimiterSTFX : public BaseEffect { template void state(Storage &storage) { - storage.info("[Basics] Limiter", "A simple lookahead limiter"); + storage.info("Limiter", "A simple lookahead limiter"); int version = storage.version(4); if (version != 4) return; @@ -202,5 +201,3 @@ private: }; }} // namespace - -#endif // include guard diff --git a/reverb.h b/reverb.h index 1d2eb54..f2ab057 100644 --- a/reverb.h +++ b/reverb.h @@ -1,7 +1,6 @@ /* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff Released under the Boost Software License (see LICENSE.txt) */ -#ifndef SIGNALSMITH_BASICS_REVERB_H -#define SIGNALSMITH_BASICS_REVERB_H +#pragma once #include "dsp/delay.h" #include "dsp/mix.h" @@ -43,7 +42,7 @@ struct ReverbSTFX : public BaseEffect { template void state(Storage &storage) { - storage.info("[Basics] Reverb", "An FDN reverb"); + storage.info("Reverb", "An FDN reverb"); int version = storage.version(5); if (version != 5) return; @@ -408,4 +407,3 @@ private: }; }} // namespace -#endif // include guard