Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions source/dsp/HalfbandDecimator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include <array>

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<size_t>(i)] = delayLine[static_cast<size_t>(i - 1)];
delayLine[0] = input;

if (++phase >= 2)
{
phase = 0;
lastOutput = 0.0f;
for (int i = 0; i < numTaps; ++i)
lastOutput += coeffs[static_cast<size_t>(i)] * delayLine[static_cast<size_t>(i)];
return true;
}
return false;
}

float getOutput() const { return lastOutput; }

private:
static constexpr int numTaps = 7;
static constexpr std::array<float, 7> coeffs = {
-0.03125f, 0.0f, 0.28125f, 0.5f, 0.28125f, 0.0f, -0.03125f
};
std::array<float, 7> delayLine {};
float lastOutput = 0.0f;
int phase = 0;
};
21 changes: 20 additions & 1 deletion source/dsp/Oscillator.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
#include "Oscillator.h"
#include <algorithm>
#include <array>

static constexpr double twoPi = 6.283185307179586476925;

static const float* getSineTable()
{
static constexpr int size = 2048;
static const auto table = [] {
std::array<float, size + 1> t {};
for (int i = 0; i <= size; ++i)
t[static_cast<size_t>(i)] = static_cast<float>(std::sin(twoPi * static_cast<double>(i) / size));
return t;
}();
return table.data();
}

void Oscillator::prepare(double sampleRate)
{
sr = std::max(1.0, sampleRate);
Expand Down Expand Up @@ -47,8 +60,14 @@ float Oscillator::nextSample()
switch (waveform)
{
case Waveform::Sine:
out = static_cast<float>(std::sin(phase * twoPi));
{
const float* table = getSineTable();
double idx = phase * 2048.0;
auto i0 = static_cast<int>(idx);
float frac = static_cast<float>(idx - i0);
out = table[i0] + frac * (table[i0 + 1] - table[i0]);
break;
}
case Waveform::Triangle:
out = static_cast<float>(2.0 * std::abs(2.0 * phase - 1.0) - 1.0);
break;
Expand Down
14 changes: 11 additions & 3 deletions source/dsp/PitchSmoother.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class PitchSmoother
sr = std::max(1.0f, static_cast<float>(sampleRate));
hasValue = false;
smoothed = 0.0f;
cachedFreq = 0.0f;
recomputeAlpha();
}

Expand All @@ -30,22 +31,28 @@ 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);

if (!hasValue)
{
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:
Expand All @@ -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;
};
136 changes: 107 additions & 29 deletions source/dsp/YinPitchDetector.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,99 @@
#include "YinPitchDetector.h"
#include <algorithm>
#include <cmath>
#include <thread>
#include <chrono>

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<size_t>(start1 + i)]);
for (int i = 0; i < size2; ++i)
processSample(o.fifoBuffer[static_cast<size_t>(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<size_t>(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<int>(std::ceil(sampleRate / 60.0));
double decimatedSR = sampleRate / 2.0;
decimator.reset();

analysisSR = decimatedSR;

halfWindow = static_cast<int>(std::ceil(decimatedSR / 60.0));
windowSize = 2 * halfWindow;
hopSize = static_cast<int>(std::ceil(sampleRate * 0.003));
hopSize = static_cast<int>(std::ceil(decimatedSR * 0.012));

fftOrder = static_cast<int>(std::ceil(std::log2(2.0 * windowSize)));
fftSize = 1 << fftOrder;
Expand All @@ -21,51 +106,44 @@ void YinPitchDetector::prepare(double sampleRate)
diff.resize(static_cast<size_t>(halfWindow));
cmndf.resize(static_cast<size_t>(halfWindow));

int fifoSize = std::max(8192, static_cast<int>(sampleRate * 2.5));
fifo.setTotalSize(fifoSize);
fifoBuffer.resize(static_cast<size_t>(fifoSize));

writePos = 0;
hopCounter = 0;
windowFilled = false;
lastResult = {};
}

void YinPitchDetector::feedSample(float sample)
{
buffer[static_cast<size_t>(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<AnalysisThread>(*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<size_t>(halfWindow);

for (int i = 0; i < windowSize; ++i)
linearBuffer[static_cast<size_t>(i)] = buffer[static_cast<size_t>((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<size_t>(i)] = linearBuffer[static_cast<size_t>(i)];
fft->performRealOnlyForwardTransform(fftOutput.data());
Expand Down
37 changes: 35 additions & 2 deletions source/dsp/YinPitchDetector.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#pragma once

#include <juce_dsp/juce_dsp.h>
#include <atomic>
#include <memory>
#include <vector>
#include "HalfbandDecimator.h"

struct PitchResult
{
Expand All @@ -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<size_t>(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;
Expand All @@ -43,4 +66,14 @@ class YinPitchDetector
PitchResult lastResult;

static constexpr float threshold = 0.15f;

juce::AbstractFifo fifo { 0 };
std::vector<float> fifoBuffer;

std::unique_ptr<AnalysisThread> analysisThread;

std::atomic<float> atomicFreq { 0.0f };
std::atomic<float> atomicConf { 0.0f };

HalfbandDecimator decimator;
};
Loading
Loading