Pitch corrector

This commit is contained in:
yaw-man 2022-09-14 19:04:19 -04:00
parent 54bee81c43
commit a629a65194
4 changed files with 322 additions and 0 deletions

View File

@ -0,0 +1,7 @@
dpf_add_plugin(yaw-totune
TARGETS vst2
FILES_DSP
dsp.cpp)
target_include_directories(yaw-totune PUBLIC
".")

View File

@ -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

View File

@ -0,0 +1,216 @@
#include <array>
#include <vector>
static constexpr double BIG_DOUBLE = 10000.0;
class Scale
{
double sampleRate = 48000.0;
// freqs in hz, periods in samples
std::vector<double> frequencies;
std::vector<double> periods;
public:
void newSampleRate(double rate)
{
double ratio = rate / sampleRate;
sampleRate = rate;
for (double &note : 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 <typename T>
class Resampler
{
private:
// Ring buffer.
//TODO: refactor into its own class and look up operator[] semantics.
std::array<T, resamplerBufferSize> 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<uint>(readIdx)];
++output;
if ((readIdx < writeIdx) &&
(readIdx > writeIdx - rate))
readIdx -= period;
readIdx += rate;
if (readIdx > resamplerBufferSize)
readIdx -= resamplerBufferSize;
if (readIdx < 0)
readIdx = 0;
};
}
};
template <typename T>
class Detector
{
std::array<T, dBufferSize> array = {};
// Pointer for pitch detection.
uint writeIdx = 0;
// Detectable periods.
std::array<T, dBufferSize / 2> squares = {};
std::array<T, dBufferSize / 2> 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;
}
};

74
src/yaw-totune/dsp.cpp Normal file
View File

@ -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<Resampler<float>, 2> resamplers;
std::array<Detector<float>, 2> detectors;
Scale scale{440.0};
DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PitchCorrector)
};
Plugin* createPlugin()
{
return new PitchCorrector();
}
END_NAMESPACE_DISTRHO