Skip to content
Open
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
7 changes: 7 additions & 0 deletions plugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ target_sources(${PROJECT_NAME}
PRIVATE
source/PluginEditor.cpp
source/PluginProcessor.cpp
source/DSP/MPMPitchDetector.cpp
source/DSP/BasicPitchDetector.cpp
source/Importer/WavImporter.cpp
${INCLUDE_DIR}/PluginEditor.h
${INCLUDE_DIR}/PluginProcessor.h
${INCLUDE_DIR}/DSP/MPMPitchDetector.h
${INCLUDE_DIR}/DSP/BasicPitchDetector.h
${INCLUDE_DIR}/Importer/WavImporter.h
)

# Sets the include directories of the plugin project.
Expand All @@ -42,6 +48,7 @@ target_link_libraries(${PROJECT_NAME}
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags
juce::juce_audio_basics
)

# These definitions are recommended by JUCE.
Expand Down
13 changes: 13 additions & 0 deletions plugin/include/VocalAnalysis/DSP/BasicPitchDetector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once
#include <juce_audio_basics/juce_audio_basics.h>

class BasicPitchDetector
{
public:
BasicPitchDetector(double sampleRate);
double detectPitch(const juce::AudioBuffer<float>& buffer, int channel);
private:
double sampleRate;
std::vector<double> computeAutocorrelation(const float* audioData, std::size_t numSamples);
int findBestLag(const std::vector<double>& autocorrelation);
};
24 changes: 24 additions & 0 deletions plugin/include/VocalAnalysis/DSP/MPMPitchDetector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <juce_audio_basics/juce_audio_basics.h>
#include <vector>

/* McLeod Pitch Method */
class MPMPitchDetector
{
public:
MPMPitchDetector(double sampleRate);
~MPMPitchDetector() = default;

float detectPitch(const juce::AudioBuffer<float>& buffer, int channel);

private:
double sampleRate;

void calculateDifferenceFunction(const float* data, int numSamples);
void calculateCumulativeDifferenceFunction();
int findPitchPeriod() const;

std::vector<float> differenceFunction;
std::vector<float> cumulativeDifferenceFunction;
};
12 changes: 12 additions & 0 deletions plugin/include/VocalAnalysis/Exporter/WavExporter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

#include <juce_audio_formats/juce_audio_formats.h>
#include <juce_core/juce_core.h>
#include <juce_audio_basics/juce_audio_basics.h>

class WavExporter {
public:
static void exportBufferToWav(const juce::AudioBuffer<float>& buffer,
const juce::File& file,
double sampleRate);
};
10 changes: 10 additions & 0 deletions plugin/include/VocalAnalysis/Importer/WavImporter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

#include <juce_audio_formats/juce_audio_formats.h>
#include <juce_core/juce_core.h>

class WavImporter
{
public:
static bool loadWavFileIntoBuffer(const juce::File& file, juce::AudioBuffer<float>& buffer, double& sampleRate);
};
50 changes: 50 additions & 0 deletions plugin/source/DSP/BasicPitchDetector.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#include "VocalAnalysis/DSP/BasicPitchDetector.h"
#include <vector>
#include <cmath>

BasicPitchDetector::BasicPitchDetector(double currentSampleRate) {
jassert(currentSampleRate > 0);
sampleRate = currentSampleRate;
}

double BasicPitchDetector::detectPitch(const juce::AudioBuffer<float>& buffer, int channel) {
std::size_t numSamples = static_cast<std::size_t>(buffer.getNumSamples());
if (numSamples == 0 || sampleRate <= 0) return 0.0;

const float* audioData = buffer.getReadPointer(channel);
std::vector<double> autocorrelation = computeAutocorrelation(audioData, numSamples);

int bestLag = findBestLag(autocorrelation);
return sampleRate / static_cast<double>(bestLag);
}

std::vector<double> BasicPitchDetector::computeAutocorrelation(const float* audioData, std::size_t numSamples) {
std::vector<double> autocorrelation(numSamples, 0.0);

for (std::size_t lag = 0; lag < numSamples; ++lag) {
double sum = 0.0;
for (std::size_t i = 0; i < numSamples - lag; ++i) {
sum += audioData[i] * audioData[i + lag];
}
autocorrelation[lag] = sum;
}

return autocorrelation;
}

int BasicPitchDetector::findBestLag(const std::vector<double>& autocorrelation) {
int minLag = static_cast<int>(sampleRate / 1000); // Ignore frequencies above 1000 Hz
int maxLag = static_cast<int>(sampleRate / 50); // Ignore frequencies below 50 Hz

int bestLag = minLag;
double maxCorrelation = 0.0;

for (int lag = minLag; lag < maxLag; ++lag) {
if (autocorrelation[static_cast<std::size_t>(lag)] > maxCorrelation) {
maxCorrelation = autocorrelation[static_cast<std::size_t>(lag)];
bestLag = lag;
}
}

return bestLag;
}
78 changes: 78 additions & 0 deletions plugin/source/DSP/MPMPitchDetector.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#include "VocalAnalysis/DSP/MPMPitchDetector.h"

MPMPitchDetector::MPMPitchDetector(double currentSampleRate) {
jassert(currentSampleRate > 0);
sampleRate = currentSampleRate;
}


// Main pitch detection method
float MPMPitchDetector::detectPitch(const juce::AudioBuffer<float>& buffer, int channel)
{
if (channel < 0 || channel >= buffer.getNumChannels())
{
return 0.0f;
}

auto* data = buffer.getReadPointer(channel);
int numSamples = buffer.getNumSamples();

// Resize buffers for the current block size
if (differenceFunction.size() < static_cast<size_t>(numSamples))
{
differenceFunction.resize(static_cast<size_t>(numSamples), 0.0f);
cumulativeDifferenceFunction.resize(static_cast<size_t>(numSamples), 0.0f);
}

calculateDifferenceFunction(data, numSamples);
calculateCumulativeDifferenceFunction();
int pitchPeriod = findPitchPeriod();
if (pitchPeriod > 0)
{
return sampleRate / static_cast<float>(pitchPeriod);
}

return 0.0f;
}

void MPMPitchDetector::calculateDifferenceFunction(const float* data, int numSamples)
{
for (int tau = 0; tau < numSamples; ++tau)
{
float sum = 0.0f;
for (int j = 0; j < numSamples - tau; ++j)
{
float diff = data[j] - data[j + tau];
sum += diff * diff;
}
differenceFunction[tau] = sum;
}
}

void MPMPitchDetector::calculateCumulativeDifferenceFunction()
{
cumulativeDifferenceFunction[0] = 1.0f;
for (int tau = 1; tau < static_cast<int>(differenceFunction.size()); ++tau)
{
float cumulativeSum = 0.0f;
for (int j = 1; j <= tau; ++j)
{
cumulativeSum += differenceFunction[j];
}
cumulativeDifferenceFunction[tau] = (cumulativeSum == 0.0f) ? 1.0f : (differenceFunction[tau] / (cumulativeSum / tau));
}
}

int MPMPitchDetector::findPitchPeriod() const
{
const float threshold = 0.1f;
for (int tau = 2; tau < static_cast<int>(cumulativeDifferenceFunction.size()); ++tau)
{
if (cumulativeDifferenceFunction[tau] < threshold &&
cumulativeDifferenceFunction[tau] < cumulativeDifferenceFunction[tau - 1])
{
return tau;
}
}
return -1;
}
32 changes: 32 additions & 0 deletions plugin/source/Exporter/WavExporter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include "VocalAnalysis/Exporter/WavExporter.h"

void WavExporter::exportBufferToWav(const juce::AudioBuffer<float>& buffer,
const juce::File& file,
double sampleRate)
{
if (file.existsAsFile()) {
//file.deleteFile(); // Ensure it's a new file
return;
}

juce::FileOutputStream fileStream(file);

if (!fileStream.openedOk()) {
DBG("Failed to open file for writing");
return;
}

juce::WavAudioFormat wavFormat;
std::unique_ptr<juce::AudioFormatWriter> writer(
wavFormat.createWriterFor(&fileStream, sampleRate, buffer.getNumChannels(),
16, {}, 0)
);

if (!writer) {
DBG("Failed to create WAV writer");
return;
}

writer->writeFromAudioSampleBuffer(buffer, 0, buffer.getNumSamples());
DBG("WAV file successfully written: " << file.getFullPathName());
}
21 changes: 21 additions & 0 deletions plugin/source/Importer/WavImporter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include "VocalAnalysis/Importer/WavImporter.h"

bool WavImporter::loadWavFileIntoBuffer(const juce::File& file, juce::AudioBuffer<float>& buffer, double& sampleRate) {
juce::AudioFormatManager formatManager;
formatManager.registerBasicFormats();

std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));

if (reader == nullptr) {
return false;
}

sampleRate = reader->sampleRate;
int numSamples = static_cast<int>(reader->lengthInSamples);
int numChannels = static_cast<int>(reader->numChannels);

buffer.setSize(numChannels, numSamples);
reader->read(&buffer, 0, numSamples, 0, true, true);

return true;
}
6 changes: 5 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ enable_testing()

# Creates the test console application.
add_executable(${PROJECT_NAME}
source/AudioProcessorTest.cpp)
source/AudioProcessorTest.cpp
source/DSP/MPMPitchDetectorTest.h
source/DSP/MPMPitchDetectorTest.cpp
source/DSP/BasicPitchDetectionTest.h
source/DSP/BasicPitchDetectionTest.cpp)

# Sets the necessary include directories: ours, JUCE's, and googletest's.
target_include_directories(${PROJECT_NAME}
Expand Down
Binary file added test/resources/.DS_Store
Binary file not shown.
Binary file not shown.
32 changes: 32 additions & 0 deletions test/source/DSP/BasicPitchDetectionTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include "BasicPitchDetectionTest.h"

namespace pitch_detection_test {
TEST_F(BasicPitchDetectorTest, DetectsPitch440HzCorrectly) {

const float frequency = 440.0f;
generateSineWave(frequency);

float detectedPitch = pitchDetector->detectPitch(buffer, channel);

EXPECT_NEAR(detectedPitch, frequency, 1.0f); // 1 Hz tolerance
}

TEST_F(BasicPitchDetectorTest, DetectsPitch220HzCorrectly) {

const float frequency = 220.0f;
generateSineWave(frequency);

float detectedPitch = pitchDetector->detectPitch(buffer, channel);

EXPECT_NEAR(detectedPitch, frequency, 1.0f); // 1 Hz tolerance
}

TEST_F(BasicPitchDetectorTest, DetectsPitchD4FileCorrectly) {
const float expectedFrequency = 293.67f; // D4

loadAndProcessAudio();

float detectedPitch = pitchDetector->detectPitch(buffer, channel);
EXPECT_NEAR(detectedPitch, expectedFrequency, 1.0f);
}
}
58 changes: 58 additions & 0 deletions test/source/DSP/BasicPitchDetectionTest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#include <gtest/gtest.h>
#include <VocalAnalysis/DSP/BasicPitchDetector.h>
#include <VocalAnalysis/Importer/WavImporter.h>

namespace pitch_detection_test {
class BasicPitchDetectorTest : public ::testing::Test {
protected:
void SetUp() override {
GTEST_SKIP() << "Skipping all pitch detection tests since the algorithm is not accurate enough.";

sampleRate = 44100.0;
numTestSamples = 4096;
zeroPadding = numTestSamples;
totalTestSamples = zeroPadding + numTestSamples + zeroPadding;
buffer.setSize(1, totalTestSamples);
pitchDetector = std::make_unique<BasicPitchDetector>(sampleRate);
}

void TearDown() override {
pitchDetector.reset();
}

double sampleRate;
int channel = 0;
int numTestSamples;
int zeroPadding;
int totalTestSamples;
juce::AudioBuffer<float> buffer;
std::unique_ptr<BasicPitchDetector> pitchDetector;
public:
void generateSineWave(float frequency) {
const float amplitude = 0.5f;
buffer.clear();
float* data = buffer.getWritePointer(0);

for (size_t i = 0; i < static_cast<size_t>(numTestSamples); ++i)
{
data[static_cast<size_t>(zeroPadding) + i] = amplitude * std::sinf(2.0f * juce::MathConstants<float>::pi * frequency * (i / static_cast<float>(sampleRate)));
}
}

void loadAndProcessAudio() {
juce::File wavFile = juce::File::getCurrentWorkingDirectory()
.getChildFile("test/resources/audio/female_voice_D4_[16bit_44100Hz].wav");

sampleRate = 0.0;

if (WavImporter::loadWavFileIntoBuffer(wavFile, buffer, sampleRate)) {
DBG("Successfully loaded WAV file!");

} else {
FAIL() << "Failed to load WAV file.";
}
}
};
}
Loading