From 8e61d6f2802d4bfe3a24d654dacc3469ed0676a3 Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 8 Jul 2023 13:25:35 +0100 Subject: [PATCH 1/3] Add core functionality for audio buffer producers and consumers --- Source/ObjComponent.cpp | 1 - Source/PluginEditor.cpp | 8 +- Source/PluginEditor.h | 3 + Source/PluginProcessor.cpp | 6 +- Source/PluginProcessor.h | 3 + Source/concurrency/BufferConsumer.h | 124 ++++++++++++++++++++++++++++ Source/concurrency/BufferProducer.h | 63 ++++++++++++++ osci-render.jucer | 6 ++ 8 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 Source/concurrency/BufferConsumer.h create mode 100644 Source/concurrency/BufferProducer.h diff --git a/Source/ObjComponent.cpp b/Source/ObjComponent.cpp index d82ec30..446881a 100644 --- a/Source/ObjComponent.cpp +++ b/Source/ObjComponent.cpp @@ -87,7 +87,6 @@ ObjComponent::ObjComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessor Util::changeSvgColour(doc.get(), "white"); fixedRotateWhite = juce::Drawable::createFromSVG(*doc); Util::changeSvgColour(doc.get(), "red"); - DBG(doc->toString()); fixedRotateRed = juce::Drawable::createFromSVG(*doc); // TODO: any way of removing this duplication? diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 452b5f4..84cd717 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -42,6 +42,9 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr path.addTriangle(0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.0f); collapseButton.setShape(path, false, true, true); + audioProcessor.audioProducer.registerConsumer(audioConsumer); + dummyConsumer.startThread(); + juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); for (int i = 0; i < audioProcessor.numFiles(); i++) { addCodeEditor(i); @@ -52,7 +55,10 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr setResizable(true, true); } -OscirenderAudioProcessorEditor::~OscirenderAudioProcessorEditor() {} +OscirenderAudioProcessorEditor::~OscirenderAudioProcessorEditor() { + audioProcessor.audioProducer.unregisterConsumer(audioConsumer); + dummyConsumer.stopThread(1000); +} //============================================================================== void OscirenderAudioProcessorEditor::paint (juce::Graphics& g) diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index abb4211..639780d 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -44,6 +44,9 @@ private: juce::XmlTokeniser xmlTokeniser; juce::ShapeButton collapseButton; + std::shared_ptr audioConsumer = std::make_shared(50000); + DummyConsumer dummyConsumer{audioConsumer}; + void codeDocumentTextInserted(const juce::String& newText, int insertIndex) override; void codeDocumentTextDeleted(int startIndex, int endIndex) override; void updateCodeDocument(); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 699867e..d083577 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -118,8 +118,7 @@ void OscirenderAudioProcessor::changeProgramName (int index, const juce::String& } //============================================================================== -void OscirenderAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) -{ +void OscirenderAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) { currentSampleRate = sampleRate; updateAngleDelta(); } @@ -449,7 +448,6 @@ void OscirenderAudioProcessor::processBlock (juce::AudioBuffer& buffer, j x = std::max(-1.0, std::min(1.0, x)); y = std::max(-1.0, std::min(1.0, y)); - if (totalNumOutputChannels >= 2) { channelData[0][sample] = x; channelData[1][sample] = y; @@ -457,6 +455,8 @@ void OscirenderAudioProcessor::processBlock (juce::AudioBuffer& buffer, j channelData[0][sample] = x; } + audioProducer.write(x, y); + actualTraceMax = std::max(actualTraceMin + MIN_TRACE, std::min(traceMax->getValue(), 1.0)); actualTraceMin = std::max(MIN_TRACE, std::min(traceMin->getValue(), actualTraceMax - MIN_TRACE)); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 23fae77..3be1759 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -15,6 +15,7 @@ #include "parser/FrameConsumer.h" #include "audio/Effect.h" #include +#include "concurrency/BufferProducer.h" //============================================================================== /** @@ -167,6 +168,8 @@ public: std::unique_ptr producer; + BufferProducer audioProducer; + void addLuaSlider(); void updateAngleDelta(); void addFrame(std::vector> frame, int fileIndex) override; diff --git a/Source/concurrency/BufferConsumer.h b/Source/concurrency/BufferConsumer.h new file mode 100644 index 0000000..29a4646 --- /dev/null +++ b/Source/concurrency/BufferConsumer.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +// This is a helper class for the producer and consumer threads. +// +// ORDER OF OPERATIONS: +// 1. Consumer is created. +// 2. The thread that owns the consumers calls registerConsumer() on the producer, which acquires the lock on the first buffer to be written to by calling getBuffer(). +// LOOP: +// 3. The consumer calls startProcessing() to signal that they want to start processing the current buffer. +// 4. The producer calls finishedWriting() to signal that they have finished writing to the current buffer, which gives the lock to the consumer. +// 5. The consumer calls finishedProcessing() to signal that they have finished processing the current buffer. +// 6. The producer calls getBuffer() to acquire the lock on the next buffer to be written to. +// GOTO LOOP +// 7. The thread that owns the consumer calls unregisterConsumer() on the producer at some point during the loop, which releases the lock on the current buffer. +// +class BufferConsumer { +public: + // bufferSize MUST be a multiple of 2. + BufferConsumer(int bufferSize) { + firstBuffer->resize(bufferSize, 0.0); + secondBuffer->resize(bufferSize, 0.0); + } + + ~BufferConsumer() {} + + // Returns the buffer that is ready to be written to. + // This is only called by the producer thread. + // force forces the lock to be acquired. + // Returns nullptr if the lock can't be acquired when force is false. + // It is only called when the global producer lock is held. + std::shared_ptr> getBuffer(bool force) { + auto buffer = firstBufferWriting ? firstBuffer : secondBuffer; + if (lockHeldForWriting) { + return buffer; + } + auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock; + + if (force) { + bufferLock->enter(); + lockHeldForWriting = true; + return buffer; + } else if (bufferLock->tryEnter()) { + lockHeldForWriting = true; + return buffer; + } else { + return nullptr; + } + } + + // This is only called by the producer thread. It is only called when the global + // producer lock is held. + void finishedWriting() { + auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock; + lockHeldForWriting = false; + firstBufferWriting = !firstBufferWriting; + // Try locking before we unlock the current buffer so that + // the consumer doesn't start processing before we + // unlock the buffer. Ignore if we can't get the lock + // because the consumer is still processing. + getBuffer(false); + bufferLock->exit(); + } + + void releaseLock() { + if (lockHeldForWriting) { + auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock; + bufferLock->exit(); + } + } + + // Returns the buffer that has been written to fully and is ready to be processed. + // This will lock the buffer so that the producer can't write to it while we're processing. + std::shared_ptr> startProcessing() { + auto buffer = firstBufferProcessing ? firstBuffer : secondBuffer; + auto bufferLock = firstBufferProcessing ? firstBufferLock : secondBufferLock; + + bufferLock->enter(); + return buffer; + } + + // This should be called after processing has finished. + // It releases the lock on the buffer so that the producer can write to it again. + void finishedProcessing() { + auto bufferLock = firstBufferProcessing ? firstBufferLock : secondBufferLock; + firstBufferProcessing = !firstBufferProcessing; + bufferLock->exit(); + } + + std::shared_ptr> firstBuffer = std::make_shared>(); + std::shared_ptr> secondBuffer = std::make_shared>(); + std::shared_ptr firstBufferLock = std::make_shared(); + std::shared_ptr secondBufferLock = std::make_shared(); +private: + // Indirectly used by the producer to signal whether it holds the lock on the buffer. + // This is accurate if the global producer lock is held as the buffer lock is acquired + // and this is set to true before the global producer lock is released. + bool lockHeldForWriting = false; + bool firstBufferWriting = true; + bool firstBufferProcessing = true; +}; + +class DummyConsumer : public juce::Thread { +public: + DummyConsumer(std::shared_ptr consumer) : juce::Thread("DummyConsumer"), consumer(consumer) {} + ~DummyConsumer() {} + + void run() override { + while (!threadShouldExit()) { + auto buffer = consumer->startProcessing(); + + float total = 0.0; + for (int i = 0; i < buffer->size(); i++) { + total += (*buffer)[i]; + } + DBG(total); + + consumer->finishedProcessing(); + } + } +private: + std::shared_ptr consumer; +}; \ No newline at end of file diff --git a/Source/concurrency/BufferProducer.h b/Source/concurrency/BufferProducer.h new file mode 100644 index 0000000..2ac95bc --- /dev/null +++ b/Source/concurrency/BufferProducer.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include "BufferConsumer.h" + +class BufferProducer { +public: + BufferProducer() {} + ~BufferProducer() {} + + // This should add the buffers and locks to the vectors + // and then lock the first buffer lock so it can start + // being written to. + // This is only called by the thread that owns the consumer thread. + void registerConsumer(std::shared_ptr consumer) { + juce::SpinLock::ScopedLockType scope(lock); + consumers.push_back(consumer); + bufferPositions.push_back(0); + consumer->getBuffer(true); + } + + // This is only called by the thread that owns the consumer thread. + // This can't happen at the same time as write() it locks the producer lock. + void unregisterConsumer(std::shared_ptr consumer) { + juce::SpinLock::ScopedLockType scope(lock); + for (int i = 0; i < consumers.size(); i++) { + if (consumers[i] == consumer) { + consumer->releaseLock(); + consumers.erase(consumers.begin() + i); + bufferPositions.erase(bufferPositions.begin() + i); + break; + } + } + } + + // Writes a sample to the current buffer for all consumers. + void write(float left, float right) { + juce::SpinLock::ScopedLockType scope(lock); + for (int i = 0; i < consumers.size(); i++) { + std::shared_ptr> buffer = consumers[i]->getBuffer(false); + if (buffer == nullptr) { + continue; + } + + (*buffer)[bufferPositions[i]] = left; + (*buffer)[bufferPositions[i] + 1] = right; + bufferPositions[i] += 2; + + // If we've reached the end of the buffer, switch + // to the other buffer and unlock it. This signals + // to the consumer that it can start processing! + if (bufferPositions[i] >= buffer->size()) { + bufferPositions[i] = 0; + consumers[i]->finishedWriting(); + } + } + } + +private: + juce::SpinLock lock; + std::vector> consumers; + std::vector bufferPositions; +}; \ No newline at end of file diff --git a/osci-render.jucer b/osci-render.jucer index 940f71f..7be2cac 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -24,6 +24,12 @@ + + + + From 061595b575005eea1eb298bd940d8b5bafb29062 Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 8 Jul 2023 18:59:05 +0100 Subject: [PATCH 2/3] Add proof of concept audio visualiser component --- Source/EffectsComponent.cpp | 1 + Source/EffectsComponent.h | 1 + Source/MainComponent.cpp | 9 +++ Source/MainComponent.h | 5 ++ Source/PluginEditor.cpp | 8 +-- Source/PluginEditor.h | 19 +----- Source/components/VisualiserComponent.cpp | 72 +++++++++++++++++++++++ Source/components/VisualiserComponent.h | 51 ++++++++++++++++ Source/concurrency/BufferConsumer.h | 5 +- osci-render.jucer | 4 ++ 10 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 Source/components/VisualiserComponent.cpp create mode 100644 Source/components/VisualiserComponent.h diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp index 0878153..c19ff95 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -1,5 +1,6 @@ #include "EffectsComponent.h" #include "audio/BitCrushEffect.h" +#include "PluginEditor.h" EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p) : audioProcessor(p), itemData(p), listBoxModel(listBox, itemData) { setText("Audio Effects"); diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h index 76917d9..213acd5 100644 --- a/Source/EffectsComponent.h +++ b/Source/EffectsComponent.h @@ -6,6 +6,7 @@ #include "components/DraggableListBox.h" #include "components/EffectsListComponent.h" +class OscirenderAudioProcessorEditor; class EffectsComponent : public juce::GroupComponent { public: EffectsComponent(OscirenderAudioProcessor&); diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index 1d7d0b4..d76414e 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -77,9 +77,15 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess fileName.onReturnKey = [this] { createFile.triggerClick(); }; + + addAndMakeVisible(visualiser); + audioProcessor.audioProducer.registerConsumer(consumer); + visualiserProcessor.startThread(); } MainComponent::~MainComponent() { + audioProcessor.audioProducer.unregisterConsumer(consumer); + visualiserProcessor.stopThread(1000); } void MainComponent::updateFileLabel() { @@ -112,4 +118,7 @@ void MainComponent::resized() { fileType.setBounds(row.removeFromLeft(buttonWidth / 2)); row.removeFromLeft(rowPadding); createFile.setBounds(row.removeFromLeft(buttonWidth)); + + bounds.removeFromTop(padding); + visualiser.setBounds(bounds); } diff --git a/Source/MainComponent.h b/Source/MainComponent.h index ac98211..2c846a5 100644 --- a/Source/MainComponent.h +++ b/Source/MainComponent.h @@ -4,6 +4,7 @@ #include "PluginProcessor.h" #include "parser/FileParser.h" #include "parser/FrameProducer.h" +#include "components/VisualiserComponent.h" class OscirenderAudioProcessorEditor; class MainComponent : public juce::GroupComponent { @@ -26,5 +27,9 @@ private: juce::ComboBox fileType; juce::TextButton createFile{"Create File"}; + VisualiserComponent visualiser{2, audioProcessor}; + std::shared_ptr consumer = std::make_shared(2048); + VisualiserProcessor visualiserProcessor{consumer, visualiser}; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) }; \ No newline at end of file diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 84cd717..452b5f4 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -42,9 +42,6 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr path.addTriangle(0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.0f); collapseButton.setShape(path, false, true, true); - audioProcessor.audioProducer.registerConsumer(audioConsumer); - dummyConsumer.startThread(); - juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); for (int i = 0; i < audioProcessor.numFiles(); i++) { addCodeEditor(i); @@ -55,10 +52,7 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr setResizable(true, true); } -OscirenderAudioProcessorEditor::~OscirenderAudioProcessorEditor() { - audioProcessor.audioProducer.unregisterConsumer(audioConsumer); - dummyConsumer.stopThread(1000); -} +OscirenderAudioProcessorEditor::~OscirenderAudioProcessorEditor() {} //============================================================================== void OscirenderAudioProcessorEditor::paint (juce::Graphics& g) diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 639780d..de9d5f3 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -1,11 +1,3 @@ -/* - ============================================================================== - - This file contains the basic framework code for a JUCE plugin editor. - - ============================================================================== -*/ - #pragma once #include @@ -15,16 +7,12 @@ #include "LuaComponent.h" #include "ObjComponent.h" -//============================================================================== -/** -*/ -class OscirenderAudioProcessorEditor : public juce::AudioProcessorEditor, private juce::CodeDocument::Listener -{ + +class OscirenderAudioProcessorEditor : public juce::AudioProcessorEditor, private juce::CodeDocument::Listener { public: OscirenderAudioProcessorEditor (OscirenderAudioProcessor&); ~OscirenderAudioProcessorEditor() override; - //============================================================================== void paint (juce::Graphics&) override; void resized() override; @@ -44,9 +32,6 @@ private: juce::XmlTokeniser xmlTokeniser; juce::ShapeButton collapseButton; - std::shared_ptr audioConsumer = std::make_shared(50000); - DummyConsumer dummyConsumer{audioConsumer}; - void codeDocumentTextInserted(const juce::String& newText, int insertIndex) override; void codeDocumentTextDeleted(int startIndex, int endIndex) override; void updateCodeDocument(); diff --git a/Source/components/VisualiserComponent.cpp b/Source/components/VisualiserComponent.cpp new file mode 100644 index 0000000..1ffaeb0 --- /dev/null +++ b/Source/components/VisualiserComponent.cpp @@ -0,0 +1,72 @@ +#include "VisualiserComponent.h" + +VisualiserComponent::VisualiserComponent(int numChannels, OscirenderAudioProcessor& p) : numChannels(numChannels), backgroundColour(juce::Colours::black), waveformColour(juce::Colour(0xff00ff00)), audioProcessor(p) { + setOpaque(true); +} + +VisualiserComponent::~VisualiserComponent() {} + +void VisualiserComponent::setBuffer(std::vector& newBuffer) { + juce::SpinLock::ScopedLockType scope(lock); + buffer.clear(); + for (int i = 0; i < newBuffer.size(); i += precision * numChannels) { + buffer.push_back(newBuffer[i]); + buffer.push_back(newBuffer[i + 1]); + } +} + +void VisualiserComponent::setColours(juce::Colour bk, juce::Colour fg) { + backgroundColour = bk; + waveformColour = fg; +} + +void VisualiserComponent::paint(juce::Graphics& g) { + g.fillAll(backgroundColour); + + auto r = getLocalBounds().toFloat(); + auto channelHeight = r.getHeight() / (float)numChannels; + + g.setColour(waveformColour); + juce::SpinLock::ScopedLockType scope(lock); + paintXY(g, r.removeFromRight(r.getHeight())); + + for (int i = 0; i < numChannels; ++i) { + paintChannel(g, r.removeFromTop(channelHeight), i); + } +} + +void VisualiserComponent::paintChannel(juce::Graphics& g, juce::Rectangle area, int channel) { + juce::Path path; + + for (int i = 0; i < buffer.size(); i += numChannels) { + auto sample = buffer[i + channel]; + + if (i == 0) { + path.startNewSubPath(0.0f, sample); + } else { + path.lineTo((float)i, sample); + } + } + + // apply affine transform to path to fit in area + auto transform = juce::AffineTransform::fromTargetPoints(0.0f, -1.0f, area.getX(), area.getY(), 0.0f, 1.0f, area.getX(), area.getBottom(), buffer.size(), -1.0f, area.getRight(), area.getY()); + path.applyTransform(transform); + + g.strokePath(path, juce::PathStrokeType(1.0f)); +} + +void VisualiserComponent::paintXY(juce::Graphics& g, juce::Rectangle area) { + juce::Path path; + + path.startNewSubPath(buffer[0], buffer[1]); + + for (int i = 2; i < buffer.size(); i += 2) { + path.lineTo(buffer[i + 0], buffer[i + 1]); + } + + // apply affine transform to path to fit in area + auto transform = juce::AffineTransform::fromTargetPoints(-1.0f, -1.0f, area.getX(), area.getBottom(), 1.0f, 1.0f, area.getRight(), area.getY(), 1.0f, -1.0f, area.getRight(), area.getBottom()); + path.applyTransform(transform); + + g.strokePath(path, juce::PathStrokeType(1.0f)); +} diff --git a/Source/components/VisualiserComponent.h b/Source/components/VisualiserComponent.h new file mode 100644 index 0000000..13b481c --- /dev/null +++ b/Source/components/VisualiserComponent.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include "../concurrency/BufferConsumer.h" +#include "../PluginProcessor.h" + +class VisualiserComponent : public juce::Component { +public: + VisualiserComponent(int numChannels, OscirenderAudioProcessor& p); + ~VisualiserComponent() override; + + void setBuffer(std::vector& buffer); + void setColours(juce::Colour backgroundColour, juce::Colour waveformColour); + void paintChannel(juce::Graphics&, juce::Rectangle bounds, int channel); + void paintXY(juce::Graphics&, juce::Rectangle bounds); + void paint(juce::Graphics&) override; + +private: + juce::SpinLock lock; + std::vector buffer; + int numChannels = 2; + juce::Colour backgroundColour, waveformColour; + OscirenderAudioProcessor& audioProcessor; + int precision = 2; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VisualiserComponent) +}; + +class VisualiserProcessor : public juce::AsyncUpdater, public juce::Thread { +public: + VisualiserProcessor(std::shared_ptr consumer, VisualiserComponent& visualiser) : juce::Thread("VisualiserProcessor"), consumer(consumer), visualiser(visualiser) {} + ~VisualiserProcessor() override {} + + void run() override { + while (!threadShouldExit()) { + auto buffer = consumer->startProcessing(); + + visualiser.setBuffer(*buffer); + consumer->finishedProcessing(); + triggerAsyncUpdate(); + } + } + + void handleAsyncUpdate() override { + visualiser.repaint(); + } + +private: + std::shared_ptr consumer; + VisualiserComponent& visualiser; +}; \ No newline at end of file diff --git a/Source/concurrency/BufferConsumer.h b/Source/concurrency/BufferConsumer.h index 29a4646..889cf16 100644 --- a/Source/concurrency/BufferConsumer.h +++ b/Source/concurrency/BufferConsumer.h @@ -17,10 +17,9 @@ // class BufferConsumer { public: - // bufferSize MUST be a multiple of 2. BufferConsumer(int bufferSize) { - firstBuffer->resize(bufferSize, 0.0); - secondBuffer->resize(bufferSize, 0.0); + firstBuffer->resize(2 * bufferSize, 0.0); + secondBuffer->resize(2 * bufferSize, 0.0); } ~BufferConsumer() {} diff --git a/osci-render.jucer b/osci-render.jucer index 7be2cac..70011a0 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -123,6 +123,10 @@ file="Source/components/LuaListComponent.cpp"/> + + Date: Sun, 9 Jul 2023 21:30:33 +0100 Subject: [PATCH 3/3] 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 d76414e..c783a2b 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 2c846a5..a05ce88 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 0000000..5ebb9f8 --- /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 0000000..8a713f7 --- /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 1ffaeb0..fba115f 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 13b481c..1d39a73 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 70011a0..4263097 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 @@ +