diff --git a/CMakeLists.txt b/CMakeLists.txt index 3964d40..b35e522 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ target_link_libraries(XOA PRIVATE spatcore-audio spatcore-control spatcore-controllers + spatcore-ui hidapi::hidapi juce::juce_audio_utils juce::juce_gui_extra diff --git a/Source/Audio/SpeakerCompProcessor.h b/Source/Audio/SpeakerCompProcessor.h index 958d980..cc0df6c 100644 --- a/Source/Audio/SpeakerCompProcessor.h +++ b/Source/Audio/SpeakerCompProcessor.h @@ -2,12 +2,11 @@ #include -#include #include #include #include "spatcore/dsp/DelayTargetSmoother.h" -#include "spatcore/dsp/OutputEQBiquadFilter.h" +#include "spatcore/dsp/MultiChannelEQBank.h" #include "spatcore/rt/RtSnapshot.h" #include "XoaConstants.h" @@ -25,12 +24,14 @@ // mute-move-unmute envelope). Target arrives in MILLISECONDS via // the RtSnapshot and is converted to samples here, so a device // restart at a new rate needs no message-side recompose. -// EQ 6x OutputEQBiquadFilter in series (Audio EQ Cookbook), ported -// from WFS-DIY's OutputEQProcessor. Coefficients are pushed from -// the message thread via setEqParameters() under benign-staleness -// (the biquad short-circuits no-change), the production-proven WFS -// pattern: at worst a torn coefficient read causes a one-tick -// transient, never a fault. +// EQ one spatcore::dsp::MultiChannelEQBank - the shared +// per-channel bank of 6 cookbook biquads in series that WFS-DIY's +// OutputEQProcessor and this stage both used to hand-roll. +// Coefficients are pushed from the message thread via +// setEqParameters() under benign-staleness (the biquad +// short-circuits no-change), the production-proven WFS pattern: at +// worst a torn coefficient read causes a one-tick transient, never +// a fault. // Gain one juce::SmoothedValue per output ramps trim x distance-atten x // mute/solo (already folded control-side into a single linear gain) // across the block - click-free mute/solo. @@ -75,16 +76,13 @@ class SpeakerCompProcessor delayLines.assign ((size_t) numChannels, DelayLine {}); smoothers.assign ((size_t) numChannels, spatcore::dsp::DelayTargetSmoother {}); - eqBanks.assign ((size_t) numChannels, EqBank {}); - eqEnabled.assign ((size_t) numChannels, (juce::uint8) 0); + eqBank.prepare (sampleRate, numChannels); gainSmoothers.assign ((size_t) numChannels, juce::SmoothedValue {}); for (int c = 0; c < numChannels; ++c) { delayLines[(size_t) c].prepare (delayCapacity); smoothers[(size_t) c].prepare (windowSamples); - for (auto& f : eqBanks[(size_t) c]) - f.prepare (sampleRate); auto& g = gainSmoothers[(size_t) c]; g.reset (sampleRate, 0.020); // 20 ms gain ramp g.setCurrentAndTargetValue (1.0f); // start settled -> neutral @@ -95,8 +93,7 @@ class SpeakerCompProcessor { delayLines.clear(); smoothers.clear(); - eqBanks.clear(); - eqEnabled.clear(); + eqBank.prepare (sampleRate, 0); // the bank's equivalent of .clear() gainSmoothers.clear(); numChannels = 0; snapshot = nullptr; @@ -106,8 +103,7 @@ class SpeakerCompProcessor { for (auto& dl : delayLines) dl.reset(); for (auto& sm : smoothers) sm.reset(); - for (auto& bank : eqBanks) - for (auto& f : bank) f.reset(); + eqBank.reset(); samplePos = 0; } @@ -120,14 +116,14 @@ class SpeakerCompProcessor const int n = juce::jmin (eq.numSpeakers, numChannels); for (int c = 0; c < n; ++c) { - eqEnabled[(size_t) c] = eq.enabled[c] ? (juce::uint8) 1 : (juce::uint8) 0; - auto& bank = eqBanks[(size_t) c]; + // Enable flag FIRST: the bank forces shape 0 (OFF) on every band of a + // disabled channel itself, so pushing the bands first would latch the + // previous enable state for one tick (the bank's ordering contract). + eqBank.setChannelEnabled (c, eq.enabled[c]); for (int b = 0; b < xoa::kNumEqBands; ++b) { const auto& bp = eq.bands[c][b]; - // Disabled channel -> every band forced to OFF (shape 0). - const int shape = eq.enabled[c] ? bp.shape : 0; - bank[(size_t) b].setParameters (shape, bp.freq, bp.gainDb, bp.q, bp.slope); + eqBank.pushBandParameters (c, b, bp.shape, bp.freq, bp.gainDb, bp.q, bp.slope); } } } @@ -167,10 +163,9 @@ class SpeakerCompProcessor dl.advance(); } - // 6-band EQ in series (each biquad no-ops when shape 0). - if (eqEnabled[(size_t) c] != 0) - for (auto& f : eqBanks[(size_t) c]) - f.processBlock (data, numSamples); + // 6-band EQ in series (no-op on a disabled channel, and each biquad + // additionally no-ops when shape 0). + eqBank.processChannel (c, data, numSamples); // Gain ramp: trim x distance atten x mute/solo. auto& g = gainSmoothers[(size_t) c]; @@ -243,8 +238,6 @@ class SpeakerCompProcessor int writePos = 0; }; - using EqBank = std::array; - double sampleRate = 48000.0; int numChannels = 0; int delayCapacity = 0; @@ -255,8 +248,7 @@ class SpeakerCompProcessor std::vector delayLines; std::vector smoothers; - std::vector eqBanks; - std::vector eqEnabled; // uint8 avoids vector proxy + spatcore::dsp::MultiChannelEQBank eqBank; std::vector> gainSmoothers; }; diff --git a/Source/GUI/EQDisplayComponent.h b/Source/GUI/EQDisplayComponent.h deleted file mode 100644 index b296cdb..0000000 --- a/Source/GUI/EQDisplayComponent.h +++ /dev/null @@ -1,1018 +0,0 @@ -/* - ============================================================================== - - XOA — tenth-order Ambisonics spatial audio processor. - EQDisplayComponent — interactive parametric EQ graph: log-frequency response - curve summed across bands, draggable colour-coded band markers, crosshair - drags (freq-only / gain-only), mouse-wheel Q, keyboard nudging. - - Ported from WFS-DIY (Source/gui/EQDisplayComponent.h); both projects are - GPLv3. Kept config-driven (EQDisplayConfig) exactly as in WFS-DIY, where the - same component serves the Output EQ and the Reverb pre/post EQs — XOA uses - the speaker-EQ config today and the reverb configs arrive with the reverb - work. Candidate for extraction into a shared spatcore UI module (with the - palette/strings injected) so Tight-WFS can reuse it too. - - Differences from the WFS-DIY original: - • The response math mirrors spatcore/dsp/OutputEQBiquadFilter.h formula - for formula (instead of juce::dsp coefficients), so the curve is exactly - what XOA's per-speaker filters do — including the separate shelf `slope` - parameter (RBJ S), which WFS-DIY folds into Q. - • Writes always delegate to onParameterChanged (the owning tab persists - through the store's scoped-undo/array seam); the component never writes - the ValueTree itself. - - This file is part of XOA, released under the GNU General Public License - v3.0. See LICENSE for details. - - ============================================================================== -*/ - -#pragma once - -#include - -#include -#include -#include -#include - -#include "ColorScheme.h" -#include "Localization/LocalizationManager.h" -#include "Parameters/XoaParameterIDs.h" -#include "Parameters/XoaParameterDefaults.h" - -//============================================================================== -/** Unified filter type for the display (both WFS-DIY shape encodings). */ -enum class EQFilterType -{ - Off = 0, - LowCut, // high-pass with resonance - LowShelf, - PeakNotch, - BandPass, - AllPass, // phase only, flat magnitude - HighShelf, - HighCut // low-pass with resonance -}; - -//============================================================================== -/** Parameter-id/config seam so one component serves different EQs (WFS-DIY: - Output EQ vs Reverb pre/post EQ — different ids, shape order and Q range). */ -struct EQDisplayConfig -{ - juce::Identifier shapeId, frequencyId, gainId, qId; - juce::Identifier slopeId; // invalid ⇒ shelves use Q as S (WFS-DIY behaviour) - - float qMin = 0.1f, qMax = 10.0f; - float slopeMin = 0.1f, slopeMax = 1.0f; - bool hasBandPass = false; // true ⇒ Output-EQ shape order, else Reverb order - - static EQDisplayConfig forSpeakerEQ() - { - EQDisplayConfig c; - c.shapeId = xoa::ids::eqShape; - c.frequencyId = xoa::ids::eqFrequency; - c.gainId = xoa::ids::eqGain; - c.qId = xoa::ids::eqQ; - c.slopeId = xoa::ids::eqSlope; - c.qMin = (float) xoa::defaults::eqQMin; - c.qMax = (float) xoa::defaults::eqQMax; - c.slopeMin = (float) xoa::defaults::eqSlopeMin; - c.slopeMax = (float) xoa::defaults::eqSlopeMax; - c.hasBandPass = true; - return c; - } - // forReverbPreEQ() / forReverbPostEQ() arrive with XOA's reverb work — the - // shape mapping below already understands the reverb (no-bandpass) order. -}; - -//============================================================================== -class EQDisplayComponent : public juce::Component, - private juce::ValueTree::Listener -{ -public: - //========================================================================== - EQDisplayComponent (juce::ValueTree eqParentTree, - int numBandsIn, - const EQDisplayConfig& configIn) - : eqTree (eqParentTree), - numBands (numBandsIn), - config (configIn) - { - eqTree.addListener (this); - bandCoefficients.resize (static_cast (numBands)); - updateAllCoefficients(); - - setInterceptsMouseClicks (true, false); - setWantsKeyboardFocus (true); - } - - ~EQDisplayComponent() override - { - eqTree.removeListener (this); - } - - //========================================================================== - void setdBRange (float newMinDb, float newMaxDb) - { - mindB = newMinDb; - maxdB = newMaxDb; - repaint(); - } - - void setSampleRate (double newSampleRate) - { - if (newSampleRate > 0.0 && sampleRate != newSampleRate) - { - sampleRate = newSampleRate; - updateAllCoefficients(); - repaint(); - } - } - - void setEQEnabled (bool enabled) - { - if (eqEnabled != enabled) - { - eqEnabled = enabled; - repaint(); - } - } - - bool isEQEnabled() const { return eqEnabled; } - - //========================================================================== - void paint (juce::Graphics& g) override - { - drawGrid (g); - drawResponseCurve (g); - drawBandMarkers (g); - - if (! eqEnabled) - { - g.setColour (ColorScheme::get().background.withAlpha (0.7f)); - g.fillRect (getLocalBounds()); - - g.setColour (ColorScheme::get().textSecondary); - g.setFont (juce::FontOptions (juce::jmax (14.0f, 24.0f * paintScale()))); - g.drawText (LOC ("eq.status.off"), getLocalBounds(), juce::Justification::centred); - } - } - - void resized() override { repaint(); } - - //========================================================================== - // Mouse interaction (with multitouch pinch support, as in WFS-DIY) - //========================================================================== - void mouseDown (const juce::MouseEvent& e) override - { - int touchIndex = e.source.getIndex(); - - TouchInfo touch; - touch.position = e.position; - touch.startPosition = e.position; - activeTouches[touchIndex] = touch; - - if (activeTouches.size() == 2) - { - isPinching = true; - pinchStartDistance = getTouchDistance(); - pinchStartQ = 0.0f; - - auto midpoint = getTouchMidpoint(); - int centeredBand = findBandNearestToPoint (midpoint); - if (centeredBand >= 0) - { - selectedBand = centeredBand; - isDragging = false; - } - if (selectedBand >= 0) - { - auto bandTree = eqTree.getChild (selectedBand); - if (bandTree.isValid()) - pinchStartQ = bandTree.getProperty (config.qId); - } - repaint(); - return; - } - - int clickedBand = findBandAtPosition (e.position); - if (clickedBand >= 0) - { - selectedBand = clickedBand; - isDragging = true; - dragMode = DragMode::Both; - dragStartPos = e.position; - setMouseCursor (juce::MouseCursor::DraggingHandCursor); - beginDragAutoRepeat (50); - grabKeyboardFocus(); - } - else - { - auto crosshairMode = findCrosshairAtPosition (e.position); - if (crosshairMode != DragMode::None) - { - isDragging = true; - dragMode = crosshairMode; - dragStartPos = e.position; - - auto bandTree = eqTree.getChild (selectedBand); - dragStartFreq = static_cast (static_cast (bandTree.getProperty (config.frequencyId))); - dragStartGain = bandTree.getProperty (config.gainId); - - setMouseCursor (crosshairMode == DragMode::GainOnly - ? juce::MouseCursor::UpDownResizeCursor - : juce::MouseCursor::LeftRightResizeCursor); - beginDragAutoRepeat (50); - } - else - { - selectedBand = -1; - isDragging = false; - dragMode = DragMode::None; - } - } - repaint(); - } - - void mouseDrag (const juce::MouseEvent& e) override - { - int touchIndex = e.source.getIndex(); - auto it = activeTouches.find (touchIndex); - if (it != activeTouches.end()) - it->second.position = e.position; - - if (isPinching && activeTouches.size() >= 2 && selectedBand >= 0) - { - float currentDistance = getTouchDistance(); - if (pinchStartDistance > 0.0f && pinchStartQ > 0.0f) - { - float scaleFactor = currentDistance / pinchStartDistance; - float newQ = juce::jlimit (config.qMin, config.qMax, pinchStartQ / scaleFactor); - setBandParameter (selectedBand, config.qId, newQ); - } - return; - } - - if (! isDragging || selectedBand < 0) - return; - - auto bandTree = eqTree.getChild (selectedBand); - if (! bandTree.isValid()) - return; - - int shape = bandTree.getProperty (config.shapeId); - EQFilterType filterType = shapeToFilterType (shape); - - if (dragMode != DragMode::GainOnly) - { - float newFreq; - if (dragMode == DragMode::FrequencyOnly) - { - float startX = frequencyToX (dragStartFreq); - newFreq = xToFrequency (startX + (e.position.x - dragStartPos.x)); - } - else - { - newFreq = xToFrequency (e.position.x); - } - newFreq = juce::jlimit (20.0f, 20000.0f, newFreq); - setBandParameter (selectedBand, config.frequencyId, static_cast (newFreq)); - } - - if (dragMode != DragMode::FrequencyOnly && hasGainControl (filterType)) - { - float newGain; - if (dragMode == DragMode::GainOnly) - { - float startY = dBToY (dragStartGain); - newGain = yTodB (startY + (e.position.y - dragStartPos.y)); - } - else - { - newGain = yTodB (e.position.y); - } - newGain = juce::jlimit (mindB, maxdB, newGain); - setBandParameter (selectedBand, config.gainId, newGain); - } - } - - void mouseUp (const juce::MouseEvent& e) override - { - activeTouches.erase (e.source.getIndex()); - if (activeTouches.size() < 2) - isPinching = false; - if (activeTouches.empty()) - { - isDragging = false; - dragMode = DragMode::None; - setMouseCursor (juce::MouseCursor::NormalCursor); - } - // Selection persists for wheel / keyboard adjustment. - } - - void mouseMove (const juce::MouseEvent& e) override - { - if (isDragging) - return; - - if (findBandAtPosition (e.position) >= 0) - { - setMouseCursor (juce::MouseCursor::PointingHandCursor); - } - else - { - auto crosshairMode = findCrosshairAtPosition (e.position); - if (crosshairMode == DragMode::GainOnly) - setMouseCursor (juce::MouseCursor::UpDownResizeCursor); - else if (crosshairMode == DragMode::FrequencyOnly) - setMouseCursor (juce::MouseCursor::LeftRightResizeCursor); - else - setMouseCursor (juce::MouseCursor::NormalCursor); - } - } - - void mouseExit (const juce::MouseEvent&) override - { - if (! isDragging) - setMouseCursor (juce::MouseCursor::NormalCursor); - } - - void mouseWheelMove (const juce::MouseEvent&, - const juce::MouseWheelDetails& wheel) override - { - if (selectedBand < 0) - return; - - auto bandTree = eqTree.getChild (selectedBand); - if (! bandTree.isValid()) - return; - - float currentQ = bandTree.getProperty (config.qId); - float newQ = juce::jlimit (config.qMin, config.qMax, - currentQ * (1.0f + wheel.deltaY * 0.5f)); - setBandParameter (selectedBand, config.qId, newQ); - } - - void mouseMagnify (const juce::MouseEvent& e, float scaleFactor) override - { - int targetBand = selectedBand >= 0 ? selectedBand : findBandAtPosition (e.position); - if (targetBand < 0) - return; - - auto bandTree = eqTree.getChild (targetBand); - if (! bandTree.isValid()) - return; - - float currentQ = bandTree.getProperty (config.qId); - float newQ = juce::jlimit (config.qMin, config.qMax, currentQ * scaleFactor); - setBandParameter (targetBand, config.qId, newQ); - selectedBand = targetBand; - repaint(); - } - - //========================================================================== - bool keyPressed (const juce::KeyPress& key) override - { - if (key.getKeyCode() == juce::KeyPress::escapeKey && selectedBand >= 0) - { - selectedBand = -1; - repaint(); - return true; - } - - if (selectedBand < 0) - return false; - - auto bandTree = eqTree.getChild (selectedBand); - if (! bandTree.isValid()) - return false; - - EQFilterType filterType = shapeToFilterType (bandTree.getProperty (config.shapeId)); - const int keyCode = key.getKeyCode(); - - if (keyCode == juce::KeyPress::leftKey || keyCode == juce::KeyPress::rightKey) - { - int currentFreq = bandTree.getProperty (config.frequencyId); - int increment = juce::jmax (1, currentFreq / 20); // log-ish step - int newFreq = juce::jlimit (20, 20000, - keyCode == juce::KeyPress::rightKey ? currentFreq + increment - : currentFreq - increment); - setBandParameter (selectedBand, config.frequencyId, newFreq); - return true; - } - - if ((keyCode == juce::KeyPress::upKey || keyCode == juce::KeyPress::downKey) - && hasGainControl (filterType)) - { - float currentGain = bandTree.getProperty (config.gainId); - float newGain = juce::jlimit (mindB, maxdB, - currentGain + (keyCode == juce::KeyPress::upKey ? 0.1f : -0.1f)); - setBandParameter (selectedBand, config.gainId, newGain); - return true; - } - - return false; - } - - //========================================================================== - int getSelectedBand() const { return selectedBand; } - - void setSelectedBand (int band) - { - selectedBand = (band >= 0 && band < numBands) ? band : -1; - repaint(); - } - - /** All writes go through here — the owning tab persists them via the store's - scoped-undo / current-channel seam (see file header). */ - std::function onParameterChanged; - - // WFS-DIY band rainbow — shared so band labels/toggles/dials can match. - static juce::Colour getBandColour (int band) - { - static const juce::Colour colours[] = { - juce::Colour (0xFFE74C3C), // 1 red - juce::Colour (0xFFE67E22), // 2 orange - juce::Colour (0xFFFFEB3B), // 3 yellow - juce::Colour (0xFF2ECC71), // 4 green - juce::Colour (0xFF3498DB), // 5 blue - juce::Colour (0xFF9B59B6), // 6 purple - juce::Colour (0xFF1ABC9C), // 7 teal - juce::Colour (0xFFE91E63), // 8 pink - }; - return colours[band % 8]; - } - -private: - /** Paint scale vs the 180px reference height (WFS-DIY convention). */ - float paintScale() const { return juce::jmax (0.65f, (float) getHeight() / 180.0f); } - - static bool hasGainControl (EQFilterType t) - { - return t != EQFilterType::LowCut && t != EQFilterType::HighCut - && t != EQFilterType::BandPass && t != EQFilterType::AllPass; - } - - //========================================================================== - // ValueTree::Listener - //========================================================================== - void valueTreePropertyChanged (juce::ValueTree& tree, const juce::Identifier&) override - { - for (int i = 0; i < numBands; ++i) - { - if (tree == eqTree.getChild (i)) - { - updateBandCoefficients (i); - repaint(); - return; - } - } - if (tree == eqTree) - { - updateAllCoefficients(); - repaint(); - } - } - - void valueTreeChildAdded (juce::ValueTree&, juce::ValueTree&) override {} - void valueTreeChildRemoved (juce::ValueTree&, juce::ValueTree&, int) override {} - void valueTreeChildOrderChanged (juce::ValueTree&, int, int) override {} - void valueTreeParentChanged (juce::ValueTree&) override {} - - //========================================================================== - // Drawing - //========================================================================== - void drawGrid (juce::Graphics& g) - { - auto bounds = getLocalBounds().toFloat(); - - g.setColour (ColorScheme::get().backgroundAlt.darker (0.3f)); - g.fillRect (bounds); - - const float freqLines[] = { - 20, 30, 40, 50, 60, 70, 80, 90, - 100, 200, 300, 400, 500, 600, 700, 800, 900, - 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, - 10000, 20000 - }; - - auto gridColor = ColorScheme::get().chromeDivider; - for (float freq : freqLines) - { - float x = frequencyToX (freq); - bool isMajor = (freq == 100 || freq == 1000 || freq == 10000); - g.setColour (isMajor ? gridColor.withAlpha (0.6f) : gridColor.withAlpha (0.3f)); - g.drawVerticalLine (static_cast (x), bounds.getY(), bounds.getBottom()); - } - - float ps = paintScale(); - g.setColour (ColorScheme::get().textSecondary); - g.setFont (juce::jmax (7.0f, 10.0f * ps)); - - const float labelFreqs[] = { 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000 }; - const char* labelTexts[] = { "20", "50", "100", "200", "500", "1k", "2k", "5k", "10k", "20k" }; - - int freqLabelW = static_cast (30 * ps); - int freqLabelH = static_cast (12 * ps); - int freqLabelOff = static_cast (15 * ps); - for (int i = 0; i < 10; ++i) - { - float x = frequencyToX (labelFreqs[i]); - g.drawText (labelTexts[i], - static_cast (x) - freqLabelOff, - static_cast (bounds.getBottom()) - freqLabelOff, - freqLabelW, freqLabelH, - juce::Justification::centred); - } - - for (float dB = mindB; dB <= maxdB; dB += 6.0f) - { - float y = dBToY (dB); - g.setColour (gridColor.withAlpha (std::abs (dB) < 0.1f ? 0.8f : 0.4f)); - g.drawHorizontalLine (static_cast (y), bounds.getX(), bounds.getRight()); - - g.setColour (ColorScheme::get().textSecondary); - juce::String label = (dB > 0 ? "+" : "") + juce::String (static_cast (dB)); - g.drawText (label, static_cast (2 * ps), static_cast (y) - static_cast (6 * ps), - static_cast (25 * ps), static_cast (12 * ps), - juce::Justification::left); - } - } - - void drawResponseCurve (juce::Graphics& g) - { - juce::Path responseCurve; - - const int numPoints = juce::jmax (2, getWidth()); - const float zeroY = dBToY (0.0f); - - for (int x = 0; x < numPoints; ++x) - { - float freq = xToFrequency (static_cast (x)); - float y = dBToY (calculateTotalResponse (freq)); - if (x == 0) - responseCurve.startNewSubPath (static_cast (x), y); - else - responseCurve.lineTo (static_cast (x), y); - } - - juce::Path filledCurve = responseCurve; - filledCurve.lineTo (static_cast (getWidth()), zeroY); - filledCurve.lineTo (0.0f, zeroY); - filledCurve.closeSubPath(); - - g.setColour (ColorScheme::get().accentBlue.withAlpha (0.2f)); - g.fillPath (filledCurve); - - g.setColour (ColorScheme::get().textPrimary); - g.strokePath (responseCurve, juce::PathStrokeType (2.0f)); - } - - void drawBandMarkers (juce::Graphics& g) - { - for (int band = 0; band < numBands; ++band) - { - auto bandTree = eqTree.getChild (band); - if (! bandTree.isValid()) - continue; - - int shape = bandTree.getProperty (config.shapeId); - bool isOff = (shape == 0); - - float freq = bandTree.getProperty (config.frequencyId); - float gain = bandTree.getProperty (config.gainId); - float x = frequencyToX (freq); - float y = isOff ? dBToY (gain) : getBandMarkerPosition (band).y; - - juce::Colour bandColour = getBandColour (band); - if (isOff) - bandColour = bandColour.darker (0.6f); - - bool isSelected = (selectedBand == band); - float ps = paintScale(); - float markerSize = isSelected ? 20.0f * ps : 14.0f * ps; - - g.setColour (bandColour); - g.fillEllipse (x - markerSize / 2, y - markerSize / 2, markerSize, markerSize); - - if (isSelected) - { - g.setColour (ColorScheme::get().textPrimary); - float ringOff = 3.0f * ps; - g.drawEllipse (x - markerSize / 2 - ringOff, y - markerSize / 2 - ringOff, - markerSize + ringOff * 2, markerSize + ringOff * 2, 2.0f * ps); - - EQFilterType filterType = shapeToFilterType (shape); - g.setColour (bandColour.withAlpha (0.35f)); - g.drawLine (x, 0.0f, x, static_cast (getHeight()), 1.0f); - if (isOff || hasGainControl (filterType)) - g.drawLine (0.0f, y, static_cast (getWidth()), y, 1.0f); - } - - g.setColour (juce::Colours::black); - g.setFont (juce::FontOptions (juce::jmax (8.0f, 13.0f * ps), juce::Font::bold)); - g.drawText (juce::String (band + 1), - static_cast (x - markerSize / 2), static_cast (y - markerSize / 2), - static_cast (markerSize), static_cast (markerSize), - juce::Justification::centred); - } - } - - //========================================================================== - // Coordinate conversion - //========================================================================== - float frequencyToX (float freq) const - { - const float minFreq = 20.0f, maxFreq = 20000.0f; - float normalized = std::log10 (freq / minFreq) / std::log10 (maxFreq / minFreq); - return static_cast (getWidth()) * normalized; - } - - float xToFrequency (float x) const - { - const float minFreq = 20.0f, maxFreq = 20000.0f; - float normalized = x / static_cast (getWidth()); - return minFreq * std::pow (maxFreq / minFreq, normalized); - } - - float dBToY (float dB) const - { - float normalized = (dB - mindB) / (maxdB - mindB); - return static_cast (getHeight()) * (1.0f - normalized); - } - - float yTodB (float y) const - { - float normalized = 1.0f - (y / static_cast (getHeight())); - return mindB + normalized * (maxdB - mindB); - } - - //========================================================================== - // Shape mapping (both WFS-DIY encodings, selected by config.hasBandPass) - //========================================================================== - EQFilterType shapeToFilterType (int shape) const - { - if (config.hasBandPass) - { - // Output/speaker EQ: 0=Off 1=LowCut 2=LowShelf 3=Peak 4=BandPass - // 5=HighShelf 6=HighCut 7=AllPass - switch (shape) - { - case 0: return EQFilterType::Off; - case 1: return EQFilterType::LowCut; - case 2: return EQFilterType::LowShelf; - case 3: return EQFilterType::PeakNotch; - case 4: return EQFilterType::BandPass; - case 5: return EQFilterType::HighShelf; - case 6: return EQFilterType::HighCut; - case 7: return EQFilterType::AllPass; - default: return EQFilterType::Off; - } - } - // Reverb pre/post EQ: 0=Off 1=LowCut 2=LowShelf 3=Peak 4=HighShelf 5=HighCut 6=BandPass - switch (shape) - { - case 0: return EQFilterType::Off; - case 1: return EQFilterType::LowCut; - case 2: return EQFilterType::LowShelf; - case 3: return EQFilterType::PeakNotch; - case 4: return EQFilterType::HighShelf; - case 5: return EQFilterType::HighCut; - case 6: return EQFilterType::BandPass; - default: return EQFilterType::Off; - } - } - - //========================================================================== - // Response math — mirrors spatcore/dsp/OutputEQBiquadFilter.h exactly - // (normalized biquad; shelves take S from the slope parameter). - //========================================================================== - struct Biquad - { - float b0 = 1.0f, b1 = 0.0f, b2 = 0.0f, a1 = 0.0f, a2 = 0.0f; - bool active = false; - - float magnitudeDbAt (float freq, double fs) const - { - if (! active) - return 0.0f; - const double w = 2.0 * juce::MathConstants::pi * freq / fs; - const std::complex z1 = std::polar (1.0, -w); - const std::complex z2 = z1 * z1; - const std::complex num = (double) b0 + (double) b1 * z1 + (double) b2 * z2; - const std::complex den = 1.0 + (double) a1 * z1 + (double) a2 * z2; - const double mag = std::abs (num / den); - return mag > 1.0e-6 ? (float) (20.0 * std::log10 (mag)) : -120.0f; - } - }; - - void updateBandCoefficients (int bandIndex) - { - if (bandIndex < 0 || bandIndex >= numBands) - return; - - auto& bq = bandCoefficients[static_cast (bandIndex)]; - bq = {}; - - auto bandTree = eqTree.getChild (bandIndex); - if (! bandTree.isValid()) - return; - - const int shape = bandTree.getProperty (config.shapeId); - const EQFilterType type = shapeToFilterType (shape); - if (type == EQFilterType::Off || sampleRate <= 0.0) - return; - - const float freq = juce::jlimit (20.0f, 20000.0f, (float) bandTree.getProperty (config.frequencyId)); - const float gainDb = bandTree.getProperty (config.gainId); - const float q = juce::jlimit (config.qMin, config.qMax, (float) bandTree.getProperty (config.qId)); - const float slope = config.slopeId.isValid() - ? juce::jlimit (config.slopeMin, config.slopeMax, - (float) bandTree.getProperty (config.slopeId)) - : q; // WFS-DIY behaviour: Q doubles as shelf S - - const float w0 = 2.0f * juce::MathConstants::pi * freq / (float) sampleRate; - const float cosw0 = std::cos (w0); - const float sinw0 = std::sin (w0); - float a0_inv = 1.0f; - - switch (type) - { - case EQFilterType::LowCut: - { - const float alpha = sinw0 / (2.0f * q); - a0_inv = 1.0f / (1.0f + alpha); - bq.b0 = ((1.0f + cosw0) / 2.0f) * a0_inv; - bq.b1 = -(1.0f + cosw0) * a0_inv; - bq.b2 = ((1.0f + cosw0) / 2.0f) * a0_inv; - bq.a1 = (-2.0f * cosw0) * a0_inv; - bq.a2 = (1.0f - alpha) * a0_inv; - break; - } - case EQFilterType::LowShelf: - { - const float A = std::pow (10.0f, gainDb / 40.0f); - const float alpha = (sinw0 / 2.0f) * std::sqrt ( - (A + 1.0f / A) * (1.0f / slope - 1.0f) + 2.0f); - const float sqrtA2alpha = 2.0f * std::sqrt (A) * alpha; - - a0_inv = 1.0f / ((A + 1.0f) + (A - 1.0f) * cosw0 + sqrtA2alpha); - bq.b0 = A * ((A + 1.0f) - (A - 1.0f) * cosw0 + sqrtA2alpha) * a0_inv; - bq.b1 = 2.0f * A * ((A - 1.0f) - (A + 1.0f) * cosw0) * a0_inv; - bq.b2 = A * ((A + 1.0f) - (A - 1.0f) * cosw0 - sqrtA2alpha) * a0_inv; - bq.a1 = -2.0f * ((A - 1.0f) + (A + 1.0f) * cosw0) * a0_inv; - bq.a2 = ((A + 1.0f) + (A - 1.0f) * cosw0 - sqrtA2alpha) * a0_inv; - break; - } - case EQFilterType::PeakNotch: - { - const float A = std::pow (10.0f, gainDb / 40.0f); - const float alpha = sinw0 / (2.0f * q); - a0_inv = 1.0f / (1.0f + alpha / A); - bq.b0 = (1.0f + alpha * A) * a0_inv; - bq.b1 = (-2.0f * cosw0) * a0_inv; - bq.b2 = (1.0f - alpha * A) * a0_inv; - bq.a1 = (-2.0f * cosw0) * a0_inv; - bq.a2 = (1.0f - alpha / A) * a0_inv; - break; - } - case EQFilterType::BandPass: - { - const float alpha = sinw0 / (2.0f * q); - a0_inv = 1.0f / (1.0f + alpha); - bq.b0 = alpha * a0_inv; - bq.b1 = 0.0f; - bq.b2 = -alpha * a0_inv; - bq.a1 = (-2.0f * cosw0) * a0_inv; - bq.a2 = (1.0f - alpha) * a0_inv; - break; - } - case EQFilterType::HighShelf: - { - const float A = std::pow (10.0f, gainDb / 40.0f); - const float alpha = (sinw0 / 2.0f) * std::sqrt ( - (A + 1.0f / A) * (1.0f / slope - 1.0f) + 2.0f); - const float sqrtA2alpha = 2.0f * std::sqrt (A) * alpha; - - a0_inv = 1.0f / ((A + 1.0f) - (A - 1.0f) * cosw0 + sqrtA2alpha); - bq.b0 = A * ((A + 1.0f) + (A - 1.0f) * cosw0 + sqrtA2alpha) * a0_inv; - bq.b1 = -2.0f * A * ((A - 1.0f) + (A + 1.0f) * cosw0) * a0_inv; - bq.b2 = A * ((A + 1.0f) + (A - 1.0f) * cosw0 - sqrtA2alpha) * a0_inv; - bq.a1 = 2.0f * ((A - 1.0f) - (A + 1.0f) * cosw0) * a0_inv; - bq.a2 = ((A + 1.0f) - (A - 1.0f) * cosw0 - sqrtA2alpha) * a0_inv; - break; - } - case EQFilterType::HighCut: - { - const float alpha = sinw0 / (2.0f * q); - a0_inv = 1.0f / (1.0f + alpha); - bq.b0 = ((1.0f - cosw0) / 2.0f) * a0_inv; - bq.b1 = (1.0f - cosw0) * a0_inv; - bq.b2 = ((1.0f - cosw0) / 2.0f) * a0_inv; - bq.a1 = (-2.0f * cosw0) * a0_inv; - bq.a2 = (1.0f - alpha) * a0_inv; - break; - } - case EQFilterType::AllPass: - { - const float alpha = sinw0 / (2.0f * q); - a0_inv = 1.0f / (1.0f + alpha); - bq.b0 = (1.0f - alpha) * a0_inv; - bq.b1 = (-2.0f * cosw0) * a0_inv; - bq.b2 = (1.0f + alpha) * a0_inv; - bq.a1 = (-2.0f * cosw0) * a0_inv; - bq.a2 = (1.0f - alpha) * a0_inv; - break; - } - case EQFilterType::Off: - default: - return; - } - - bq.active = true; - } - - void updateAllCoefficients() - { - for (int i = 0; i < numBands; ++i) - updateBandCoefficients (i); - } - - float calculateTotalResponse (float frequency) - { - float totalGaindB = 0.0f; - for (const auto& bq : bandCoefficients) - totalGaindB += bq.magnitudeDbAt (frequency, sampleRate); - return juce::jlimit (mindB - 6.0f, maxdB + 6.0f, totalGaindB); - } - - //========================================================================== - // Band marker positioning and hit testing - //========================================================================== - juce::Point getBandMarkerPosition (int bandIndex) const - { - auto bandTree = eqTree.getChild (bandIndex); - if (! bandTree.isValid()) - return { 0.0f, 0.0f }; - - int shape = bandTree.getProperty (config.shapeId); - float freq = bandTree.getProperty (config.frequencyId); - float gain = bandTree.getProperty (config.gainId); - - float x = frequencyToX (freq); - EQFilterType filterType = shapeToFilterType (shape); - float y = hasGainControl (filterType) ? dBToY (gain) : dBToY (0.0f); - return { x, y }; - } - - int findBandAtPosition (juce::Point pos) - { - const float hitRadius = 15.0f * paintScale(); - const float hitRadiusSq = hitRadius * hitRadius; - - for (int band = 0; band < numBands; ++band) - { - auto p = markerPositionIncludingOff (band); - float dx = pos.x - p.x, dy = pos.y - p.y; - if (dx * dx + dy * dy < hitRadiusSq) - return band; - } - return -1; - } - - enum class DragMode { None, Both, FrequencyOnly, GainOnly }; - - juce::Point markerPositionIncludingOff (int band) const - { - auto bandTree = eqTree.getChild (band); - if (! bandTree.isValid()) - return { -1000.0f, -1000.0f }; - - int shape = bandTree.getProperty (config.shapeId); - float freq = bandTree.getProperty (config.frequencyId); - float x = frequencyToX (freq); - float y; - if (shape == 0) - { - float gain = bandTree.getProperty (config.gainId); - y = dBToY (gain); - } - else - { - y = getBandMarkerPosition (band).y; - } - return { x, y }; - } - - DragMode findCrosshairAtPosition (juce::Point pos) const - { - if (selectedBand < 0) - return DragMode::None; - - auto bandTree = eqTree.getChild (selectedBand); - if (! bandTree.isValid()) - return DragMode::None; - - int shape = bandTree.getProperty (config.shapeId); - bool isOff = (shape == 0); - auto marker = markerPositionIncludingOff (selectedBand); - - const float hitTolerance = 8.0f * paintScale(); - EQFilterType filterType = shapeToFilterType (shape); - - if (std::abs (pos.x - marker.x) < hitTolerance) - return DragMode::FrequencyOnly; - - if ((isOff || hasGainControl (filterType)) && std::abs (pos.y - marker.y) < hitTolerance) - return DragMode::GainOnly; - - return DragMode::None; - } - - //========================================================================== - // Multitouch helpers - //========================================================================== - float getTouchDistance() const - { - if (activeTouches.size() < 2) - return 0.0f; - auto it = activeTouches.begin(); - auto pos1 = it->second.position; - ++it; - return pos1.getDistanceFrom (it->second.position); - } - - juce::Point getTouchMidpoint() const - { - if (activeTouches.size() < 2) - return { 0.0f, 0.0f }; - auto it = activeTouches.begin(); - auto pos1 = it->second.position; - ++it; - auto pos2 = it->second.position; - return { (pos1.x + pos2.x) * 0.5f, (pos1.y + pos2.y) * 0.5f }; - } - - int findBandNearestToPoint (juce::Point pos) const - { - int nearestBand = -1; - float nearestDistSq = std::numeric_limits::max(); - const float maxSearchRadius = 150.0f * paintScale(); - const float maxSearchRadiusSq = maxSearchRadius * maxSearchRadius; - - for (int band = 0; band < numBands; ++band) - { - auto p = markerPositionIncludingOff (band); - float dx = pos.x - p.x, dy = pos.y - p.y; - float distSq = dx * dx + dy * dy; - if (distSq < nearestDistSq && distSq < maxSearchRadiusSq) - { - nearestDistSq = distSq; - nearestBand = band; - } - } - return nearestBand; - } - - //========================================================================== - void setBandParameter (int bandIndex, const juce::Identifier& paramId, const juce::var& value) - { - // The owner persists (store scoped-write with undo + array semantics); - // its write lands on this same live tree, so our listener repaints. - if (onParameterChanged) - onParameterChanged (bandIndex, paramId, value); - } - - //========================================================================== - juce::ValueTree eqTree; - int numBands; - EQDisplayConfig config; - - float mindB = -24.0f; - float maxdB = 24.0f; - double sampleRate = 48000.0; - bool eqEnabled = true; - - int selectedBand = -1; - bool isDragging = false; - DragMode dragMode = DragMode::None; - juce::Point dragStartPos; - float dragStartFreq = 0.0f; - float dragStartGain = 0.0f; - - std::vector bandCoefficients; - - struct TouchInfo - { - juce::Point position; - juce::Point startPosition; - }; - std::map activeTouches; - bool isPinching = false; - float pinchStartDistance = 0.0f; - float pinchStartQ = 0.0f; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EQDisplayComponent) -}; diff --git a/Source/GUI/EqDisplayStyle.h b/Source/GUI/EqDisplayStyle.h new file mode 100644 index 0000000..7d4ecbd --- /dev/null +++ b/Source/GUI/EqDisplayStyle.h @@ -0,0 +1,78 @@ +/* + ============================================================================== + + XOA — tenth-order Ambisonics spatial audio processor. + EqDisplayStyle — the app-side seam between XOA and the shared spatcore EQ + UI (spatcore/ui/EQDisplayComponent.h, spatcore/ui/EQBandToggle.h). spatcore + may never name an app symbol, so the parameter ids, ranges, theme colours + and localised strings its widgets need are supplied from here. + + Both providers below are invoked AT PAINT TIME by the widgets and are never + cached, so they must read ColorScheme::get() / LOC() afresh on every call — + that is exactly what makes live theme and language switching work. + + This file is part of XOA, released under the GNU General Public License + v3.0. See LICENSE for details. + + ============================================================================== +*/ + +#pragma once + +#include "spatcore/ui/EQBandToggle.h" +#include "spatcore/ui/EQDisplayComponent.h" + +#include "ColorScheme.h" +#include "Localization/LocalizationManager.h" +#include "Parameters/XoaParameterIDs.h" +#include "Parameters/XoaParameterDefaults.h" + +namespace xoa +{ + +//============================================================================== +/** Theme colours for the speaker-EQ graph, mapped onto the six palette roles + the spatcore header documents. Every role is the raw scheme colour except + panelBackground, whose .darker (0.3f) the original component applied at + paint time and which must therefore be applied here to keep the look. */ +inline spatcore::ui::EQDisplayPalette speakerEqPalette() +{ + const auto& scheme = ColorScheme::get(); + return { scheme.background, // disabled-state scrim + scheme.backgroundAlt.darker (0.3f), // graph plotting area + scheme.chromeDivider, // frequency + dB grid rules + scheme.textPrimary, // curve stroke + selection ring + scheme.textSecondary, // axis labels + "EQ OFF" + scheme.accentBlue }; // fill under the curve +} + +/** Config for XOA's per-speaker EQ: the speaker-EQ parameter ids and ranges, + the Output-EQ shape order (hasBandPass), and the two injected providers. */ +inline spatcore::ui::EQDisplayConfig makeSpeakerEQConfig() +{ + spatcore::ui::EQDisplayConfig c; + c.shapeId = xoa::ids::eqShape; + c.frequencyId = xoa::ids::eqFrequency; + c.gainId = xoa::ids::eqGain; + c.qId = xoa::ids::eqQ; + c.slopeId = xoa::ids::eqSlope; + c.qMin = (float) xoa::defaults::eqQMin; + c.qMax = (float) xoa::defaults::eqQMax; + c.slopeMin = (float) xoa::defaults::eqSlopeMin; + c.slopeMax = (float) xoa::defaults::eqSlopeMax; + c.hasBandPass = true; + c.paletteProvider = [] { return speakerEqPalette(); }; + c.disabledTextProvider = [] { return LOC ("eq.status.off"); }; + return c; +} + +//============================================================================== +/** Theme colours for an EQ band toggle; assign as its colourProvider. The ON + fill is the band colour and is owned by the widget. */ +inline spatcore::ui::EQBandToggleColours speakerEqToggleColours() +{ + return { ColorScheme::get().sliderTrackBg, // OFF fill + ColorScheme::get().buttonBorder }; // outline, both states +} + +} // namespace xoa diff --git a/Source/GUI/GuiKitCompileCheck.cpp b/Source/GUI/GuiKitCompileCheck.cpp index 10eca0e..b42671c 100644 --- a/Source/GUI/GuiKitCompileCheck.cpp +++ b/Source/GUI/GuiKitCompileCheck.cpp @@ -14,8 +14,12 @@ ============================================================================== */ +#include "spatcore/ui/EQBandToggle.h" +#include "spatcore/ui/EQDisplayComponent.h" + #include "ColorScheme.h" #include "ColorUtilities.h" +#include "EqDisplayStyle.h" #include "XoaLookAndFeel.h" #include "ColumnFocusTraverser.h" #include "StatusBar.h" @@ -38,7 +42,6 @@ #include "Widgets/XoaEndlessDial.h" #include "Widgets/CountdownTextButton.h" #include "Widgets/WrappingTextButton.h" -#include "Widgets/EQBandToggle.h" #include "Widgets/LongPressButton.h" #include "Widgets/XoaLevelMeterBar.h" #include "Widgets/XoaThreadPerformanceBar.h" diff --git a/Source/GUI/Tabs/EqTab.cpp b/Source/GUI/Tabs/EqTab.cpp index 58f9c20..a45cd8f 100644 --- a/Source/GUI/Tabs/EqTab.cpp +++ b/Source/GUI/Tabs/EqTab.cpp @@ -52,13 +52,14 @@ EqTab::EqTab (AppContext& ctx) : TabPage (ctx, Surface::eq) for (int b = 0; b < xoa::kNumEqBands; ++b) { const auto i = (size_t) b; - const auto colour = EQDisplayComponent::getBandColour (b); + const auto colour = spatcore::ui::EQDisplayComponent::getBandColour (b); bandLabel[i].setText (LOC ("eq.band") + " " + juce::String (b + 1), juce::dontSendNotification); bandLabel[i].setJustificationType (juce::Justification::centredLeft); addAndMakeVisible (bandLabel[i]); // colour applied by applyLabelColours() bandToggle[i].setBandColour (colour); + bandToggle[i].colourProvider = [] { return xoa::speakerEqToggleColours(); }; bandToggle[i].onClick = [this, b] { if (bandToggle[(size_t) b].getToggleState()) @@ -140,7 +141,7 @@ void EqTab::applyLabelColours() { const auto i = (size_t) b; bandLabel[i].setColour (juce::Label::textColourId, - EQDisplayComponent::getBandColour (b)); // band rainbow: theme-invariant + spatcore::ui::EQDisplayComponent::getBandColour (b)); // band rainbow: theme-invariant for (auto* l : { &gainHdr[i], &qHdr[i], &slopeHdr[i] }) l->setColour (juce::Label::textColourId, secondary); } @@ -165,8 +166,12 @@ void EqTab::rebuildDisplay() if (! eqTree.isValid()) return; - display = std::make_unique (eqTree, xoa::kNumEqBands, - EQDisplayConfig::forSpeakerEQ()); + display = std::make_unique (eqTree, xoa::kNumEqBands, + xoa::makeSpeakerEQConfig()); + // XOA persists every edit itself, through the store's scoped-undo / band + // seam in writeBand(). Without this the shared component would ALSO write + // the tree directly (its default), double-writing and bypassing that seam. + display->ownerPersistsValue = true; display->onParameterChanged = [this] (int band, const juce::Identifier& id, const juce::var& v) { writeBand (band, id, v); diff --git a/Source/GUI/Tabs/EqTab.h b/Source/GUI/Tabs/EqTab.h index 1c4d487..1056b16 100644 --- a/Source/GUI/Tabs/EqTab.h +++ b/Source/GUI/Tabs/EqTab.h @@ -4,8 +4,9 @@ XOA — tenth-order Ambisonics spatial audio processor. EqTab — dedicated per-speaker EQ surface, mirroring WFS-DIY's Output-EQ screen: speaker rail, EQ enable + flatten row, the interactive response - graph (EQDisplayComponent), and one colour-coded column per band with - toggle / shape / reset, frequency slider and gain-Q-slope dials. + graph (the shared spatcore::ui::EQDisplayComponent), and one colour-coded + column per band with toggle / shape / reset, frequency slider and + gain-Q-slope dials. This file is part of XOA, released under the GNU General Public License v3.0. See LICENSE for details. @@ -18,10 +19,12 @@ #include #include +#include "spatcore/ui/EQBandToggle.h" +#include "spatcore/ui/EQDisplayComponent.h" + #include "TabPage.h" #include "../ChannelRail.h" -#include "../EQDisplayComponent.h" -#include "../Widgets/EQBandToggle.h" +#include "../EqDisplayStyle.h" #include "../Widgets/LongPressButton.h" #include "../Widgets/XoaBasicDial.h" #include "../Widgets/XoaStandardSlider.h" @@ -58,10 +61,10 @@ class EqTab : public TabPage, juce::TextButton eqEnabledButton { "EQ" }; // latching (speakerEqEnabled) LongPressButton flattenButton; // long-press: reset every band - std::unique_ptr display; + std::unique_ptr display; std::array bandLabel; - std::array bandToggle; + std::array bandToggle; std::array bandReset; std::array eqShape; std::array eqFreq; diff --git a/Source/GUI/Widgets/EQBandToggle.h b/Source/GUI/Widgets/EQBandToggle.h deleted file mode 100644 index 3eaafcb..0000000 --- a/Source/GUI/Widgets/EQBandToggle.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include "../ColorScheme.h" - -/** - * EQBandToggle - * - * A tiny colored indicator button for toggling individual EQ bands on/off. - * When ON, displays a filled rounded rectangle in the band's color. - * When OFF, displays a dark grey rounded rectangle. - */ -class EQBandToggle : public juce::Button -{ -public: - EQBandToggle() : juce::Button ("") { setClickingTogglesState (true); } - - void setBandColour (juce::Colour c) { bandColour = c; repaint(); } - -private: - juce::Colour bandColour { juce::Colours::white }; - - void paintButton (juce::Graphics& g, bool /*highlighted*/, bool /*down*/) override - { - auto bounds = getLocalBounds().toFloat().reduced (1.0f); - bool on = getToggleState(); - g.setColour (on ? bandColour : ColorScheme::get().sliderTrackBg); - g.fillRoundedRectangle (bounds, 3.0f); - g.setColour (ColorScheme::get().buttonBorder); - g.drawRoundedRectangle (bounds, 3.0f, 1.0f); - } - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EQBandToggle) -}; diff --git a/spatcore b/spatcore index bf96b3c..8296f28 160000 --- a/spatcore +++ b/spatcore @@ -1 +1 @@ -Subproject commit bf96b3c811089001a31c4216d558b13730f24a1e +Subproject commit 8296f28ace30ed4bdd16fb1f7f407947a4f179d0