Pitch detectors now search for local minima, and interpolate for subsample accuracy.

Resamplers still have some issues.
This commit is contained in:
yaw-man 2022-09-22 08:31:44 -04:00
parent 4e3fa9f24d
commit bca9e0381d
5 changed files with 236 additions and 60 deletions

View File

@ -1,7 +1,9 @@
dpf_add_plugin(yaw-totune
TARGETS vst2
FILES_DSP
dsp.cpp)
dsp.cpp
FILES_UI
ui.cpp)
target_include_directories(yaw-totune PUBLIC

View File

@ -5,11 +5,11 @@
#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_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
#define DISTRHO_UI_USE_NANOVG 1
/*enum Parameters {
ktpax = 0,

View File

@ -9,17 +9,32 @@ private: \
ClassName& operator=(const ClassName&) = delete;
// 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)
static inline double secondOrderMinimum(int x0, float y0, int x1, float y1, int 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.
// solve the 2nd order vmonde matrix, then return -b / 2a, the discriminant and xmin of qdrtic polynomial.
// The x_i are assumed to be nonzero and distinct.
double w0 = y0 / ( (x1 - x0) * (x2 - x0) );
double w1 = y1 / ( (x0 - x1) * (x2 - x1) );
double w2 = y2 / ( (x0 - x2) * (x1 - x2) );
return x;
double a = w0 + w1 + w2;
double b = - w0 * (x2 + x1) - w1 * (x0 + x2) - w2 * (x1 + x0);
if ( a < 0.00001) return x1; //y not sufficiently distinct, can't get numerically meaningful answer.
double xmin = -b / (2.0 * a);
if ( xmin > x2 ) return x2; //We're looking for a minimum in the given interval, so just clamp it.
if ( xmin < x0 ) return x0;
return xmin;
};
static inline double sinc(float x)
{
//Remove discontinuity at zero.
if (abs( x ) < 0.001) return 1.f;
return sinf(M_PI * x) / (M_PI * x);
};
static constexpr uint pSize = 10;
static constexpr size_t resamplerBufferSize = 1 << pSize;
@ -30,10 +45,6 @@ static constexpr uint dFactor = 1 << dLogFactor;
static constexpr size_t dBufferSize = 1 << (pSize - dLogFactor);
static constexpr size_t dBufferMask = dBufferSize - 1;
static constexpr double BIG_DOUBLE = 10000.0;
template <typename T>
class Resampler
{
@ -45,13 +56,19 @@ private:
//TODO: refactor into its own class and look up operator[] semantics.
std::array<T, resamplerBufferSize> array = {};
//Autocorrelations computed from scratch in a neighbourhood of some estimate.
std::array<double, dFactor * 2> autoCorrelations;
// Pointer for pitch correction.
double readIdx = 0;
// Pointer for pitch detection.
uint writeIdx = 0;
// Simple one-pole low pass filter for smoothing over period mismatches.
double lpf = 0.0;
// Calculate autocorrelation from scratch for given period
inline double ac(const uint per)
inline double const ac(const uint per)
{
double ac = 0;
uint idx = writeIdx;
@ -60,7 +77,7 @@ private:
T x = array[idx & resamplerMask];
T y = array[(idx - per) & resamplerMask];
ac += x * x + x * y;
ac += (x - y) * (x - y);
--idx;
}
return ac;
@ -70,21 +87,43 @@ public:
Resampler(){};
// Detect accurate period from scratch near a target period.
inline double detectPeriodNear(const uint nearPeriod)
inline double const getAccuratePeriod(uint firstEstimate, uint secondEstimate)
{
double minAC = BIG_DOUBLE;
double period = nearPeriod;
for (uint per = nearPeriod - dFactor / 2; per < nearPeriod + dFactor / 2; ++per)
//First try the two candidates for the appropriate nbhd.
uint estimate = firstEstimate;
double fac = ac(firstEstimate);
if ( secondEstimate )
{
double curAC = ac(per);
if (curAC < minAC)
double altAC = ac(secondEstimate);
if( fac > altAC )
{
minAC = curAC;
period = per;
fac = altAC;
estimate = secondEstimate;
}
}
return period;
//Get autocorrelations in the nbhd, find the minimum.
uint minIdx = 0;
int p = estimate - autoCorrelations.size() / 2;
for (uint i = 0; i < autoCorrelations.size(); ++i, ++p)
{
autoCorrelations[i] = ac(p);
if (autoCorrelations[i] <= fac ) {
minIdx = i;
estimate = p;
fac = autoCorrelations[i];
}
}
//Interpolate to get accuracy greater than one sample (yes, you need it!)
uint x0 = estimate - 1;
uint x1 = estimate;
uint x2 = estimate + 1;
double y0 = (minIdx == 0) ? ac(x0) : autoCorrelations[minIdx - 1];
double y1 = autoCorrelations[minIdx];
double y2 = ((minIdx + 1) >= autoCorrelations.size()) ? ac(x2) : autoCorrelations[minIdx + 1];
return secondOrderMinimum(x0, y0, x1, y1, x2, y2);
}
inline void write(const T *const input, const uint32_t frames)
@ -96,6 +135,22 @@ public:
};
}
//Eight tap windowed sinc filter.
//Interpolate at readIdx - 4.0.
inline T const read(const double rate)
{
const uint idx = static_cast<uint>(readIdx);
const double frac = readIdx - idx;
T val = 0.f;
for( uint i = 0; i < 8; ++i )
{
val += sinc((idx - i - readIdx + 3.0) / rate) * array[(idx - i) & resamplerMask];
}
return val;
}
// Arguments:
// output buffer and length
// ratio of new sample rate to old sample rate
@ -106,19 +161,14 @@ public:
//Bounds of read index.
double end = writeIdx;
while ( readIdx > end ) { end += resamplerBufferSize; };
double start = end - period;
const double start = end - period;
for (uint32_t i = 0; i < frames; ++i)
{
// TODO: add interpolation filter. Linear for now.
uint idx = static_cast<uint>(readIdx);
float frac = readIdx - idx;
*output = (1.f - frac) * array[(idx - 1) & resamplerMask] + frac * array[idx & resamplerMask];
output[i] = read(rate);
readIdx += rate;
while ( readIdx < start )
while ( rate < 1.0 && readIdx < start )
{
readIdx += period;
}
@ -127,10 +177,11 @@ public:
{
readIdx -= period;
}
++output;
};
//Prevent read index overflow / precision loss.
while (readIdx > resamplerBufferSize) {readIdx -= resamplerBufferSize;};
}
DISTRHO_DECLARE_NON_COPYABLE(Resampler)
@ -144,8 +195,8 @@ class Detector
// Pointer for pitch detection.
uint writeIdx = 0;
// Detectable periods.
std::array<T, dBufferSize / 2> squares = {};
std::array<T, dBufferSize / 2> crosses = {};
std::array<double, dBufferSize / 2> squares = {};
std::array<double, dBufferSize / 2> crosses = {};
static constexpr uint minPeriod = 16;
static constexpr uint maxPeriod = dBufferSize / 2;
@ -165,40 +216,54 @@ public:
};
}
// Incrementally detect all possible pitches. Return coarse pitch match.
inline uint detectPitch(uint32_t frames)
std::array<uint, 2> detectPitch(uint32_t frames)
{
frames /= dFactor;
uint idx = writeIdx - frames;
bool a = false;
std::array<uint, 2> pitches{};
//Update autocorrelations.
while (frames)
{
for (uint per = 1; per < squares.size(); ++per)
for (uint per = 3; per < squares.size(); ++per)
{
T x = array[idx & dBufferMask];
T y = array[(idx - per) & dBufferMask];
T z = array[(idx - 2 * per) & dBufferMask];
//Autocorrelation and RMS computation.
squares[per] += x * x - z * z;
crosses[per] += x * y - y * z;
if(squares[per] - 2.0 * crosses[per] < -0.1)
break;
}
--frames;
++idx;
}
double least = BIG_DOUBLE;
uint per = 0;
for (uint i = 1; i < squares.size(); ++i)
//Search for local minima of autocorrelation function.
for ( uint per = 3; per < squares.size() - 1; ++per)
{
double uac = squares[i] - 2 * crosses[i];
if (uac < least)
if ( 0.49 * squares[per] < crosses[per] )
{
least = uac;
per = i;
double a = 2.0 * crosses[per - 1] - squares[per - 1];
double b = 2.0 * crosses[per] - squares[per];
double c = 2.0 * crosses[per + 1] - squares[per + 1];
if ( (b > a) && (b > c) )
{
if (pitches[0])
{
pitches[1] = dFactor * per;
return pitches;
}
else
{
pitches[0] = dFactor * per;
if (per * 2 > squares.size() ) return pitches;
}
}
}
}
return per * dFactor;
return pitches;
}
DISTRHO_DECLARE_NON_COPYABLE(Detector)

View File

@ -40,39 +40,50 @@ protected:
float getParameterValue(uint32_t index) const override
{
return curPitch;
return period;
}
void setParameterValue(uint32_t idx, float val) override
{
}
double debugTone(float* output, uint32_t frames, float period, float phase)
{
for(uint i = 0; i < frames; ++i) {
*output = 0.5 * sinf( M_PI * 2.0 * phase );
phase += 1.f / period;
phase -= (int)phase;
++output;
};
return phase;
}
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);
uint dpitch = detectors[chn].detectPitch(frames);
curPitch = dpitch;
auto detectedPitches = detectors[chn].detectPitch(frames);
double taux = 1.0;
double period = 1.0;
if (dpitch)
period = 1023.0;
if (detectedPitches[0])
{
period = resamplers[chn].detectPeriodNear(dpitch);
period = resamplers[chn].getAccuratePeriod(detectedPitches[0], detectedPitches[1]);
double correctPeriod = scale.getNearestPeriod(period);
if( correctPeriod > 1.0 ) taux = period / correctPeriod;
}
//resamplers[chn].resample(outputs[chn], frames, taux, period);
resamplers[chn].resample(outputs[chn], frames, 1.3, period);
};
resamplers[chn].resample(outputs[chn], frames, 0.95, 109);
}
}
private:
uint curPitch = 0;
double period = 1023.0;
double hzNyq;
std::array<Resampler<float>, 2> resamplers;
std::array<Detector<float>, 2> detectors;
std::array<double, 2> debugPhases = {0};
Scale scale{440.0};
DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PitchCorrector)
};

98
src/yaw-totune/ui.cpp Normal file
View File

@ -0,0 +1,98 @@
/*
* DISTRHO Plugin Framework (DPF)
* Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
*
* Permission to use, copy, modify, and/or distribute this software for any purpose with
* or without fee is hereby granted, provided that the above copyright notice and this
* permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
* TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
* NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "DistrhoUI.hpp"
#include <format>
START_NAMESPACE_DISTRHO
// -----------------------------------------------------------------------------------------------------------
class InfoExampleUI : public UI
{
static const uint kInitialWidth = 405;
static const uint kInitialHeight = 256;
public:
InfoExampleUI()
: UI(kInitialWidth, kInitialHeight)
{
#ifdef DGL_NO_SHARED_RESOURCES
createFontFromFile("sans", "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf");
#else
loadSharedResources();
#endif
}
protected:
/* --------------------------------------------------------------------------------------------------------
* DSP/Plugin Callbacks */
/**
A parameter has changed on the plugin side.
This is called by the host to inform the UI about parameter changes.
*/
void parameterChanged(uint32_t index, float value) override
{
period = value;
repaint();
}
/* --------------------------------------------------------------------------------------------------------
* DSP/Plugin Callbacks (optional) */
/* --------------------------------------------------------------------------------------------------------
* Widget Callbacks */
/**
The NanoVG drawing function.
*/
void onNanoDisplay() override
{
// Numerical feedback.
beginPath();
fillColor(200, 200, 200);
textBox(0.f, 15.f, 250.f,
std::format("p: {:.3f}\n",
period)
.c_str(),
nullptr);
closePath();
}
// -------------------------------------------------------------------------------------------------------
private:
// Parameters
float period = 0.f;
double fSampleRate;
DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(InfoExampleUI)
};
/* ------------------------------------------------------------------------------------------------------------
* UI entry point, called by DPF to create a new UI instance. */
UI* createUI()
{
return new InfoExampleUI();
}
// -----------------------------------------------------------------------------------------------------------
END_NAMESPACE_DISTRHO