1. Структура

Создай папку:

SymbioteLeveler
│
├── CMakeLists.txt
│
└── Source
    ├── PluginProcessor.h
    ├── PluginProcessor.cpp
    ├── PluginEditor.h
    ├── PluginEditor.cpp
    └── LevelerDSP.h
2. CMakeLists.txt
cmake_minimum_required(VERSION 3.22)

project(SymbioteLeveler VERSION 0.1.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)

# JUCE
FetchContent_Declare(
    JUCE
    GIT_REPOSITORY https://github.com/juce-framework/JUCE.git
    GIT_TAG 8.0.12
    GIT_SHALLOW TRUE
)

FetchContent_MakeAvailable(JUCE)

juce_add_plugin(SymbioteLeveler
    COMPANY_NAME "Symbiote Audio"
    PLUGIN_MANUFACTURER_CODE Symb
    PLUGIN_CODE Lv01

    FORMATS VST3

    PRODUCT_NAME "Symbiote Leveler"

    IS_SYNTH FALSE
    NEEDS_MIDI_INPUT FALSE
    NEEDS_MIDI_OUTPUT FALSE
    IS_MIDI_EFFECT FALSE

    COPY_PLUGIN_AFTER_BUILD FALSE

    VST3_CATEGORIES Dynamics
)

juce_generate_juce_header(SymbioteLeveler)

target_sources(SymbioteLeveler
    PRIVATE
        Source/PluginProcessor.cpp
        Source/PluginProcessor.h
        Source/PluginEditor.cpp
        Source/PluginEditor.h
        Source/LevelerDSP.h
)

target_compile_definitions(SymbioteLeveler
    PRIVATE
        JUCE_WEB_BROWSER=0
        JUCE_USE_CURL=0
        JUCE_VST3_CAN_REPLACE_VST2=0
)

target_link_libraries(SymbioteLeveler
    PRIVATE
        juce::juce_audio_utils
        juce::juce_dsp

    PUBLIC
        juce::juce_recommended_config_flags
        juce::juce_recommended_lto_flags
        juce::juce_recommended_warning_flags
)

CMake официально поддерживает Visual Studio generators и выбор x64 через -A x64.

3. Source/LevelerDSP.h

Это сердце нашего плагина.

#pragma once

#include <JuceHeader.h>
#include <cmath>
#include <algorithm>

class EnvelopeFollower
{
public:
    void prepare(double sampleRate)
    {
        sr = sampleRate;
        reset();
    }

    void reset()
    {
        envelope = 0.0f;
    }

    void setTimes(float attackMs, float releaseMs)
    {
        attack = std::exp(-1.0f /
                          static_cast<float>(sr * attackMs * 0.001));

        release = std::exp(-1.0f /
                           static_cast<float>(sr * releaseMs * 0.001));
    }

    float process(float input)
    {
        input = std::abs(input);

        const float coefficient =
            input > envelope ? attack : release;

        envelope =
            coefficient * envelope +
            (1.0f - coefficient) * input;

        return envelope;
    }

    float getValue() const
    {
        return envelope;
    }

private:
    double sr = 44100.0;
    float envelope = 0.0f;

    float attack = 0.99f;
    float release = 0.999f;
};


class SymbioteLeveler
{
public:

    void prepare(double sampleRate, int channels)
    {
        sr = sampleRate;
        numChannels = channels;

        fet.prepare(sr);
        opto.prepare(sr);
        vca.prepare(sr);
        targetFollower.prepare(sr);
        transientFollower.prepare(sr);

        /*
            FET:
            very fast, peak-oriented
        */
        fet.setTimes(0.35f, 25.0f);

        /*
            Opto:
            slower and smoother
        */
        opto.setTimes(12.0f, 300.0f);

        /*
            VCA:
            controlled, medium-speed leveling
        */
        vca.setTimes(2.0f, 120.0f);

        /*
            Long-term reference.
            This is deliberately slow.
        */
        targetFollower.setTimes(1000.0f, 1600.0f);

        /*
            Used to identify fast transients.
        */
        transientFollower.setTimes(0.3f, 35.0f);

        reset();
    }

    void reset()
    {
        fet.reset();
        opto.reset();
        vca.reset();
        targetFollower.reset();
        transientFollower.reset();

        currentGain = 1.0f;
    }

    void setParameters(float amount,
                       float fetAmount,
                       float optoAmount,
                       float vcaAmount,
                       float transientPreserve,
                       float speed,
                       float mix)
    {
        levelingAmount = juce::jlimit(0.0f, 1.0f, amount);

        fetWeight = juce::jlimit(0.0f, 1.0f, fetAmount);
        optoWeight = juce::jlimit(0.0f, 1.0f, optoAmount);
        vcaWeight = juce::jlimit(0.0f, 1.0f, vcaAmount);

        transient = juce::jlimit(0.0f, 1.0f,
                                  transientPreserve);

        speedControl = juce::jlimit(0.0f, 1.0f, speed);

        wetDry = juce::jlimit(0.0f, 1.0f, mix);
    }

    float processSample(float input)
    {
        const float absolute =
            std::abs(input);

        /*
            Three different detector personalities.
        */
        const float fetLevel =
            fet.process(absolute);

        const float optoLevel =
            opto.process(absolute);

        const float vcaLevel =
            vca.process(absolute);

        /*
            Weighted detector combination.
        */
        const float weightSum =
            std::max(0.001f,
                      fetWeight +
                      optoWeight +
                      vcaWeight);

        const float combined =
            (fetLevel * fetWeight +
             optoLevel * optoWeight +
             vcaLevel * vcaWeight)
            / weightSum;

        /*
            Long-term program reference.
        */
        const float target =
            targetFollower.process(combined);

        /*
            Avoid instability at silence.
        */
        if (target < 0.00001f ||
            combined < 0.000001f)
        {
            return input;
        }

        /*
            How far are we from the desired level?
        */
        float correctionDb =
            juce::Decibels::gainToDecibels(
                target / std::max(combined, 0.000001f));

        /*
            Limit the amount of correction.
            The algorithm is NOT allowed to create
            ridiculous boosts.
        */
        correctionDb =
            juce::jlimit(-12.0f,
                         12.0f,
                         correctionDb);

        /*
            User amount.
        */
        correctionDb *= levelingAmount;

        /*
            Speed controls how aggressively the
            correction follows the signal.
        */
        const float speedFactor =
            0.25f + speedControl * 1.75f;

        correctionDb *= speedFactor;

        /*
            Transient preservation.

            If a very fast peak appears above the
            slower envelope, reduce downward
            correction so the attack survives.
        */
        const float transientLevel =
            transientFollower.process(absolute);

        const float transientRatio =
            transientLevel /
            std::max(combined, 0.000001f);

        if (transientRatio > 1.15f)
        {
            const float excess =
                juce::jlimit(
                    0.0f,
                    1.0f,
                    (transientRatio - 1.15f) / 1.5f);

            if (correctionDb < 0.0f)
            {
                correctionDb *=
                    1.0f -
                    excess * transient;
            }
        }

        const float desiredGain =
            juce::Decibels::decibelsToGain(
                correctionDb);

        /*
            Smooth the gain itself.

            This is important:
            we don't want zippering or pumping.
        */
        const float gainSpeed =
            0.0002f +
            speedControl * 0.0020f;

        currentGain +=
            (desiredGain - currentGain)
            * gainSpeed;

        currentGain =
            juce::jlimit(0.25f,
                         4.0f,
                         currentGain);

        const float wet =
            input * currentGain;

        return input +
               (wet - input) * wetDry;
    }

private:
    double sr = 44100.0;
    int numChannels = 2;

    EnvelopeFollower fet;
    EnvelopeFollower opto;
    EnvelopeFollower vca;

    EnvelopeFollower targetFollower;
    EnvelopeFollower transientFollower;

    float levelingAmount = 0.5f;

    float fetWeight = 0.33f;
    float optoWeight = 0.34f;
    float vcaWeight = 0.33f;

    float transient = 0.75f;
    float speedControl = 0.5f;
    float wetDry = 1.0f;

    float currentGain = 1.0f;
};
4. Source/PluginProcessor.h
#pragma once

#include <JuceHeader.h>
#include "LevelerDSP.h"

class SymbioteLevelerAudioProcessor
    : public juce::AudioProcessor
{
public:

    SymbioteLevelerAudioProcessor();

    ~SymbioteLevelerAudioProcessor() override = default;

    void prepareToPlay(double sampleRate,
                       int samplesPerBlock) override;

    void releaseResources() override;

    bool isBusesLayoutSupported(
        const BusesLayout& layouts) const override;

    void processBlock(
        juce::AudioBuffer<float>&,
        juce::MidiBuffer&) override;

    juce::AudioProcessorEditor*
    createEditor() override;

    bool hasEditor() const override
    {
        return true;
    }

    const juce::String getName() const override
    {
        return "Symbiote Leveler";
    }

    bool acceptsMidi() const override
    {
        return false;
    }

    bool producesMidi() const override
    {
        return false;
    }

    bool isMidiEffect() const override
    {
        return false;
    }

    double getTailLengthSeconds() const override
    {
        return 0.0;
    }

    int getNumPrograms() override
    {
        return 1;
    }

    int getCurrentProgram() override
    {
        return 0;
    }

    void setCurrentProgram(int) override {}

    const juce::String getProgramName(int) override
    {
        return {};
    }

    void changeProgramName(int,
                           const juce::String&) override {}

    void getStateInformation(
        juce::MemoryBlock&) override;

    void setStateInformation(
        const void* data,
        int sizeInBytes) override;

    juce::AudioProcessorValueTreeState parameters;

private:

    SymbioteLeveler leveler;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(
        SymbioteLevelerAudioProcessor)
};
5. Source/PluginProcessor.cpp
#include "PluginProcessor.h"
#include "PluginEditor.h"

SymbioteLevelerAudioProcessor::
SymbioteLevelerAudioProcessor()

    : AudioProcessor(
        BusesProperties()
            .withInput(
                "Input",
                juce::AudioChannelSet::stereo(),
                true)
            .withOutput(
                "Output",
                juce::AudioChannelSet::stereo(),
                true)
      ),

      parameters(
          *this,
          nullptr,
          "PARAMETERS",
          {
              std::make_unique<
                  juce::AudioParameterFloat>(
                      "amount",
                      "Leveling",
                      0.0f,
                      1.0f,
                      0.50f),

              std::make_unique<
                  juce::AudioParameterFloat>(
                      "fet",
                      "FET",
                      0.0f,
                      1.0f,
                      0.33f),

              std::make_unique<
                  juce::AudioParameterFloat>(
                      "opto",
                      "Opto",
                      0.0f,
                      1.0f,
                      0.34f),

              std::make_unique<
                  juce::AudioParameterFloat>(
                      "vca",
                      "VCA",
                      0.0f,
                      1.0f,
                      0.33f),

              std::make_unique<
                  juce::AudioParameterFloat>(
                      "transient",
                      "Transient",
                      0.0f,
                      1.0f,
                      0.75f),

              std::make_unique<
                  juce::AudioParameterFloat>(
                      "speed",
                      "Speed",
                      0.0f,
                      1.0f,
                      0.50f),

              std::make_unique<
                  juce::AudioParameterFloat>(
                      "mix",
                      "Mix",
                      0.0f,
                      1.0f,
                      1.0f)
          })
{
}

void SymbioteLevelerAudioProcessor::
prepareToPlay(double sampleRate,
              int samplesPerBlock)
{
    juce::ignoreUnused(samplesPerBlock);

    leveler.prepare(
        sampleRate,
        getTotalNumOutputChannels());
}

void SymbioteLevelerAudioProcessor::
releaseResources()
{
    leveler.reset();
}

bool SymbioteLevelerAudioProcessor::
isBusesLayoutSupported(
    const BusesLayout& layouts) const
{
    const auto mainIn =
        layouts.getMainInputChannelSet();

    const auto mainOut =
        layouts.getMainOutputChannelSet();

    if (mainOut != mainIn)
        return false;

    return mainOut == juce::AudioChannelSet::mono()
        || mainOut == juce::AudioChannelSet::stereo();
}

void SymbioteLevelerAudioProcessor::
processBlock(
    juce::AudioBuffer<float>& buffer,
    juce::MidiBuffer& midiMessages)
{
    juce::ignoreUnused(midiMessages);

    juce::ScopedNoDenormals noDenormals;

    const float amount =
        parameters.getRawParameterValue(
            "amount")->load();

    const float fet =
        parameters.getRawParameterValue(
            "fet")->load();

    const float opto =
        parameters.getRawParameterValue(
            "opto")->load();

    const float vca =
        parameters.getRawParameterValue(
            "vca")->load();

    const float transient =
        parameters.getRawParameterValue(
            "transient")->load();

    const float speed =
        parameters.getRawParameterValue(
            "speed")->load();

    const float mix =
        parameters.getRawParameterValue(
            "mix")->load();

    leveler.setParameters(
        amount,
        fet,
        opto,
        vca,
        transient,
        speed,
        mix);

    const int numChannels =
        buffer.getNumChannels();

    const int numSamples =
        buffer.getNumSamples();

    for (int sample = 0;
         sample < numSamples;
         ++sample)
    {
        /*
            Stereo-linked detector.

            Both channels receive the same gain,
            preventing stereo image movement.
        */
        float linked = 0.0f;

        for (int channel = 0;
             channel < numChannels;
             ++channel)
        {
            linked =
                std::max(
                    linked,
                    std::abs(
                        buffer.getSample(
                            channel,
                            sample)));
        }

        const float processed =
            leveler.processSample(linked);

        const float gain =
            linked > 0.000001f
                ? processed / linked
                : 1.0f;

        for (int channel = 0;
             channel < numChannels;
             ++channel)
        {
            buffer.setSample(
                channel,
                sample,
                buffer.getSample(
                    channel,
                    sample) * gain);
        }
    }
}

juce::AudioProcessorEditor*
SymbioteLevelerAudioProcessor::
createEditor()
{
    return new SymbioteLevelerAudioProcessorEditor(
        *this);
}

void SymbioteLevelerAudioProcessor::
getStateInformation(
    juce::MemoryBlock& destData)
{
    auto state =
        parameters.copyState();

    std::unique_ptr<
        juce::XmlElement> xml(
            state.createXml());

    copyXmlToBinary(
        *xml,
        destData);
}

void SymbioteLevelerAudioProcessor::
setStateInformation(
    const void* data,
    int sizeInBytes)
{
    std::unique_ptr<
        juce::XmlElement> xml(
            getXmlFromBinary(
                data,
                sizeInBytes));

    if (xml != nullptr &&
        xml->hasTagName(
            parameters.state.getType()))
    {
        parameters.replaceState(
            juce::ValueTree::fromXml(
                *xml));
    }
}

juce::AudioProcessor*
JUCE_CALLTYPE
createPluginFilter()
{
    return new SymbioteLevelerAudioProcessor();
}
6. Source/PluginEditor.h

Для первой рабочей версии используем встроенный JUCE editor параметров. Это позволит сразу собрать и протестировать DSP, а красивый интерфейс сделаем следующим этапом.

#pragma once

#include <JuceHeader.h>
#include "PluginProcessor.h"

class SymbioteLevelerAudioProcessorEditor
    : public juce::AudioProcessorEditor
{
public:

    explicit SymbioteLevelerAudioProcessorEditor(
        SymbioteLevelerAudioProcessor&);

    ~SymbioteLevelerAudioProcessorEditor()
        override = default;

    void paint(
        juce::Graphics&) override;

    void resized() override;

private:

    SymbioteLevelerAudioProcessor& processor;

    juce::Slider amount;
    juce::Slider fet;
    juce::Slider opto;
    juce::Slider vca;
    juce::Slider transient;
    juce::Slider speed;
    juce::Slider mix;

    juce::Label title;

    std::unique_ptr<
        juce::AudioProcessorValueTreeState::
        SliderAttachment> amountAttachment;

    std::unique_ptr<
        juce::AudioProcessorValueTreeState::
        SliderAttachment> fetAttachment;

    std::unique_ptr<
        juce::AudioProcessorValueTreeState::
        SliderAttachment> optoAttachment;

    std::unique_ptr<
        juce::AudioProcessorValueTreeState::
        SliderAttachment> vcaAttachment;

    std::unique_ptr<
        juce::AudioProcessorValueTreeState::
        SliderAttachment> transientAttachment;

    std::unique_ptr<
        juce::AudioProcessorValueTreeState::
        SliderAttachment> speedAttachment;

    std::unique_ptr<
        juce::AudioProcessorValueTreeState::
        SliderAttachment> mixAttachment;

    void setupSlider(
        juce::Slider& slider,
        const juce::String& name);

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(
        SymbioteLevelerAudioProcessorEditor)
};
7. Source/PluginEditor.cpp
#include "PluginEditor.h"

SymbioteLevelerAudioProcessorEditor::
SymbioteLevelerAudioProcessorEditor(
    SymbioteLevelerAudioProcessor& p)

    : AudioProcessorEditor(&p),
      processor(p)
{
    setSize(700, 420);

    title.setText(
        "SYMBIOTE LEVELER",
        juce::dontSendNotification);

    title.setFont(
        juce::Font(
            juce::FontOptions()
                .withHeight(28.0f)
                .withStyle("Bold")));

    title.setColour(
        juce::Label::textColourId,
        juce::Colours::white);

    addAndMakeVisible(title);

    setupSlider(amount, "LEVELING");
    setupSlider(fet, "FET");
    setupSlider(opto, "OPTO");
    setupSlider(vca, "VCA");
    setupSlider(transient, "TRANSIENT");
    setupSlider(speed, "SPEED");
    setupSlider(mix, "MIX");

    amountAttachment =
        std::make_unique<
            juce::AudioProcessorValueTreeState::
            SliderAttachment>(
                processor.parameters,
                "amount",
                amount);

    fetAttachment =
        std::make_unique<
            juce::AudioProcessorValueTreeState::
            SliderAttachment>(
                processor.parameters,
                "fet",
                fet);

    optoAttachment =
        std::make_unique<
            juce::AudioProcessorValueTreeState::
            SliderAttachment>(
                processor.parameters,
                "opto",
                opto);

    vcaAttachment =
        std::make_unique<
            juce::AudioProcessorValueTreeState::
            SliderAttachment>(
                processor.parameters,
                "vca",
                vca);

    transientAttachment =
        std::make_unique<
            juce::AudioProcessorValueTreeState::
            SliderAttachment>(
                processor.parameters,
                "transient",
                transient);

    speedAttachment =
        std::make_unique<
            juce::AudioProcessorValueTreeState::
            SliderAttachment>(
                processor.parameters,
                "speed",
                speed);

    mixAttachment =
        std::make_unique<
            juce::AudioProcessorValueTreeState::
            SliderAttachment>(
                processor.parameters,
                "mix",
                mix);
}

void SymbioteLevelerAudioProcessorEditor::
setupSlider(
    juce::Slider& slider,
    const juce::String& name)
{
    slider.setSliderStyle(
        juce::Slider::RotaryHorizontalVerticalDrag);

    slider.setTextBoxStyle(
        juce::Slider::TextBoxBelow,
        false,
        90,
        20);

    slider.setRange(
        0.0,
        1.0,
        0.001);

    slider.setDoubleClickReturnValue(
        true,
        0.5);

    slider.setName(name);

    addAndMakeVisible(slider);
}

void SymbioteLevelerAudioProcessorEditor::
paint(juce::Graphics& g)
{
    g.fillAll(
        juce::Colour(0xff111318));

    g.setColour(
        juce::Colour(0xff20242c));

    g.fillRoundedRectangle(
        getLocalBounds()
            .toFloat()
            .reduced(12.0f),
        12.0f);

    g.setColour(
        juce::Colour(0xff6ee7b7));

    g.fillRoundedRectangle(
        25.0f,
        66.0f,
        650.0f,
        3.0f,
        1.5f);

    g.setColour(
        juce::Colours::white);

    g.setFont(
        juce::Font(
            juce::FontOptions()
                .withHeight(14.0f)));

    g.drawText(
        "FET     fast peaks      •      OPTO     smooth body      •      VCA     level control",
        25,
        335,
        650,
        25,
        juce::Justification::centred);
}

void SymbioteLevelerAudioProcessorEditor::
resized()
{
    title.setBounds(
        25,
        20,
        650,
        40);

    const int y = 90;
    const int w = 90;
    const int h = 190;

    amount.setBounds(25,  y, w, h);
    fet.setBounds(120, y, w, h);
    opto.setBounds(215, y, w, h);
    vca.setBounds(310, y, w, h);
    transient.setBounds(405, y, w, h);
    speed.setBounds(500, y, w, h);
    mix.setBounds(595, y, w, h);
}
8. Установка и сборка

Установи Visual Studio с Desktop development with C++, MSVC и Windows SDK.

Также нужен CMake. Официальная Windows x64-версия доступна на странице CMake.

Затем открой x64 Native Tools Command Prompt for VS.

Перейди в папку проекта:

cd C:\Projects\SymbioteLeveler

И выполни:

cmake -S . -B Build -G "Visual Studio 18 2026" -A x64

Потом:

cmake --build Build --config Release

Если у тебя Visual Studio 2022, используй:

cmake -S . -B Build -G "Visual Studio 17 2022" -A x64

и:

cmake --build Build --config Release

CMake официально перечисляет Visual Studio 17 2022 и Visual Studio 18 2026 среди генераторов.

9. Где будет VST3

После успешной сборки ищи:

Build
└── SymbioteLeveler_artefacts
    └── Release
        └── VST3
            └── Symbiote Leveler.vst3

Скопируй весь:

Symbiote Leveler.vst3

в:

C:\Program Files\Common Files\VST3\

Это стандартное расположение VST3 в Windows.

После этого в DAW делаем rescan plugins.

Что получится сейчас

Это уже не макет, а рабочий DSP:

FET

→ быстрый detector для пиков.

Opto

→ медленное музыкальное выравнивание.

VCA

→ среднескоростной контроль уровня.

Adaptive Target

→ плагин сравнивает текущий уровень с долгосрочным уровнем программы.

Transient Preserve

→ быстрые атаки получают меньше отрицательной коррекции.

Stereo Link

→ левый и правый каналы используют общий gain, поэтому стереообраз не должен «дышать» отдельно по каналам.

И главное:

громкий участок ──► немного вниз
средний участок ──► почти без изменений
тихий участок ────► немного вверх
транзиент ─────────► сохраняется

То есть это именно leveling, а не обычный downward compressor.

Следующая версия будет значительно интереснее

После того как этот вариант соберётся, я бы сделал v0.2 уже как настоящий коммерческий интерфейс:

╔══════════════════════════════════════════╗
║          S Y M B I O T E                 ║
║             LEVELER                      ║
║                                          ║
║        ┌────── LEVEL ──────┐             ║
║        │       47%         │             ║
║        └───────────────────┘             ║
║                                          ║
║    FET       OPTO        VCA             ║
║    ●──────────●──────────●               ║
║                                          ║
║       TRANSIENT PRESERVE                 ║
║              ●                           ║
║                                          ║
║    INPUT    ─────────────                ║
║    LEVEL    ─────────────                ║
║    OUTPUT   ─────────────                ║
║                                          ║
║       [ AUTO ]      MIX  ●────           ║
╚══════════════════════════════════════════╝

И туда уже стоит добавить real-time график динамики, где будет видно BEFORE → TARGET → AFTER, а также Auto FET/Opto/VCA, oversampling и более качественный loudness detector.

Важно: нынешний v0.1 — именно первая DSP-прототипная версия. Для релизного плагина я бы ещё обязательно прогнал её через тесты на вокале, басе, барабанах, мастере и проверил поведение на тишине/транзиентах, прежде чем считать алгоритм законченным.