diff --git a/src/yaw-totune/CMakeLists.txt b/src/yaw-totune/CMakeLists.txt index 109ca7e..3723f29 100644 --- a/src/yaw-totune/CMakeLists.txt +++ b/src/yaw-totune/CMakeLists.txt @@ -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 diff --git a/src/yaw-totune/DistrhoPluginInfo.h b/src/yaw-totune/DistrhoPluginInfo.h index 7cfea06..26204f0 100644 --- a/src/yaw-totune/DistrhoPluginInfo.h +++ b/src/yaw-totune/DistrhoPluginInfo.h @@ -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, diff --git a/src/yaw-totune/Resampler.hpp b/src/yaw-totune/Resampler.hpp index 27bcf5e..d3ab144 100644 --- a/src/yaw-totune/Resampler.hpp +++ b/src/yaw-totune/Resampler.hpp @@ -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 class Resampler { @@ -45,13 +56,19 @@ private: //TODO: refactor into its own class and look up operator[] semantics. std::array array = {}; + //Autocorrelations computed from scratch in a neighbourhood of some estimate. + std::array 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(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(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 squares = {}; - std::array crosses = {}; + std::array squares = {}; + std::array 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 detectPitch(uint32_t frames) { frames /= dFactor; uint idx = writeIdx - frames; - bool a = false; + std::array 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) diff --git a/src/yaw-totune/dsp.cpp b/src/yaw-totune/dsp.cpp index e99db59..25ea678 100644 --- a/src/yaw-totune/dsp.cpp +++ b/src/yaw-totune/dsp.cpp @@ -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, 2> resamplers; std::array, 2> detectors; + std::array debugPhases = {0}; Scale scale{440.0}; DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PitchCorrector) }; diff --git a/src/yaw-totune/ui.cpp b/src/yaw-totune/ui.cpp new file mode 100644 index 0000000..d33b587 --- /dev/null +++ b/src/yaw-totune/ui.cpp @@ -0,0 +1,98 @@ +/* + * DISTRHO Plugin Framework (DPF) + * Copyright (C) 2012-2021 Filipe Coelho + * + * 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 + +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