Skip to content

Commit

Permalink
Add spectrum
Browse files Browse the repository at this point in the history
  • Loading branch information
TaroPie1214 committed Jan 16, 2024
1 parent 9fb6b3b commit 43d89a5
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 10 deletions.
8 changes: 7 additions & 1 deletion Moha.jucer
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
<FILE id="vMDRO4" name="RotarySlider.cpp" compile="1" resource="0"
file="Source/GUI/RotarySlider.cpp"/>
<FILE id="vH5Jlq" name="RotarySlider.h" compile="0" resource="0" file="Source/GUI/RotarySlider.h"/>
<FILE id="MH7lh1" name="SpectrumComponent.cpp" compile="1" resource="0"
file="Source/GUI/SpectrumComponent.cpp"/>
<FILE id="BsHp7P" name="SpectrumComponent.h" compile="0" resource="0"
file="Source/GUI/SpectrumComponent.h"/>
</GROUP>
<GROUP id="{D73D156C-4598-41B2-381C-0F88E26EDF02}" name="DSP">
<FILE id="Z5Xx3D" name="CircularBuffer.cpp" compile="1" resource="0"
Expand All @@ -36,6 +40,8 @@
<FILE id="goOH3x" name="NotchFilter.h" compile="0" resource="0" file="Source/DSP/NotchFilter.h"/>
<FILE id="wKexCt" name="PeakFilter.cpp" compile="1" resource="0" file="Source/DSP/PeakFilter.cpp"/>
<FILE id="AfkIf0" name="PeakFilter.h" compile="0" resource="0" file="Source/DSP/PeakFilter.h"/>
<FILE id="A8jkQp" name="SpectrumProcessor.h" compile="0" resource="0"
file="Source/DSP/SpectrumProcessor.h"/>
<FILE id="VrP4wd" name="TestBufferGene.h" compile="0" resource="0"
file="Source/DSP/TestBufferGene.h"/>
<FILE id="tmA3y6" name="WavHandler.h" compile="0" resource="0" file="Source/DSP/WavHandler.h"/>
Expand Down Expand Up @@ -89,7 +95,7 @@
<MODULEPATH id="juce_graphics" path="C:/JUCE/modules"/>
<MODULEPATH id="juce_gui_basics" path="C:/JUCE/modules"/>
<MODULEPATH id="juce_gui_extra" path="C:/JUCE/modules"/>
<MODULEPATH id="melatonin_blur" path="..\..\GitHub"/>
<MODULEPATH id="melatonin_blur" path="../../Github"/>
</MODULEPATHS>
</VS2022>
</EXPORTFORMATS>
Expand Down
6 changes: 4 additions & 2 deletions Source/DSP/CircularBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ void CircularBuffer::resetBufferFromCircularBuffer(juce::AudioBuffer<float>& buf
}
}
}

void CircularBuffer::copyFromEveryChannel(juce::AudioBuffer<float>& dest, const int destStartSample, const juce::AudioBuffer<float>& source, const int sourceStartSample, const int numSamples)

void CircularBuffer::copyFromEveryChannel(juce::AudioBuffer<float>& dest, const int destStartSample,
const juce::AudioBuffer<float>& source, const int sourceStartSample,
const int numSamples)
{
if (dest.getNumChannels() != source.getNumChannels())
{
Expand Down
4 changes: 3 additions & 1 deletion Source/DSP/CircularBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ class CircularBuffer
int resetWritePosition{ 0 }, resetReadPosition{ 0 };
int CBNumChannels{ 0 }, CBBufferSize{ 0 };

void copyFromEveryChannel(juce::AudioBuffer<float>& dest, const int destStartSample, const juce::AudioBuffer<float>& source, const int sourceStartSample, const int numSamples);
void copyFromEveryChannel(juce::AudioBuffer<float>& dest, const int destStartSample,
const juce::AudioBuffer<float>& source, const int sourceStartSample,
const int numSamples);
};
70 changes: 70 additions & 0 deletions Source/DSP/SpectrumProcessor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include <JuceHeader.h>
#pragma once

class SpectrumProcessor
{
public:
SpectrumProcessor() : forwardFFT (fftOrder), window (fftSize, juce::dsp::WindowingFunction<float>::hamming)
{
window.fillWindowingTables (fftSize, juce::dsp::WindowingFunction<float>::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<float>& 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<float> window;
int fifoIndex = 0;
};
227 changes: 227 additions & 0 deletions Source/GUI/SpectrumComponent.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
==============================================================================
SpectrumComponent.cpp
Created: 19 Oct 2023 10:34:27am
Author: TaroPie
==============================================================================
*/

#include <JuceHeader.h>
#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<juce::String> (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<juce::String> (frequenciesForLines[i] / 1000) + "k", xPos - 30, 0, 60, getHeight() / 5, juce::Justification::centred, 2);
else if (frequenciesForLines[i] == 20)
g.drawFittedText(static_cast<juce::String> (frequenciesForLines[i]), xPos - 30, 0, 60, getHeight() / 5, juce::Justification::right, 2);
else if (frequenciesForLines[i] == 20000)
g.drawFittedText(static_cast<juce::String> (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<int> (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<float>(numberOfBins));
//float maxDecibel = juce::Decibels::gainToDecibels(maxData[i])
// - juce::Decibels::gainToDecibels(static_cast<float>(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<float> (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<float> (value);
}
47 changes: 47 additions & 0 deletions Source/GUI/SpectrumComponent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
==============================================================================
SpectrumComponent.h
Created: 19 Oct 2023 10:34:27am
Author: TaroPie
==============================================================================
*/

#pragma once

#include <JuceHeader.h>

//==============================================================================
/*
*/
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<float> 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)
};
Loading

0 comments on commit 43d89a5

Please sign in to comment.