Equaliser
Key Features:
- Done with JUCE framework by using filter processor duplicator (IIR filter, juce::dsp::ProcessorDuplicator)
- Response curve (logarithmic), spectrum, audio waveform, peak and RMS level meters
- Volume and low-high-band pass (with 0.707 Q value) and two peak filters for simple modification test purposes
- Mouse draggable playing position bar on audio waveform window
- Smooth levelling applied to spectrum and peak-RMS level meters
- Mostly fixed hard-coded GUI style for components
Improvement Areas:
- General refactoring to reduce the complexity
- Better knob adjustments
- Spectrum setup structure should be improved
- Some tweaks for dB level measurements
Download Link:
- StandaloneEQTest.zip (zipped)
- Only audio player and response curve code published here to keep simple
References:
- JUCE Tutorial: Visualise the frequencies of a signal in real time
- Drawing Audio Thumbnail
- Customising “Look and Feel”

ResponseCurveComponent.h
#pragma once
#include <JuceHeader.h>
class ResponseCurveComponent : public juce::Component, public juce::Timer
{
public:
ResponseCurveComponent()
{
init();
startTimerHz(25);
}
~ResponseCurveComponent() = default;
void init()
{
generateLogSpacedFrequencies();
lowCutData.gainDB = 0.0f;
lowCutData.freq = 10.0f;
lowCutData.Q = 0.7f;
midData.gainDB = 0.0f;
midData.freq = 1000.0f;
midData.Q = 0.7f;
highMidData.gainDB = 0.0f;
highMidData.freq = 3000.0f;
highMidData.Q = 0.7f;
highCutData.gainDB = 0.0f;
highCutData.freq = 20000.0f;
highCutData.Q = 0.7f;
}
void paint(juce::Graphics& g) override
{
auto bounds = getLocalBounds();
g.setColour(juce::Colours::black);
g.fillRect(bounds);
// Create the Response Curve Path & Fill Path
g.setColour(juce::Colours::white); // Curve color
juce::Path responseCurve;
juce::Path fillArea; // Path for the filled area
bool firstPoint = true;
float zeroDB_Y = juce::jmap<float>(0.0f, -12.0f, 12.0f, bounds.getHeight(), 0);
for (int i = 0; i < numPoints; ++i)
{
float f = frequencies[i];
float gainDB = frequencyGainValues[i];
if (std::isinf(gainDB) || std::isnan(gainDB))
{
// Set gainDB outside visual context
gainDB = -20.0f;
}
// Convert frequency (log scale) to X pixel position
float x = juce::jmap<float>(std::log10(f), std::log10(20.0f), std::log10(20000.0f), 2, getWidth() - 2);
// Convert dB gain to Y pixel position (center = 0 dB)
float y = juce::jmap<float>(gainDB, -12.0f, 12.0f, bounds.getHeight() - 2, 2);
if (firstPoint)
{
responseCurve.startNewSubPath(x, y);
fillArea.startNewSubPath(x, zeroDB_Y); // Start from 0 dB line
fillArea.lineTo(x, y);
firstPoint = false;
}
else
{
responseCurve.lineTo(x, y);
fillArea.lineTo(x, y);
}
}
// Close the fill area path by connecting to 0 dB line
fillArea.lineTo(getWidth() - 2.0f, zeroDB_Y);
fillArea.closeSubPath();
// === Fill the area between the curve and the 0 dB line ===
g.setColour(juce::Colours::lightblue.withAlpha(0.8f)); // Semi-transparent blue fill
g.fillPath(fillArea);
// === Draw the EQ response curve ===
g.setColour(juce::Colours::white);
g.strokePath(responseCurve, juce::PathStrokeType(2.0f));
}
void resized() override
{
const auto bounds = getLocalBounds().toFloat();
}
void timerCallback() override
{
updateFrequencyGainValuesForDraw();
repaint();
}
//==============================================================================
enum FilterType
{
LowCut,
Mid,
HighMid,
HighCut
};
struct FilterData
{
float freq;
float Q;
float gainDB;
};
void updateLowCutData(const float f0)
{
lowCutData.freq = f0;
lowCutData.gainDB = 0.0f;
lowCutData.Q = 0.7f;
}
void updateMidData(const float f0, const float gainDB, const float Q)
{
midData.freq = f0;
midData.gainDB = gainDB;
midData.Q = Q;
}
void updateHighMidData(const float f0, const float gainDB, const float Q)
{
highMidData.freq = f0;
highMidData.gainDB = gainDB;
highMidData.Q = Q;
}
void updateHighCutData(const float f0)
{
highCutData.freq = f0;
highCutData.gainDB = 0.0f;
highCutData.Q = 0.7f;
}
void updateFrequencyGainValuesForDraw()
{
const size_t size = frequencies.size();
for (size_t i = 0; i < size; ++i)
{
// Clean previous data
frequencyGainValues[i] = 0.0f;
frequencyGainValues[i] += computeLowCutGain(frequencies[i], lowCutData.freq, 0.7f);
frequencyGainValues[i] += computeGainAtFrequency(frequencies[i], midData.freq, midData.gainDB, midData.Q);
frequencyGainValues[i] += computeGainAtFrequency(frequencies[i], highMidData.freq, highMidData.gainDB, highMidData.Q);
frequencyGainValues[i] += computeHighCutGain(frequencies[i], highCutData.freq, 0.7f);
}
}
float computeGainAtFrequency(float f, float f0, float gainValue, float Q)
{
// Compute EQ response at a given frequency f by center frequency f0
const float linearGainFactor = std::pow(10.0f, gainValue / 20.0f);
// Use log-based frequency ratio instead of linear difference
float logRatio = std::log(f / f0) / std::log(1 + 1.0f / Q);
// Symmetrical gain calculation
float gain = 1 + (linearGainFactor - 1) / (1 + logRatio * logRatio);
// Clamp the result between -20 and +20 dB
gain = std::clamp(gain, -20.0f, 20.0f);
return 20.0f * std::log10(gain); // Convert back to dB
}
float computeLowCutGain(float f, float f0, float Q)
{
if (f == 0.0f) return -100.0f; // Prevent log(0) issues, set a deep cut at DC
// Frequency ratio
float frequencyRatio = f0 / f; // f0 is cutoff frequency
// Compute gain for high-pass filter
float gain = 1.0f / std::sqrt(1.0f + std::pow(frequencyRatio, 2.0f * Q));
float gainDB = 20.0f * std::log10(gain);
if (std::isinf(gainDB) || std::isnan(gainDB))
{
gainDB = -100.0f; // Extreme low frequencies should be deeply cut
}
return gainDB;
}
float computeHighCutGain(float f, float f0, float Q)
{
if (f == 0.0f) return 0.0f; // No attenuation at DC for a high-cut filter
float frequencyRatio = f0 / f;
// Compute gain for high-cut (low-pass) filter
float gain = 1.0f / std::sqrt(1.0f + std::pow(frequencyRatio, -2.0f * Q));
float gainDB = 20.0f * std::log10(gain);
if (std::isinf(gainDB) || std::isnan(gainDB))
{
gainDB = -100.0f;
}
return gainDB;
}
void generateLogSpacedFrequencies(float minFreq = 1.0f, float maxFreq = 20000.0f)
{
frequencies.clear();
frequencyGainValues.clear();
for (int i = 0; i < numPoints; ++i)
{
float fraction = static_cast<float>(i) / (numPoints - 1);
float freq = minFreq * std::pow(maxFreq / minFreq, fraction); // Log scale
frequencies.push_back(freq);
frequencyGainValues.push_back(0.0f);
}
}
void generateLinearSpacedFrequencies(float minFreq = 1.0f, float maxFreq = 20000.0f)
{
frequencies.clear();
frequencyGainValues.clear();
for (int i = 0; i < numPoints; ++i)
{
float freq = minFreq + i * (maxFreq - minFreq) / (numPoints - 1); // Linear scale
frequencies.push_back(freq);
frequencyGainValues.push_back(0.0f);
}
}
//==============================================================================
private:
std::vector<float> frequencies;
std::vector<float> frequencyGainValues;
int numPoints = 1000;
FilterData lowCutData;
FilterData midData;
FilterData highMidData;
FilterData highCutData;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ResponseCurveComponent)
};
AudioplayerComponent.h
#pragma once
#include "AudioThumbnailComponent.h"
#include "SpectrumComponent.h"
#include "LevelMeterComponent.h"
#include "ResponseCurveComponent.h"
#include "UIHelper.h"
class AudioPlayerComponent final : public juce::Component,
public juce::AudioSource,
public juce::ChangeBroadcaster,
private juce::TimeSliceThread,
private juce::ChangeListener
{
public:
AudioPlayerComponent() :
juce::TimeSliceThread("Audio Player Thread"),
thumbnailComponent(audioDeviceManager, formatManager)
{
// Initialize audio device manager
audioDeviceManager.initialiseWithDefaultDevices(0, 2);
// Register audio formats
formatManager.registerBasicFormats();
// Add the audio callback
audioDeviceManager.addAudioCallback(&audioSourcePlayer);
init();
startThread();
addAndMakeVisible(loadButton);
addAndMakeVisible(playButton);
addAndMakeVisible(byPassButton);
addAndMakeVisible(volumeSlider);
loadButton.setColour(juce::TextButton::buttonColourId,
getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
loadButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
playButton.setButtonText("Play");
playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
playButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);
byPassButton.setButtonText("Bypass Off");
byPassButton.setColour(juce::TextButton::buttonColourId,
getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
volumeSlider.setLookAndFeel(&customVolumeKnob);
customVolumeKnob.setThumbnailRatio(0.7f);
volumeSlider.setSliderStyle(juce::Slider::Rotary);
volumeSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 35, 13);
volumeSlider.setRange(0, 100, 1);
volumeSlider.setValue(100);
//volumeSlider.setPopupDisplayEnabled(true, true, this);
loadButton.onClick = [this] { openFile(); };
playButton.onClick = [this] { togglePlay(); };
byPassButton.onClick = [this] { toggleByPass(); };
volumeSlider.onValueChange = [this] { updateGain(); };
addAndMakeVisible(thumbnailComponent);
thumbnailComponent.addChangeListener(this);
//==============================================================================
// LOW CUT FILTER
createVerticalSlider(lowCutFreq, 10, 10, 300, 1);
createByPassToggle(lowCutByPass, isLowCutOn);
lowCutFreq.onValueChange = [this] { updateLowCutFrequency(); };
lowCutByPass.onClick = [this] { toggleLowCutByPass(); };
// MID FILTER
createRotarySlider(midGain, 0.0f, -12.0f, 12.0f, 0.1f);
createRotarySlider(midFreq, 1000, 20.0f, 20000.0f, 1.0f);
createRotarySlider(midQ, 0.7f, 0.1f, 20.0f, 0.1f);
createByPassToggle(midByPass, isMidCutOn);
midGain.onValueChange = [this] { updateMidFilterGainLevel(); };
midFreq.onValueChange = [this] { updateMidFilterFrequency(); };
midQ.onValueChange = [this] { updateMidFilterQ(); };
midByPass.onClick = [this] { toggleMidByPass(); };
// HIGH MID FILTER
createRotarySlider(highMidGain, 0.0f, -12.0f, 12.0f, 0.1f);
createRotarySlider(highMidFreq, 3000, 20.0f, 20000.0f, 1.0f);
createRotarySlider(highMidQ, 0.7f, 0.1f, 20.0f, 0.1f);
createByPassToggle(highMidByPass, isHighMidCutOn);
highMidFreq.onValueChange = [this] { updateHighMidFilterFrequency(); };
highMidQ.onValueChange = [this] { updateHighMidFilterQ(); };
highMidGain.onValueChange = [this] { updateHighMidFilterGainLevel(); };
highMidByPass.onClick = [this] { toggleHighMidByPass(); };
// HIGH CUT FILTER
createVerticalSlider(highCutFreq, 20000, 3000, 20000, 1);
createByPassToggle(highCutByPass, isHighCutOn);
highCutFreq.onValueChange = [this] { updateHighCutFrequency(); };
highCutByPass.onClick = [this] { toggleHighCutByPass(); };
//==============================================================================
}
~AudioPlayerComponent() override
{
signalThreadShouldExit();
stop();
audioDeviceManager.removeAudioCallback(&audioSourcePlayer);
waitForThreadToExit(10000);
audioSourcePlayer.setSource(nullptr);
}
//==============================================================================
// INITILIASING SOURCES
void init()
{
if (transportSource.get() == nullptr)
{
transportSource.reset(new juce::AudioTransportSource());
transportSource->addChangeListener(this);
if (readerSource != nullptr)
{
if (auto* device = audioDeviceManager.getCurrentAudioDevice())
{
transportSource->setSource(readerSource.get(), juce::roundToInt(device->getCurrentSampleRate()), this, reader->sampleRate);
getThumbnailComponent().setTransportSource(transportSource.get());
}
}
}
// Disconnect any previous audio source
audioSourcePlayer.setSource(nullptr);
// Set up filters in the processor chain
auto* device = audioDeviceManager.getCurrentAudioDevice();
const float sampleRate = static_cast<float>(device->getCurrentSampleRate());
*lowCutProcessor.state = highPassFilterCoefficients(sampleRate, 10.0f);
*midProcessor.state = peakFilterCoefficients(sampleRate, 1000.0f, 0.7f, 1.0f);
*highMidProcessor.state = peakFilterCoefficients(sampleRate, 3000.0f, 0.7f, 1.0f);
*highCutProcessor.state = lowPassFilterCoefficients(sampleRate, 20000.0f);
setFilterData(lowCutData, 0.0f, 1.0f, 10.0f, 0.7f);
setFilterData(midData, 0.0f, 1.0f, 1000.0f, 0.7f);
setFilterData(highMidData, 0.0f, 1.0f, 3000.0f, 0.7f);
setFilterData(highCutData, 0.0f, 1.0f, 20000.0f, 0.7f);
// Reconnect processorChain as the audio source
audioSourcePlayer.setSource(this);
// After loading any file
volumeSlider.setValue(100);
lowCutFreq.setValue(10.0f);
lowCutByPass.setButtonText("off");
isLowCutOn = false;
midFreq.setValue(1000.0f);
midQ.setValue(0.7f);
midGain.setValue(0.0f);
midByPass.setButtonText("off");
isMidCutOn = false;
highMidFreq.setValue(3000.0f);
highMidQ.setValue(0.7f);
highMidGain.setValue(0.0f);
highMidByPass.setButtonText("off");
isHighMidCutOn = false;
highCutFreq.setValue(20000.0f);
highCutByPass.setButtonText("off");
isHighCutOn = false;
byPassButton.setButtonText("Bypass Off");
byPassButton.setColour(juce::TextButton::buttonColourId,
getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
byPassState = false;
}
void setSpectrumAnalyser(SpectrumComponent* analyser)
{
spectrumAnalyser = analyser;
}
void setLevelMeter(LevelMeterComponent* leftPeak,
LevelMeterComponent* rightPeak,
LevelMeterComponent* leftRMS,
LevelMeterComponent* rightRMS)
{
leftPeakMeter = leftPeak;
rightPeakMeter = rightPeak;
leftRMSMeter = leftRMS;
rightRMSMeter = rightRMS;
}
void setResponseCurve(ResponseCurveComponent* _responseCurve)
{
responseCurve = _responseCurve;
}
//==============================================================================
//==============================================================================
// PREPARE AND PROCESS
void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override
{
juce::dsp::ProcessSpec spec{ sampleRate, (juce::uint32)samplesPerBlockExpected, 2 };
lowCutProcessor.prepare(spec);
midProcessor.prepare(spec);
highMidProcessor.prepare(spec);
highCutProcessor.prepare(spec);
// Setup filters with default values
const float fs = static_cast<float>(sampleRate);
*lowCutProcessor.state = highPassFilterCoefficients(fs, 10.0f);
*midProcessor.state = peakFilterCoefficients(fs, 1000.0f, 0.7f, 1.0f);
*highMidProcessor.state = peakFilterCoefficients(fs, 3000.0f, 0.7f, 1.0f);
*highCutProcessor.state = lowPassFilterCoefficients(fs, 20000.0f);
setFilterData(lowCutData, 0.0f, 1.0f, 10.0f, 0.7f);
setFilterData(midData, 0.0f, 1.0f, 1000.0f, 0.7f);
setFilterData(highMidData, 0.0f, 1.0f, 3000.0f, 0.7f);
setFilterData(highCutData, 0.0f, 1.0f, 20000.0f, 0.7f);
gainProcessor.prepare(spec);
if (transportSource)
{
transportSource->prepareToPlay(samplesPerBlockExpected, sampleRate);
}
//==============================================================================
// SPECTRUM SETUP
if (spectrumAnalyser)
{
spectrumAnalyser->CalculateFFTIndices(sampleRate);
spectrumAnalyser->setSmoothingLevel(sampleRate, 0.065f);
}
//==============================================================================
//==============================================================================
// LEVEL METERS SETUP
if (leftPeakMeter)
{
leftPeakMeter->setSmoothingLevel(sampleRate, 0.1f, -100.0f);
rightPeakMeter->setSmoothingLevel(sampleRate, 0.1f, -100.0f);
leftRMSMeter->setSmoothingLevel(sampleRate, 0.5f, -100.0f);
rightRMSMeter->setSmoothingLevel(sampleRate, 0.5f, -100.0f);
}
//==============================================================================
}
void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override
{
juce::ScopedLock audioLock(audioCallbackLock);
juce::ScopedNoDenormals noDenormals;
transportSource->getNextAudioBlock(bufferToFill);
juce::dsp::AudioBlock<float> block(*bufferToFill.buffer);
juce::dsp::ProcessContextReplacing<float> context(block);
lowCutProcessor.process(context);
midProcessor.process(context);
highMidProcessor.process(context);
highCutProcessor.process(context);
gainProcessor.process(context);
//==============================================================================
// FOR LEVEL METERS
if (leftPeakMeter)
{
leftPeakMeter->setLevelMeter(bufferToFill, LevelMeterComponent::Peak_Level, LevelMeterComponent::Left);
rightPeakMeter->setLevelMeter(bufferToFill, LevelMeterComponent::Peak_Level, LevelMeterComponent::Right);
leftRMSMeter->setLevelMeter(bufferToFill, LevelMeterComponent::RMS_Level, LevelMeterComponent::Left);
rightRMSMeter->setLevelMeter(bufferToFill, LevelMeterComponent::RMS_Level, LevelMeterComponent::Right);
}
// FOR SPECTRUM ANALYSER
if (spectrumAnalyser)
{
spectrumAnalyser->getNextAudioBlock(bufferToFill); // For modified data
}
//==============================================================================
}
void releaseResources() override
{
transportSource->releaseResources();
}
//==============================================================================
//==============================================================================
// PAINT AND RESIZE
void paint(juce::Graphics& g) override
{
g.setColour(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
g.fillRect(getLocalBounds());
}
void resized() override
{
// Hardcoded for test purposes, the main frame is fixed size
playButton.setBounds(20, 245, 80, 30);
loadButton.setBounds(120, 245, 80, 30);
byPassButton.setBounds(280, 245, 100, 30);
volumeSlider.setBounds(795, 435, 70, 70);
thumbnailComponent.setBounds(437, 245, 314, 60);
lowCutFreq.setBounds(50, 315, 50, 150);
lowCutByPass.setBounds(60, 290, 32, 22);
midGain.setBounds(140, 313, 65, 65);
midFreq.setBounds(140, 373, 65, 65);
midQ.setBounds(140, 430, 68, 65);
midByPass.setBounds(158, 290, 32, 22);
highMidGain.setBounds(218, 313, 65, 65);
highMidFreq.setBounds(218, 373, 65, 65);
highMidQ.setBounds(218, 430, 63, 65);
highMidByPass.setBounds(236, 290, 32, 22);
highCutFreq.setBounds(304, 315, 50, 150);
highCutByPass.setBounds(314, 290, 32, 22);
}
//==============================================================================
//==============================================================================
// PLAY AND STOP
void togglePlay()
{
if (playState)
stop();
else
play();
}
void play()
{
if (readerSource == nullptr) return;
if (transportSource->getCurrentPosition() >= transportSource->getLengthInSeconds() || transportSource->getCurrentPosition() < 0)
{
transportSource->setPosition(0);
}
transportSource->start();
playState = true;
playButton.setButtonText("Stop");
playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::darkorange);
playButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
}
void stop()
{
playState = false;
if (transportSource.get() != nullptr)
{
transportSource->stop();
transportSource->setPosition(0);
playButton.setButtonText("Play");
playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
playButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);
}
}
//==============================================================================
//==============================================================================
// UPDATE PARAMETERS
enum FilterType
{
LowCut,
Mid,
HighMid,
HighCut
};
struct FilterData
{
float freq;
float Q;
float gainFactor;
float gainDB;
};
void createRotarySlider(juce::Slider& slider, float peakValue, float minRange, float maxRange, float step)
{
addAndMakeVisible(slider);
slider.setLookAndFeel(&customFilterKnob);
customFilterKnob.setThumbnailRatio(0.7f);
slider.setAlwaysOnTop(true);
slider.setSliderStyle(juce::Slider::Rotary);
slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 10, 10);
slider.setRange(minRange, maxRange, step);
slider.setValue(peakValue);
slider.setPopupDisplayEnabled(true, true, this);
}
void createVerticalSlider(juce::Slider& slider, int peakValue, int minRange, int maxRange, int step)
{
addAndMakeVisible(slider);
slider.setLookAndFeel(&customSlider);
customSlider.setThumbnailRatio(0.9f);
slider.setAlwaysOnTop(true);
slider.setSliderStyle(juce::Slider::LinearVertical);
slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 10, 10);
slider.setRange(minRange, maxRange, step);
slider.setValue(peakValue);
slider.setPopupDisplayEnabled(true, true, this);
}
void createByPassToggle(juce::TextButton& byPassButton, bool byPassState)
{
addAndMakeVisible(byPassButton);
byPassButton.setLookAndFeel(&customTextButton);
byPassButton.setClickingTogglesState(true);
byPassButton.setButtonText("off");
byPassState = false;
}
void setFilterData(FilterData& data, float gainDB, float gainFactor, float freq, float Q)
{
data.gainDB = gainDB;
data.gainFactor = gainFactor;
data.freq = freq;
data.Q = Q;
}
void updateGain()
{
juce::ScopedLock audioLock(audioCallbackLock);
// Gain slider's range is set between 0 and 100
const float volumeLevel = static_cast<float>(volumeSlider.getValue()) / 100.0f;
gainProcessor.setGainLinear(juce::jlimit(0.0f, 1.0f, volumeLevel));
}
void updateFilterParameters(const FilterType type, const FilterData& data)
{
juce::ScopedLock audioLock(audioCallbackLock);
auto* device = audioDeviceManager.getCurrentAudioDevice();
const float sampleRate = static_cast<float>(device->getCurrentSampleRate());
switch (type)
{
case LowCut:
*lowCutProcessor.state = highPassFilterCoefficients(sampleRate, data.freq);
break;
case Mid:
*midProcessor.state = peakFilterCoefficients(sampleRate, data.freq, data.Q, data.gainFactor);
responseCurve->updateMidData(data.freq, data.gainDB, data.Q);
break;
case HighMid:
*highMidProcessor.state = peakFilterCoefficients(sampleRate, data.freq, data.Q, data.gainFactor);
responseCurve->updateHighMidData(data.freq, data.gainDB, data.Q);
break;
case HighCut:
*highCutProcessor.state = lowPassFilterCoefficients(sampleRate, data.freq);
break;
default:
break;
}
{
// TEST SECTION
//auto high1 = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, data.freq, 0.7f);
//auto high2 = highPassFilterCoefficients(sampleRate, data.freq);
//std::array<float, 3> diff = { high1[0] - high2[0], high1[1] - high2[1] , high1[2] - high2[2] };
}
}
//==============================================================================
// FILTER COEFFICIENTS
static std::array<float, 6> highPassFilterCoefficients
(const float sampleRate, const float frequency)
{
jassert(sampleRate > 0.0f);
jassert(frequency > 0.0f && frequency <= sampleRate * 0.5f);
// Second order butterworth implementation
constexpr float pi = 3.14159f;
constexpr float invQ = 1 / 0.70711f; // where Q = 0.70711f
const float n = std::tan(pi * frequency / sampleRate);
const float nSquared = n * n;
const float c1 = 1 / (1 + invQ * n + nSquared);
// { b0, b1, b2, a0, a1, a2 }
return { { c1, -2.0f * c1, c1, 1.0f, 2.0f * c1 * (nSquared - 1.0f), c1 * (1.0f - invQ * n + nSquared) } };
}
static std::array<float, 6> peakFilterCoefficients
(const float sampleRate, const float frequency, const float Q, const float gainFactor)
{
jassert(sampleRate > 0.0f);
jassert(frequency > 2.0f && frequency <= sampleRate * 0.5f);
jassert(Q > 0.0f);
jassert(gainFactor > 0.0f);
// Biquad filter direct form 1 implementation
constexpr float twoPi = 6.28318f;
const float A = std::sqrt(gainFactor);
const float omega = (twoPi * frequency) / sampleRate;
const float alpha = std::sin(omega) / (2.0f * Q);
const float c2 = -2.0f * std::cos(omega);
const float alphaTimesA = alpha * A;
const float alphaOverA = alpha / A;
// { b0, b1, b2, a0, a1, a2 }
return { { 1.0f + alphaTimesA, c2, 1.0f - alphaTimesA, 1.0f + alphaOverA, c2, 1.0f - alphaOverA } };
}
static std::array<float, 6> lowPassFilterCoefficients
(const float sampleRate, const float frequency)
{
jassert(sampleRate > 0.0f);
jassert(frequency > 0.0f && frequency <= sampleRate * 0.5f);
// Second order butterworth implementation
constexpr float pi = 3.14159f;
constexpr float invQ = 1 / 0.70711f; // where Q = 0.70711f
const float n = 1 / std::tan(pi * frequency / sampleRate);
const float nSquared = n * n;
const float c = 1 / (1 + invQ * n + nSquared); // scaling factor
// { b0, b1, b2, a0, a1, a2 }
return { c, 2 * c, c, 1, 2 * c * (1 - nSquared), c * (1 - invQ * n + nSquared) };
}
//==============================================================================
// BYPASS UPDATE
void toggleLowCutByPass()
{
isLowCutOn = lowCutByPass.getToggleState();
lowCutByPass.setButtonText(isLowCutOn ? "on" : "off");
updateLowCutFrequency();
}
void toggleMidByPass()
{
isMidCutOn = midByPass.getToggleState();
midByPass.setButtonText(isMidCutOn ? "on" : "off");
updateMidFilterGainLevel();
}
void toggleHighMidByPass()
{
isHighMidCutOn = highMidByPass.getToggleState();
highMidByPass.setButtonText(isHighMidCutOn ? "on" : "off");
updateHighMidFilterGainLevel();
}
void toggleHighCutByPass()
{
isHighCutOn = highCutByPass.getToggleState();
highCutByPass.setButtonText(isHighCutOn ? "on" : "off");
updateHighCutFrequency();
}
void toggleByPass()
{
if (!byPassState)
{
byPassButton.setButtonText("Bypass On");
byPassButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);
byPassState = true;
}
else
{
byPassButton.setButtonText("Bypass Off");
byPassButton.setColour(juce::TextButton::buttonColourId,
getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
byPassState = false;
}
updateLowCutFrequency();
updateMidFilterGainLevel();
updateHighMidFilterGainLevel();
updateHighCutFrequency();
}
// LOW CUT FILTER
void updateLowCutFrequency()
{
auto& data = lowCutData;
if (isLowCutOn && !byPassState) data.freq = lowCutFreq.getValue();
else data.freq = 10.0f;
updateFilterParameters(FilterType::LowCut, data);
responseCurve->updateLowCutData(lowCutFreq.getValue());
}
// MID FILTER
void updateMidFilterGainLevel()
{
auto& data = midData;
data.gainDB = midGain.getValue();
if (isMidCutOn && !byPassState) data.gainFactor = std::pow(10.0f, midGain.getValue() / 20.0f);
else data.gainFactor = 1.0f;
updateFilterParameters(FilterType::Mid, data);
}
void updateMidFilterFrequency()
{
auto & data = midData;
data.freq = midFreq.getValue();
updateFilterParameters(FilterType::Mid, data);
}
void updateMidFilterQ()
{
auto & data = midData;
data.Q = midQ.getValue();
updateFilterParameters(FilterType::Mid, data);
}
// HIGH MID FILTER
void updateHighMidFilterGainLevel()
{
auto& data = highMidData;
data.gainDB = highMidGain.getValue();
if (isHighMidCutOn && !byPassState) data.gainFactor = std::pow(10.0f, highMidGain.getValue() / 20.0f);
else data.gainFactor = 1.0f;
updateFilterParameters(FilterType::HighMid, data);
}
void updateHighMidFilterFrequency()
{
auto& data = highMidData;
data.freq = highMidFreq.getValue();
updateFilterParameters(FilterType::HighMid, data);
}
void updateHighMidFilterQ()
{
auto& data = highMidData;
data.Q = highMidQ.getValue();
updateFilterParameters(FilterType::HighMid, data);
}
// HIGH CUT FILTER
void updateHighCutFrequency()
{
auto& data = highCutData;
if (isHighCutOn && !byPassState) data.freq = highCutFreq.getValue();
else data.freq = 20000.0f;
updateFilterParameters(FilterType::HighCut, data);
responseCurve->updateHighCutData(highCutFreq.getValue());
}
//==============================================================================
AudioThumbnailComponent& getThumbnailComponent()
{
return thumbnailComponent;
}
//==============================================================================
// OPEN AND LOAD
void openFile()
{
//stop(); It can be activated if the current play to be stopped once the file folder is opened
if (fileChooser != nullptr) return;
if (!juce::RuntimePermissions::isGranted(juce::RuntimePermissions::readExternalStorage))
{
SafePointer<AudioPlayerComponent> safeThis(this);
juce::RuntimePermissions::request(juce::RuntimePermissions::readExternalStorage,
[safeThis](bool granted) mutable
{
if (safeThis != nullptr && granted)
{
safeThis->openFile();
}
});
return;
}
fileChooser.reset(new juce::FileChooser("Select an audio file...", juce::File(), "*.wav;*.mp3;"));
fileChooser->launchAsync(juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
[this](const juce::FileChooser& fc) mutable
{
if (fc.getURLResults().size() > 0)
{
const auto u = fc.getURLResult();
if (loadURL(u))
{
thumbnailComponent.setCurrentURL(u);
}
else
{
auto options = juce::MessageBoxOptions().withIconType(juce::MessageBoxIconType::WarningIcon)
.withTitle("Error loading file")
.withMessage("Unable to load audio file")
.withButton("OK");
messageBox = juce::NativeMessageBox::showScopedAsync(options, nullptr);
}
}
fileChooser = nullptr;
}, nullptr);
}
bool loadURL(const juce::URL& fileToPlay)
{
stop();
audioSourcePlayer.setSource(nullptr);
getThumbnailComponent().setTransportSource(nullptr);
transportSource.reset();
readerSource.reset();
auto source = std::make_unique<juce::URLInputSource>(fileToPlay);
// Create stream and ensure proper ownership transfer
auto stream = std::unique_ptr<juce::InputStream>(source->createInputStream());
if (!stream) return false;
// Create reader for stream to transfer ownership
reader = std::unique_ptr<juce::AudioFormatReader>(formatManager.createReaderFor(std::move(stream)));
if (!reader) return false;
// Reset readerSource with the new reader
readerSource.reset(new juce::AudioFormatReaderSource(reader.get(), false));
init();
resized();
return true;
}
//==============================================================================
//==============================================================================
void changeListenerCallback(ChangeBroadcaster* source) override
{
if (thumbnailComponent.isEnded())
{
stop();
}
if (source == &thumbnailComponent)
{
repaint();
}
}
//==============================================================================
private:
juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> lowCutProcessor;
juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> midProcessor;
juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> highMidProcessor;
juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> highCutProcessor;
FilterData lowCutData;
FilterData midData;
FilterData highMidData;
FilterData highCutData;
juce::dsp::Gain<float> gainProcessor;
AudioThumbnailComponent thumbnailComponent;
ResponseCurveComponent* responseCurve = nullptr;
SpectrumComponent* spectrumAnalyser = nullptr;
LevelMeterComponent* leftPeakMeter = nullptr;
LevelMeterComponent* rightPeakMeter = nullptr;
LevelMeterComponent* leftRMSMeter = nullptr;
LevelMeterComponent* rightRMSMeter = nullptr;
std::unique_ptr<juce::FileChooser> fileChooser;
juce::ScopedMessageBox messageBox;
juce::AudioDeviceManager audioDeviceManager;
juce::AudioFormatManager formatManager;
juce::AudioSourcePlayer audioSourcePlayer;
std::unique_ptr<juce::AudioFormatReader> reader;
std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
std::unique_ptr<juce::AudioTransportSource> transportSource;
juce::CriticalSection audioCallbackLock;
CustomLookAndFeel customVolumeKnob;
CustomLookAndFeel customFilterKnob;
CustomLookAndFeel customSlider;
CustomLookAndFeel customTextButton;
//==============================================================================
// LABEL SECTION
juce::TextButton loadButton{ "Load" }, playButton{ "Play" }, byPassButton{ "Bypass Off" };
bool playState = false;
bool byPassState = false;
juce::Slider volumeSlider;
juce::Slider lowCutFreq, highCutFreq;
juce::Slider midFreq, midQ, midGain;
juce::Slider highMidFreq, highMidQ, highMidGain;
juce::TextButton lowCutByPass;
juce::TextButton midByPass;
juce::TextButton highMidByPass;
juce::TextButton highCutByPass;
bool isLowCutOn = false;
bool isMidCutOn = false;
bool isHighMidCutOn = false;
bool isHighCutOn = false;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPlayerComponent)
};