From a629a651941823b42950e198b82e533b231175de Mon Sep 17 00:00:00 2001 From: yaw-man Date: Wed, 14 Sep 2022 19:04:19 -0400 Subject: [PATCH] Pitch corrector --- src/yaw-totune/CMakeLists.txt | 7 + src/yaw-totune/DistrhoPluginInfo.h | 25 ++++ src/yaw-totune/Resampler.hpp | 216 +++++++++++++++++++++++++++++ src/yaw-totune/dsp.cpp | 74 ++++++++++ 4 files changed, 322 insertions(+) create mode 100644 src/yaw-totune/CMakeLists.txt create mode 100644 src/yaw-totune/DistrhoPluginInfo.h create mode 100644 src/yaw-totune/Resampler.hpp create mode 100644 src/yaw-totune/dsp.cpp diff --git a/src/yaw-totune/CMakeLists.txt b/src/yaw-totune/CMakeLists.txt new file mode 100644 index 0000000..e29e0d1 --- /dev/null +++ b/src/yaw-totune/CMakeLists.txt @@ -0,0 +1,7 @@ +dpf_add_plugin(yaw-totune + TARGETS vst2 + FILES_DSP + dsp.cpp) + +target_include_directories(yaw-totune PUBLIC + ".") \ No newline at end of file diff --git a/src/yaw-totune/DistrhoPluginInfo.h b/src/yaw-totune/DistrhoPluginInfo.h new file mode 100644 index 0000000..7cfea06 --- /dev/null +++ b/src/yaw-totune/DistrhoPluginInfo.h @@ -0,0 +1,25 @@ +#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED +#define DISTRHO_PLUGIN_INFO_H_INCLUDED + +#define DISTRHO_PLUGIN_BRAND "yaw-audio" +#define DISTRHO_PLUGIN_NAME "yaw-totune" +#define DISTRHO_PLUGIN_URI "https://yaw.man/plugins/yaw-totune" + +// #define DISTRHO_PLUGIN_HAS_UI 1 +#define DISTRHO_PLUGIN_IS_RT_SAFE 1 +#define DISTRHO_PLUGIN_NUM_INPUTS 2 +#define DISTRHO_PLUGIN_NUM_OUTPUTS 2 +// #define DISTRHO_UI_USE_NANOVG 1 + +/*enum Parameters { + ktpax = 0, + ktpay, + ktpaz, + ktpap, + kParameterButtonA, + kParameterButtonB, + kParameterTime, + kParameterCount +};*/ + +#endif // DISTRHO_PLUGIN_INFO_H_INCLUDED diff --git a/src/yaw-totune/Resampler.hpp b/src/yaw-totune/Resampler.hpp new file mode 100644 index 0000000..768c3ec --- /dev/null +++ b/src/yaw-totune/Resampler.hpp @@ -0,0 +1,216 @@ +#include +#include + +static constexpr double BIG_DOUBLE = 10000.0; + +class Scale +{ + double sampleRate = 48000.0; + + // freqs in hz, periods in samples + std::vector frequencies; + std::vector periods; + +public: + void newSampleRate(double rate) + { + double ratio = rate / sampleRate; + sampleRate = rate; + for (double ¬e : periods) + note *= ratio; + }; + + // Default ctor: 12TET @ 48kHz + Scale(double hz = 440.0) + { + hz /= 32.0; + + for (int i = 0; i < 12 * 8; ++i) + { + frequencies.push_back(hz); + periods.push_back(sampleRate / hz); + hz *= exp2(1.0 / 12.0); + } + } + + double getNearestPeriod(double period) + { + for (auto note : periods) + { + if (period > note) + return note; + } + + // This should NOT happen. + return 0; + } + + // TODO: parse scala files. new ctor that parses arbitrary scales +}; + +// x that minimizes the quadratic function determined by the three points +static double secondOrderMinimum(float x0, float y0, float x1, float y1, float x2, float y2) +{ + double x = 0; + + // Linear system: + // 2nd order Vandermonde matrix in x * [a b c]^T = [y0 y1 y2]^T + // solve the 2nd order vmonde matrix, then return -b / 2a, discriminant of qdrtic polynomial. + + return x; +}; + + +static constexpr uint pSize = 10; +static constexpr size_t resamplerBufferSize = 1 << pSize; +static constexpr size_t resamplerMask = resamplerBufferSize - 1; + +static constexpr uint dLogFactor = 3; +static constexpr uint dFactor = 1 << dLogFactor; +static constexpr size_t dBufferSize = 1 << (pSize - dLogFactor); +static constexpr size_t dBufferMask = dBufferSize - 1; + +template +class Resampler +{ +private: + + // Ring buffer. + //TODO: refactor into its own class and look up operator[] semantics. + std::array array = {}; + + // Pointer for pitch correction. + double readIdx = 0; + // Pointer for pitch detection. + uint writeIdx = 0; + + + // Calculate autocorrelation from scratch for given period + inline double ac(const uint per) + { + double ac = 0; + uint idx = writeIdx; + for (uint l = 0; l < per; ++l) + { + T x = array[idx & resamplerMask]; + T y = array[(idx - per) & resamplerMask]; + + ac += x * x + x * y; + --idx; + } + return ac; + } + +public: + + // Detect accurate period from scratch near a target period. + inline double detectPeriodNear(const uint nearPeriod) + { + double minAC = BIG_DOUBLE; + double period = nearPeriod; + for (uint per = nearPeriod - dFactor; per < nearPeriod + dFactor; ++per) + { + double curAC = ac(per); + if (curAC < minAC) + { + minAC = curAC; + period = per; + } + } + return period; + } + + inline void write(const T *const input, const uint32_t frames) + { + for (uint32_t i = 0; i < frames; + ++i, writeIdx = (writeIdx + 1) & resamplerMask) + { + array[writeIdx] = input[i]; + }; + } + + // Arguments: + // output buffer and length + // ratio of new sample rate to old sample rate + // length of period in number of samples (may be a fraction) + inline void resample(T *output, uint32_t frames, const double rate, const double period) + { + + for (; frames; --frames) + { + // TODO: add interpolation filter. + *output = array[static_cast(readIdx)]; + ++output; + + if ((readIdx < writeIdx) && + (readIdx > writeIdx - rate)) + readIdx -= period; + readIdx += rate; + + if (readIdx > resamplerBufferSize) + readIdx -= resamplerBufferSize; + if (readIdx < 0) + readIdx = 0; + }; + } +}; + +template +class Detector +{ + + std::array array = {}; + // Pointer for pitch detection. + uint writeIdx = 0; + // Detectable periods. + std::array squares = {}; + std::array crosses = {}; + static constexpr uint minPeriod = 16; + static constexpr uint maxPeriod = dBufferSize / 2; + +public: + // Read inputFrames from input buffer, downsample by factor and write to our ring buffer + // TODO: implement downsampling filter. This will probably have way too much antialiasing! + inline void downsample(const T* const input, const uint32_t inputFrames) + { + for (uint32_t i = 0; i < inputFrames; + i += dFactor, writeIdx = (writeIdx + 1) & dBufferMask) + { + array[writeIdx] = input[i]; + }; + } + + // Incrementally detect all possible pitches. Return coarse pitch match. + inline uint detectPitch(uint32_t frames) + { + uint idx = writeIdx - frames; + while (frames) + { + + // TODO: start at 1 rather than 0 + for (int per = 0; per < squares.size(); ++per) + { + T x = array[idx & dBufferMask]; + T y = array[(idx - per) & dBufferMask]; + T z = array[(idx - 2 * per) & dBufferMask]; + squares[per] += x * x - z * z; + crosses[per] += x * y - y * z; + } + --frames; + ++idx; + } + + double least = BIG_DOUBLE; + uint per = 0; + for (uint i = 0; i < squares.size(); ++i) + { + double uac = squares[i] - 2 * crosses[i]; + if (uac < least) + { + least = uac; + per = i; + } + } + return per * dFactor; + } +}; \ No newline at end of file diff --git a/src/yaw-totune/dsp.cpp b/src/yaw-totune/dsp.cpp new file mode 100644 index 0000000..c2aa317 --- /dev/null +++ b/src/yaw-totune/dsp.cpp @@ -0,0 +1,74 @@ +#include "DistrhoPlugin.hpp" +#include "Resampler.hpp" + +START_NAMESPACE_DISTRHO + +class PitchCorrector : public Plugin +{ +public: + PitchCorrector() + : Plugin(0, 0, 0) + { + } + +protected: + const char* getLabel() const override { return "yaw-totune"; } + const char* getDescription() const override { return "Pitch corrector"; } + const char* getMaker() const override { return "yaw-audio"; } + const char* getHomePage() const override { return "https://yaw.man/plugins/yaw-totune"; } + const char* getLicense() const override { return "Fuck you pay me"; } + uint32_t getVersion() const override { return d_version(1, 0, 0); } + int64_t getUniqueId() const override { return d_cconst('y', 't', 't', 'n'); } + + + void initParameter(uint32_t index, Parameter& parameter) override + { + parameter.hints = kParameterIsAutomable; + parameter.ranges.def = 0.0f; + parameter.ranges.min = 0.0f; + parameter.ranges.max = 1.0f; + parameter.name = "param"; + parameter.symbol = "param"; + } + + void sampleRateChanged(double newRate) override + { + hzNyq = newRate / 2; + } + + float getParameterValue(uint32_t index) const override + { + return 0; + } + + void setParameterValue(uint32_t idx, float val) override + { + + } + + void run(const float** inputs, float** outputs, uint32_t frames) override + { + for(int chn = 0; chn < 2; ++chn){ + resamplers[chn].write(inputs[chn], frames); + detectors[chn].downsample(inputs[chn], frames); + double period = resamplers[chn].detectPeriodNear(detectors[chn].detectPitch(frames)); + double correctPeriod = scale.getNearestPeriod(period); + double taux = period / correctPeriod; + resamplers[chn].resample(outputs[chn], frames, taux, period); + } + } + +private: + double hzNyq; + std::array, 2> resamplers; + std::array, 2> detectors; + Scale scale{440.0}; + DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PitchCorrector) +}; + +Plugin* createPlugin() +{ + return new PitchCorrector(); +} + +END_NAMESPACE_DISTRHO