diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..880ac59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index cca049b..ee75c97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -project(GelykEQ VERSION 1.0.0 LANGUAGES CXX) +project(GelykEQ VERSION 1.0.0 LANGUAGES C CXX) if(NOT DEFINED JUCE_ROOT) include(FetchContent) @@ -98,6 +98,10 @@ target_include_directories(GelykEQ PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/Source ) +target_link_libraries(GelykEQ PRIVATE juce::juce_dsp) + +target_compile_definitions(GelykEQ PRIVATE JUCE_MODULE_AVAILABLE_juce_dsp=1) + target_compile_definitions(GelykEQ PRIVATE JucePlugin_Build_VST=1 JucePlugin_Build_AU=1 diff --git a/Makefile b/Makefile index 285abd3..0386651 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,42 @@ BUILD_DIR := build +# Standard macOS user plugin folders. +AU_DIR := $(HOME)/Library/Audio/Plug-Ins/Components +VST3_DIR := $(HOME)/Library/Audio/Plug-Ins/VST3 + +PLUGIN := "$(BUILD_DIR)/GelykEQ_artefacts/Release/AU/Gelyk EQ.component" +VST3PLUG := "$(BUILD_DIR)/GelykEQ_artefacts/Release/VST3/Gelyk EQ.vst3" + +J = $(shell sysctl -n hw.ncpu) + all: build +# Configure + build with CMake (default make). build: cmake-configure - cmake --build $(BUILD_DIR) --config Release -j$(shell sysctl -n hw.ncpu) + cmake --build $(BUILD_DIR) --config Release -j$(J) cmake-configure: cmake -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release +# Fully install the plugin into the macOS user plugin folders, then +# refresh the system so the host (AudioUnit/VST3.scan) picks it up. install: build - cmake --install $(BUILD_DIR) - -# Install directly to the standard macOS user plugin folders (AU + VST3). -install-local: build - cmake --install $(BUILD_DIR) --prefix "$(HOME)" + rm -rf "$(AU_DIR)/Gelyk EQ.component" "$(VST3_DIR)/Gelyk EQ.vst3" + mkdir -p "$(AU_DIR)" "$(VST3_DIR)" + cp -R $(PLUGIN) "$(AU_DIR)/" + cp -R $(VST3PLUG) "$(VST3_DIR)/" + @# Ad-hoc codesign the plugins so macOS/DAWs accept them. + @if command -v codesign >/dev/null; then \ + (codesign --force --deep --sign - "$(AU_DIR)/Gelyk EQ.component" 2>/dev/null || true); \ + (codesign --force --deep --sign - "$(VST3_DIR)/Gelyk EQ.vst3" 2>/dev/null || true); \ + fi + @echo "" + @echo "Installed:" + @echo " $(AU_DIR)/Gelyk EQ.component" + @echo " $(VST3_DIR)/Gelyk EQ.vst3" + @touch ~/Library/Preferences/com.apple.audio.InfoHelper 2>/dev/null || true clean: rm -rf $(BUILD_DIR) -.PHONY: all build cmake-configure install install-local clean +.PHONY: all build cmake-configure install clean diff --git a/Source/EqualizerDSP.cpp b/Source/EqualizerDSP.cpp index e2e6e1e..217c5c4 100644 --- a/Source/EqualizerDSP.cpp +++ b/Source/EqualizerDSP.cpp @@ -22,14 +22,14 @@ void FilterBand::prepare(double sampleRate, int blockSize) switch (type) { case Type::lowShelf: - c = new Coeffs(Coeffs::makeLowShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db))); + c = Coeffs::makeLowShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db)); break; case Type::highShelf: - c = new Coeffs(Coeffs::makeHighShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db))); + c = Coeffs::makeHighShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db)); break; case Type::peak: default: - c = new Coeffs(Coeffs::makePeakFilter(sampleRate, freq, q, juce::Decibels::decibelsToGain(db))); + c = Coeffs::makePeakFilter(sampleRate, freq, q, juce::Decibels::decibelsToGain(db)); break; } @@ -60,7 +60,10 @@ void FilterBand::process(juce::dsp::AudioBlock& block) // --------------------------------------------------------------------------- SpectrumAnalyser::SpectrumAnalyser() + : fft(12) // 4096 point FFT { + const int size = fft.getSize(); + fftBuffer.resize((size_t) (size * 2), 0.0f); // The timer drives periodic background analysis. startTimerHz(30); } @@ -68,6 +71,7 @@ SpectrumAnalyser::SpectrumAnalyser() void SpectrumAnalyser::prepare(double sr, int maxBlockSize) { const int size = 4096; + (void) maxBlockSize; sampleRate = sr; ring.setSize(2, size); ring.clear(); @@ -115,31 +119,29 @@ void SpectrumAnalyser::runAnalysis() if (n == 0) return; - juce::AudioBuffer block(1, n); - // Mesa of the two channels and window with a Hann. - block.copyFrom(0, 0, ring, 0, 0, n); - if (ringChannels > 1) - block.addFrom(0, 0, ring, 1, 0, n); - - juce::dsp::FFT fft(12); // 4096 point - juce::HeapBlock> fftData; - fftData.calloc((size_t) n); - for (int i = 0; i < n; ++i) + // Mesa of the two channels, Hann-windowed into the FFT buffer. + const auto fftSize = fft.getSize(); + for (int i = 0; i < fftSize; ++i) { - float v = block.getSample(0, i) * 0.5f; - const float win = 0.5f * (1.0f - std::cos(2.0f * juce::MathConstants::pi * i / (n - 1))); - fftData[i] = std::complex(v * win, 0.0f); + const int src = (ringFill - fftSize + i + n) % n; // contiguously oldest of last fftSize + float v = ring.getSample(0, src); + if (ringChannels > 1) + v = 0.5f * (v + ring.getSample(1, src)); + const float win = 0.5f * (1.0f - std::cos(2.0f * juce::MathConstants::pi * i / (fftSize - 1))); + fftBuffer[(size_t) i] = v * win; } - fft.perform((juce::dsp::Complex*) fftData.getData(), false); + for (size_t i = (size_t) fftSize; i < fftBuffer.size(); ++i) + fftBuffer[i] = 0.0f; - const int numBins = n / 2; + fft.performFrequencyOnlyForwardTransform(fftBuffer.data()); + + // Magnitude values (non-negative frequencies) are in the first size/2+1 entries. + const int numBins = juce::jmin((int) fftBuffer.size(), fftSize / 2 + 1); juce::Array mags; mags.ensureStorageAllocated(numBins); - for (int i = 1; i < numBins; ++i) + for (int i = 0; i < numBins; ++i) { - float re = fftData[i].real(); - float im = fftData[i].imag(); - float mag = std::sqrt(re * re + im * im); + float mag = fftBuffer[(size_t) i]; float db = 20.0f * std::log10(mag + 1.0e-9f); mags.add(db); } diff --git a/Source/EqualizerDSP.h b/Source/EqualizerDSP.h index 317cae8..cdc764c 100644 --- a/Source/EqualizerDSP.h +++ b/Source/EqualizerDSP.h @@ -37,6 +37,7 @@ class SpectrumAnalyser : public juce::Timer { public: SpectrumAnalyser(); + ~SpectrumAnalyser() override = default; void prepare(double sampleRate, int maxBlockSize); void push(const float* const* channels, int numChannels, int numSamples); @@ -57,7 +58,10 @@ private: void runAnalysis(); mutable juce::CriticalSection spectrumLock; - juce::Array readySpectrum; // dB magnitudes per bin + juce::Array readySpectrum; // magnitude per bin + + juce::dsp::FFT fft; + std::vector fftBuffer; juce::AudioBuffer ring; int ringFill = 0; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 2bce8a7..83f707a 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -3,6 +3,12 @@ namespace gelyk { +static juce::Font monoFont(float height) +{ + return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height, + juce::Font::plain)); +} + // --------------------------------------------------------------------------- // NeonLookAndFeel // --------------------------------------------------------------------------- @@ -26,23 +32,15 @@ void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int widt const float cx = x + width * 0.5f; // Track - juce::Path track; if (vertical) { const float tw = 2.0f; + juce::Path track; track.addRoundedRectangle(cx - tw * 0.5f, (float) y, tw, (float) height, 1.0f); - } - else - { - const float th = 2.0f; - track.addRoundedRectangle((float) x, y + height * 0.5f - th * 0.5f, (float) width, th, 1.0f); - } - g.setColour(line); - g.fillPath(track); + g.setColour(line); + g.fillPath(track); - // Filled region from centre (0 dB) down to the handle, glowing. - if (vertical) - { + // Filled region from centre (0 dB) down to the handle, glowing. const float midY = y + height * 0.5f; const float top = juce::jmin(midY, sliderPos); const float bottom = juce::jmax(midY, sliderPos); @@ -52,15 +50,20 @@ void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int widt g.fillPath(fill); // Handle. - g.setColour(slider.isEnabled() && isMouseOverOrDragging(true) ? violet : cyan); + g.setColour(slider.isMouseOverOrDragging(true) ? violet : cyan); g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f); g.setColour(bg); g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f); } else { + const float th = 2.0f; + juce::Path track; + track.addRoundedRectangle((float) x, y + height * 0.5f - th * 0.5f, (float) width, th, 1.0f); + g.setColour(line); + g.fillPath(track); g.setColour(cyan); - g.fillRect(x, y + height * 0.5f - 3.0f, (float) width, 6.0f); + g.drawHorizontalLine((int) (y + height * 0.5f), sliderPos - 5.0f, sliderPos + 5.0f); } } @@ -86,7 +89,7 @@ void NeonLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, boo void NeonLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label) { label.setBounds(2, 1, box.getWidth() - 22, box.getHeight() - 2); - label.setFont(juce::FontOptions(13.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName())); + label.setFont(monoFont(13.0f)); } // --------------------------------------------------------------------------- @@ -95,17 +98,14 @@ void NeonLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& lab GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& p) : AudioProcessorEditor(&p), - processor(p), - analyserAttachment(processor.getAPVTS().parameterState, - "analyserEnabled", - [this](float v) { processor.getAnalyser().setEnabled(v > 0.5f); }) + processor(p) { setLookAndFeel(&lookAndFeel); setOpaque(true); titleLabel = std::make_unique("title", "GELYKEQ"); - titleLabel->setFont(juce::FontOptions(26.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName())); - titleLabel->setColour(juce::Label::textColourId, cyan); + titleLabel->setFont(monoFont(26.0f)); + titleLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan); titleLabel->setJustificationType(juce::Justification::centredLeft); addAndMakeVisible(*titleLabel); @@ -116,25 +116,25 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& scaleBox->addItem("175%", 175); scaleBox->addItem("200%", 200); scaleBox->setSelectedId(100, juce::dontSendNotification); + scaleBox->setTooltip("UI scaling"); scaleBox->onChange = [this] { scaleUI((float) scaleBox->getSelectedId()); }; addAndMakeVisible(*scaleBox); // Master gain (vertical fader, far right). masterLabel = std::make_unique("mlab", "MASTER"); - masterLabel->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName())); - masterLabel->setColour(juce::Label::textColourId, magenta); + masterLabel->setFont(monoFont(11.0f)); + masterLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta); masterLabel->setJustificationType(juce::Justification::centred); - master = std::make_unique(juce::Slider::LinearVertical, - juce::Slider::TextBoxBelow); + master = std::make_unique(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow); master->setLookAndFeel(&lookAndFeel); master->setSliderStyle(juce::Slider::LinearVertical); master->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14); master->setRange(-24.0, 24.0, 0.01); + master->setDoubleClickReturnValue(true, 0.0); addAndMakeVisible(*masterLabel); addAndMakeVisible(*master); masterAttachment = std::make_unique( processor.getAPVTS(), "master", *master); - // Band faders. const char* const names[numBands] = { "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH" }; const char* const freqs[numBands] = { "60", "200", "1K", "4K", "10K" }; @@ -143,13 +143,14 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& auto& w = bands[i]; w.freqLabel = std::make_unique("freq", freqs[i]); - w.freqLabel->setFont(juce::FontOptions(13.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName())); - w.freqLabel->setColour(juce::Label::textColourId, textDim); + w.freqLabel->setFont(monoFont(13.0f)); + w.freqLabel->setColour(juce::Label::textColourId, lookAndFeel.textDim); w.freqLabel->setJustificationType(juce::Justification::centred); w.gainLabel = std::make_unique("gname", names[i]); - w.gainLabel->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName())); - w.gainLabel->setColour(juce::Label::textColourId, i % 2 == 0 ? cyan : magenta); + w.gainLabel->setFont(monoFont(11.0f)); + w.gainLabel->setColour(juce::Label::textColourId, + i % 2 == 0 ? lookAndFeel.cyan : lookAndFeel.magenta); w.gainLabel->setJustificationType(juce::Justification::centred); w.gain = std::make_unique(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow); @@ -177,14 +178,14 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& // Status bar. statusGit = std::make_unique("git", processor.getGitInfo()); - statusGit->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName())); - statusGit->setColour(juce::Label::textColourId, textDim); + statusGit->setFont(monoFont(11.0f)); + statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim); statusGit->setJustificationType(juce::Justification::centredRight); statusBar = std::make_unique("status", - "GELYKEQ v" + juce::String(ProjectInfo::versionString) + " | AU / VST3 "); - statusBar->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName())); - statusBar->setColour(juce::Label::textColourId, textDim); + "GELYKEQ v" + juce::String(ProjectInfo::versionString) + " | ANALYSER"); + statusBar->setFont(monoFont(11.0f)); + statusBar->setColour(juce::Label::textColourId, lookAndFeel.cyan.withAlpha(0.8f)); statusBar->setJustificationType(juce::Justification::centredLeft); addAndMakeVisible(*statusBar); addAndMakeVisible(*statusGit); @@ -193,7 +194,6 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& freqBins.ensureStorageAllocated(128); startTimerHz(30); - rebuildFaders(); scaleUI(100.0f); } @@ -211,7 +211,6 @@ void GelykEQAudioProcessorEditor::timerCallback() void GelykEQAudioProcessorEditor::rebuildFaders() { - // Nothing needed at construction; sliders are attachments. } void GelykEQAudioProcessorEditor::scaleUI(float percent) @@ -233,7 +232,7 @@ void GelykEQAudioProcessorEditor::resized() // Header. juce::Rectangle header = area.removeFromTop(headerH); - titleLabel->setBounds(header.removeFromLeft((int) (160 * s)).reduced(pad, 0)); + titleLabel->setBounds(header.removeFromLeft((int) (170 * s)).reduced(pad, 0)); scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s))); // Footer. @@ -244,12 +243,12 @@ void GelykEQAudioProcessorEditor::resized() // Main grid. const int masterW = (int) (64 * s); - juce::Rectangle mainArea = area; - juce::Rectangle masterArea = mainArea.removeFromRight(masterW); + juce::Rectangle masterArea = area.removeFromRight(masterW); - const int x0 = mainArea.getX(); - const int y0 = mainArea.getY(); - const int totalW = mainArea.getWidth(); + const int x0 = area.getX(); + const int y0 = area.getY(); + const int totalW = area.getWidth(); + const int height = area.getHeight(); const int gap = (int) (10 * s); const int colW = (totalW - gap * (numBands - 1)) / numBands; @@ -257,35 +256,37 @@ void GelykEQAudioProcessorEditor::resized() for (int i = 0; i < numBands; ++i) { auto& w = bands[i]; - juce::Rectangle col(x0 + i * (colW + gap), y0, colW, mainArea.getHeight()); + juce::Rectangle col(x0 + i * (colW + gap), y0, colW, height); - const int topLabelH = (int) (18 * s); + const int topLabelH = (int) (20 * s); const int freqH = (int) (16 * s); - const int gainH = (int) (14 * s); + const int gainSliderH = (int) (58 * s); + const int qH = (int) (18 * s); - juce::Rectangle sub = col.reduced((int)(8*s), 0); - w.gainLabel->setBounds(sub.removeFromTop(topLabelH)); + w.gainLabel->setBounds(col.removeFromTop(topLabelH)); - juce::Rectangle analyserArea = sub.removeFromTop(sub.getHeight() - freqH - gainH - (int)(18*s) - (int)(10*s)); + const int analyserH = col.getHeight() - freqH - gainSliderH - qH - (int)(8*s); + // reserve analyser space by subtracting it (faders sit on top of the spectrum) + col.removeFromTop(juce::jmax(0, analyserH)); - w.freqLabel->setBounds(sub.removeFromTop(freqH)); - w.gain->setBounds(sub.removeFromTop(gainH + (int)(34*s))); + w.freqLabel->setBounds(col.removeFromTop(freqH)); + w.gain->setBounds(col.removeFromTop(gainSliderH)); - w.q->setBounds(col.removeFromBottom((int)(18*s)).reduced((int)(8*s), 0)); + juce::Rectangle qArea = col.removeFromBottom(qH); + w.q->setBounds(qArea.reduced((int)(12*s), 0)); } // Master column. - juce::Rectangle msub = masterArea.reduced((int)(14*s), 0); - masterLabel->setBounds(msub.removeFromTop((int) (18 * s))); + juce::Rectangle msub = masterArea.reduced((int)(14*s), (int)(6*s)); + masterLabel->setBounds(msub.removeFromTop((int) (20 * s))); master->setBounds(msub); } void GelykEQAudioProcessorEditor::paint(juce::Graphics& g) { const float s = scaling.load(); - g.fillAll(bg); + g.fillAll(lookAndFeel.bg); - // Analyser canvas area matches fader columns. juce::Rectangle area = getLocalBounds(); const int headerH = (int) (46 * s); const int footerH = (int) (26 * s); @@ -294,38 +295,37 @@ void GelykEQAudioProcessorEditor::paint(juce::Graphics& g) mainArea.removeFromBottom(footerH); mainArea.removeFromRight((int) (64 * s)); - // Neon grid lines behind faders. - g.setColour(line); - for (int i = 0; i < 4; ++i) + const int topPad = (int) (20 * s); + const int botPad = (int) (28 * s); + const float topY = (float) (mainArea.getY() + topPad); + const float botY = (float) (mainArea.getY() + mainArea.getHeight() - botPad); + + // Neon grid lines. + g.setColour(lookAndFeel.line); + for (int i = 1; i <= 4; ++i) { - float f = (i + 1) / 5.0f; - // 0 dB line. - if (i == 2) - continue; - float yy = mainArea.getY() + 18.0f * s + (mainArea.getHeight() - 18.0f * s - 60.0f * s) * f; - g.drawHorizontalLine((int) yy + (int)(18*s), (float) mainArea.getX(), (float) mainArea.getRight()); + if (i == 3) continue; // skip the 0 dB centre (drawn separately) + float yy = topY + (botY - topY) * (i / 5.0f); + g.drawHorizontalLine((int) yy, (float) mainArea.getX(), (float) mainArea.getRight()); } - // The 0 dB centre line (glowing). - const float centreLineY = mainArea.getY() + (int)(18*s) + - (mainArea.getHeight() - (int)(18*s) - (int)(60*s)) * 0.5f; - g.setColour(cyan.withAlpha(0.55f)); + // 0 dB centre line (glowing cyan). + const float centreLineY = topY + (botY - topY) * 0.5f; + g.setColour(lookAndFeel.cyan.withAlpha(0.55f)); g.drawHorizontalLine((int) centreLineY, (float) mainArea.getX(), (float) mainArea.getRight()); // Spectrum overlay. if (spectrum.size() > 2) { juce::Path path; - const int x0 = mainArea.getX(); - const int width = mainArea.getWidth(); - const float topY = mainArea.getY() + 18.0f * s; - const float botY = mainArea.getY() + (int)(8*s) + (mainArea.getHeight() - (int)(18*s) - (int)(60*s)) * 0.92f; + const float x0 = (float) mainArea.getX(); + const float width = (float) mainArea.getWidth(); for (int i = 0; i < spectrum.size(); ++i) { float db = juce::jlimit(-72.0f, 12.0f, spectrum[i]); float norm = (db + 72.0f) / 84.0f; // 0..1 up - float xx = (float) x0 + (float) i / (float) (spectrum.size() - 1) * width; + float xx = x0 + (float) i / (float) (spectrum.size() - 1) * width; float yy = botY - norm * (botY - topY); if (i == 0) path.startNewSubPath(xx, yy); else path.lineTo(xx, yy); @@ -336,11 +336,12 @@ void GelykEQAudioProcessorEditor::paint(juce::Graphics& g) filled.lineTo((float) mainArea.getX(), botY); filled.closeSubPath(); - g.setGradientFill(juce::ColourGradient(magenta.withAlpha(0.35f), (float) mainArea.getX(), topY, - magenta.withAlpha(0.05f), (float) mainArea.getX(), botY, - false)); + g.setGradientFill(juce::ColourGradient(lookAndFeel.magenta.withAlpha(0.30f), + (float) mainArea.getX(), topY, + lookAndFeel.magenta.withAlpha(0.05f), + (float) mainArea.getX(), botY, false)); g.fillPath(filled); - g.setColour(cyan.withAlpha(0.9f)); + g.setColour(lookAndFeel.cyan.withAlpha(0.9f)); g.strokePath(path, juce::PathStrokeType(1.6f * s)); } } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index bc12783..b70a472 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -67,12 +67,10 @@ private: std::unique_ptr statusBar; std::unique_ptr statusGit; - juce::AudioProcessorValueTreeState::SliderAttachment gainAttachments[numBands]; - juce::AudioProcessorValueTreeState::SliderAttachment qAttachments[numBands]; + std::unique_ptr gainAttachments[numBands]; + std::unique_ptr qAttachments[numBands]; std::unique_ptr masterAttachment; - juce::AudioProcessorValueTreeState::ParameterAttachment analyserAttachment; - juce::Array spectrum; juce::Array freqBins; std::atomic scaling { 1.0f }; // for resized()