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; //==============================================================================