diff --git a/source/dsp/HalfbandDecimator.h b/source/dsp/HalfbandDecimator.h new file mode 100644 index 0000000..6fae1aa --- /dev/null +++ b/source/dsp/HalfbandDecimator.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +class HalfbandDecimator +{ +public: + void reset() + { + delayLine.fill(0.0f); + phase = 0; + } + + bool processSample(float input) + { + for (int i = numTaps - 1; i > 0; --i) + delayLine[static_cast(i)] = delayLine[static_cast(i - 1)]; + delayLine[0] = input; + + if (++phase >= 2) + { + phase = 0; + lastOutput = 0.0f; + for (int i = 0; i < numTaps; ++i) + lastOutput += coeffs[static_cast(i)] * delayLine[static_cast(i)]; + return true; + } + return false; + } + + float getOutput() const { return lastOutput; } + +private: + static constexpr int numTaps = 7; + static constexpr std::array coeffs = { + -0.03125f, 0.0f, 0.28125f, 0.5f, 0.28125f, 0.0f, -0.03125f + }; + std::array delayLine {}; + float lastOutput = 0.0f; + int phase = 0; +}; diff --git a/source/dsp/Oscillator.cpp b/source/dsp/Oscillator.cpp index e46677d..6af18de 100644 --- a/source/dsp/Oscillator.cpp +++ b/source/dsp/Oscillator.cpp @@ -1,8 +1,21 @@ #include "Oscillator.h" #include +#include static constexpr double twoPi = 6.283185307179586476925; +static const float* getSineTable() +{ + static constexpr int size = 2048; + static const auto table = [] { + std::array t {}; + for (int i = 0; i <= size; ++i) + t[static_cast(i)] = static_cast(std::sin(twoPi * static_cast(i) / size)); + return t; + }(); + return table.data(); +} + void Oscillator::prepare(double sampleRate) { sr = std::max(1.0, sampleRate); @@ -47,8 +60,14 @@ float Oscillator::nextSample() switch (waveform) { case Waveform::Sine: - out = static_cast(std::sin(phase * twoPi)); + { + const float* table = getSineTable(); + double idx = phase * 2048.0; + auto i0 = static_cast(idx); + float frac = static_cast(idx - i0); + out = table[i0] + frac * (table[i0 + 1] - table[i0]); break; + } case Waveform::Triangle: out = static_cast(2.0 * std::abs(2.0 * phase - 1.0) - 1.0); break; diff --git a/source/dsp/PitchSmoother.h b/source/dsp/PitchSmoother.h index 3cbf1dc..fe1021b 100644 --- a/source/dsp/PitchSmoother.h +++ b/source/dsp/PitchSmoother.h @@ -11,6 +11,7 @@ class PitchSmoother sr = std::max(1.0f, static_cast(sampleRate)); hasValue = false; smoothed = 0.0f; + cachedFreq = 0.0f; recomputeAlpha(); } @@ -30,7 +31,7 @@ class PitchSmoother inline float process(float detectedFreq, float confidence) { if (detectedFreq <= 0.0f || confidence < sensitivityThreshold) - return hasValue ? std::exp2(smoothed) : 0.0f; + return cachedFreq; float logFreq = std::log2(detectedFreq); @@ -38,14 +39,20 @@ class PitchSmoother { smoothed = logFreq; hasValue = true; - return std::exp2(smoothed); + cachedFreq = std::exp2(smoothed); + return cachedFreq; } float delta = std::abs(logFreq - smoothed); float effectiveAlpha = delta > 0.08f ? 1.0f : alpha; + float prev = smoothed; smoothed += effectiveAlpha * (logFreq - smoothed); - return std::exp2(smoothed); + + if (smoothed != prev) + cachedFreq = std::exp2(smoothed); + + return cachedFreq; } private: @@ -63,5 +70,6 @@ class PitchSmoother float smoothed = 0.0f; float alpha = 0.5f; float sensitivityThreshold = 0.5f; + float cachedFreq = 0.0f; bool hasValue = false; }; diff --git a/source/dsp/YinPitchDetector.cpp b/source/dsp/YinPitchDetector.cpp index 678083d..43b5460 100644 --- a/source/dsp/YinPitchDetector.cpp +++ b/source/dsp/YinPitchDetector.cpp @@ -1,14 +1,99 @@ #include "YinPitchDetector.h" #include #include +#include +#include + +class YinPitchDetector::AnalysisThread : public juce::Thread +{ +public: + AnalysisThread(YinPitchDetector& owner) + : Thread("PitchAnalysis"), o(owner) {} + + void run() override + { + while (!threadShouldExit()) + { + int ready = o.fifo.getNumReady(); + if (ready == 0) + { + wait(1); + continue; + } + + int start1, size1, start2, size2; + o.fifo.prepareToRead(ready, start1, size1, start2, size2); + + for (int i = 0; i < size1; ++i) + processSample(o.fifoBuffer[static_cast(start1 + i)]); + for (int i = 0; i < size2; ++i) + processSample(o.fifoBuffer[static_cast(start2 + i)]); + + o.fifo.finishedRead(size1 + size2); + } + } + +private: + void processSample(float sample) + { + if (!o.decimator.processSample(sample)) + return; + float decimated = o.decimator.getOutput(); + + o.buffer[static_cast(o.writePos)] = decimated; + if (++o.writePos >= o.windowSize) o.writePos = 0; + ++o.hopCounter; + + if (!o.windowFilled) + { + if (o.writePos == 0) + o.windowFilled = true; + else + return; + } + + if (o.hopCounter >= o.hopSize) + { + o.hopCounter = 0; + o.analyse(); + o.atomicFreq.store(o.lastResult.frequency, std::memory_order_relaxed); + o.atomicConf.store(o.lastResult.confidence, std::memory_order_relaxed); + } + } + + YinPitchDetector& o; +}; + +YinPitchDetector::YinPitchDetector() = default; + +YinPitchDetector::~YinPitchDetector() +{ + if (analysisThread) + { + analysisThread->signalThreadShouldExit(); + analysisThread->notify(); + analysisThread->waitForThreadToExit(1000); + } +} void YinPitchDetector::prepare(double sampleRate) { - analysisSR = sampleRate; + if (analysisThread) + { + analysisThread->signalThreadShouldExit(); + analysisThread->notify(); + analysisThread->waitForThreadToExit(1000); + analysisThread.reset(); + } - halfWindow = static_cast(std::ceil(sampleRate / 60.0)); + double decimatedSR = sampleRate / 2.0; + decimator.reset(); + + analysisSR = decimatedSR; + + halfWindow = static_cast(std::ceil(decimatedSR / 60.0)); windowSize = 2 * halfWindow; - hopSize = static_cast(std::ceil(sampleRate * 0.003)); + hopSize = static_cast(std::ceil(decimatedSR * 0.012)); fftOrder = static_cast(std::ceil(std::log2(2.0 * windowSize))); fftSize = 1 << fftOrder; @@ -21,51 +106,44 @@ void YinPitchDetector::prepare(double sampleRate) diff.resize(static_cast(halfWindow)); cmndf.resize(static_cast(halfWindow)); + int fifoSize = std::max(8192, static_cast(sampleRate * 2.5)); + fifo.setTotalSize(fifoSize); + fifoBuffer.resize(static_cast(fifoSize)); + writePos = 0; hopCounter = 0; windowFilled = false; lastResult = {}; -} - -void YinPitchDetector::feedSample(float sample) -{ - buffer[static_cast(writePos)] = sample; - writePos = (writePos + 1) % windowSize; - ++hopCounter; + atomicFreq.store(0.0f, std::memory_order_relaxed); + atomicConf.store(0.0f, std::memory_order_relaxed); - if (!windowFilled) - { - if (writePos == 0) - windowFilled = true; - else - return; - } - - if (hopCounter >= hopSize) - { - hopCounter = 0; - analyse(); - } + analysisThread = std::make_unique(*this); + analysisThread->startThread(juce::Thread::Priority::normal); } -PitchResult YinPitchDetector::getResult() const +void YinPitchDetector::flushForTest() { - return lastResult; + if (analysisThread) + analysisThread->notify(); + while (fifo.getNumReady() > 0) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } void YinPitchDetector::analyse() { auto n = static_cast(halfWindow); - for (int i = 0; i < windowSize; ++i) - linearBuffer[static_cast(i)] = buffer[static_cast((writePos + i) % windowSize)]; + int tail = windowSize - writePos; + std::copy_n(buffer.data() + writePos, tail, linearBuffer.data()); + std::copy_n(buffer.data(), writePos, linearBuffer.data() + tail); - std::fill(fftInput.begin(), fftInput.end(), 0.0f); + juce::FloatVectorOperations::clear(fftInput.data(), fftSize * 2); for (size_t i = 0; i < n; ++i) fftInput[i] = linearBuffer[i]; fft->performRealOnlyForwardTransform(fftInput.data()); - std::fill(fftOutput.begin(), fftOutput.end(), 0.0f); + juce::FloatVectorOperations::clear(fftOutput.data(), fftSize * 2); for (int i = 0; i < windowSize; ++i) fftOutput[static_cast(i)] = linearBuffer[static_cast(i)]; fft->performRealOnlyForwardTransform(fftOutput.data()); diff --git a/source/dsp/YinPitchDetector.h b/source/dsp/YinPitchDetector.h index a327fa7..4bdb8ca 100644 --- a/source/dsp/YinPitchDetector.h +++ b/source/dsp/YinPitchDetector.h @@ -1,8 +1,10 @@ #pragma once #include +#include #include #include +#include "HalfbandDecimator.h" struct PitchResult { @@ -13,13 +15,34 @@ struct PitchResult class YinPitchDetector { public: + YinPitchDetector(); + ~YinPitchDetector(); + void prepare(double sampleRate); - void feedSample(float sample); - PitchResult getResult() const; + + inline void feedSample(float sample) + { + int start1, size1, start2, size2; + fifo.prepareToWrite(1, start1, size1, start2, size2); + if (size1 > 0) + fifoBuffer[static_cast(start1)] = sample; + fifo.finishedWrite(size1 + size2); + } + + inline PitchResult getResult() const + { + return { atomicFreq.load(std::memory_order_relaxed), + atomicConf.load(std::memory_order_relaxed) }; + } + + void flushForTest(); private: void analyse(); + class AnalysisThread; + friend class AnalysisThread; + double analysisSR = 44100.0; int windowSize = 0; int halfWindow = 0; @@ -43,4 +66,14 @@ class YinPitchDetector PitchResult lastResult; static constexpr float threshold = 0.15f; + + juce::AbstractFifo fifo { 0 }; + std::vector fifoBuffer; + + std::unique_ptr analysisThread; + + std::atomic atomicFreq { 0.0f }; + std::atomic atomicConf { 0.0f }; + + HalfbandDecimator decimator; }; diff --git a/tests/TestYinPitchDetector.cpp b/tests/TestYinPitchDetector.cpp index d823ee7..d795b8b 100644 --- a/tests/TestYinPitchDetector.cpp +++ b/tests/TestYinPitchDetector.cpp @@ -4,16 +4,19 @@ #include static constexpr double twoPi = 6.283185307179586476925; +static constexpr int decimationFactor = 2; static int computeWindowSize(double sampleRate) { - int halfWindow = static_cast(std::ceil(sampleRate / 60.0)); + double decimatedSR = sampleRate / decimationFactor; + int halfWindow = static_cast(std::ceil(decimatedSR / 60.0)); return 2 * halfWindow; } static int computeHopSize(double sampleRate) { - return static_cast(std::ceil(sampleRate * 0.003)); + double decimatedSR = sampleRate / decimationFactor; + return static_cast(std::ceil(decimatedSR * 0.012)); } static void feedSine(YinPitchDetector& yin, double sampleRate, float freq, int numSamples) @@ -27,6 +30,7 @@ static void feedSine(YinPitchDetector& yin, double sampleRate, float freq, int n yin.feedSample(sample); phase += inc; } + yin.flushForTest(); } static void feedHarmonicComplex(YinPitchDetector& yin, double sampleRate, float freq, int numSamples, float amplitude) @@ -44,21 +48,30 @@ static void feedHarmonicComplex(YinPitchDetector& yin, double sampleRate, float yin.feedSample(static_cast(s * static_cast(amplitude))); phase += inc; } + yin.flushForTest(); } static int feedSineUntilDetection(YinPitchDetector& yin, double sampleRate, float freq, int maxSamples) { double phase = 0.0; double inc = twoPi * static_cast(freq) / sampleRate; + int hopOriginal = computeHopSize(sampleRate) * decimationFactor; + int totalFed = 0; - for (int i = 0; i < maxSamples; ++i) + while (totalFed < maxSamples) { - float sample = static_cast(std::sin(phase)); - yin.feedSample(sample); - phase += inc; + int toFeed = std::min(hopOriginal, maxSamples - totalFed); + for (int i = 0; i < toFeed; ++i) + { + float sample = static_cast(std::sin(phase)); + yin.feedSample(sample); + phase += inc; + } + totalFed += toFeed; + yin.flushForTest(); if (yin.getResult().frequency > 0.0f) - return i + 1; + return totalFed; } return maxSamples; } @@ -139,6 +152,7 @@ TEST_CASE("YIN: returns zero for silence") for (int i = 0; i < 44100; ++i) yin.feedSample(0.0f); + yin.flushForTest(); auto result = yin.getResult(); REQUIRE(result.frequency == 0.0f); @@ -184,11 +198,11 @@ TEST_CASE("YIN: first detection within one window fill") YinPitchDetector yin; yin.prepare(44100.0); - int winSize = computeWindowSize(44100.0); - int hop = computeHopSize(44100.0); + int winOriginal = computeWindowSize(44100.0) * decimationFactor; + int hopOriginal = computeHopSize(44100.0) * decimationFactor; int samplesNeeded = feedSineUntilDetection(yin, 44100.0, 440.0f, 44100); - REQUIRE(samplesNeeded <= winSize + hop); + REQUIRE(samplesNeeded <= winOriginal + hopOriginal); REQUIRE(yin.getResult().frequency > 0.0f); } @@ -197,17 +211,18 @@ TEST_CASE("YIN: detects pitch change after silence") YinPitchDetector yin; yin.prepare(44100.0); - int winSize = computeWindowSize(44100.0); - int hop = computeHopSize(44100.0); + int winOriginal = computeWindowSize(44100.0) * decimationFactor; + int hopOriginal = computeHopSize(44100.0) * decimationFactor; - for (int i = 0; i < winSize * 2; ++i) + for (int i = 0; i < winOriginal * 2; ++i) yin.feedSample(0.0f); + yin.flushForTest(); REQUIRE(yin.getResult().frequency == 0.0f); int samplesNeeded = feedSineUntilDetection(yin, 44100.0, 440.0f, 44100); - REQUIRE(samplesNeeded <= winSize + hop); + REQUIRE(samplesNeeded <= winOriginal + hopOriginal); auto result = yin.getResult(); REQUIRE(result.frequency > 0.0f); @@ -220,16 +235,16 @@ TEST_CASE("YIN: tracks frequency sweep across hop intervals") YinPitchDetector yin; yin.prepare(44100.0); - int winSize = computeWindowSize(44100.0); + int winOriginal = computeWindowSize(44100.0) * decimationFactor; - feedSine(yin, 44100.0, 440.0f, winSize + 1000); + feedSine(yin, 44100.0, 440.0f, winOriginal + 4000); auto resultBefore = yin.getResult(); REQUIRE(resultBefore.frequency > 0.0f); REQUIRE_THAT(static_cast(resultBefore.frequency), Catch::Matchers::WithinRel(440.0, 0.03)); - feedSine(yin, 44100.0, 880.0f, winSize + 1000); + feedSine(yin, 44100.0, 880.0f, winOriginal + 4000); auto resultAfter = yin.getResult(); REQUIRE(resultAfter.frequency > 0.0f); @@ -257,6 +272,7 @@ TEST_CASE("YIN: silence still returns zero with fallback") for (int i = 0; i < 44100; ++i) yin.feedSample(0.0f); + yin.flushForTest(); auto result = yin.getResult(); REQUIRE(result.frequency == 0.0f);