// SPDX-FileCopyrightText: 2026 0064ff
// SPDX-License-Identifier: MIT-0

#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================

ChannelAudioProcessor::ChannelAudioProcessor() :
    AudioProcessor(BusesProperties().withInput("Input", juce::AudioChannelSet::stereo())
                                    .withOutput("Output", juce::AudioChannelSet::stereo())
                                    .withOutput("Send", juce::AudioChannelSet::stereo())
                                    .withInput("Return", juce::AudioChannelSet::stereo()))
{}

//==============================================================================
// parameters

juce::AudioProcessorValueTreeState::ParameterLayout ChannelAudioProcessor::createParameterLayout()
{
    juce::AudioProcessorValueTreeState::ParameterLayout layout;

    //--------------------------------------------------------------------------
    // range lambdas

    // linear range
    auto linRange = [](float start, float end) -> juce::NormalisableRange<float>
    {
        return juce::NormalisableRange<float>(start, end);
    };

    // exponential range
    auto expRange = [](float start, float end) -> juce::NormalisableRange<float>
    {
        return juce::NormalisableRange<float>(start, end,
            [](float start, float end, float normValue)
                {return start*std::pow(end/start, normValue);},
            [](float start, float end, float value)
                {return std::log(value/start) / std::log(end/start);},
            [](float start, float end, float value)
                {return std::clamp(value, start, end);}
        );
    };

    // inverse exponential range
    auto invExpRange = [](float start, float end) -> juce::NormalisableRange<float>
    {
        return juce::NormalisableRange<float>(start, end,
            [](float start, float end, float normValue)
                {return start + end - start*std::pow(end/start, 1.0f-normValue);},
            [](float start, float end, float value)
                {return 1.0f - std::log((start+end-value)/start) / std::log(end/start);},
            [](float start, float end, float value)
                {return std::clamp(value, start, end);}
        );
    };

    // discrete range
    auto discreteRange = [](std::initializer_list<float> discreteValues) -> juce::NormalisableRange<float>
    {
        std::vector<float> values(discreteValues);

        auto findNearestIndex = [](std::vector<float> const& values, float value)
        {
            int numOfValues = static_cast<int>(values.size());
            int indexOfBestDiff = 0;
            float bestDiff = std::abs(value - values[static_cast<std::size_t>(indexOfBestDiff)]);
            for (int index = 1; index < numOfValues; ++index) {
                float diff = std::abs(value - values[static_cast<std::size_t>(index)]);
                if (diff < bestDiff) {bestDiff=diff; indexOfBestDiff=index;}
            }
            return indexOfBestDiff;
        };

        return juce::NormalisableRange<float>(values.front(), values.back(),
            [values](float /*start*/, float /*end*/, float normValue) {
                int numOfValues = static_cast<int>(values.size());
                int index = std::clamp(juce::roundToInt(normValue * static_cast<float>(numOfValues-1)),
                                       0,
                                       numOfValues-1);
                return values[static_cast<std::size_t>(index)];
            },
            [values, findNearestIndex](float /*start*/, float /*end*/, float value) {
                return static_cast<float>(findNearestIndex(values, value)) /
                       static_cast<float>((values.size()-1));
            },
            [values, findNearestIndex](float /*start*/, float /*end*/, float value) {
                return values[static_cast<std::size_t>(findNearestIndex(values, value))];
            }
        );
    };

    //--------------------------------------------------------------------------
    // ranges

    auto decRange = linRange(0.0f, 1.0f);

    auto fxLoopSendDBRange = linRange(-24.0f, 24.0f);
    auto fxLoopReturnDBRange = fxLoopSendDBRange;

    auto freqRange = expRange(20.0f, 20000.0f);
    auto hpQRange = expRange(0.7f, 22.4f);
    auto lpQRange = expRange(0.7f, 22.4f);
    auto eqQRange = expRange(0.3f, 4.0f);
    auto eqPeakDBRange = linRange(-18.0f, 18.0f);
    auto eqTiltDBRange = linRange(-18.0f, 18.0f);

    auto dynDriveDBRange = linRange(-18.0f, 18.0f);
    auto dynThreshRange = linRange(-42.0f, -1.0f);
    auto dynTransientTimeRange = expRange(0.015625f, 16.0f);
    auto dynAttackTimeRange = expRange(0.125f, 4096.0f);
    auto dynReleaseTimeRange = dynAttackTimeRange;
    auto dynSlewRange = invExpRange(0.0001f, 0.9999f);
    auto dynAdaptabilityRange = expRange(1.0f, 16.0f);
    auto dynDetectionPowerRange = discreteRange({0.25f, 0.5f, 1.0f, 2.0f, 4.0f});
    auto dynResponsePowerRange = discreteRange({0.25f, 0.5f, 1.0f, 2.0f, 4.0f});
    auto dynRatioRange = expRange(1.0f, 16.0f);
    auto dynVariMuMultRange = linRange(1.0f, 8.0f);
    auto dynKneeRange = linRange(0.0f, 42.0f);
    auto dynSmoothTimeRange = expRange(1.0f, 128.0f);
    auto dynSagTimeRange = expRange(1.0f, 8192.0f);
    auto dynMakeupDBRange = linRange(-30.0f, 30.0f);

    auto outputDBRange = linRange(-30.0f, 30.0f);

    //--------------------------------------------------------------------------
    // attributes helpers

    using APFA = juce::AudioParameterFloatAttributes;
    using APBA = juce::AudioParameterBoolAttributes;

    // max string length is never used, so this lambda can be used to save space
    auto toStringFunction = [](auto function) {
        return [function](float value, int /*maxStringLength*/) -> juce::String {
            return function(value);
        };
    };

    auto stringFunctionToAPFA = [toStringFunction](auto toString, auto fromString) {
        return APFA().withStringFromValueFunction(toStringFunction(toString))
                     .withValueFromStringFunction(fromString);
    };

    auto stringFunctionToAPBA = [toStringFunction](auto function) {
        return APBA().withStringFromValueFunction(toStringFunction(function));
    };

    auto fromStringDec = [](juce::String const& s) {return s.getFloatValue();};
    auto fromStringPerc = [](juce::String const& s) {return s.getFloatValue() / 100.0f;};
    auto fromStringFreq = [](juce::String const& s)
        {return s.containsIgnoreCase("k") ? s.getFloatValue()*1000.0f : s.getFloatValue();};

    //--------------------------------------------------------------------------
    // general attributes

    auto decAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v, 2);},
        fromStringDec
    );
    auto decToPercAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(juce::roundToInt(v * 100)) + "%";},
        fromStringPerc
    );
    auto freqAttr = stringFunctionToAPFA(
        [](float v) {
            if (v >= 1000.0f)
            {
                v /= 1000.0f;
                return juce::String(v, 1) + " khz";
            }
            else
                return juce::String(juce::roundToInt(v)) + " hz";
        },
        fromStringFreq
    );
    auto dbAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v, 1) + " db";},
        fromStringDec
    );
    auto gainAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(MyCompressor<float>::linearToDB(v), 1) + " db";},
        [](juce::String const& s) {
            float const db = s.getFloatValue();
            return db <= MyCompressor<float>::noiseFloorDB ? 0.0f
                                                           : MyCompressor<float>::dBToLinear(db);
        }
    );
    auto multAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v, 1) + "x";},
        fromStringDec
    );

    //--------------------------------------------------------------------------
    // effects loop attributes

    auto fxLoopPositionAttr = stringFunctionToAPBA([](float v) {return !static_cast<bool>(v) ? "input" : "pre-vol";});

    //--------------------------------------------------------------------------
    // stereo attributes

    auto panAttr = stringFunctionToAPFA(
        [](float v) {
            float vPercent = v*200.0f - 100.0f;
            int vPercentRounded = juce::roundToInt(vPercent);
            if (vPercentRounded == 0)
                return juce::String("centered");
            return juce::String(std::abs(vPercentRounded)) + ((vPercentRounded > 0) ?  "% right" : "% left");
        },
        [](juce::String const& s) {
            float const panPercent = s.getFloatValue();
            return 0.5f + (s.containsIgnoreCase("left") ? -panPercent : panPercent)/200.0f;
        }
    );
    auto stereoOrderAttr = stringFunctionToAPBA([](float v) {
        return !static_cast<bool>(v) ? "width first" : "pan first";
    });

    //--------------------------------------------------------------------------
    // filter + eq attributes

    auto qAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v, 1);},
        fromStringDec
    );
    auto eqSplitAttr = stringFunctionToAPFA(
        [](float v) {
            return juce::String(juce::roundToInt(         100.0f * v)) + " / " +
                   juce::String(juce::roundToInt(100.0f - 100.0f * v));
        },
        fromStringPerc
    );

    //--------------------------------------------------------------------------
    // dynamics attributes

    auto dynLowerThreshAttr = stringFunctionToAPFA(
        [this](float v) {
            return static_cast<bool>(v) ? juce::String(normalizeDynLowerThreshold(v), 1) + " db"
                                        : "off";
        },
        [this](juce::String const& s) {
            if (s.containsIgnoreCase("off"))
                return 0.0f;
            float const db = s.getFloatValue();
            float const threshold = dynThresholdPtr->load();
            float const noiseFloorDB = MyCompressor<float>::noiseFloorDB;
            return (db-noiseFloorDB) / (threshold-noiseFloorDB);
        }
    );
    auto dynThresh2Attr = stringFunctionToAPFA(
        [this](float v) {return juce::String(normalizeDynThreshold2(v), 1) + " db";},
        [this](juce::String const& s) {
            float const db = s.getFloatValue();
            float const threshold = dynThresholdPtr->load();
            float const ratio = dynRatioPtr->load();
            return ratio * (1.0f-db/threshold);
        }
    );
    auto dynNegMixAttr = stringFunctionToAPFA(
        [](float v) {
            return static_cast<bool>(v) ? juce::String(juce::roundToInt(v * 100)) + "%"
                                        : "off";
        },
        fromStringPerc
    );
    auto dynTransientTimeAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v, 3) + " ms";},
        fromStringDec
    );

    auto dynTimeAttr = stringFunctionToAPFA(
        [this](float v) {
            float const crossover1 = getParameterValueByID(ParamIDs::dynCrossover1);
            float const crossover2 = getParameterValueByID(ParamIDs::dynCrossover2);
            float const compensation = getParameterValueByID(ParamIDs::dynCompensation);

            auto const bandConfig = MyMultibandCompressor<float>::calculateBandConfig(crossover1, crossover2);
            int const bandCount = bandConfig.bandCount;
            float const crossoverLower = bandConfig.crossoverLower;
            float const crossoverHigher = bandConfig.crossoverHigher;

            // calculate times for each band
            float const lowTime = MyMultibandCompressor<float>::cycleMatchTime(v, crossoverLower, compensation);
            float const midTime = MyMultibandCompressor<float>::cycleMatchTime(v, crossoverHigher, compensation);
            float const highTime = v;

            // if a time is less than 1, it should be displayed to three decimal places
            // else, it should be rounded to a whole number
            juce::String lowString = (lowTime < 1.0f)
                ? juce::String(lowTime, 3)
                : juce::String(juce::roundToInt(lowTime));
            juce::String midString = (midTime < 1.0f)
                ? juce::String(midTime, 3)
                : juce::String(juce::roundToInt(midTime));
            juce::String highString = (highTime < 1.0f)
                ? juce::String(highTime, 3)
                : juce::String(juce::roundToInt(highTime));

            // display each time separated by a forward slash
            juce::String finalString = highString;
            if (bandCount > 1)
            {
                finalString = midString + " / " + finalString;
                if (bandCount > 2)
                    finalString = lowString + " / " + finalString;
            }
            finalString += " ms";

            return finalString;
        },
        [](juce::String const& s) {return s.fromLastOccurrenceOf("/", false, false).getFloatValue();}
    );

    auto dynSlewAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v*100.0f, 2) + "%";},
        fromStringPerc
    );
    auto dynCrossoverAttr = stringFunctionToAPFA(
        [](float v) {
            if (v == 20.0f || v == 20000.0f)
                return juce::String("off");

            if (v >= 1000.0f)
            {
                v /= 1000.0f;
                return juce::String(v, 1) + " khz";
            }
            else
                return juce::String(juce::roundToInt(v)) + " hz";
        },
        fromStringFreq
    );
    auto dynRatioAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v, 1) + " : 1.0";},
        fromStringDec
    );
    auto dynSmoothTimeAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(v, 1) + " ms";},
        fromStringDec
    );
    auto dynSagTimeAttr = stringFunctionToAPFA(
        [](float v) {return juce::String(juce::roundToInt(v)) + " ms";},
        fromStringDec
    );

    //--------------------------------------------------------------------------
    // master attributes

    auto filterAndDynBypAttr = stringFunctionToAPBA([](float v) {return !static_cast<bool>(v) ? "active" : "bypassed";});
    auto polAttr = stringFunctionToAPBA([](float v) {return !static_cast<bool>(v) ? "normal" : "inverted";});

    //--------------------------------------------------------------------------
    // live mode attributes

    auto liveModeAttributes = stringFunctionToAPBA([](float v) {return !static_cast<bool>(v) ? "off" : "on";});

    //--------------------------------------------------------------------------
    // parameter layout creation

    auto makeBool = [](auto&&... args) {
        return std::make_unique<juce::AudioParameterBool>(std::forward<decltype(args)>(args)...);
    };

    auto makeFloat = [](auto&&... args) {
        return std::make_unique<juce::AudioParameterFloat>(std::forward<decltype(args)>(args)...);
    };

    auto pID = [](char const* paramID, int const versionHint) {return juce::ParameterID(paramID, versionHint);};
    auto pID1 = [pID](char const* paramID) {return pID(paramID, 1);};

    auto fxLoopGroup = std::make_unique<juce::AudioProcessorParameterGroup>("fxloop", "fx loop", "|");
    fxLoopGroup->addChild(makeFloat(pID1(ParamIDs::fxLoopMix),      "fxloop",   decRange,            0.0f,  decToPercAttr));
    fxLoopGroup->addChild(makeFloat(pID1(ParamIDs::fxLoopSendDB),   "send",     fxLoopSendDBRange,   0.0f,  dbAttr));
    fxLoopGroup->addChild(makeFloat(pID1(ParamIDs::fxLoopReturnDB), "return",   fxLoopReturnDBRange, 0.0f,  dbAttr));
    fxLoopGroup->addChild(makeBool(pID1(ParamIDs::fxLoopPosition),  "position",                      false, fxLoopPositionAttr));
    layout.add(std::move(fxLoopGroup));

    auto stereoGroup = std::make_unique<juce::AudioProcessorParameterGroup>("stereo", "stereo", "|");
    stereoGroup->addChild(makeFloat(pID1(ParamIDs::width),      "width", decRange, 1.0f,  decToPercAttr));
    stereoGroup->addChild(makeFloat(pID1(ParamIDs::pan),        "pan",   decRange, 0.5f,  panAttr));
    stereoGroup->addChild(makeBool(pID1(ParamIDs::stereoOrder), "order",           false, stereoOrderAttr));
    layout.add(std::move(stereoGroup));

    auto filterGroup = std::make_unique<juce::AudioProcessorParameterGroup>("filter", "filter", "|");
    filterGroup->addChild(makeFloat(pID1(ParamIDs::hpFreq), "hp",   freqRange, 20.0f,    freqAttr));
    filterGroup->addChild(makeFloat(pID1(ParamIDs::hpQ),    "hp q", hpQRange,  0.7f,     qAttr));
    filterGroup->addChild(makeFloat(pID1(ParamIDs::lpFreq), "lp",   freqRange, 20000.0f, freqAttr));
    filterGroup->addChild(makeFloat(pID1(ParamIDs::lpQ),    "lp q", lpQRange,  0.7f,     qAttr));
    layout.add(std::move(filterGroup));

    auto eqGroup = std::make_unique<juce::AudioProcessorParameterGroup>("eq", "eq", "|");
    eqGroup->addChild(makeFloat(pID1(ParamIDs::eqFreq),     "eq",      freqRange,     1400.0f, freqAttr));
    eqGroup->addChild(makeFloat(pID1(ParamIDs::eqQ),        "eq q",    eqQRange,      0.6f,    qAttr));
    eqGroup->addChild(makeFloat(pID1(ParamIDs::eqPeakDB),   "eq peak", eqPeakDBRange, 0.0f,    dbAttr));
    eqGroup->addChild(makeFloat(pID1(ParamIDs::eqTiltDB),   "eq tilt", eqTiltDBRange, 0.0f,    dbAttr));
    eqGroup->addChild(makeFloat(pID1(ParamIDs::eqPosition), "eq pos",  decRange,      0.5f,    eqSplitAttr));
    layout.add(std::move(eqGroup));

    auto dynGroup = std::make_unique<juce::AudioProcessorParameterGroup>("dyn", "dynamics", "|");
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynBandLink),       "band link",    decRange,              0.0f,    decToPercAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynStereoLink),     "stereo link",  decRange,              0.0f,    decToPercAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynDriveDB),        "drive",        dynDriveDBRange,       0.0f,    dbAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynThreshold),      "thrsh",        dynThreshRange,       -18.0f,   dbAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynLowerThreshold), "thrsh low",    decRange,              0.0f,    dynLowerThreshAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynThreshold2),     "+ negator",    decRange,              0.6f,    dynThresh2Attr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynNegMix),         "|| negator",   decRange,              0.4f,    dynNegMixAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynRatio),          "ratio",        dynRatioRange,         2.0f,    dynRatioAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynVariMuMult),     "vari-mu",      dynVariMuMultRange,    1.0f,    multAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynTransientTime),  "transient",    dynTransientTimeRange, 0.125f,  dynTransientTimeAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynAttackTime),     "attack",       dynAttackTimeRange,    10.0f,   dynTimeAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynReleaseTime),    "release",      dynReleaseTimeRange,   100.0f,  dynTimeAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynSlew),           "slew",         dynSlewRange,          0.5f,    dynSlewAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynCompensation),   "compensation", decRange,              0.5f,    decToPercAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynAdaptability),   "adaptability", dynAdaptabilityRange,  2.0f,    multAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynCrossover1),     "xover 1",      freqRange,             100.0f,  dynCrossoverAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynCrossover2),     "xover 2",      freqRange,             1400.0f, dynCrossoverAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynLowCompPower),   "low-comp detection power",   dynDetectionPowerRange, 0.5f, decAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynHighCompPower),  "high-comp detection power",  dynDetectionPowerRange, 4.0f, decAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynLowCompOffset),  "low-comp detection offset",  decRange,               1.0f, decToPercAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynHighCompOffset), "high-comp detection offset", decRange,               0.0f, decToPercAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynResponsePower),  "response power",   dynResponsePowerRange, 0.5f,   decAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynKneeDB),         "knee",             dynKneeRange,          18.0f,  dbAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynSmoothTime),     "onset",            dynSmoothTimeRange,    3.0f,   dynSmoothTimeAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynSagTime),        "sag",              dynSagTimeRange,       512.0f, dynSagTimeAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynMakeupDB),       "makeup",           dynMakeupDBRange,      0.0f,   dbAttr));
    dynGroup->addChild(makeFloat(pID1(ParamIDs::dynMix),            "mix",              decRange,              1.0f,   decToPercAttr));
    layout.add(std::move(dynGroup));

    auto masterGroup = std::make_unique<juce::AudioProcessorParameterGroup>("master", "master", "|");
    masterGroup->addChild(makeBool(pID1(ParamIDs::filterAndDynByp), "eq + dyn",                false, filterAndDynBypAttr));
    masterGroup->addChild(makeFloat(pID1(ParamIDs::volumeLinear),   "vol",      decRange,      1.0f,  gainAttr));
    masterGroup->addChild(makeFloat(pID1(ParamIDs::outputDB),       "output",   outputDBRange, 0.0f,  dbAttr));
    masterGroup->addChild(makeBool(pID1(ParamIDs::polarity),        "polarity",                false, polAttr));
    layout.add(std::move(masterGroup));

    layout.add(makeBool(pID1(ParamIDs::liveMode), "live mode", false, liveModeAttributes));

//    auto testGroup = std::make_unique<juce::AudioProcessorParameterGroup>("test", "Test", "|");
//    testGroup->addChild(makeFloat(pID1(ParamIDs::dynTestValue1), "test 1", decRange, 0.0f, decToPercAttr));
//    testGroup->addChild(makeFloat(pID1(ParamIDs::dynTestValue2), "test 2", decRange, 0.0f, decToPercAttr));
//    testGroup->addChild(makeFloat(pID1(ParamIDs::dynTestValue3), "test 3", decRange, 0.0f, decToPercAttr));
//    layout.add(std::move(testGroup));

    return layout;
}

//==============================================================================
// bus layout

bool ChannelAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const
{
    auto mainInSet = layouts.getMainInputChannelSet();
    auto mainOutSet = layouts.getMainOutputChannelSet();
    auto sendSet = layouts.getChannelSet(false, 1);
    auto returnSet = layouts.getChannelSet(true, 1);

    // main input must be mono or stereo
    if (mainInSet != juce::AudioChannelSet::mono() && mainInSet != juce::AudioChannelSet::stereo())
        return false;

    // main output must be stereo
    if (mainOutSet != juce::AudioChannelSet::stereo())
        return false;

    // send and return optional, but if present must be stereo
    if (!sendSet.isDisabled() && sendSet != juce::AudioChannelSet::stereo()) return false;
    if (!returnSet.isDisabled() && returnSet != juce::AudioChannelSet::stereo()) return false;

    return true;
}

//==============================================================================
// stereo

void ChannelAudioProcessor::processWidth(juce::dsp::AudioBlock<double> block)
{
    auto* channelDataL = block.getChannelPointer(0);
    auto* channelDataR = block.getChannelPointer(1);
    int blockSampleCount = static_cast<int>(block.getNumSamples());

    smoothedWidth.setTargetValue(widthPtr->load());

    // calculations for operands (width should only narrow)
    auto calculateOneMinusWidth = [](float width) -> float {
        return 1.0f - width;
    };
    auto calculateChannelGain = [](float oneMinusWidth) -> float {
        return 1.0f / (1.0f + (oneMinusWidth));
    };

    // when not smoothing, calculate operands per block
    if (!smoothedWidth.isSmoothing())
    {
        float const width = smoothedWidth.getCurrentValue();
        if (width == 1.0f)
            return;

        float const oneMinusWidth = calculateOneMinusWidth(width);
        float const channelGain = calculateChannelGain(oneMinusWidth);

        for (int sample = 0; sample < blockSampleCount; ++sample)
        {
            auto const left = channelDataL[sample];
            auto const right = channelDataR[sample];

            channelDataL[sample] = (left + oneMinusWidth * right) * channelGain;
            channelDataR[sample] = (right + oneMinusWidth * left) * channelGain;
        }
    }
    else // when smoothing, calculate operands per sample
    {
        for (int sample = 0; sample < blockSampleCount; ++sample)
        {
            auto const left = channelDataL[sample];
            auto const right = channelDataR[sample];

            float const width = smoothedWidth.getNextValueClamped();
            float const oneMinusWidth = calculateOneMinusWidth(width);
            float const channelGain = calculateChannelGain(oneMinusWidth);

            channelDataL[sample] = (left + oneMinusWidth * right) * channelGain;
            channelDataR[sample] = (right + oneMinusWidth * left) * channelGain;
        }
    }
}

void ChannelAudioProcessor::processPan(juce::dsp::AudioBlock<double> block)
{
    auto* channelDataL = block.getChannelPointer(0);
    auto* channelDataR = block.getChannelPointer(1);
    int blockSampleCount = static_cast<int>(block.getNumSamples());

    smoothedPan.setTargetValue(normalizePan(panPtr->load()));

    // calculations for operands
    auto calculateChannelGain = [](float x) {return std::sqrt(x * twoOverPi * std::sin(x));};

    auto calculateLeftGain = [calculateChannelGain](float pan) -> float {
        return calculateChannelGain(piOverTwo - pan) * unityAtCenterMultiplier;
    };
    auto calculateRightGain = [calculateChannelGain](float pan) -> float {
        return calculateChannelGain(pan) * unityAtCenterMultiplier;
    };

    // when not smoothing, calculate operands per block
    if (!smoothedPan.isSmoothing())
    {
        float const pan = smoothedPan.getCurrentValue();
        if (pan == piOverFour)
            return;

        float const leftGain = calculateLeftGain(pan);
        float const rightGain = calculateRightGain(pan);

        block.getSingleChannelBlock(0).multiplyBy(leftGain);
        block.getSingleChannelBlock(1).multiplyBy(rightGain);
    }
    else // when smoothing, calculate operands per sample
    {
        for (int sample = 0; sample < blockSampleCount; ++sample)
        {
            float const pan = smoothedPan.getNextValueClamped();
            float const leftGain = calculateLeftGain(pan);
            float const rightGain = calculateRightGain(pan);

            channelDataL[sample] *= leftGain;
            channelDataR[sample] *= rightGain;
        }
    }
}

void ChannelAudioProcessor::processStereo(juce::dsp::AudioBlock<double> block)
{
    bool const panFirst = static_cast<bool>(stereoOrderPtr->load());

    if (!panFirst)
        processWidth(block);

    processPan(block);

    if (panFirst)
        processWidth(block);
}

//==============================================================================
// polarity

void ChannelAudioProcessor::processPolarity(juce::dsp::AudioBlock<double> block)
{
    auto* channelDataL = block.getChannelPointer(0);
    auto* channelDataR = block.getChannelPointer(1);
    int blockSampleCount = static_cast<int>(block.getNumSamples());

    static_cast<bool>(polarityPtr->load())
        ? smoothedPolarity.setTargetValue(-1.0f)
        : smoothedPolarity.setTargetValue(1.0f);

    // when not smoothing, either do nothing, or negate entire block
    if (!smoothedPolarity.isSmoothing())
    {
        if (smoothedPolarity.getCurrentValue() != 1.0f)
            block.negate();
    }
    else // when smoothing, recalculate operand per sample
    {
        for (int sample = 0; sample < blockSampleCount; ++sample)
        {
            auto left = channelDataL[sample];
            auto right = channelDataR[sample];

            float const polarity = smoothedPolarity.getNextValueClamped();

            left *= polarity;
            right *= polarity;

            channelDataL[sample] = left;
            channelDataR[sample] = right;
        }
    }
}

//==============================================================================
// effects loop

void ChannelAudioProcessor::processFXLoop(juce::dsp::AudioBlock<double> block,
                                          juce::dsp::AudioBlock<double> sendBlock,
                                          juce::dsp::AudioBlock<double> returnBlock)
{
    bool const sendAvailable = getChannelCountOfBus(false, 1) >= 2;
    bool const returnAvailable = getChannelCountOfBus(true, 1) >= 2;

    if (sendAvailable)
    {
        sendBlock.copyFrom(block);

        juce::dsp::ProcessContextReplacing<double> sendContext(sendBlock);
        float const fxLoopSendDB = fxLoopSendDBPtr->load();
        fxLoopSendGain.setGainLinear(MyCompressor<double>::dBToLinear(fxLoopSendDB));
        fxLoopSendGain.process(sendContext);
    }
    else if (getChannelCountOfBus(false, 1) > 0)
        sendBlock.clear();

    if (returnAvailable)
    {
        fxLoopMixer.pushDrySamples(block);

        juce::dsp::ProcessContextReplacing<double> returnContext(returnBlock);
        float const fxLoopReturnDB = fxLoopReturnDBPtr->load();
        fxLoopReturnGain.setGainLinear(MyCompressor<double>::dBToLinear(fxLoopReturnDB));
        fxLoopReturnGain.process(returnContext);

        fxLoopMixer.setWetMixProportion(fxLoopMixPtr->load());
        fxLoopMixer.mixWetSamples(returnBlock);
        block.copyFrom(returnBlock);
    }
}

//==============================================================================
// filters

void ChannelAudioProcessor::processHPLPFilters(juce::dsp::AudioBlock<double> block)
{
    auto* channelDataL = block.getChannelPointer(0);
    auto* channelDataR = block.getChannelPointer(1);
    int blockSampleCount = static_cast<int>(block.getNumSamples());

    // set targets for freq and q smoothers
    smoothedHPFreq.setTargetValue(hpFreqPtr->load());
    smoothedHPQ.setTargetValue(hpQPtr->load());
    smoothedLPFreq.setTargetValue(lpFreqPtr->load());
    smoothedLPQ.setTargetValue(lpQPtr->load());

    for (int sample = 0; sample < blockSampleCount; ++sample)
    {
        auto left = channelDataL[sample];
        auto right = channelDataR[sample];

        // set freq and q if smoothing
        if (smoothedHPFreq.isSmoothing())
            hp.setCutoffFrequency(smoothedHPFreq.getNextValueClamped());
        if (smoothedHPQ.isSmoothing())
            hp.setResonance(smoothedHPQ.getNextValueClamped());
        if (smoothedLPFreq.isSmoothing())
            lp.setCutoffFrequency(smoothedLPFreq.getNextValueClamped());
        if (smoothedLPQ.isSmoothing())
            lp.setResonance(smoothedLPQ.getNextValueClamped());

        // process filters
        left = hp.processSample(0, left);
        right = hp.processSample(1, right);
        left = lp.processSample(0, left);
        right = lp.processSample(1, right);

        channelDataL[sample] = left;
        channelDataR[sample] = right;
    }
}

//==============================================================================
// eq

void ChannelAudioProcessor::processEQBand(juce::dsp::ProcessContextReplacing<double> context,
    Filter* filter, FilterCache* filterCache,
    ChannelAudioProcessor::FilterType filterType,
    float freq, float q, float db)
{
    float sampleRate = static_cast<float>(getSampleRate());
    if (oversampling) sampleRate *= 2;

    // only recalculate coefficients on change
    if (sampleRate != filterCache->sampleRate ||
        filterType != filterCache->filterType ||
        freq != filterCache->freq ||
        q != filterCache->q ||
        db != filterCache->db)
    {
        float gain = MyCompressor<float>::dBToLinear(db);
        switch (filterType)
        {
            case FilterType::HighPass:
                *filter->state = juce::dsp::IIR::ArrayCoefficients<double>::makeHighPass(sampleRate, freq, q);
                break;
            case FilterType::LowPass:
                *filter->state = juce::dsp::IIR::ArrayCoefficients<double>::makeLowPass(sampleRate, freq, q);
                break;
            case FilterType::BandPass:
                *filter->state = juce::dsp::IIR::ArrayCoefficients<double>::makeBandPass(sampleRate, freq, q);
                break;
            case FilterType::Peak:
                *filter->state = juce::dsp::IIR::ArrayCoefficients<double>::makePeakFilter(sampleRate, freq, q, gain);
                break;
            case FilterType::LowShelf:
                *filter->state = juce::dsp::IIR::ArrayCoefficients<double>::makeLowShelf(sampleRate, freq, q, gain);
                break;
            case FilterType::HighShelf:
                *filter->state = juce::dsp::IIR::ArrayCoefficients<double>::makeHighShelf(sampleRate, freq, q, gain);
                break;
            case FilterType::AllPass:
                *filter->state = juce::dsp::IIR::ArrayCoefficients<double>::makeAllPass(sampleRate, freq, q);
                break;
        }
        *filterCache = {sampleRate, filterType, freq, q, db};
    }

    filter->process(context);
}

//==============================================================================
// compressor

void ChannelAudioProcessor::processComp(juce::dsp::AudioBlock<double> block)
{
    auto* channelDataL = block.getChannelPointer(0);
    auto* channelDataR = block.getChannelPointer(1);
    int blockSampleCount = static_cast<int>(block.getNumSamples());

    // lambda for smoothing at block rate
    auto sTAS = [=](auto& smoother, std::atomic<float>* paramPtr) {
        return smoother.setTargetAndSkip(paramPtr->load(), blockSampleCount);
    };

    // advance and get values for block-rate smoothers
    float const dynBandLink = sTAS(smoothedDynBandLink, dynBandLinkPtr);
    float const dynStereoLink = sTAS(smoothedDynStereoLink, dynStereoLinkPtr);
    float const dynThreshold = sTAS(smoothedDynThreshold, dynThresholdPtr);
    float const dynThreshold2 =
        smoothedDynThreshold2.setTargetAndSkip(normalizeDynThreshold2(dynThreshold2Ptr->load()), blockSampleCount);
    float const dynLowerThreshold =
        smoothedDynLowerThreshold.setTargetAndSkip(normalizeDynLowerThreshold(dynLowerThresholdPtr->load()), blockSampleCount);
    float const dynTransientTime = sTAS(smoothedDynTransientTime, dynTransientTimePtr);
    float const dynAttackTime = sTAS(smoothedDynAttackTime, dynAttackTimePtr);
    float const dynReleaseTime = sTAS(smoothedDynReleaseTime, dynReleaseTimePtr);
    float const dynSlew = sTAS(smoothedDynSlew, dynSlewPtr);
    float const dynCompensation = sTAS(smoothedDynCompensation, dynCompensationPtr);
    float const dynAdaptability = sTAS(smoothedDynAdaptability, dynAdaptabilityPtr);
    float const dynCrossover1 = sTAS(smoothedDynCrossover1, dynCrossover1Ptr);
    float const dynCrossover2 = sTAS(smoothedDynCrossover2, dynCrossover2Ptr);
    float const dynLowCompPower = dynLowCompPowerPtr->load(); // quickpow doesn't work with smoothing
    float const dynHighCompPower = dynHighCompPowerPtr->load(); // quickpow doesn't work with smoothing
    float const dynLowCompOffset = sTAS(smoothedDynLowCompOffset, dynLowCompOffsetPtr);
    float const dynHighCompOffset = sTAS(smoothedDynHighCompOffset, dynHighCompOffsetPtr);
    float const dynResponsePower = dynResponsePowerPtr->load(); // quickpow doesn't work with smoothing
    float const dynRatio = sTAS(smoothedDynRatio, dynRatioPtr);
    float const dynVariMuMult = sTAS(smoothedDynVariMuMult, dynVariMuMultPtr);
    float const dynKneeDB = sTAS(smoothedDynKneeDB, dynKneeDBPtr);
    float const dynSmoothTime = sTAS(smoothedDynSmoothTime, dynSmoothTimePtr);
    float const dynSagTime = sTAS(smoothedDynSagTime, dynSagTimePtr);

    // block-rate setters
    comp.setBandLink(dynBandLink);
    comp.setStereoLink(dynStereoLink);
    comp.setThresholds(dynLowerThreshold, dynThreshold, dynThreshold2);
    comp.setTransientTime(dynTransientTime);
    comp.setAttackTime(dynAttackTime);
    comp.setReleaseTime(dynReleaseTime);
    comp.setSlew(dynSlew);
    comp.setCompensation(dynCompensation);
    comp.setAdaptability(dynAdaptability);
    comp.setCrossovers(dynCrossover1, dynCrossover2);
    comp.setLowCompPower(dynLowCompPower);
    comp.setHighCompPower(dynHighCompPower);
    comp.setLowCompOffset(dynLowCompOffset);
    comp.setHighCompOffset(dynHighCompOffset);
    comp.setResponsePower(dynResponsePower);
    comp.setRatio(dynRatio);
    comp.setVariMuMultiplier(dynVariMuMult);
    comp.setKneeDB(dynKneeDB);
    comp.setSmoothTime(dynSmoothTime);
    comp.setSagTime(dynSagTime);
//    comp.setTestValues(dynTestValue1Ptr->load(),
//                       dynTestValue2Ptr->load(),
//                       dynTestValue3Ptr->load());

    // set targets for sample-rate smoothers
    smoothedDynDriveDB.setTargetValue(dynDriveDBPtr->load());
    smoothedDynNegMix.setTargetValue(dynNegMixPtr->load());
    smoothedDynMakeupDB.setTargetValue(dynMakeupDBPtr->load());
    smoothedDynMix.setTargetValue(dynMixPtr->load());

    for (int sample = 0; sample < blockSampleCount; ++sample)
    {
        auto left = channelDataL[sample];
        auto right = channelDataR[sample];

        // sample-rate setters
        comp.setDriveDB(smoothedDynDriveDB.getNextValueClamped());
        comp.setNegMix(smoothedDynNegMix.getNextValueClamped());
        comp.setMakeupDB(smoothedDynMakeupDB.getNextValueClamped());
        comp.setMix(smoothedDynMix.getNextValueClamped());

        // process comp
        comp.process(left, right);

        channelDataL[sample] = left;
        channelDataR[sample] = right;
    }
}

//==============================================================================
// processors

void ChannelAudioProcessor::prepareProcessors(double sampleRate, int samplesPerBlock)
{
    auto prepareProcessors = [](auto const& spec, auto&... processors) {
        (processors.prepare(spec), ...);
    };
    juce::dsp::ProcessSpec spec{sampleRate, static_cast<juce::uint32>(samplesPerBlock), 2};
    prepareProcessors(spec,
                      fxLoopMixer, fxLoopSendGain, fxLoopReturnGain,
                      filterAndDynBypMixer, outputGain, volumeGain);
}

void ChannelAudioProcessor::prepareOversampledProcessors(double sampleRate, int maxSamplesPerBlock)
{
    // set latency
    setLatencySamples(oversampling ? static_cast<int>(oversampler.getLatencyInSamples()) : 0);
    filterAndDynBypMixer.setWetLatency(oversampling ? oversampler.getLatencyInSamples() : 0);

    // double sample rate for preparing processors and filters if oversampling
    if (oversampling) sampleRate *= 2;
    juce::dsp::ProcessSpec spec{sampleRate, static_cast<juce::uint32>(maxSamplesPerBlock), 2};

    // prepare processors
    auto prepareProcessors = [](auto const& spec, auto&... processors) {
        (processors.prepare(spec), ...);
    };
    prepareProcessors(spec,
                      hp, lp, peak, tiltShelf, tiltPeak, peak2, tiltShelf2, tiltPeak2,
                      comp);

    // set high-pass and low-pass filter types
    hp.setType(juce::dsp::StateVariableTPTFilterType::highpass);
    lp.setType(juce::dsp::StateVariableTPTFilterType::lowpass);

    // allocate the other filters
    auto initCoeffs = [sampleRate](auto&... filters) {
        ((*filters.state = juce::dsp::IIR::ArrayCoefficients<double>::makeHighPass(sampleRate, 20000.0, 0.7)), ...);
    };
    initCoeffs(peak, tiltShelf, tiltPeak, peak2, tiltShelf2, tiltPeak2);

    // initialize filter cache
    auto initFilterCache = [](auto&... cache) {
        ((cache = FilterCache{}), ...);
    };
    initFilterCache(peakCache, tiltShelfCache, tiltPeakCache, peak2Cache, tiltShelf2Cache, tiltPeak2Cache);
}

//==============================================================================
// smoothers

void ChannelAudioProcessor::prepareSmoothers(double sampleRate)
{
    int rampSamples = static_cast<int>(rampDuration * sampleRate);

    // reset smoothers
    auto resetSmoothers = [rampSamples](auto&... smoothers) {
        (smoothers.reset(rampSamples), ...);
    };
    resetSmoothers(smoothedWidth, smoothedPan,
                   smoothedEQFreq, smoothedEQQ, smoothedEQPeakDB, smoothedEQTiltDB, smoothedEQPosition,
                   smoothedPolarity);

    // use built-in smoothers for gain processors
    fxLoopSendGain.setRampDurationSeconds(rampDuration);
    fxLoopReturnGain.setRampDurationSeconds(rampDuration);
    volumeGain.setRampDurationSeconds(rampDuration);
    outputGain.setRampDurationSeconds(rampDuration);
}

void ChannelAudioProcessor::prepareOversampledSmoothers(double sampleRate)
{
    if (oversampling) sampleRate *= 2;

    int rampSamples = static_cast<int>(rampDuration * sampleRate);

    // reset smoothers
    auto resetSmoothers = [rampSamples](auto&... smoothers) {
        (smoothers.reset(rampSamples), ...);
    };
    resetSmoothers(smoothedHPFreq, smoothedHPQ, smoothedLPFreq, smoothedLPQ,
                   smoothedDynBandLink, smoothedDynStereoLink,
                   smoothedDynDriveDB,
                   smoothedDynThreshold, smoothedDynThreshold2, smoothedDynLowerThreshold,
                   smoothedDynTransientTime, smoothedDynAttackTime, smoothedDynReleaseTime,
                   smoothedDynSlew, smoothedDynCompensation, smoothedDynAdaptability,
                   smoothedDynCrossover1, smoothedDynCrossover2,
                   smoothedDynLowCompOffset, smoothedDynHighCompOffset,
                   smoothedDynNegMix, smoothedDynRatio, smoothedDynVariMuMult, smoothedDynKneeDB,
                   smoothedDynSmoothTime, smoothedDynSagTime,
                   smoothedDynMakeupDB, smoothedDynMix);
}

void ChannelAudioProcessor::initializeSmoothers()
{
    auto initializeSmoother = [this](auto& smoother, juce::StringRef paramID) {
        auto const& range = parameters.getParameter(paramID)->getNormalisableRange();
        smoother.setRange(range.start, range.end);
        smoother.setCurrentAndTargetValue(getParameterValueByID(paramID));
    };

    initializeSmoother(smoothedWidth, ParamIDs::width);
    smoothedPan.setCurrentAndTargetValue(normalizePan(getParameterValueByID(ParamIDs::pan)));

    initializeSmoother(smoothedHPFreq, ParamIDs::hpFreq);
    initializeSmoother(smoothedHPQ, ParamIDs::hpQ);
    initializeSmoother(smoothedLPFreq, ParamIDs::lpFreq);
    initializeSmoother(smoothedLPQ, ParamIDs::lpQ);

    initializeSmoother(smoothedEQFreq, ParamIDs::eqFreq);
    initializeSmoother(smoothedEQQ, ParamIDs::eqQ);
    initializeSmoother(smoothedEQPeakDB, ParamIDs::eqPeakDB);
    initializeSmoother(smoothedEQTiltDB, ParamIDs::eqTiltDB);
    initializeSmoother(smoothedEQPosition, ParamIDs::eqPosition);

    initializeSmoother(smoothedDynBandLink, ParamIDs::dynBandLink);
    initializeSmoother(smoothedDynStereoLink, ParamIDs::dynStereoLink);
    initializeSmoother(smoothedDynDriveDB, ParamIDs::dynDriveDB);
    initializeSmoother(smoothedDynThreshold, ParamIDs::dynThreshold);
    smoothedDynThreshold2.setCurrentAndTargetValue(normalizeDynThreshold2(getParameterValueByID(ParamIDs::dynThreshold2)));
    smoothedDynLowerThreshold.setCurrentAndTargetValue(normalizeDynLowerThreshold(getParameterValueByID(ParamIDs::dynLowerThreshold)));

    initializeSmoother(smoothedDynTransientTime, ParamIDs::dynTransientTime);
    initializeSmoother(smoothedDynAttackTime, ParamIDs::dynAttackTime);
    initializeSmoother(smoothedDynReleaseTime, ParamIDs::dynReleaseTime);
    initializeSmoother(smoothedDynSlew, ParamIDs::dynSlew);
    initializeSmoother(smoothedDynCompensation, ParamIDs::dynCompensation);
    initializeSmoother(smoothedDynAdaptability, ParamIDs::dynAdaptability);
    initializeSmoother(smoothedDynCrossover1, ParamIDs::dynCrossover1);
    initializeSmoother(smoothedDynCrossover2, ParamIDs::dynCrossover2);
    initializeSmoother(smoothedDynLowCompOffset, ParamIDs::dynLowCompOffset);
    initializeSmoother(smoothedDynHighCompOffset, ParamIDs::dynHighCompOffset);
    initializeSmoother(smoothedDynNegMix, ParamIDs::dynNegMix);
    initializeSmoother(smoothedDynRatio, ParamIDs::dynRatio);
    initializeSmoother(smoothedDynVariMuMult, ParamIDs::dynVariMuMult);
    initializeSmoother(smoothedDynKneeDB, ParamIDs::dynKneeDB);
    initializeSmoother(smoothedDynSmoothTime, ParamIDs::dynSmoothTime);
    initializeSmoother(smoothedDynSagTime, ParamIDs::dynSagTime);
    initializeSmoother(smoothedDynMakeupDB, ParamIDs::dynMakeupDB);
    initializeSmoother(smoothedDynMix, ParamIDs::dynMix);

    static_cast<bool>(getParameterValueByID(ParamIDs::polarity)) ? smoothedPolarity.setCurrentAndTargetValue(-1.0f)
                                                            : smoothedPolarity.setCurrentAndTargetValue(1.0f);
}

//==============================================================================
// prepare

// pre-playback initialisation
void ChannelAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
    // oversample if sample rate is below 80 khz
    oversampling = (sampleRate < 80000.0);

    // avoid reallocating buffers
    auto numChannels = std::max(getTotalNumInputChannels(), getTotalNumOutputChannels());
    bufferDouble.setSize(numChannels, samplesPerBlock, false, false, true);
    returnBuffer.setSize(2, samplesPerBlock, false, false, true);

    // prepare processors that don't need oversampling
    prepareProcessors(sampleRate, samplesPerBlock);

    // prepare oversampler and processors that may need oversampling
    oversampler.initProcessing(static_cast<std::size_t>(samplesPerBlock));
    prepareOversampledProcessors(sampleRate, samplesPerBlock*2);

    // prepare smoothers
    prepareSmoothers(sampleRate);
    prepareOversampledSmoothers(sampleRate);

    // initialize smoothers
    initializeSmoothers();

    // set initial values for processors that're only updated when smoothing
    hp.setCutoffFrequency(smoothedHPFreq.getCurrentValue());
    hp.setResonance(smoothedHPQ.getCurrentValue());
    lp.setCutoffFrequency(smoothedLPFreq.getCurrentValue());
    lp.setResonance(smoothedLPQ.getCurrentValue());

    // reset processors
    reset();
}

//==============================================================================
// reset

void ChannelAudioProcessor::reset()
{
    // reset processors
    auto resetProcessors = [](auto&... processors) {
        (processors.reset(), ...);
    };
    resetProcessors(fxLoopMixer, fxLoopSendGain, fxLoopReturnGain,
                    hp, lp, peak, tiltShelf, tiltPeak, peak2, tiltShelf2, tiltPeak2,
                    comp,
                    filterAndDynBypMixer, outputGain, volumeGain);

    // reset oversampler if oversampling
    if (oversampling)
        oversampler.reset();
}

//==============================================================================
// processing

void ChannelAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    juce::ScopedNoDenormals noDenormals;

    // always process with double
    bufferDouble.makeCopyOf(buffer, true);
    processBlock(bufferDouble, midiMessages);
    buffer.makeCopyOf(bufferDouble, true);
}

void ChannelAudioProcessor::processBlock(juce::AudioBuffer<double>& buffer, juce::MidiBuffer& /*midiMessages*/)
{
    juce::ScopedNoDenormals noDenormals;

    int samplesPerBuffer = buffer.getNumSamples();

    //--------------------------------------------------------------------------
    // variables

    // lambda for smoothing at block rate
    auto sTAS = [=](auto& smoother, std::atomic<float>* paramPtr) {
        return smoother.setTargetAndSkip(paramPtr->load(), samplesPerBuffer);
    };

    bool const fxLoopPosition = static_cast<bool>(fxLoopPositionPtr->load());

    float const eqFreq = sTAS(smoothedEQFreq, eqFreqPtr);
    float const eqQ = sTAS(smoothedEQQ, eqQPtr);
    float const eqPeakDB = sTAS(smoothedEQPeakDB, eqPeakDBPtr);

    // non-standard tilt filter (calculations below)
    float eqTiltShelfDB = sTAS(smoothedEQTiltDB, eqTiltDBPtr);

    // when above 0 dB, create a high-shelf boost with the set dB,
    // along with a peak cut at 1/2 the frequency and 1/3 the dB
    FilterType eqTiltShelfFilterType = FilterType::HighShelf;
    float eqTiltPeakFreq = eqFreq * 0.5f;
    float eqTiltPeakDB = eqTiltShelfDB * -0.333333f;

    // When below 0 dB, create a low-shelf boost with the set dB magnitude,
    // along with a peak cut at 2x the set frequency and 1/3 the dB magnitude
    if (eqTiltShelfDB < 0.0f)
    {
        eqTiltShelfFilterType = FilterType::LowShelf;
        eqTiltShelfDB *= -1.0f;
        eqTiltPeakDB *= -1.0f;
        eqTiltPeakFreq = eqFreq * 2.0f;
    }

    // prevent boosts from being cuts at frequency extremities
    // starting two octaves from 20 hz and 20000 hz,
    // the db gain of the peak filter is faded to zero and clamped
    // to 20 hz and 20000 hz
    // this plugin has automatic oversampling, so nyquist isn't a
    // problem, but in the event that the oversampling is removed,
    // it will be
    if (40.0f < eqFreq && eqFreq < 80.0f)
    {
        eqTiltPeakDB *= 1.0f - (80.0f-eqFreq)/40.0f;
    }
    else if (eqFreq <= 40.0f)
    {
        eqTiltPeakDB = 0.0f;
        eqTiltPeakFreq = 20.0f;
    }
    else if (5000.0f < eqFreq && eqFreq < 10000.0f)
    {
        eqTiltPeakDB *= (10000.0f-eqFreq)/5000.0f;
    }
    else if (eqFreq >= 10000.0f) // prevent filters past nyquist
    {
        eqTiltPeakDB = 0.0f;
        eqTiltPeakFreq = 20000.0f;
    }

    float const eqPosition = sTAS(smoothedEQPosition, eqPositionPtr);

    float const filterAndDynByp = static_cast<bool>(filterAndDynBypPtr->load()) ? 0.0f : 1.0f;
    float const volumeLinear = volumeLinearPtr->load(); // uses built-in smoother
    float const outputDB = outputDBPtr->load(); // uses built-in smoother

    //--------------------------------------------------------------------------
    // buffers, blocks, context

    auto mainBuffer = getBusBuffer(buffer, false, 0);
    auto sendBuffer = getBusBuffer(buffer, false, 1);
    returnBuffer.makeCopyOf(getBusBuffer(buffer, true, 1), true);

    if (getMainBusNumInputChannels() == 1)
        mainBuffer.copyFrom(1, 0, mainBuffer, 0, 0, samplesPerBuffer);

    juce::dsp::AudioBlock<double> block(mainBuffer);
    juce::dsp::AudioBlock<double> sendBlock(sendBuffer);
    juce::dsp::AudioBlock<double> returnBlock(returnBuffer);

    juce::dsp::ProcessContextReplacing<double> context(block);

    //--------------------------------------------------------------------------
    // processing

    // effects loop (input)
    if (!fxLoopPosition)
        processFXLoop(block, sendBlock, returnBlock);

    // stereo
    processStereo(block);

    // filter and dynamics bypass dry capture
    filterAndDynBypMixer.pushDrySamples(context.getInputBlock());

    // oversampling for filters, eq, and dynamics only
    auto effectiveBlock = oversampling ? oversampler.processSamplesUp(block) : block;
    juce::dsp::ProcessContextReplacing<double> effectiveContext(effectiveBlock);

    // hp + lp
    processHPLPFilters(effectiveBlock);

    // eq pre-dynamics
    processEQBand(effectiveContext, &peak, &peakCache, FilterType::Peak, eqFreq, eqQ, eqPeakDB * (1.0f - eqPosition));
    processEQBand(effectiveContext, &tiltShelf, &tiltShelfCache, eqTiltShelfFilterType, eqFreq, eqQ, eqTiltShelfDB * (1.0f - eqPosition));
    processEQBand(effectiveContext, &tiltPeak, &tiltPeakCache, FilterType::Peak, eqTiltPeakFreq, eqQ, eqTiltPeakDB * (1.0f - eqPosition));

    // dynamics
    processComp(effectiveBlock);

    // eq post-dynamics
    processEQBand(effectiveContext, &peak2, &peak2Cache, FilterType::Peak, eqFreq, eqQ, eqPeakDB * eqPosition);
    processEQBand(effectiveContext, &tiltShelf2, &tiltShelf2Cache, eqTiltShelfFilterType, eqFreq, eqQ, eqTiltShelfDB * eqPosition);
    processEQBand(effectiveContext, &tiltPeak2, &tiltPeak2Cache, FilterType::Peak, eqTiltPeakFreq, eqQ, eqTiltPeakDB * eqPosition);

    // if oversampling, sample back down
    if (oversampling)
        oversampler.processSamplesDown(block);

    // filter and dynamics bypass mix
    filterAndDynBypMixer.setWetMixProportion(filterAndDynByp);
    filterAndDynBypMixer.mixWetSamples(context.getOutputBlock());

    // effects loop (pre-vol)
    if (fxLoopPosition)
        processFXLoop(block, sendBlock, returnBlock);

    // volume gain
    volumeGain.setGainLinear(volumeLinear);
    volumeGain.process(context);

    // output gain
    outputGain.setGainLinear(MyCompressor<double>::dBToLinear(outputDB));
    outputGain.process(context);

    // polarity
    processPolarity(block);
}

//==============================================================================
// gui

// change this to false to remove gui
bool ChannelAudioProcessor::hasEditor() const {return true;}

// create plugin gui
juce::AudioProcessorEditor* ChannelAudioProcessor::createEditor()
{
    return new ChannelAudioProcessorEditor(*this);
//    return new juce::GenericAudioProcessorEditor(*this);
}

//==============================================================================
// save parameters

void ChannelAudioProcessor::getStateInformation(juce::MemoryBlock& destData)
{
    if (auto xmlParameters = parameters.copyState().createXml())
    {
        xmlParameters->setAttribute("version", 1);
        copyXmlToBinary(*xmlParameters, destData);
    }
}

void ChannelAudioProcessor::setStateInformation(void const* data, int sizeInBytes)
{
    if (auto xmlParameters = getXmlFromBinary(data, sizeInBytes))
        if (xmlParameters->hasTagName(parameters.state.getType()))
        {
            // unused for now
//            int const version = xmlParameters->getIntAttribute("version");

            parameters.replaceState(juce::ValueTree::fromXml(*xmlParameters));
        }
}

//==============================================================================
// create new plugin instance

juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
    return new ChannelAudioProcessor();
}
