Pitch corrector
This commit is contained in:
parent
54bee81c43
commit
a629a65194
|
@ -0,0 +1,7 @@
|
||||||
|
dpf_add_plugin(yaw-totune
|
||||||
|
TARGETS vst2
|
||||||
|
FILES_DSP
|
||||||
|
dsp.cpp)
|
||||||
|
|
||||||
|
target_include_directories(yaw-totune PUBLIC
|
||||||
|
".")
|
|
@ -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
|
|
@ -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 ¬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 <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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
Loading…
Reference in New Issue