From be64e7325ebbfcd88c5f9e2d82c008aa0710ef69 Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 9 Jul 2023 21:30:33 +0100 Subject: [PATCH] Set audio visualiser to 60fps and show current frequency --- Source/MainComponent.cpp | 6 +++ Source/MainComponent.h | 12 ++++++ Source/audio/PitchDetector.cpp | 51 +++++++++++++++++++++++ Source/audio/PitchDetector.h | 29 +++++++++++++ Source/components/VisualiserComponent.cpp | 5 +++ Source/components/VisualiserComponent.h | 10 ++--- osci-render.jucer | 7 ++++ 7 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 Source/audio/PitchDetector.cpp create mode 100644 Source/audio/PitchDetector.h diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index d76414e1..c783a2bf 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -81,6 +81,9 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess addAndMakeVisible(visualiser); audioProcessor.audioProducer.registerConsumer(consumer); visualiserProcessor.startThread(); + + addAndMakeVisible(frequencyLabel); + pitchDetector.startThread(); } MainComponent::~MainComponent() { @@ -119,6 +122,9 @@ void MainComponent::resized() { row.removeFromLeft(rowPadding); createFile.setBounds(row.removeFromLeft(buttonWidth)); + bounds.removeFromTop(padding); + frequencyLabel.setBounds(bounds.removeFromTop(20)); + bounds.removeFromTop(padding); visualiser.setBounds(bounds); } diff --git a/Source/MainComponent.h b/Source/MainComponent.h index 2c846a5f..a05ce88b 100644 --- a/Source/MainComponent.h +++ b/Source/MainComponent.h @@ -5,6 +5,7 @@ #include "parser/FileParser.h" #include "parser/FrameProducer.h" #include "components/VisualiserComponent.h" +#include "audio/PitchDetector.h" class OscirenderAudioProcessorEditor; class MainComponent : public juce::GroupComponent { @@ -31,5 +32,16 @@ private: std::shared_ptr consumer = std::make_shared(2048); VisualiserProcessor visualiserProcessor{consumer, visualiser}; + juce::Label frequencyLabel; + PitchDetector pitchDetector{ + audioProcessor, + [this](float frequency) { + // round to nearest integer + int roundedFrequency = static_cast(frequency + 0.5f); + frequencyLabel.setText(juce::String(roundedFrequency) + "Hz", juce::dontSendNotification); + + } + }; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) }; \ No newline at end of file diff --git a/Source/audio/PitchDetector.cpp b/Source/audio/PitchDetector.cpp new file mode 100644 index 00000000..5ebb9f85 --- /dev/null +++ b/Source/audio/PitchDetector.cpp @@ -0,0 +1,51 @@ +#include "PitchDetector.h" +#include "PitchDetector.h" + +PitchDetector::PitchDetector(OscirenderAudioProcessor& p, std::function frequencyCallback) : juce::Thread("PitchDetector"), audioProcessor(p), frequencyCallback(frequencyCallback) {} + +PitchDetector::~PitchDetector() { + audioProcessor.audioProducer.unregisterConsumer(consumer); + stopThread(1000); +} + +void PitchDetector::run() { + audioProcessor.audioProducer.registerConsumer(consumer); + + while (!threadShouldExit()) { + auto buffer = consumer->startProcessing(); + + // buffer is for 2 channels, so we need to only use one + for (int i = 0; i < fftSize; i++) { + fftData[i] = buffer->at(2 * i); + } + + forwardFFT.performFrequencyOnlyForwardTransform(fftData.data()); + + // get frequency of the peak + int maxIndex = 0; + for (int i = 0; i < fftSize / 2; ++i) { + if (frequencyFromIndex(i) < 20 || frequencyFromIndex(i) > 20000) { + continue; + } + + auto current = fftData[i]; + if (current > fftData[maxIndex]) { + maxIndex = i; + } + } + + frequency = frequencyFromIndex(maxIndex); + + consumer->finishedProcessing(); + triggerAsyncUpdate(); + } +} + +void PitchDetector::handleAsyncUpdate() { + frequencyCallback(frequency); +} + +float PitchDetector::frequencyFromIndex(int index) { + auto binWidth = audioProcessor.currentSampleRate / fftSize; + return index * binWidth; +} diff --git a/Source/audio/PitchDetector.h b/Source/audio/PitchDetector.h new file mode 100644 index 00000000..8a713f78 --- /dev/null +++ b/Source/audio/PitchDetector.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include "../concurrency/BufferConsumer.h" +#include "../PluginProcessor.h" + +class PitchDetector : public juce::Thread, public juce::AsyncUpdater { +public: + PitchDetector(OscirenderAudioProcessor& p, std::function frequencyCallback); + ~PitchDetector(); + + void run() override; + void handleAsyncUpdate() override; + + std::atomic frequency = 0.0f; + +private: + static constexpr int fftOrder = 15; + static constexpr int fftSize = 1 << fftOrder; + + std::shared_ptr consumer = std::make_shared(fftSize); + juce::dsp::FFT forwardFFT{fftOrder}; + std::array fftData; + OscirenderAudioProcessor& audioProcessor; + std::function frequencyCallback; + + float frequencyFromIndex(int index); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PitchDetector) +}; \ No newline at end of file diff --git a/Source/components/VisualiserComponent.cpp b/Source/components/VisualiserComponent.cpp index 1ffaeb0c..fba115f7 100644 --- a/Source/components/VisualiserComponent.cpp +++ b/Source/components/VisualiserComponent.cpp @@ -2,6 +2,7 @@ VisualiserComponent::VisualiserComponent(int numChannels, OscirenderAudioProcessor& p) : numChannels(numChannels), backgroundColour(juce::Colours::black), waveformColour(juce::Colour(0xff00ff00)), audioProcessor(p) { setOpaque(true); + startTimerHz(60); } VisualiserComponent::~VisualiserComponent() {} @@ -35,6 +36,10 @@ void VisualiserComponent::paint(juce::Graphics& g) { } } +void VisualiserComponent::timerCallback() { + repaint(); +} + void VisualiserComponent::paintChannel(juce::Graphics& g, juce::Rectangle area, int channel) { juce::Path path; diff --git a/Source/components/VisualiserComponent.h b/Source/components/VisualiserComponent.h index 13b481ce..1d39a73d 100644 --- a/Source/components/VisualiserComponent.h +++ b/Source/components/VisualiserComponent.h @@ -4,7 +4,7 @@ #include "../concurrency/BufferConsumer.h" #include "../PluginProcessor.h" -class VisualiserComponent : public juce::Component { +class VisualiserComponent : public juce::Component, public juce::Timer { public: VisualiserComponent(int numChannels, OscirenderAudioProcessor& p); ~VisualiserComponent() override; @@ -14,6 +14,7 @@ public: void paintChannel(juce::Graphics&, juce::Rectangle bounds, int channel); void paintXY(juce::Graphics&, juce::Rectangle bounds); void paint(juce::Graphics&) override; + void timerCallback() override; private: juce::SpinLock lock; @@ -26,7 +27,7 @@ private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VisualiserComponent) }; -class VisualiserProcessor : public juce::AsyncUpdater, public juce::Thread { +class VisualiserProcessor : public juce::Thread { public: VisualiserProcessor(std::shared_ptr consumer, VisualiserComponent& visualiser) : juce::Thread("VisualiserProcessor"), consumer(consumer), visualiser(visualiser) {} ~VisualiserProcessor() override {} @@ -37,14 +38,9 @@ public: visualiser.setBuffer(*buffer); consumer->finishedProcessing(); - triggerAsyncUpdate(); } } - void handleAsyncUpdate() override { - visualiser.repaint(); - } - private: std::shared_ptr consumer; VisualiserComponent& visualiser; diff --git a/osci-render.jucer b/osci-render.jucer index 70011a02..42630974 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -146,6 +146,9 @@ file="Source/audio/EffectApplication.h"/> + + @@ -274,6 +277,7 @@ + @@ -295,6 +299,7 @@ + @@ -316,6 +321,7 @@ + @@ -329,6 +335,7 @@ +