/* ============================================================================== PluginEditor.cpp Editor implementation for CrystalizerEQ Handles UI layout, real-time spectrum analysis visualization, parameter attachments, and user interactions ============================================================================== */ #include "PluginProcessor.h" #include "PluginEditor.h" #include "AXIOMDesignSystem.h" #include "JuceLibraryCode/BinaryData.h" using AXIOM::DesignSystem; using Colours = DesignSystem::Colours; using Typography = DesignSystem::Typography; using Spacing = DesignSystem::Spacing; using Shape = DesignSystem::Shape; using Opacity = DesignSystem::Opacity; using Layout = DesignSystem::Layout; using Components = DesignSystem::Components; using SliderStyles = Components::SliderStyles; void CrystalizerEQAudioProcessorEditor::setupSliders() { auto style = juce::Slider::RotaryHorizontalVerticalDrag; for (auto *s: sliders) { s->setSliderStyle(style); } gainLookAndFeel = std::make_unique(); freqQLookAndFeel = std::make_unique(); slopeLookAndFeel = std::make_unique(); globalLookAndFeel = std::make_unique(); lowBandGainSlider.setLookAndFeel(gainLookAndFeel.get()); peak1GainSlider.setLookAndFeel(gainLookAndFeel.get()); peak2GainSlider.setLookAndFeel(gainLookAndFeel.get()); peak3GainSlider.setLookAndFeel(gainLookAndFeel.get()); highBandGainSlider.setLookAndFeel(gainLookAndFeel.get()); lowBandSlopeSlider.setLookAndFeel(slopeLookAndFeel.get()); highBandSlopeSlider.setLookAndFeel(slopeLookAndFeel.get()); inputSlider.setLookAndFeel(globalLookAndFeel.get()); outputSlider.setLookAndFeel(globalLookAndFeel.get()); for (auto *s: { &lowBandFreqSlider, &lowBandQSlider, &peak1FreqSlider, &peak1QSlider, &peak2FreqSlider, &peak2QSlider, &peak3FreqSlider, &peak3QSlider, &highBandFreqSlider, &highBandQSlider }) { s->setLookAndFeel(freqQLookAndFeel.get()); } lowBandSlopeSlider.setTextBoxStyle(juce::Slider::NoTextBox, true, 0, 0); highBandSlopeSlider.setTextBoxStyle(juce::Slider::NoTextBox, true, 0, 0); } void CrystalizerEQAudioProcessorEditor::setupAttachments() { lowBandFreqAttach = std::make_unique(audioProcessor.apvts, "LowBandFreq", lowBandFreqSlider); lowBandSlopeAttach = std::make_unique(audioProcessor.apvts, "LowBandSlope", lowBandSlopeSlider); lowBandGainAttach = std::make_unique(audioProcessor.apvts, "LowBandGain", lowBandGainSlider); lowBandQAttach = std::make_unique(audioProcessor.apvts, "LowBandQ", lowBandQSlider); peak1FreqAttach = std::make_unique(audioProcessor.apvts, "Peak1Freq", peak1FreqSlider); peak1GainAttach = std::make_unique(audioProcessor.apvts, "Peak1Gain", peak1GainSlider); peak1QAttach = std::make_unique(audioProcessor.apvts, "Peak1Q", peak1QSlider); peak2FreqAttach = std::make_unique(audioProcessor.apvts, "Peak2Freq", peak2FreqSlider); peak2GainAttach = std::make_unique(audioProcessor.apvts, "Peak2Gain", peak2GainSlider); peak2QAttach = std::make_unique(audioProcessor.apvts, "Peak2Q", peak2QSlider); peak3FreqAttach = std::make_unique(audioProcessor.apvts, "Peak3Freq", peak3FreqSlider); peak3GainAttach = std::make_unique(audioProcessor.apvts, "Peak3Gain", peak3GainSlider); peak3QAttach = std::make_unique(audioProcessor.apvts, "Peak3Q", peak3QSlider); highBandFreqAttach = std::make_unique(audioProcessor.apvts, "HighBandFreq", highBandFreqSlider); highBandSlopeAttach = std::make_unique(audioProcessor.apvts, "HighBandSlope", highBandSlopeSlider); highBandGainAttach = std::make_unique(audioProcessor.apvts, "HighBandGain", highBandGainSlider); highBandQAttach = std::make_unique(audioProcessor.apvts, "HighBandQ", highBandQSlider); inputAttach = std::make_unique(audioProcessor.apvts, "InputGain", inputSlider); outputAttach = std::make_unique(audioProcessor.apvts, "OutputGain", outputSlider); peak1BypassAttach = std::make_unique(audioProcessor.apvts, "Peak1Bypass", peak1BypassButton); peak2BypassAttach = std::make_unique(audioProcessor.apvts, "Peak2Bypass", peak2BypassButton); peak3BypassAttach = std::make_unique(audioProcessor.apvts, "Peak3Bypass", peak3BypassButton); crystalizeAttach = std::make_unique(audioProcessor.apvts, "CrystalizeButton", crystalizeButton); masterBypassAttach = std::make_unique(audioProcessor.apvts, "MasterBypass", masterBypassButton); } void CrystalizerEQAudioProcessorEditor::setupDisplayNames() { titleLabel.setText("Crystalizer", juce::dontSendNotification); lowBypass.setName("LowBypass"); lowBell.setName("LowBell"); lowCut.setName("LowCut"); lowShelf.setName("LowShelf"); lowBandModeBox.setName("LowBandModeBox"); lowBandFreqSlider.setName("LowBand Freq"); lowBandSlopeSlider.setName("LowBand Slope"); lowBandGainSlider.setName("LowBand Gain"); lowBandQSlider.setName("LowBand Q"); peak1FreqSlider.setName("Peak1 Freq"); peak1GainSlider.setName("Peak1 Gain"); peak1QSlider.setName("Peak1 Q"); peak2FreqSlider.setName("Peak2 Freq"); peak2GainSlider.setName("Peak2 Gain"); peak2QSlider.setName("Peak2 Q"); peak3FreqSlider.setName("Peak3 Freq"); peak3GainSlider.setName("Peak3 Gain"); peak3QSlider.setName("Peak3 Q"); highBypass.setName("HighBypass"); highBell.setName("HighBell"); highCut.setName("HighCut"); highShelf.setName("HighShelf"); highBandModeBox.setName("HighBandModeBox"); highBandFreqSlider.setName("HighBand Freq"); highBandSlopeSlider.setName("HighBand Slope"); highBandGainSlider.setName("HighBand Gain"); highBandQSlider.setName("HighBand Q"); inputSlider.setName("Input"); outputSlider.setName("Output"); lowBandGainSlider.setTextValueSuffix("\ndB"); peak1GainSlider.setTextValueSuffix("\ndB"); peak1BypassButton.setName("peak1Bypass"); peak2GainSlider.setTextValueSuffix("\ndB"); peak2BypassButton.setName("peak2Bypass"); peak3GainSlider.setTextValueSuffix("\ndB"); peak3BypassButton.setName("peak3Bypass"); highBandGainSlider.setTextValueSuffix("\ndB"); inputSlider.setTextValueSuffix("\nIN"); outputSlider.setTextValueSuffix("\nOUT"); crystalizeButton.setName("CrystalizeButton"); resetButton.setName("ResetButton"); resetButton.setButtonText("Reset"); masterBypassButton.setName("MasterBypass"); savePresetButton.setName("SavePresetButton"); savePresetButton.setButtonText("Save Preset"); deletePresetButton.setName("DeletePresetButton"); deletePresetButton.setButtonText("Delete Preset"); } void CrystalizerEQAudioProcessorEditor::setupToggleButtons() { bypassButtonLookAndFeel = std::make_unique(); svgToggleButtonLookAndFeel = std::make_unique(); presetMenuButtonLookAndFeel = std::make_unique(); lowBandButtonLookAndFeel = std::make_unique(); highBandButtonLookAndFeel = std::make_unique(); peak1BypassButton.setLookAndFeel(bypassButtonLookAndFeel.get()); peak2BypassButton.setLookAndFeel(bypassButtonLookAndFeel.get()); peak3BypassButton.setLookAndFeel(bypassButtonLookAndFeel.get()); masterBypassButton.setLookAndFeel(bypassButtonLookAndFeel.get()); crystalizeButton.setLookAndFeel(svgToggleButtonLookAndFeel.get()); presetMenuButton.setLookAndFeel(presetMenuButtonLookAndFeel.get()); lowShelf.setToggleState(true, juce::dontSendNotification); highShelf.setToggleState(true, juce::dontSendNotification); lowBypass.setLookAndFeel(lowBandButtonLookAndFeel.get()); lowCut.setLookAndFeel(lowBandButtonLookAndFeel.get()); lowBell.setLookAndFeel(lowBandButtonLookAndFeel.get()); lowShelf.setLookAndFeel(lowBandButtonLookAndFeel.get()); highBypass.setLookAndFeel(highBandButtonLookAndFeel.get()); highCut.setLookAndFeel(highBandButtonLookAndFeel.get()); highBell.setLookAndFeel(highBandButtonLookAndFeel.get()); highShelf.setLookAndFeel(highBandButtonLookAndFeel.get()); for (auto *b: { &peak1BypassButton, &peak2BypassButton, &peak3BypassButton, &crystalizeButton, &masterBypassButton }) { b->onClick = [this, b]() { juce::String mode; if (b == &peak1BypassButton) { mode = "Low-Mid"; } else if (b == &peak2BypassButton) { mode = "Mid"; } else if (b == &peak3BypassButton) { mode = "High-Mid"; } else if (b == &masterBypassButton) { mode = "Master"; } const float target = b->getToggleState() ? 1.0f : 0.0f; if (mode == "Low-Mid") disableLowMidBand(target); else if (mode == "Mid") disableMidBand(target); else if (mode == "High-Mid") disableHighMidBand(target); else if (mode == "Master") disableEverything(target); if (b == &crystalizeButton) { isAnimatingCrystalize = true; isFadingToActive = (svgToggleButtonLookAndFeel->activeIconOpacity < 1.0f); } }; } } void CrystalizerEQAudioProcessorEditor::disableLowBand(const float target) { bool isToggled = target <= 0.5f; lowBandFreqSlider.setEnabled(isToggled); lowBandFreqLabel.setEnabled(isToggled); lowBandSlopeSlider.setEnabled(isToggled); lowBandSlopeLabel.setEnabled(isToggled); lowBandGainSlider.setEnabled(isToggled); lowBandGainLabel.setEnabled(isToggled); lowBandQSlider.setEnabled(isToggled); lowBandQLabel.setEnabled(isToggled); }; void CrystalizerEQAudioProcessorEditor::disableLowMidBand(const float target) { bool isToggled = target <= 0.5f; peak1GainSlider.setEnabled(isToggled); peak1QSlider.setEnabled(isToggled); peak1FreqSlider.setEnabled(isToggled); peak1GainLabel.setEnabled(isToggled); peak1QLabel.setEnabled(isToggled); peak1FreqLabel.setEnabled(isToggled); } void CrystalizerEQAudioProcessorEditor::disableMidBand(const float target) { bool isToggled = target <= 0.5f; peak2GainSlider.setEnabled(isToggled); peak2QSlider.setEnabled(isToggled); peak2FreqSlider.setEnabled(isToggled); peak2GainLabel.setEnabled(isToggled); peak2QLabel.setEnabled(isToggled); peak2FreqLabel.setEnabled(isToggled); } void CrystalizerEQAudioProcessorEditor::disableHighMidBand(const float target) { bool isToggled = target <= 0.5f; peak3GainSlider.setEnabled(isToggled); peak3QSlider.setEnabled(isToggled); peak3FreqSlider.setEnabled(isToggled); peak3GainLabel.setEnabled(isToggled); peak3QLabel.setEnabled(isToggled); peak3FreqLabel.setEnabled(isToggled); } void CrystalizerEQAudioProcessorEditor::disableHighBand(const float target) { bool isToggled = target <= 0.5f; highBandFreqSlider.setEnabled(isToggled); highBandFreqLabel.setEnabled(isToggled); highBandSlopeSlider.setEnabled(isToggled); highBandSlopeLabel.setEnabled(isToggled); highBandGainSlider.setEnabled(isToggled); highBandGainLabel.setEnabled(isToggled); highBandQSlider.setEnabled(isToggled); highBandQLabel.setEnabled(isToggled); }; void CrystalizerEQAudioProcessorEditor::disableEverything(const float target) { bool isToggled = target <= 0.5f; for (auto *s: sliders) { s->setEnabled(isToggled); } for (auto *l: sliderLabels) { l->setEnabled(isToggled); } for (auto *b: peakBypassButtons) { b->setEnabled(isToggled); const auto bypassTarget = b->getToggleState() ? 1.0f : 0.0f; if (b == &peak1BypassButton && isToggled) { disableLowMidBand(bypassTarget); } else if (b == &peak2BypassButton && isToggled) { disableMidBand(bypassTarget); } else if (b == &peak3BypassButton && isToggled) { disableHighMidBand(bypassTarget); } } resetButton.setEnabled(isToggled); crystalizeButton.setEnabled(isToggled); lowBandModeBox.setEnabled(isToggled); highBandModeBox.setEnabled(isToggled); if (!isToggled) { if (svgToggleButtonLookAndFeel->activeIconOpacity == 1.0f) { svgToggleButtonLookAndFeel->activeIconOpacity = 0.7f; crystalizeButton.repaint(); } else if (svgToggleButtonLookAndFeel->passiveIconOpacity == 1.0f) { svgToggleButtonLookAndFeel->passiveIconOpacity = 0.7f; crystalizeButton.repaint(); } } else { if (svgToggleButtonLookAndFeel->activeIconOpacity == 0.7f) { svgToggleButtonLookAndFeel->activeIconOpacity = 1.0f; crystalizeButton.repaint(); } else if (svgToggleButtonLookAndFeel->passiveIconOpacity == 0.7f) { svgToggleButtonLookAndFeel->passiveIconOpacity = 1.0f; crystalizeButton.repaint(); } } } void CrystalizerEQAudioProcessorEditor::addComponentsToLayout() { addAndMakeVisible(headerBar); headerBar.addAndMakeVisible(titleLabel); headerBar.addAndMakeVisible(presetArea); presetArea.addAndMakeVisible(presetBoxLabel); presetArea.addAndMakeVisible(resetButton); presetArea.addAndMakeVisible(presetBox); presetArea.addAndMakeVisible(presetMenuButton); headerBar.addAndMakeVisible(presetMenu); addAndMakeVisible(mainPanel); mainPanel.addAndMakeVisible(analyzerArea); mainPanel.addAndMakeVisible(crystalizeButton); //FILTERAREA { mainPanel.addAndMakeVisible(filterArea); { filterArea.addAndMakeVisible(lowFilterArea); lowBandModeBox.addAndMakeVisible(lowBandModeLabel); lowBandModeBox.addAndMakeVisible(lowBypass); lowBandModeBox.addAndMakeVisible(lowCut); lowBandModeBox.addAndMakeVisible(lowBell); lowBandModeBox.addAndMakeVisible(lowShelf); lowFilterArea.addAndMakeVisible(lowBandModeBox); lowBandModeButtons.add(&lowBypass, &lowCut, &lowShelf, &lowBell); lowBandBools = {false, false, true, false}; lowFilterArea.addAndMakeVisible(lowBandSlopeLabel); lowFilterArea.addAndMakeVisible(lowBandGainLabel); lowFilterArea.addAndMakeVisible(lowBandQLabel); lowFilterArea.addAndMakeVisible(lowBandFreqLabel); lowBandSlopeSlider.addAndMakeVisible(low12); lowBandSlopeSlider.addAndMakeVisible(low24); lowBandSlopeSlider.addAndMakeVisible(low36); lowBandSlopeSlider.addAndMakeVisible(low48); lowFilterArea.addAndMakeVisible(lowBandSlopeSlider); lowFilterArea.addAndMakeVisible(lowdBOctLabel); lowFilterArea.addAndMakeVisible(lowBandGainSlider); lowFilterArea.addAndMakeVisible(lowBandQSlider); lowFilterArea.addAndMakeVisible(lowBandFreqSlider); } { filterArea.addAndMakeVisible(lowMidFilterArea); lowMidFilterArea.addAndMakeVisible(peak1FreqLabel); lowMidFilterArea.addAndMakeVisible(peak1GainLabel); lowMidFilterArea.addAndMakeVisible(peak1QLabel); lowMidFilterArea.addAndMakeVisible(peak1FreqSlider); lowMidFilterArea.addAndMakeVisible(peak1GainSlider); lowMidFilterArea.addAndMakeVisible(peak1QSlider); lowMidFilterArea.addAndMakeVisible(peak1BypassButton); } { filterArea.addAndMakeVisible(midFilterArea); midFilterArea.addAndMakeVisible(peak2FreqLabel); midFilterArea.addAndMakeVisible(peak2GainLabel); midFilterArea.addAndMakeVisible(peak2QLabel); midFilterArea.addAndMakeVisible(peak2FreqSlider); midFilterArea.addAndMakeVisible(peak2GainSlider); midFilterArea.addAndMakeVisible(peak2QSlider); midFilterArea.addAndMakeVisible(peak2BypassButton); } { filterArea.addAndMakeVisible(highMidFilterArea); highMidFilterArea.addAndMakeVisible(peak3FreqLabel); highMidFilterArea.addAndMakeVisible(peak3GainLabel); highMidFilterArea.addAndMakeVisible(peak3QLabel); highMidFilterArea.addAndMakeVisible(peak3FreqSlider); highMidFilterArea.addAndMakeVisible(peak3GainSlider); highMidFilterArea.addAndMakeVisible(peak3QSlider); highMidFilterArea.addAndMakeVisible(peak3BypassButton); } { filterArea.addAndMakeVisible(highFilterArea); highBandModeBox.addAndMakeVisible(highBandModeLabel); highBandModeBox.addAndMakeVisible(highBypass); highBandModeBox.addAndMakeVisible(highCut); highBandModeBox.addAndMakeVisible(highBell); highBandModeBox.addAndMakeVisible(highShelf); highFilterArea.addAndMakeVisible(highBandModeBox); highBandModeButtons.add(&highBypass, &highCut, &highShelf, &highBell); highBandBools = {false, false, true, false}; highFilterArea.addAndMakeVisible(highBandSlopeLabel); highFilterArea.addAndMakeVisible(highBandGainLabel); highFilterArea.addAndMakeVisible(highBandQLabel); highFilterArea.addAndMakeVisible(highBandFreqLabel); highBandSlopeSlider.addAndMakeVisible(high12); highBandSlopeSlider.addAndMakeVisible(high24); highBandSlopeSlider.addAndMakeVisible(high36); highBandSlopeSlider.addAndMakeVisible(high48); highFilterArea.addAndMakeVisible(highBandSlopeSlider); highFilterArea.addAndMakeVisible(highdBOctLabel); highFilterArea.addAndMakeVisible(highBandGainSlider); highFilterArea.addAndMakeVisible(highBandQSlider); highFilterArea.addAndMakeVisible(highBandFreqSlider); } } globalControlArea.addAndMakeVisible(inputSlider); globalControlArea.addAndMakeVisible(outputSlider); globalControlArea.addAndMakeVisible(masterBypassButton); footerBar.addAndMakeVisible(globalControlArea); addAndMakeVisible(footerBar); } void CrystalizerEQAudioProcessorEditor::setupLabels() { auto setupLabel = [](juce::Label &label, const juce::String &text) { label.setText(text, juce::dontSendNotification); label.setJustificationType(juce::Justification::centred); label.setColour(juce::Label::textColourId, juce::Colours::black); label.setInterceptsMouseClicks(false, false); // Klicks gehen an den Slider }; { //LOW-BAND setupLabel(lowBandFreqLabel, "Low\nHz"); setupLabel(lowBandSlopeLabel, "Slope"); setupLabel(lowBandGainLabel, "Low\nGain"); setupLabel(lowBandQLabel, "Low\nQ"); setupLabel(lowBandModeLabel, "Band Mode"); setupLabel(low12, "12"); setupLabel(low24, "24"); setupLabel(low36, "36"); setupLabel(low48, "48"); setupLabel(lowdBOctLabel, "dB/Oct"); //PEAK 1 setupLabel(peak1FreqLabel, "Low-Mid\nHz"); setupLabel(peak1GainLabel, "Low-Mid\nGain"); setupLabel(peak1QLabel, "Low-Mid\nQ"); //PEAK 2 setupLabel(peak2FreqLabel, "Mid\nHz"); setupLabel(peak2GainLabel, "Mid\nGain"); setupLabel(peak2QLabel, "Mid\nQ"); //PEAK 3 setupLabel(peak3FreqLabel, "High-Mid\nHz"); setupLabel(peak3GainLabel, "High-Mid\nGain"); setupLabel(peak3QLabel, "High-Mid\nQ"); //HIGH-BAND setupLabel(highBandFreqLabel, "High\nHz"); setupLabel(highBandSlopeLabel, "Slope"); setupLabel(highBandGainLabel, "High\nGain"); setupLabel(highBandQLabel, "High\nQ"); setupLabel(highBandModeLabel, "Band Mode"); setupLabel(high12, "12"); setupLabel(high24, "24"); setupLabel(high36, "36"); setupLabel(high48, "48"); setupLabel(highdBOctLabel, "dB/Oct"); setupLabel(presetBoxLabel, "Presets"); } } void CrystalizerEQAudioProcessorEditor::setupFontsWithColours() { Typography::applyToLabel(titleLabel, Typography::Style::Display, 1.f); titleLabel.setColour(juce::Label::textColourId, Colours::ACCENTCOLOUR); Typography::applyToLabel(presetBoxLabel, Typography::Style::H3, 1.f); presetBoxLabel.setColour(juce::Label::textColourId, Colours::FOREGROUNDCOLOUR); for (auto *l: sliderLabels) { l->setColour(juce::Label::textColourId, Colours::FOREGROUNDCOLOUR); Typography::applyToLabel(*l, Typography::Style::Mono, 1.f); } } void CrystalizerEQAudioProcessorEditor::setupSliderTextBoxes() { baseLookAndFeel = std::make_unique(); for (auto *s: sliders) { s->setLookAndFeel(baseLookAndFeel.get()); } } void CrystalizerEQAudioProcessorEditor::handleLowBandModes() { for (int i = 0; i < lowBandModeButtons.size(); ++i) { lowBandModeButtons[i]->onClick = [this, i] { if (!lowBandBools[i]) { if (auto *param = audioProcessor.apvts.getParameter("LowBandModes")) { param->beginChangeGesture(); for (int j = 0; j < lowBandBools.size(); ++j) { if (j != i) { lowBandBools[j] = false; param->setValueNotifyingHost(param->convertTo0to1(0.0f)); lowBandModeButtons[j]->setToggleState(false, juce::dontSendNotification); } } lowBandBools[i] = true; param->setValueNotifyingHost(param->convertTo0to1((float) i)); param->endChangeGesture(); updateFrequencyRanges(); } } else if (lowBandBools[i]) { lowBandModeButtons[i]->setToggleState(true, juce::dontSendNotification); } }; } } void CrystalizerEQAudioProcessorEditor::handleHighBandModes() { for (int i = 0; i < highBandModeButtons.size(); ++i) { highBandModeButtons[i]->onClick = [this, i] { if (!highBandBools[i]) { if (auto *param = audioProcessor.apvts.getParameter("HighBandModes")) { param->beginChangeGesture(); for (int j = 0; j < highBandBools.size(); ++j) { if (j != i) { highBandBools[j] = false; param->setValueNotifyingHost(param->convertTo0to1(0.0f)); highBandModeButtons[j]->setToggleState(false, juce::dontSendNotification); } } highBandBools[i] = true; param->setValueNotifyingHost(param->convertTo0to1((float) i)); param->endChangeGesture(); updateFrequencyRanges(); } } else if (highBandBools[i]) { highBandModeButtons[i]->setToggleState(true, juce::dontSendNotification); } }; } } void CrystalizerEQAudioProcessorEditor::setupEventListeners() { presetMenuButton.onClick = [this]() { auto *menu = new DesignSystem::PresetMenu(&presetNameInput, &savePresetButton, &deletePresetButton); const auto presetMenuBounds = presetMenu.getLocalBounds(); menu->setBounds(presetMenuBounds); presetMenuLookAndFeel = std::make_unique(); juce::Rectangle areaToPointTo = presetMenuButton.getScreenBounds(); auto *menuRaw = menu; presetMenuSafePtr = menuRaw; auto &box = juce::CallOutBox::launchAsynchronously( std::unique_ptr(menu), areaToPointTo, nullptr ); box.setLookAndFeel(presetMenuLookAndFeel.get()); }; savePresetButton.onClick = [this]() { if (presetNameInput.getText() != "") audioProcessor.setPresetName(presetNameInput.getText()); else audioProcessor.setPresetName("Preset"); const auto presetName = audioProcessor.savePresetToFile(); presetBox.clear(); const auto updatedPresets = audioProcessor.getPresetNamesArray(); presetBox.addItemList(updatedPresets, 1); for (int i = 0; i < presetBox.getNumItems(); i++) { if (presetName != presetBox.getItemText(i)) continue; presetBox.setSelectedId(i + 1, juce::dontSendNotification); } presetNameInput.setText(""); }; deletePresetButton.onClick = [this]() { auto selectedPreset = presetBox.getText(); if (selectedPreset == "Init") return; const auto selectedPresetWithExt = selectedPreset + ".xml"; audioProcessor.deletePreset(selectedPresetWithExt); audioProcessor.resetAllParameters(); presetBox.clear(); const auto updatedPresets = audioProcessor.getPresetNamesArray(); presetBox.addItemList(updatedPresets, 1); presetBox.setSelectedId(1, juce::dontSendNotification); }; presetBox.onChange = [this]() { auto selectedPreset = presetBox.getText(); if (selectedPreset == "Init") { audioProcessor.resetAllParameters(); updateModeButtons(); return; } const auto selectedPresetWithExt = selectedPreset + ".xml"; audioProcessor.loadPreset(selectedPresetWithExt); updateModeButtons(); // ← NEU! updateFrequencyRanges(); }; resetButton.onClick = [this]() { audioProcessor.resetAllParameters(); if (crystalizeButton.getToggleState()) { isAnimatingCrystalize = true; isFadingToActive = (svgToggleButtonLookAndFeel->activeIconOpacity < 1.0f); } resetAllCheckboxes(); for (auto *s: sliders) { s->setEnabled(true); } for (auto *l: sliderLabels) { l->setEnabled(true); } presetBox.setSelectedId(1, juce::dontSendNotification); updateFrequencyRanges(); }; handleLowBandModes(); handleHighBandModes(); presetNameInput.onMouseDown = [this]() { inputFocused = true; }; } void CrystalizerEQAudioProcessorEditor::updateModeButtons() { int lowMode = static_cast(audioProcessor.apvts.getRawParameterValue("LowBandModes")->load()); for (int i = 0; i < lowBandModeButtons.size(); ++i) { lowBandBools[i] = (i == lowMode); lowBandModeButtons[i]->setToggleState(lowBandBools[i], juce::dontSendNotification); } int highMode = static_cast(audioProcessor.apvts.getRawParameterValue("HighBandModes")->load()); for (int i = 0; i < highBandModeButtons.size(); ++i) { highBandBools[i] = (i == highMode); highBandModeButtons[i]->setToggleState(highBandBools[i], juce::dontSendNotification); } } void CrystalizerEQAudioProcessorEditor::parameterChanged(const juce::String& parameterID, float newValue) { if (parameterID == "LowBandModes" || parameterID == "HighBandModes") { juce::MessageManager::callAsync([this]() { updateModeButtons(); repaint(); }); } } void CrystalizerEQAudioProcessorEditor::initPresetSystem() { auto presets = audioProcessor.getPresetNamesArray(); presetBox.addItemList(presets, 1); presetBox.setSelectedId(1, juce::dontSendNotification); presetMenuButton.setName("PresetMenuButton"); presetMenuButton.setColour(juce::ToggleButton::textColourId, juce::Colours::black); presetMenuButton.setColour(juce::ToggleButton::tickColourId, juce::Colours::black); presetMenuButton.setToggleState(false, juce::dontSendNotification); presetNameInput.setJustification(juce::Justification::centred); presetButtonLookAndFeel = std::make_unique(); presetComboBoxLookAndFeel = std::make_unique(); presetInputLookAndFeel = std::make_unique(); savePresetButton.setLookAndFeel(presetButtonLookAndFeel.get()); deletePresetButton.setLookAndFeel(presetButtonLookAndFeel.get()); resetButton.setLookAndFeel(presetButtonLookAndFeel.get()); presetNameInput.setLookAndFeel(presetInputLookAndFeel.get()); presetBox.setLookAndFeel(presetComboBoxLookAndFeel.get()); } void CrystalizerEQAudioProcessorEditor::syncPresetBox() { const auto currentPresetName = audioProcessor.getPresetName(); // Durch alle Items in der ComboBox gehen for (int i = 0; i < presetBox.getNumItems(); ++i) { if (presetBox.getItemText(i) == currentPresetName) { presetBox.setSelectedId(i + 1, juce::dontSendNotification); return; } } presetBox.setSelectedId(1, juce::dontSendNotification); } void CrystalizerEQAudioProcessorEditor::syncAllButtonStates() { // Peak Bypass Buttons const bool peak1Bypassed = audioProcessor.apvts.getRawParameterValue("Peak1Bypass")->load() > 0.5f; if (peak1Bypassed) { disableLowMidBand(1.0f); } const bool peak2Bypassed = audioProcessor.apvts.getRawParameterValue("Peak2Bypass")->load() > 0.5f; if (peak2Bypassed) { disableMidBand(1.0f); } const bool peak3Bypassed = audioProcessor.apvts.getRawParameterValue("Peak3Bypass")->load() > 0.5f; if (peak3Bypassed) { disableHighMidBand(1.0f); } // Master Bypass const bool masterBypassed = audioProcessor.apvts.getRawParameterValue("MasterBypass")->load() > 0.5f; if (masterBypassed) { disableEverything(1.0f); } // Crystalize Button - Icon-Opacity setzen const bool crystalized = audioProcessor.apvts.getRawParameterValue("CrystalizeButton")->load() > 0.5f; if (crystalized) { svgToggleButtonLookAndFeel->activeIconOpacity = 1.0f; svgToggleButtonLookAndFeel->passiveIconOpacity = 0.0f; } else { svgToggleButtonLookAndFeel->activeIconOpacity = 0.0f; svgToggleButtonLookAndFeel->passiveIconOpacity = 1.0f; } crystalizeButton.repaint(); syncPresetBox(); } //============================================================================== CrystalizerEQAudioProcessorEditor::CrystalizerEQAudioProcessorEditor(CrystalizerEQAudioProcessor &p) : AudioProcessorEditor(&p), audioProcessor(p), spectrumAnalyzer(p.audioFIFO, p) { setSize(1280, 720); startTimerHz(60); addComponentsToLayout(); setupLabels(); setupFontsWithColours(); setupAttachments(); setupDisplayNames(); setupToggleButtons(); setupEventListeners(); setupSliderTextBoxes(); setupSliders(); initPresetSystem(); updateFrequencyRanges(); audioProcessor.apvts.addParameterListener("LowBandModes", this); audioProcessor.apvts.addParameterListener("HighBandModes", this); updateModeButtons(); syncAllButtonStates(); } CrystalizerEQAudioProcessorEditor::~CrystalizerEQAudioProcessorEditor() { stopTimer(); for (auto *s: sliders) { s->setLookAndFeel(nullptr); } peak1BypassButton.setLookAndFeel(nullptr); peak2BypassButton.setLookAndFeel(nullptr); peak3BypassButton.setLookAndFeel(nullptr); masterBypassButton.setLookAndFeel(nullptr); crystalizeButton.setLookAndFeel(nullptr); presetMenuButton.setLookAndFeel(nullptr); presetNameInput.setLookAndFeel(nullptr); presetBox.setLookAndFeel(nullptr); audioProcessor.apvts.removeParameterListener("LowBandModes", this); audioProcessor.apvts.removeParameterListener("HighBandModes", this); for (auto *b: lowBandModeButtons) { b->setLookAndFeel(nullptr); } for (auto *b: highBandModeButtons) { b->setLookAndFeel(nullptr); } }; float CrystalizerEQAudioProcessorEditor::getInterpolatedDb(float exactBin) { int bin1 = static_cast(std::floor(exactBin)); int bin2 = static_cast(std::ceil(exactBin)); const int maxBin = static_cast(spectrumAnalyzer.renderValuesDb.size()) - 1; bin1 = juce::jlimit(0, maxBin, bin1); bin2 = juce::jlimit(0, maxBin, bin2); if (bin1 == bin2) return spectrumAnalyzer.renderValuesDb[bin1]; float y1 = juce::jlimit(spectrumAnalyzer.MINDB, spectrumAnalyzer.MAXDB, spectrumAnalyzer.renderValuesDb[bin1]); float y2 = juce::jlimit(spectrumAnalyzer.MINDB, spectrumAnalyzer.MAXDB, spectrumAnalyzer.renderValuesDb[bin2]); float fraction = exactBin - bin1; return y1 + fraction * (y2 - y1); } void CrystalizerEQAudioProcessorEditor::paintAnalyzer(juce::Graphics &g) { analyzerRect = getLocalArea(&analyzerArea, analyzerArea.getLocalBounds()); juce::Graphics::ScopedSaveState guard(g); g.reduceClipRegion(analyzerRect); auto r = analyzerRect.toFloat(); // BACKGROUND g.setColour(juce::Colours::black); g.fillRect(r); // BORDER g.setColour(juce::Colours::grey); g.drawRect(analyzerRect); // SPECTRUM g.setColour(Colours::ACCENTCOLOUR); const float analyzerWidth = analyzerRect.getWidth(); const float analyzerHeight = analyzerRect.getHeight(); const float analyzerBottom = analyzerRect.getBottom(); const float minDb = spectrumAnalyzer.MINDB; const float maxDb = spectrumAnalyzer.MAXDB; const float dbRange = maxDb - minDb; // = 96 dB const float minFreq = spectrumAnalyzer.MINFREQ; // 20 Hz const float maxFreq = spectrumAnalyzer.MAXFREQ; // 20 kHz float deltaF = spectrumAnalyzer.deltaF; juce::Path spectrumPath; bool pathStarted = false; const int resolutionMultiplier = 4; const int numPoints = static_cast(analyzerWidth) * resolutionMultiplier; for (int i = 0; i < numPoints; ++i) { float x = (i / static_cast(numPoints)) * analyzerWidth; float normalizedX = i / static_cast(numPoints); float logMin = std::log10(minFreq); float logMax = std::log10(maxFreq); float logFreq = logMin + normalizedX * (logMax - logMin); float freq = std::pow(10.0f, logFreq); float exactBin = freq / deltaF; float dB = getInterpolatedDb(exactBin); float normalized = (dB - minDb) / dbRange; normalized = juce::jlimit(0.0f, 1.0f, normalized); float pixelHeight = normalized * analyzerHeight; float y = analyzerBottom - pixelHeight; if (!pathStarted) { spectrumPath.startNewSubPath(x + analyzerRect.getX(), y); pathStarted = true; } else { spectrumPath.lineTo(x + analyzerRect.getX(), y); } } if (pathStarted) { juce::PathStrokeType stroke(2.0f); stroke.setJointStyle(juce::PathStrokeType::curved); // Runde Ecken g.strokePath(spectrumPath, stroke); spectrumPath.lineTo(analyzerRect.getRight(), analyzerBottom); spectrumPath.lineTo(analyzerRect.getX(), analyzerBottom); spectrumPath.closeSubPath(); g.setColour(Colours::ACCENTCOLOUR.withAlpha(0.3f)); g.fillPath(spectrumPath); } drawFrequencyGrid(g, dbRange); } float CrystalizerEQAudioProcessorEditor::mapFrequencyToX(float freq, float minFreq, float maxFreq, float width) { float logMin = std::log10(minFreq); float logMax = std::log10(maxFreq); float logFreq = std::log10(freq); float normalized = (logFreq - logMin) / (logMax - logMin); return normalized * width; } void CrystalizerEQAudioProcessorEditor::drawFrequencyGrid(juce::Graphics &g, const float dbRange) { g.setColour(Colours::FOREGROUNDCOLOUR.withAlpha(0.3f)); const float minFreq = spectrumAnalyzer.MINFREQ; const float maxFreq = spectrumAnalyzer.MAXFREQ; const float analyzerWidth = analyzerRect.getWidth(); const float minDB = spectrumAnalyzer.MINDB; const float maxDB = spectrumAnalyzer.MAXDB; const float analyzerHeight = analyzerRect.getHeight(); const float analyzerBottom = analyzerRect.getBottom(); std::vector gridFreqs = {50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000}; std::vector gridDB = {0, -12, -24, -36, -48, -60, -72}; for (int i = 0; i < gridDB.size(); ++i) { float db = gridDB[i]; float normalized = (db - minDB) / dbRange; normalized = juce::jlimit(0.0f, 1.0f, normalized); float pixelHeight = normalized * analyzerHeight; float y = analyzerBottom - pixelHeight; g.drawHorizontalLine(static_cast(y), static_cast(analyzerRect.getX()), static_cast(analyzerRect.getRight())); g.setFont(Typography::getFont(Typography::Style::Mono, 1.0f)); juce::String label = juce::String(gridDB[i]); g.drawText(label, analyzerRect.getX(), y, 30, 15, juce::Justification::centred); } for (float freq: gridFreqs) { float x = mapFrequencyToX(freq, minFreq, maxFreq, analyzerWidth); x += analyzerRect.getX(); g.drawVerticalLine(static_cast(x), static_cast(analyzerRect.getY()), static_cast(analyzerRect.getBottom())); if (freq == 20) continue; g.setFont(Typography::getFont(Typography::Style::Mono, 1.0f)); juce::String label = (freq >= 1000) ? juce::String(freq / 1000) + "k" : juce::String(freq); g.drawText(label, static_cast(x) - 15, analyzerRect.getY(), 30, 15, juce::Justification::centred); } } void CrystalizerEQAudioProcessorEditor::paintModeBoxBorders(juce::Graphics &g) { for (auto *b: lowBandModeButtons) { if (b->getToggleState()) { auto r = getLocalArea(&lowBandModeBox, b->getBounds()); int side = juce::jmin(r.getWidth(), r.getHeight()); r = r.withSizeKeepingCentre(side, side); auto rf = r.toFloat(); auto rfFilled = rf.toNearestInt().toFloat(); const float stroke = 1.0f; auto rfStroke = rfFilled.reduced(stroke * 0.5f); g.setColour(juce::Colours::white.withAlpha(0.3f)); g.fillRoundedRectangle(r.toFloat(), 5); g.setColour(juce::Colours::white.withAlpha(0.9f)); g.drawRoundedRectangle(rfStroke, 5, stroke); } } } void CrystalizerEQAudioProcessorEditor::paint(juce::Graphics &g) { g.fillAll(Colours::BACKGROUNDCOLOUR); g.setColour(AXIOM::DesignSystem::Colours::FOREGROUNDCOLOUR); const auto mainPanelArea = getLocalArea(&mainPanel, mainPanel.getLocalBounds()); auto mainPanelY = mainPanelArea.getY(); paintAnalyzer(g); g.setColour(Colours::SURFACEBYPASS); const auto headerBarArea = getLocalArea(&headerBar, headerBar.getLocalBounds()); auto headerBarAreaX = headerBarArea.getX(); auto headerBarAreaY = headerBarArea.getY(); auto headerBarAreaW = headerBarArea.getWidth(); auto headerBarAreaH = headerBarArea.getHeight(); auto headerBarAreaPad = ((headerBarAreaY + mainPanelY) - headerBarAreaH) / 2; g.fillRect(headerBarAreaX, headerBarAreaY, headerBarAreaW, mainPanelY - headerBarAreaPad); g.setColour(Colours::SURFACECOLOUR); const auto presetLocalArea = getLocalArea(&presetArea, presetArea.getLocalBounds()); auto presetLocalAreaX = presetLocalArea.getX(); auto presetLocalAreaY = presetLocalArea.getY(); auto presetLocalAreaWidth = presetLocalArea.getWidth(); auto presetLocalAreaHeight = presetLocalArea.getHeight(); g.fillRoundedRectangle(presetLocalAreaX, presetLocalAreaY - 10.f, presetLocalAreaWidth, presetLocalAreaHeight + 10.f, 10.0f); const auto filterLocalArea = getLocalArea(&filterArea, filterArea.getLocalBounds()); const int filterLocalAreaBorderWidth = 3; auto filterLocalAreaBorder = filterLocalArea.withSizeKeepingCentre(filterLocalArea.getWidth() + filterLocalAreaBorderWidth, filterLocalArea.getHeight() + filterLocalAreaBorderWidth); filterLocalAreaBorder = filterLocalAreaBorder.withX(filterLocalAreaBorder.getX() + filterLocalAreaBorderWidth).withY(filterLocalAreaBorder.getY() + filterLocalAreaBorderWidth); g.setColour(Colours::SURFACECOLOUR); g.fillRect(filterLocalAreaBorder); filterLocalAreaBorder = filterLocalAreaBorder.withX(filterLocalAreaBorder.getX() - 2 * filterLocalAreaBorderWidth).withY(filterLocalAreaBorder.getY() - 2 * filterLocalAreaBorderWidth); g.fillRect(filterLocalAreaBorder); g.setColour(Colours::BACKGROUNDBYPASS); g.fillRect(filterLocalArea); g.setColour(Colours::SURFACEBYPASS); const auto footerBarArea = getLocalArea(&footerBar, footerBar.getLocalBounds()); g.fillRect(footerBarArea); paintBorderLines(g); } void CrystalizerEQAudioProcessorEditor::paintBorderLines(juce::Graphics &g) { g.setColour(DesignSystem::Colours::BACKGROUNDCOLOUR); auto prevRight = (float) lowFilterArea.getRight(); for (auto *c: filterAreas) { if (c == &lowFilterArea) continue; const auto area = getLocalArea(c, c->getLocalBounds()); const float xAvg = ((float) area.getX() - prevRight) / 2; const int x = area.getX() - xAvg + (filterAreaMargin / 2); prevRight = (float) c->getRight(); const auto top = (float) area.getY(); const auto bot = (float) area.getBottom(); const float center = (top + bot) * 0.5f; const float halfLen = (bot - top) * 0.375f; g.drawVerticalLine(x, center - halfLen, center + halfLen); } g.setColour(DesignSystem::Colours::SURFACECOLOUR); auto prevRightGlobal = (float) globalControlArea.getRight(); for (auto *c: globalControlArea.getChildren()) { if (c == &inputSlider) continue; if (c == &masterBypassButton) continue; const auto area = getLocalArea(c, c->getLocalBounds()); const float xAvg = ((float) area.getX() - prevRightGlobal) / 2; const int x = area.getX() - xAvg; prevRightGlobal = (float) c->getRight(); const auto top = (float) area.getY(); const auto bot = (float) area.getBottom(); const float center = (top + bot) * 0.5f; const float halfLen = (bot - top) * 0.375f; g.drawVerticalLine(x, center - halfLen, center + halfLen); } } void CrystalizerEQAudioProcessorEditor::setKnobVisibility() { int lowMode = (int) audioProcessor.apvts .getRawParameterValue("LowBandModes")->load(); const bool masterIsToggled = masterBypassButton.getToggleState(); const float target = masterIsToggled ? 1.0f : 0.0f; if (masterIsToggled) { lowMode = 0; disableLowBand(target); } lowBandFreqSlider.setEnabled(lowMode >= 1); lowBandFreqLabel.setEnabled(lowMode >= 1); lowBandSlopeSlider.setEnabled(lowMode == 1); lowBandSlopeLabel.setEnabled(lowMode == 1); lowdBOctLabel.setEnabled(lowMode == 1); lowBandGainSlider.setEnabled(lowMode >= 2); lowBandGainLabel.setEnabled(lowMode >= 2); lowBandQSlider.setEnabled(lowMode >= 1); lowBandQLabel.setEnabled(lowMode >= 1); lowBandModeLabel.setEnabled(lowMode >= 1); int highMode = (int) audioProcessor.apvts .getRawParameterValue("HighBandModes")->load(); if (masterIsToggled) { highMode = 0; disableHighBand(target); } highBandFreqSlider.setEnabled(highMode >= 1); highBandFreqLabel.setEnabled(highMode >= 1); highBandSlopeSlider.setEnabled(highMode == 1); highBandSlopeLabel.setEnabled(highMode == 1); highdBOctLabel.setEnabled(highMode == 1); highBandGainSlider.setEnabled(highMode >= 2); highBandGainLabel.setEnabled(highMode >= 2); highBandQSlider.setEnabled(highMode >= 1); highBandQLabel.setEnabled(highMode >= 1); highBandModeLabel.setEnabled(highMode >= 1); } void CrystalizerEQAudioProcessorEditor::updateFrequencyRanges() { int lowMode = (int) audioProcessor.apvts.getRawParameterValue("LowBandModes")->load(); if (lowMode == 3) { lowBandFreqSlider.setRange(20.0, 500.0, 1.0); float currentFreq = audioProcessor.apvts.getRawParameterValue("LowBandFreq")->load(); if (currentFreq > 500.0f) { lowBandFreqSlider.setValue(500.0, juce::sendNotificationSync); } } else if (lowMode >= 1) { lowBandFreqSlider.setRange(20.0, 20000.0, 1.0); } lowBandFreqSlider.repaint(); int highMode = (int) audioProcessor.apvts.getRawParameterValue("HighBandModes")->load(); if (highMode == 3) { highBandFreqSlider.setRange(8000.0, 20000.0, 1.0); float currentFreq = audioProcessor.apvts.getRawParameterValue("HighBandFreq")->load(); if (currentFreq < 8000.0f) { highBandFreqSlider.setValue(8000.0, juce::sendNotificationSync); } } else if (highMode >= 1) { highBandFreqSlider.setRange(20.0, 20000.0, 1.0); } highBandFreqSlider.repaint(); } void CrystalizerEQAudioProcessorEditor::animateCrystalizeButton() { if (isAnimatingCrystalize) { const float step = 0.1f; if (isFadingToActive) { svgToggleButtonLookAndFeel->activeIconOpacity += step; svgToggleButtonLookAndFeel->passiveIconOpacity -= step; if (svgToggleButtonLookAndFeel->activeIconOpacity >= 1.0f) { svgToggleButtonLookAndFeel->activeIconOpacity = 1.0f; svgToggleButtonLookAndFeel->passiveIconOpacity = 0.0f; isAnimatingCrystalize = false; } } else { svgToggleButtonLookAndFeel->activeIconOpacity -= step; svgToggleButtonLookAndFeel->passiveIconOpacity += step; if (svgToggleButtonLookAndFeel->passiveIconOpacity >= 1.0f) { svgToggleButtonLookAndFeel->activeIconOpacity = 0.0f; svgToggleButtonLookAndFeel->passiveIconOpacity = 1.0f; isAnimatingCrystalize = false; } } crystalizeButton.repaint(); } } void CrystalizerEQAudioProcessorEditor::timerCallback() { setKnobVisibility(); spectrumAnalyzer.processSamples(); repaint(analyzerRect); resized(); animateCrystalizeButton(); updateFrequencyRanges(); if (presetMenuSafePtr == nullptr) { presetMenuButton.setToggleState(false, juce::dontSendNotification); inputFocused = false; } else if (!inputFocused) { presetNameInput.giveAwayKeyboardFocus(); } } void CrystalizerEQAudioProcessorEditor::resized() { auto pluginArea = getLocalBounds(); scalePluginWindow(pluginArea); setupMainGrid(pluginArea); setupHeader(); setupBody(); setupFooter(); } void CrystalizerEQAudioProcessorEditor::resetAllCheckboxes() { const auto notify = juce::dontSendNotification; masterBypassButton.setToggleState(false, notify); crystalizeButton.setToggleState(false, notify); peak1BypassButton.setToggleState(false, notify); peak2BypassButton.setToggleState(false, notify); peak3BypassButton.setToggleState(false, notify); lowBypass.setToggleState(false, notify); lowCut.setToggleState(false, notify); lowBell.setToggleState(false, notify); lowShelf.setToggleState(true, notify); highBypass.setToggleState(false, notify); highCut.setToggleState(false, notify); highBell.setToggleState(false, notify); highShelf.setToggleState(true, notify); } void CrystalizerEQAudioProcessorEditor::scalePluginWindow(juce::Rectangle area) { if (area.getWidth() < 520) Spacing::setUiScale(Spacing::uiScaleMode::XS); else if (area.getWidth() < 720) Spacing::setUiScale(Spacing::uiScaleMode::S); else if (area.getWidth() < 1024) Spacing::setUiScale(Spacing::uiScaleMode::M); else if (area.getWidth() < 1366) Spacing::setUiScale(Spacing::uiScaleMode::L); else Spacing::setUiScale(Spacing::uiScaleMode::XL); } void CrystalizerEQAudioProcessorEditor::setupMainGrid(juce::Rectangle area) { const float headerHeight = static_cast(getHeight()) * 0.1f; const float footerHeight = static_cast(getHeight()) * 0.1f; Layout::GridSpec spec{ /* cols */ {Layout::fr(1)}, /* rows */ {Layout::pxTrack(headerHeight), Layout::fr(1), Layout::pxTrack(footerHeight)}, // Header / Body / Footer /* colGap */ Spacing::SizeMode::S, /* rowGap */ Spacing::SizeMode::S, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(area, spec, {&headerBar, &mainPanel, &footerBar}); } void CrystalizerEQAudioProcessorEditor::setupHeader() { const auto bounds = headerBar.getLocalBounds(); const float presetAreaWidth = static_cast(bounds.getWidth()) * 0.5f; const auto titleWidthCentre = titleLabel.getFont().getStringWidth("Crystalizer") / 4; Layout::GridSpec headerSpec{ /* cols */ {Layout::fr(1), Layout::pxTrack(presetAreaWidth), Layout::fr(1)}, /* rows */ {Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, headerSpec, { Layout::area(presetArea, 1, 2, 2, 3), Layout::area(titleLabel, 1, 1, 2, 2) .withMargin(juce::GridItem::Margin(0, 0, 0, titleWidthCentre)), Layout::area(presetMenu, 1, 3, 2, 4), }); const auto presetMenuBounds = presetMenu.getLocalBounds(); Layout::GridSpec presetMenuSpec{ /* cols */ {Layout::fr(1), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(presetMenuBounds, presetMenuSpec, { Layout::area(presetNameInput, 1, 1, 2, 3), Layout::area(savePresetButton, 2, 1, 3, 2), Layout::area(deletePresetButton, 2, 2, 3, 3), }); const auto presetAreaBounds = presetArea.getLocalBounds(); const auto presetBoxWidth = static_cast(presetArea.getWidth()); const auto presetBoxHeight = static_cast(presetArea.getHeight()); const float rowHeight = presetBoxHeight * 0.25f; const float iconSize = rowHeight; Layout::GridSpec presetSpec{ /* cols */ {Layout::fr(1), Layout::pxTrack(presetBoxWidth * 0.5f), Layout::fr(1)}, /* rows */ {Layout::pxTrack(rowHeight), Layout::pxTrack(rowHeight)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::S) }; Layout::grid(presetAreaBounds, presetSpec, { Layout::area(presetBoxLabel, 1, 2, 2, 3), Layout::area(presetBox, 2, 2, 3, 3), Layout::area(resetButton, 2, 1, 2, 2), Layout::area(presetMenuButton, 2, 3, 3, 4) .withWidth(iconSize) .withHeight(iconSize) }); } void CrystalizerEQAudioProcessorEditor::setupBody() { const auto bounds = mainPanel.getLocalBounds(); const auto bodyHeight = static_cast(bounds.getHeight()); const auto bodyWidth = static_cast(bounds.getWidth()); const auto bodyColWidth = bodyWidth / 5.0f; const auto crystalizeIconSize = 140.0f; Layout::GridSpec bodySpec{ /* cols */ { Layout::fr(1), Layout::pxTrack(bodyColWidth), Layout::pxTrack(bodyColWidth), Layout::pxTrack(bodyColWidth), Layout::fr(1) }, /* rows */ {Layout::fr(1), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, bodySpec, { Layout::area(analyzerArea, 1, 2, 2, 5) .withMargin(juce::GridItem::Margin(filterAreaMargin, 0, filterAreaMargin, 0)), Layout::area(crystalizeButton, 1, 5, 2, 6) .withWidth(crystalizeIconSize) .withHeight(crystalizeIconSize * 0.5f) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(filterArea, 2, 1, 3, 6) .withMargin(juce::GridItem::Margin(0, filterAreaMargin, 0, filterAreaMargin)) }); const auto filterAreaBounds = filterArea.getLocalBounds(); const auto filterAreaWidth = static_cast(filterArea.getWidth()); const auto filterColWidth = filterAreaWidth * 0.2f; Layout::GridSpec filterSpec{ /* cols */ { Layout::fr(1), Layout::pxTrack(filterColWidth), Layout::pxTrack(filterColWidth), Layout::pxTrack(filterColWidth), Layout::fr(1) }, /* rows */ {Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::S) }; Layout::grid(filterAreaBounds, filterSpec, { Layout::area(lowFilterArea, 1, 1, 2, 2), Layout::area(lowMidFilterArea, 1, 2, 2, 3), Layout::area(midFilterArea, 1, 3, 2, 4), Layout::area(highMidFilterArea, 1, 4, 2, 5), Layout::area(highFilterArea, 1, 5, 2, 6), }); setupLowBandLayout(); setupLowMidBandLayout(); setupMidBandLayout(); setupHighMidBandLayout(); setupHighBandLayout(); } void CrystalizerEQAudioProcessorEditor::setupLowBandLayout() { const auto bounds = lowFilterArea.getLocalBounds(); const auto areaWidth = static_cast(bounds.getWidth()); const auto areaHeight = static_cast(bounds.getHeight()); const auto knobColWidth = areaWidth / 3.0f; const auto knobRowHeight = areaHeight / 3.0f; const auto refW = getReferenceCell()[0]; const auto refH = getReferenceCell()[1]; const auto gainSize = refH * gainMod; const auto offSetToGainTop = gainSize; const float labelPadding = Components::SlopeSliderLookAndFeel::labelPadding * 2; Layout::GridSpec lowBandSpec{ /* cols */ {Layout::fr(1), Layout::pxTrack(knobColWidth), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::pxTrack(knobRowHeight * 1.25f), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, lowBandSpec, { Layout::area(lowBandFreqSlider, 2, 1, 3, 2) .withWidth(refW * freqMod * 0.8f) .withHeight(refH * freqMod * 0.8f) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(lowBandGainSlider, 2, 2, 3, 3) .withWidth(refW * gainMod * 0.8f) .withHeight(refH * gainMod * 0.8f) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(lowBandQSlider, 2, 3, 3, 4) .withWidth(refW * qMod * 0.8f) .withHeight(refH * qMod * 0.8f) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(lowBandSlopeSlider, 3, 1, 4, 2) .withWidth(refW * slopeMod + labelPadding) .withHeight(refH * slopeMod + labelPadding) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(lowBandModeBox, 3, 2, 4, 4), Layout::area(lowBandFreqLabel, 2, 1, 3, 2) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(lowBandGainLabel, 1, 2, 2, 3), Layout::area(lowBandQLabel, 2, 3, 3, 4) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(lowBandSlopeLabel, 3, 1, 4, 2) .withMargin(juce::GridItem::Margin(-offSetToGainTop / 1.5f, 0, 0, 0)), Layout::area(lowdBOctLabel, 3, 1, 4, 2) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), }); const auto modeBoxBounds = lowBandModeBox.getLocalBounds(); const auto slopeH = lowBandSlopeSlider.getY() - lowBandModeBox.getY() + lowBandModeLabel.getFont().getHeight() / 2; Layout::GridSpec lowBandModeBoxSpec{ /* cols */ {Layout::fr(1), Layout::fr(1), Layout::fr(1), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(modeBoxBounds, lowBandModeBoxSpec, { Layout::area(lowBandModeLabel, 1, 2, 2, 4) .withMargin(juce::GridItem::Margin(slopeH, 0, 0, 0)), Layout::area(lowBypass, 1, 1, 3, 2), Layout::area(lowCut, 1, 2, 3, 3), Layout::area(lowBell, 1, 3, 3, 4), Layout::area(lowShelf, 1, 4, 3, 5) }); } void CrystalizerEQAudioProcessorEditor::setupLowMidBandLayout() { const auto bounds = lowMidFilterArea.getLocalBounds(); const auto areaWidth = static_cast(bounds.getWidth()); const auto areaHeight = static_cast(bounds.getHeight()); const auto knobColWidth = areaWidth / 3.0f; const auto knobRowHeight = areaHeight / 3.0f; const auto refW = getReferenceCell()[0]; const auto refH = getReferenceCell()[1]; const auto gainSize = refH * gainMod; const auto offSetToGainTop = gainSize; Layout::GridSpec lowMidBandSpec{ /* cols */ {Layout::fr(1), Layout::pxTrack(knobColWidth), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::pxTrack(knobRowHeight * 1.25f), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, lowMidBandSpec, { Layout::area(peak1FreqSlider, 2, 1, 3, 2) .withWidth(refW * freqMod) .withHeight(refH * freqMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak1GainSlider, 2, 2, 3, 3) .withWidth(refW * gainMod) .withHeight(refH * gainMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak1QSlider, 2, 3, 3, 4) .withWidth(refW * qMod) .withHeight(refH * qMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak1FreqLabel, 2, 1, 3, 2) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(peak1GainLabel, 1, 2, 2, 3), Layout::area(peak1QLabel, 2, 3, 3, 4) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(peak1BypassButton, 3, 2, 4, 3) }); } //endregion setupLowMidBandLayout //region setupMidBandLayout void CrystalizerEQAudioProcessorEditor::setupMidBandLayout() { const auto bounds = midFilterArea.getLocalBounds(); const auto areaWidth = static_cast(bounds.getWidth()); const auto areaHeight = static_cast(bounds.getHeight()); const auto knobColWidth = areaWidth / 3.0f; const auto knobRowHeight = areaHeight / 3.0f; const auto refW = getReferenceCell()[0]; const auto refH = getReferenceCell()[1]; const auto gainSize = refH * gainMod; const auto offSetToGainTop = gainSize; Layout::GridSpec midBandSpec{ /* cols */ {Layout::fr(1), Layout::pxTrack(knobColWidth), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::pxTrack(knobRowHeight * 1.25f), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, midBandSpec, { Layout::area(peak2FreqSlider, 2, 1, 3, 2) .withWidth(refW * freqMod) .withHeight(refH * freqMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak2GainSlider, 2, 2, 3, 3) .withWidth(refW * gainMod) .withHeight(refH * gainMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak2QSlider, 2, 3, 3, 4) .withWidth(refW * qMod) .withHeight(refH * qMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak2FreqLabel, 2, 1, 3, 2) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(peak2GainLabel, 1, 2, 2, 3), Layout::area(peak2QLabel, 2, 3, 3, 4) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(peak2BypassButton, 3, 2, 4, 3) }); } void CrystalizerEQAudioProcessorEditor::setupHighMidBandLayout() { const auto bounds = highMidFilterArea.getLocalBounds(); const auto areaWidth = static_cast(bounds.getWidth()); const auto areaHeight = static_cast(bounds.getHeight()); const auto knobColWidth = areaWidth / 3.0f; const auto knobRowHeight = areaHeight / 3.0f; const auto refW = getReferenceCell()[0]; const auto refH = getReferenceCell()[1]; const auto gainSize = refH * gainMod; const auto offSetToGainTop = gainSize; Layout::GridSpec highMidBandSpec{ /* cols */ {Layout::fr(1), Layout::pxTrack(knobColWidth), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::pxTrack(knobRowHeight * 1.25f), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, highMidBandSpec, { Layout::area(peak3FreqSlider, 2, 1, 3, 2) .withWidth(refW * freqMod) .withHeight(refH * freqMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak3GainSlider, 2, 2, 3, 3) .withWidth(refW * gainMod) .withHeight(refH * gainMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak3QSlider, 2, 3, 3, 4) .withWidth(refW * qMod) .withHeight(refH * qMod) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(peak3FreqLabel, 2, 1, 3, 2) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(peak3GainLabel, 1, 2, 2, 3), Layout::area(peak3QLabel, 2, 3, 3, 4) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(peak3BypassButton, 3, 2, 4, 3) }); } void CrystalizerEQAudioProcessorEditor::setupHighBandLayout() { const auto bounds = highFilterArea.getLocalBounds(); const auto areaWidth = static_cast(bounds.getWidth()); const auto areaHeight = static_cast(bounds.getHeight()); const auto knobColWidth = areaWidth / 3.0f; const auto knobRowHeight = areaHeight / 3.0f; const auto refW = getReferenceCell()[0]; const auto refH = getReferenceCell()[1]; const auto gainSize = refH * gainMod; const auto offSetToGainTop = gainSize; const float labelPadding = Components::SlopeSliderLookAndFeel::labelPadding * 2; Layout::GridSpec highBandSpec{ /* cols */ {Layout::fr(1), Layout::pxTrack(knobColWidth), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::pxTrack(knobRowHeight * 1.25f), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, highBandSpec, { Layout::area(highBandFreqSlider, 2, 1, 3, 2) .withWidth(refW * freqMod * 0.8f) .withHeight(refH * freqMod * 0.8f) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(highBandGainSlider, 2, 2, 3, 3) .withWidth(refW * gainMod * 0.8f) .withHeight(refH * gainMod * 0.8f) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(highBandQSlider, 2, 3, 3, 4) .withWidth(refW * qMod * 0.8f) .withHeight(refH * qMod * 0.8f) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(highBandModeBox, 3, 1, 4, 3), Layout::area(highBandSlopeSlider, 3, 3, 4, 4) .withWidth(refW * slopeMod + labelPadding) .withHeight(refH * slopeMod + labelPadding) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(highBandFreqLabel, 2, 1, 3, 2) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(highBandGainLabel, 1, 2, 2, 3), Layout::area(highBandQLabel, 2, 3, 3, 4) .withMargin(juce::GridItem::Margin(-offSetToGainTop, 0, 0, 0)), Layout::area(highBandSlopeLabel, 3, 3, 4, 4) .withMargin(juce::GridItem::Margin(-offSetToGainTop / 1.5f, 0, 0, 0)), Layout::area(highdBOctLabel, 3, 3, 4, 4) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), }); const auto modeBoxBounds = lowBandModeBox.getLocalBounds(); const auto slopeH = highBandSlopeSlider.getY() - highBandModeBox.getY() + highBandModeLabel.getFont().getHeight() / 2; Layout::GridSpec highBandModeBoxSpec{ /* cols */ {Layout::fr(1), Layout::fr(1), Layout::fr(1), Layout::fr(1)}, /* rows */ {Layout::fr(1), Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(modeBoxBounds, highBandModeBoxSpec, { Layout::area(highBandModeLabel, 1, 2, 2, 4) .withMargin(juce::GridItem::Margin(slopeH, 0, 0, 0)), Layout::area(highBypass, 1, 4, 3, 5), Layout::area(highCut, 1, 3, 3, 4), Layout::area(highBell, 1, 2, 3, 3), Layout::area(highShelf, 1, 1, 3, 2) }); } void CrystalizerEQAudioProcessorEditor::setupFooter() { const auto bounds = footerBar.getLocalBounds(); const auto refW = getReferenceCell()[0]; const auto refH = getReferenceCell()[1]; Layout::GridSpec footerSpec{ /* cols */ {Layout::fr(1), Layout::fr(1), Layout::fr(1)}, /* rows */ {Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(bounds, footerSpec, { Layout::area(globalControlArea, 1, 3, 2, 4) }); const auto globalControlAreaBounds = globalControlArea.getLocalBounds(); const auto sliderRadius = outputSlider.getWidth() / 2.0f; const auto sliderWidth = outputSlider.getWidth(); Layout::GridSpec globalControlAreaSpec{ /* cols */ {Layout::fr(1), Layout::fr(1), Layout::fr(1)}, /* rows */ {Layout::fr(1)}, /* colGap */ Spacing::SizeMode::XS, /* rowGap */ Spacing::SizeMode::XS, /* pad */ Layout::padding(Spacing::SizeMode::XS) }; Layout::grid(globalControlAreaBounds, globalControlAreaSpec, { Layout::area(outputSlider, 1, 2, 3, 3) .withWidth(refW * globalMod) .withHeight(refH * globalMod) .withMargin(juce::GridItem::Margin(0, 0, 0, sliderRadius * 6)) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(inputSlider, 1, 1, 3, 2) .withWidth(refW * globalMod) .withHeight(refH * globalMod) .withMargin(juce::GridItem::Margin(0, 0, 0, sliderRadius * 10)) .withAlignSelf(juce::GridItem::AlignSelf::center) .withJustifySelf(juce::GridItem::JustifySelf::center), Layout::area(masterBypassButton, 1, 3, 3, 4) .withWidth(sliderWidth) .withMargin(juce::GridItem::Margin(0, 0, 0, sliderRadius * 2)), }); } juce::Array CrystalizerEQAudioProcessorEditor::getReferenceCell() { const auto bounds = midFilterArea.getLocalBounds(); const auto areaWidth = static_cast(bounds.getWidth()); const auto areaHeight = static_cast(bounds.getHeight()); const auto knobColWidth = areaWidth / 3.0f; const auto knobRowHeight = areaHeight / 3.0f; const auto gainCellW = knobColWidth; const auto gainCellH = knobRowHeight * 1.25f; const juce::Array refCell = {gainCellW, gainCellH}; return refCell; } void SpectrumAnalyzer::processSamples() { auto samples = audioFIFO.sendSamplesToEditor(); getFftFrame(samples); } void SpectrumAnalyzer::getFftFrame(juce::Array &samples) { const int needed = FFTSIZE - fftFrame.size(); if (needed <= 0) return; const int available = samples.size(); if (available <= 0) return; const int take = std::min(needed, available); for (int i = 0; i < take; ++i) fftFrame.add(samples.getUnchecked(i)); samples.removeRange(0, take); if (fftFrame.size() == FFTSIZE) { std::vector fullFrame(fftFrame.begin(), fftFrame.end()); fftFrame.removeRange(0, HOPSIZE); applyWindowOnFftFrame(fullFrame); } } void SpectrumAnalyzer::applyWindowOnFftFrame(std::vector &fullFrame) { if (fullFrame.size() != FFTSIZE) return; window.multiplyWithWindowingTable(fullFrame.data(), FFTSIZE); processWindowedFrame(fullFrame); } void SpectrumAnalyzer::processWindowedFrame(std::vector &windowedFrame) { if (windowedFrame.size() != FFTSIZE || fftData.size() != FFTSIZE * 2) return; fillFftDataFromFrame(windowedFrame); fft.performFrequencyOnlyForwardTransform(fftData.data(), true); buildMagnitudeSpectrum(); convertToDb(); applySmoothing(); renderValuesDb = getRenderValues(); } void SpectrumAnalyzer::fillFftDataFromFrame(std::vector &windowedFrame) { for (int n = 0; n < FFTSIZE; ++n) { fftData[n] = windowedFrame[n]; } for (int n = FFTSIZE; n < fftData.size(); ++n) { fftData[n] = 0.0f; } } void SpectrumAnalyzer::buildMagnitudeSpectrum() { for (int k = 0; k < BINS; ++k) { float mag = fftData[k]; mag /= FFTSIZE; mag = std::max(mag, 1e-24f); magnitudes[k] = mag; } } float getFrequencyWeighting(float freq) { // Pre-Emphasis: +3dB pro Oktave (kompensiert Pink Noise Charakteristik) // Basis: 1kHz = 0dB const float referenceFreq = 1000.0f; float octaves = std::log2(freq / referenceFreq); float weightingDb = octaves * 3.0f; // 3dB pro Oktave // Begrenzen auf sinnvolle Werte weightingDb = juce::jlimit(-12.0f, 12.0f, weightingDb); return weightingDb; } void SpectrumAnalyzer::convertToDb() { for (int k = 0; k < magnitudes.size(); ++k) { float mag = std::max(magnitudes[k], 1e-9f); float dB = juce::Decibels::gainToDecibels(mag); float freq = k * deltaF; float weighting = getFrequencyWeighting(freq); dB += weighting; if (dB < -60.0f) { dB = MINDB; } magnitudesDb[k] = dB; } } void SpectrumAnalyzer::applySmoothing() { applyEMA(); applyFreqSmoothing(); applyPeakHoldAndFalloff(); } void SpectrumAnalyzer::applyEMA() { for (int k = 0; k < magnitudesDb.size(); ++k) { float smoothedVal = smoothingFactor * magnitudesDb[k] + (1 - smoothingFactor) * emaSmoothedMagnitudesDb[k]; emaSmoothedMagnitudesDb[k] = smoothedVal; } } void SpectrumAnalyzer::applyFreqSmoothing() { for (int k = 0; k < BINS; ++k) { double freq = k * deltaF; double lowestFreq = freq * pow(2.0, -OCTAVERADIUS); double highestFreq = freq * pow(2.0, OCTAVERADIUS); int lowestBin = std::max(0, static_cast(floor(lowestFreq / deltaF))); int highestBin = std::min(BINS - 1, static_cast(ceil(highestFreq / deltaF))); if (lowestBin > highestBin) { lowestBin = k; highestBin = k; }; if (k == 0) { lowestBin = 0; highestBin = std::min(1, BINS - 1); } float sum = 0.f; for (int bin = lowestBin; bin <= highestBin; ++bin) { sum += emaSmoothedMagnitudesDb[bin]; } float avg = sum / static_cast(highestBin - lowestBin + 1); freqSmoothedMagnitudesDb[k] = avg; } } void SpectrumAnalyzer::applyPeakHoldAndFalloff() { std::vector prevPeak = peakHoldMagnitudesDb; for (int k = 0; k < BINS; ++k) { float current = freqSmoothedMagnitudesDb[k]; float prev = prevPeak[k]; peakHoldMagnitudesDb[k] = std::max(current, prev - FALLOFFRATE * DELTAT); } } std::vector SpectrumAnalyzer::getRenderValues() { std::vector renderValues = peakHoldMagnitudesDb; return renderValues; }