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 double sampleRate = device->getCurrentSampleRate(); *lowCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, 10.0f, 0.7f); *midProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 1000.0f, 0.7f, 1.0f); *highMidProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 5000.0f, 0.7f, 1.0f); *highCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, 20000.0f, 0.7f); 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 *lowCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, 10.0f, 0.7f); *midProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 1000.0f, 0.7f, 1.0f); *highMidProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 3000.0f, 0.7f, 1.0f); *highCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, 20000.0f, 0.7f); 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 double sampleRate = device->getCurrentSampleRate(); switch (type) { case LowCut: *lowCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, data.freq, 0.7f); break; case Mid: *midProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, data.freq, data.Q, data.gainFactor); responseCurve->updateMidData(data.freq, data.gainDB, data.Q); break; case HighMid: *highMidProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, data.freq, data.Q, data.gainFactor); responseCurve->updateHighMidData(data.freq, data.gainDB, data.Q); break; case HighCut: *highCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, data.freq, 0.7f); break; default: break; } } // 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) };