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