diff --git a/Moha.jucer b/Moha.jucer
index 6205ad2..4442d33 100644
--- a/Moha.jucer
+++ b/Moha.jucer
@@ -19,6 +19,10 @@
+
+
+
@@ -89,7 +95,7 @@
-
+
diff --git a/Source/DSP/CircularBuffer.cpp b/Source/DSP/CircularBuffer.cpp
index 35438eb..6df9359 100644
--- a/Source/DSP/CircularBuffer.cpp
+++ b/Source/DSP/CircularBuffer.cpp
@@ -118,8 +118,10 @@ void CircularBuffer::resetBufferFromCircularBuffer(juce::AudioBuffer& buf
}
}
}
-
-void CircularBuffer::copyFromEveryChannel(juce::AudioBuffer& dest, const int destStartSample, const juce::AudioBuffer& source, const int sourceStartSample, const int numSamples)
+
+void CircularBuffer::copyFromEveryChannel(juce::AudioBuffer& dest, const int destStartSample,
+ const juce::AudioBuffer& source, const int sourceStartSample,
+ const int numSamples)
{
if (dest.getNumChannels() != source.getNumChannels())
{
diff --git a/Source/DSP/CircularBuffer.h b/Source/DSP/CircularBuffer.h
index f77e4fd..357e3e2 100644
--- a/Source/DSP/CircularBuffer.h
+++ b/Source/DSP/CircularBuffer.h
@@ -36,5 +36,7 @@ class CircularBuffer
int resetWritePosition{ 0 }, resetReadPosition{ 0 };
int CBNumChannels{ 0 }, CBBufferSize{ 0 };
- void copyFromEveryChannel(juce::AudioBuffer& dest, const int destStartSample, const juce::AudioBuffer& source, const int sourceStartSample, const int numSamples);
+ void copyFromEveryChannel(juce::AudioBuffer& dest, const int destStartSample,
+ const juce::AudioBuffer& source, const int sourceStartSample,
+ const int numSamples);
};
\ No newline at end of file
diff --git a/Source/DSP/SpectrumProcessor.h b/Source/DSP/SpectrumProcessor.h
new file mode 100644
index 0000000..5a41c5a
--- /dev/null
+++ b/Source/DSP/SpectrumProcessor.h
@@ -0,0 +1,70 @@
+#include
+#pragma once
+
+class SpectrumProcessor
+{
+public:
+ SpectrumProcessor() : forwardFFT (fftOrder), window (fftSize, juce::dsp::WindowingFunction::hamming)
+ {
+ window.fillWindowingTables (fftSize, juce::dsp::WindowingFunction::blackman);
+ }
+
+ enum
+ {
+ fftOrder = 11,
+ fftSize = 1 << fftOrder, // 2048£, frequency resolution is 48kHz / 2048 = 23.44Hz
+ numBins = fftSize / 2 // 1024
+ };
+
+ float fftData[2 * fftSize] = { 0 };
+ bool nextFFTBlockReady = false;
+
+ // Push each sample into fifo, if the size of fifo reaches fftSize and nextFFTBlockReady is false, copy the data from fifo to fftData
+ void pushNextSampleIntoFifo (float sample) noexcept
+ {
+ if (fifoIndex == fftSize)
+ {
+ if (!nextFFTBlockReady)
+ {
+ juce::zeromem(fftData, sizeof(fftData));
+ memmove(fftData, fifo, sizeof (fifo)); // memmove is safer, but memcpy is faster
+ nextFFTBlockReady = true;
+ }
+
+ fifoIndex = 0;
+ }
+
+ fifo[fifoIndex++] = sample;
+ }
+
+ // Get current buffer from processBlock
+ void pushDataToFFT(juce::AudioBuffer& audioBuffer)
+ {
+ if (audioBuffer.getNumChannels() > 0)
+ {
+ auto* channelData = audioBuffer.getReadPointer(0);
+
+ for (auto i = 0; i < audioBuffer.getNumSamples(); ++i)
+ pushNextSampleIntoFifo(channelData[i]);
+ }
+ }
+
+ void processFFT(float* tempFFTData)
+ {
+ // Add windows before processing to prevent spectrum leakage
+ window.multiplyWithWindowingTable(tempFFTData, fftSize);
+ forwardFFT.performFrequencyOnlyForwardTransform(tempFFTData);
+
+ nextFFTBlockReady = false;
+ }
+
+ float* getFFTData() { return fftData; }
+ int getNumBins() { return numBins; }
+ int getFFTSize() { return fftSize; }
+
+private:
+ float fifo[fftSize];
+ juce::dsp::FFT forwardFFT;
+ juce::dsp::WindowingFunction window;
+ int fifoIndex = 0;
+};
diff --git a/Source/GUI/SpectrumComponent.cpp b/Source/GUI/SpectrumComponent.cpp
new file mode 100644
index 0000000..acd8038
--- /dev/null
+++ b/Source/GUI/SpectrumComponent.cpp
@@ -0,0 +1,227 @@
+/*
+ ==============================================================================
+
+ SpectrumComponent.cpp
+ Created: 19 Oct 2023 10:34:27am
+ Author: TaroPie
+
+ ==============================================================================
+*/
+
+#include
+#include "SpectrumComponent.h"
+
+const int SpectrumComponent::frequenciesForLines[] = { 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 20000 };
+const int SpectrumComponent::numberOfLines = 28;
+//==============================================================================
+SpectrumComponent::SpectrumComponent() : numberOfBins(1024), mBinWidth(44100 / (float)2048)
+{
+}
+
+SpectrumComponent::~SpectrumComponent()
+{
+}
+
+void SpectrumComponent::paint(juce::Graphics& g)
+{
+ // paint background
+ //g.setColour(juce::Colour(40, 40, 40));
+ //g.fillAll();
+
+ // paint horizontal lines and frequency numbers
+ g.setColour(juce::Colours::lightgrey.withAlpha(0.2f));
+ g.drawLine(0, getHeight() / 5, getWidth(), getHeight() / 5, 1);
+
+ for (int i = 0; i < numberOfLines; ++i)
+ {
+ const double proportion = frequenciesForLines[i] / 20000.0;
+ int xPos = transformToLog(proportion * 20000) * (getWidth());
+ g.drawVerticalLine(xPos, getHeight() / 5, getHeight());
+ if (frequenciesForLines[i] == 10 || frequenciesForLines[i] == 100 || frequenciesForLines[i] == 200)
+ g.drawFittedText(static_cast (frequenciesForLines[i]), xPos - 30, 0, 60, getHeight() / 5, juce::Justification::centred, 2);
+ else if (frequenciesForLines[i] == 1000 || frequenciesForLines[i] == 10000 || frequenciesForLines[i] == 2000)
+ g.drawFittedText(static_cast (frequenciesForLines[i] / 1000) + "k", xPos - 30, 0, 60, getHeight() / 5, juce::Justification::centred, 2);
+ else if (frequenciesForLines[i] == 20)
+ g.drawFittedText(static_cast (frequenciesForLines[i]), xPos - 30, 0, 60, getHeight() / 5, juce::Justification::right, 2);
+ else if (frequenciesForLines[i] == 20000)
+ g.drawFittedText(static_cast (frequenciesForLines[i] / 1000) + "k", xPos - 30, 0, 60, getHeight() / 5, juce::Justification::left, 2);
+ }
+
+ // paint vertical db numbers
+ // float fontWidth = 50;
+ // float fontHeight = getHeight() / 5;
+ // float centerAlign = fontHeight / 2;
+ // g.drawFittedText("-20 db", 0, getHeight() / 6 * 2 - centerAlign, fontWidth, fontHeight, juce::Justification::centred, 2);
+ // g.drawFittedText("-40 db", 0, getHeight() / 6 * 3 - centerAlign, fontWidth, fontHeight, juce::Justification::centred, 2);
+ // g.drawFittedText("-60 db", 0, getHeight() / 6 * 4 - centerAlign, fontWidth, fontHeight, juce::Justification::centred, 2);
+ // g.drawFittedText("-80 db", 0, getHeight() / 6 * 5 - centerAlign, fontWidth, fontHeight, juce::Justification::centred, 2);
+
+ // paint current spectrum
+ g.setColour(juce::Colours::white);
+ paintSpectrum();
+ currentSpectrumImage.multiplyAllAlphas(0.9);
+ currentSpectrumImage.moveImageSection(0, 10, 0, 0, currentSpectrumImage.getWidth(), currentSpectrumImage.getHeight());
+ g.drawImageAt(currentSpectrumImage, 0, 0);
+
+ // paint peak spectrum
+ //maxSpectrumImage.multiplyAllAlphas(0.5);
+ //g.drawImageAt(maxSpectrumImage, 0, 0);
+
+ // paint peak text
+ //float mouseX = getMouseXYRelative().getX();
+ //float mouseY = getMouseXYRelative().getY();
+
+ //if (mouseX > 0 && mouseX < getWidth()
+ // && mouseY > 0 && mouseY < getHeight())
+ //{
+ // mouseOver = true;
+ //}
+ //else
+ //{
+ // mouseOver = false;
+ //}
+
+ //if (maxDecibelValue >= -99.9f && mouseOver)
+ //{
+ // float boxWidth = 100.0f;
+ // g.setColour(juce::Colours::lightgrey);
+ // g.drawText(juce::String(maxDecibelValue, 1) + " db", maxDecibelPoint.getX() - boxWidth / 2.0f, maxDecibelPoint.getY() - boxWidth / 4.0f, boxWidth, boxWidth, juce::Justification::centred);
+ // g.drawText(juce::String(static_cast (maxFreq)) + " Hz", maxDecibelPoint.getX() - boxWidth / 2.0f, maxDecibelPoint.getY(), boxWidth, boxWidth, juce::Justification::centred);
+ //}
+ //else
+ //{
+ // maxDecibelValue = -100.0f;
+ // maxFreq = 0.0f;
+ // maxDecibelPoint.setXY(-10.0f, -10.0f);
+ // for (int i = 0; i < 1024; i++)
+ // {
+ // maxData[i] = 0;
+ // }
+ //}
+}
+
+void SpectrumComponent::resized()
+{
+ // This method is where you should set the bounds of any child
+ // components that your component contains..
+ currentSpectrumImage = currentSpectrumImage.rescaled(getWidth(), getHeight());
+ maxSpectrumImage = maxSpectrumImage.rescaled(getWidth(), getHeight());
+}
+
+void SpectrumComponent::paintSpectrum()
+{
+ // this method is to paint spectrogram
+
+ // init graphics
+ juce::Graphics gCurrent(currentSpectrumImage);
+ //juce::Graphics gMax(maxSpectrumImage);
+
+ auto width = getLocalBounds().getWidth();
+ auto height = getLocalBounds().getHeight();
+ auto mindB = -100.0f;
+ auto maxdB = 0.0f;
+
+ juce::Path currentSpecPath;
+ currentSpecPath.startNewSubPath(0, height);
+
+ //juce::Path maxSpecPath;
+ //maxSpecPath.startNewSubPath(0, height + 1);
+ int resolution = 2;
+ for (int i = 1; i < numberOfBins; i += resolution)
+ {
+ // sample range [0, 1] to decibel range[-100, 0] to [0, 1]
+ auto fftSize = 1 << 11;
+ float currentDecibel = juce::Decibels::gainToDecibels(spectrumData[i] / static_cast(numberOfBins));
+ //float maxDecibel = juce::Decibels::gainToDecibels(maxData[i])
+ // - juce::Decibels::gainToDecibels(static_cast(fftSize));
+ float yPercent = juce::jmap(juce::jlimit(mindB, maxdB, currentDecibel),
+ mindB,
+ maxdB,
+ 0.0f,
+ 1.0f);
+ //float yMaxPercent = juce::jmap(juce::jlimit(mindB, maxdB, maxDecibel),
+ // mindB,
+ // maxdB,
+ // 0.0f,
+ // 1.0f);
+ // skip some points to save cpu
+ // if (i > numberOfBins / 8 && i % 2 != 0) continue;
+ // if (i > numberOfBins / 4 && i % 3 != 0) continue;
+ // if (i > numberOfBins / 2 && i % 4 != 0) continue;
+ // if (i > numberOfBins / 4 * 3 && i % 10 != 0) continue;
+
+ // connect points
+ double currentFreq = i * mBinWidth;
+ float currentX = transformToLog(currentFreq) * width;
+ float currentY = juce::jmap(yPercent, 0.0f, 1.0f, (float)height, 0.0f);
+ //float maxY = juce::jmap(yMaxPercent, 0.0f, 1.0f, (float)height, 0.0f);
+ currentSpecPath.lineTo(currentX, currentY);
+
+ //maxSpecPath.lineTo(currentX, maxY);
+
+ //if (currentDecibel > maxDecibelValue)
+ //{
+ // maxDecibelValue = currentDecibel;
+ // maxFreq = currentFreq;
+ // maxDecibelPoint.setXY(currentX, currentY);
+ //}
+ //if (spectrumData[i] > maxData[i])
+ //{
+ // maxData[i] = spectrumData[i];
+ //}
+
+ // reference: https://docs.juce.com/master/tutorial_spectrum_analyser.html
+ }
+
+ // this step is to round the path
+ juce::Path roundedCurrentPath = currentSpecPath.createPathWithRoundedCorners(10.0f);
+
+ // draw the outline of the path
+ roundedCurrentPath.lineTo(width, height);
+ roundedCurrentPath.lineTo(0, height);
+ roundedCurrentPath.closeSubPath();
+
+ //juce::Path roundedMaxPath = maxSpecPath.createPathWithRoundedCorners(10.0f);
+ //roundedMaxPath.lineTo(width, height + 1);
+ // roundedMaxPath.lineTo(0, height);
+ // roundedMaxPath.closeSubPath();
+
+ gCurrent.setColour(juce::Colour(244, 208, 63));
+
+ juce::ColourGradient grad(juce::Colours::red.withAlpha(0.8f), 0, 0, juce::Colour(244, 208, 63).withAlpha(0.8f), 0, getLocalBounds().getHeight(), false);
+
+ gCurrent.setGradientFill(grad);
+ gCurrent.fillPath(currentSpecPath);
+ // g.strokePath(roundedPath, juce::PathStrokeType(2));
+
+ //if (mouseOver)
+ //{
+ // gMax.setColour(juce::Colours::white);
+ // gMax.strokePath(roundedMaxPath, juce::PathStrokeType(2));
+ // gMax.drawEllipse(maxDecibelPoint.getX() - 2.0f, maxDecibelPoint.getY() + 10.0f, 4.0f, 4.0f, 1.0f);
+ //}
+}
+
+void SpectrumComponent::prepareToPaintSpectrum(int numBins, float* data, float binWidth)
+{
+ numberOfBins = numBins;
+ memmove(spectrumData, data, sizeof(spectrumData));
+ mBinWidth = binWidth;
+}
+
+float SpectrumComponent::transformToLog(double valueToTransform) // freq to x
+{
+ // input: 20-20000
+ // output: x
+ auto value = juce::mapFromLog10(valueToTransform, 20.0, 20000.0);
+ return static_cast (value);
+}
+
+float SpectrumComponent::transformFromLog(double between0and1) // x to freq
+{
+ // input: 0.1-0.9 x pos
+ // output: freq
+
+ auto value = juce::mapToLog10(between0and1, 20.0, 20000.0);
+ return static_cast (value);
+}
diff --git a/Source/GUI/SpectrumComponent.h b/Source/GUI/SpectrumComponent.h
new file mode 100644
index 0000000..75f1ef8
--- /dev/null
+++ b/Source/GUI/SpectrumComponent.h
@@ -0,0 +1,47 @@
+/*
+ ==============================================================================
+
+ SpectrumComponent.h
+ Created: 19 Oct 2023 10:34:27am
+ Author: TaroPie
+
+ ==============================================================================
+*/
+
+#pragma once
+
+#include
+
+//==============================================================================
+/*
+*/
+class SpectrumComponent : public juce::Component
+{
+public:
+ SpectrumComponent();
+ ~SpectrumComponent();
+
+ void paint(juce::Graphics& g) override;
+ void prepareToPaintSpectrum(int numberOfBins, float* spectrumData, float binWidth);
+ static float transformToLog(double valueToTransform);
+ static float transformFromLog(double between0and1);
+ void resized() override;
+ void paintSpectrum();
+
+private:
+ int numberOfBins;
+ float spectrumData[1024] = { 0 };
+ float maxData[1024] = { 0 };
+ float maxDecibelValue = -100.0f;
+ float maxFreq = 0.0f;
+ bool mouseOver = false;
+ juce::Point maxDecibelPoint;
+
+ juce::Image currentSpectrumImage = juce::Image(juce::Image::ARGB, 1000, 300, true);
+ juce::Image maxSpectrumImage = juce::Image(juce::Image::ARGB, 1000, 300, true);
+
+ static const int frequenciesForLines[];
+ static const int numberOfLines;
+ float mBinWidth;
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SpectrumComponent)
+};
diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp
index f36e61e..814176d 100644
--- a/Source/PluginEditor.cpp
+++ b/Source/PluginEditor.cpp
@@ -13,9 +13,13 @@
MohaAudioProcessorEditor::MohaAudioProcessorEditor (MohaAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p)
{
+ compileTime = juce::String(__DATE__) + " " + juce::String(__TIME__);
+ // timer
+ juce::Timer::startTimerHz(60.0f);
+
// Make sure that before the constructor has finished, you've set the
// editor's size to whatever you need it to be.
- setSize (800, 400);
+ setSize (800, 600);
//juce::LookAndFeel::setDefaultLookAndFeel(&customLookAndFeel);
//juce::LookAndFeel::setDefaultLookAndFeel(&customStyle);
@@ -57,6 +61,10 @@ MohaAudioProcessorEditor::MohaAudioProcessorEditor (MohaAudioProcessor& p)
volumeSliderAttachment = std::make_unique(audioProcessor.apvts, "Volume", volumeSlider);
addAndMakeVisible(linearSlider);
+
+ addAndMakeVisible(spectrum);
+ spectrum.setInterceptsMouseClicks(false, false);
+ spectrum.prepareToPaintSpectrum(audioProcessor.spectrumProcessor.getNumBins(), audioProcessor.spectrumProcessor.getFFTData(), audioProcessor.getSampleRate() / (float)audioProcessor.spectrumProcessor.getFFTSize());
}
MohaAudioProcessorEditor::~MohaAudioProcessorEditor()
@@ -69,9 +77,10 @@ void MohaAudioProcessorEditor::paint (juce::Graphics& g)
g.fillAll (juce::Colour::fromFloatRGBA(0.f, 0.f, 0.f, 0.65f));
//g.fillAll(juce::Colours::white);
- g.setColour (juce::Colours::white);
- g.setFont (32.0f);
- g.drawFittedText ("MOHA FROG PEDAL FX", getLocalBounds(), juce::Justification::centred, 1);
+ g.setColour(juce::Colours::white);
+ g.setFont(32.0f);
+ //g.drawFittedText("MOHA FROG PEDAL FX", getLocalBounds(), juce::Justification::centred, 1);
+ g.drawFittedText(compileTime, getLocalBounds(), juce::Justification::centred, 1);
}
void MohaAudioProcessorEditor::resized()
@@ -86,6 +95,8 @@ void MohaAudioProcessorEditor::resized()
linearSlider.setBounds(leftRightMargin, topBottomMargin, 200, 30);
+ spectrum.setBounds(leftRightMargin + 250, topBottomMargin, 500, 300);
+
gainSlider.setBounds(leftRightMargin + dialWidth - 6, getHeight() - topBottomMargin - dialHeight, dialWidth, dialHeight);
preHighPassFreqSlider.setBounds(getWidth() - leftRightMargin - dialWidth * 3, getHeight() - 3 * topBottomMargin - 2 * dialHeight, dialWidth, dialHeight);
@@ -120,3 +131,21 @@ void MohaAudioProcessorEditor::createLabel(juce::Label& label, juce::String text
label.setBorderSize(juce::BorderSize(0));
label.attachToComponent(slider, false);
}
+
+void MohaAudioProcessorEditor::timerCallback()
+{
+ if (audioProcessor.spectrumProcessor.nextFFTBlockReady)
+ {
+ // create a temp ddtData because sometimes pushNextSampleIntoFifo will replace the original
+ // fftData after doingProcess and before painting.
+ float tempFFTData[2 * 2048] = { 0 };
+ memmove(tempFFTData, audioProcessor.spectrumProcessor.getFFTData(), sizeof(tempFFTData));
+ // doing process, fifo data to fft data
+ audioProcessor.spectrumProcessor.processFFT(tempFFTData);
+ // prepare to paint the spectrum
+ spectrum.prepareToPaintSpectrum(audioProcessor.spectrumProcessor.getNumBins(), tempFFTData, audioProcessor.getSampleRate() / (float)audioProcessor.spectrumProcessor.getFFTSize());
+
+ spectrum.repaint();
+ //repaint();
+ }
+}
\ No newline at end of file
diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h
index 71bc0c4..501019a 100644
--- a/Source/PluginEditor.h
+++ b/Source/PluginEditor.h
@@ -14,11 +14,12 @@
#include "GUI/CustomLookAndFeel.h"
#include "GUI/RotarySlider.h"
#include "GUI/LinearSlider.h"
+#include "GUI/SpectrumComponent.h"
//==============================================================================
/**
*/
-class MohaAudioProcessorEditor : public juce::AudioProcessorEditor
+class MohaAudioProcessorEditor : public juce::AudioProcessorEditor, public juce::Timer
{
public:
MohaAudioProcessorEditor (MohaAudioProcessor&);
@@ -27,6 +28,7 @@ class MohaAudioProcessorEditor : public juce::AudioProcessorEditor
//==============================================================================
void paint (juce::Graphics&) override;
void resized() override;
+ void timerCallback() override;
private:
// This reference is provided as a quick way for your editor to
@@ -79,5 +81,9 @@ class MohaAudioProcessorEditor : public juce::AudioProcessorEditor
RotarySlider rotarySlider;
LinearSlider linearSlider;
+ SpectrumComponent spectrum;
+
+ juce::String compileTime;
+
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MohaAudioProcessorEditor)
};
diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp
index bf86f95..109a8ce 100644
--- a/Source/PluginProcessor.cpp
+++ b/Source/PluginProcessor.cpp
@@ -178,6 +178,8 @@ void MohaAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::M
{
circularBuffer.isLooping = false;
}
+
+ spectrumProcessor.pushDataToFFT(buffer);
}
//==============================================================================
diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h
index 3d31f68..8524123 100644
--- a/Source/PluginProcessor.h
+++ b/Source/PluginProcessor.h
@@ -12,6 +12,7 @@
#include "Moha.h"
#include "DSP/CircularBuffer.h"
+#include "DSP/SpectrumProcessor.h"
//#include "DSP/TestBufferGene.h"
//==============================================================================
@@ -65,12 +66,14 @@ class MohaAudioProcessor : public juce::AudioProcessor
static APVTS::ParameterLayout createParameterLayout();
APVTS apvts{ *this, nullptr, "Parameters", createParameterLayout() };
+ SpectrumProcessor spectrumProcessor;
+
private:
//==============================================================================
// Components
Moha moha_fx;
CircularBuffer circularBuffer{ 2 , 96000 };
-
+
double makeUpGain = 1;
//==============================================================================