├── .gitattributes ├── .gitignore ├── .gitmodules ├── LICENSE ├── Source ├── APCommon.cpp ├── APCommon.h ├── CircularBuffer.h ├── Configuration.cpp ├── Constants.h ├── DSP.cpp ├── KnobLooks.cpp ├── KnobLooks.h ├── Parameters.cpp ├── PluginEditor.cpp ├── PluginEditor.h ├── PluginProcessor.cpp ├── PluginProcessor.h └── media │ ├── JetBrainsMono-Light.otf │ ├── Knockout-Flyweight.otf │ ├── bg.png │ ├── knob.png │ ├── knob2.png │ ├── knobArrow.png │ └── knobPointer.png └── Versatile Compressor.jucer /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | Source/.DS_Store 3 | Source/media/.DS_Store 4 | .DS_Store 5 | .DS_Store 6 | .DS_Store 7 | /JuceLibraryCode 8 | /Builds/MacOSX 9 | /Builds 10 | Versatile Compressor Pro.jucer 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "JUCE"] 2 | path = JUCE 3 | url = https://github.com/juce-framework/JUCE.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Alain Paul 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission (including but not limited to the use of the AP Mastering brand name). 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /Source/APCommon.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "APCommon.h" 7 | 8 | 9 | double linearToExponential(double linearValue, double minValue, double maxValue) 10 | { 11 | linearValue = std::clamp(linearValue, minValue, maxValue); 12 | double normalized = (linearValue - minValue) / (maxValue - minValue); 13 | double exponentialValue = std::pow(normalized, 2.0); 14 | double result = minValue + exponentialValue * (maxValue - minValue); 15 | return result; 16 | } 17 | 18 | double gainToDecibels(double gain) { 19 | if (gain <= 0.0) 20 | return -1000; 21 | 22 | if (gain > 1000) gain = 1000; 23 | 24 | return 20.0 * std::log10(gain); 25 | } 26 | 27 | double decibelsToGain(double decibels) { 28 | if (decibels <= -1000) 29 | return 0.0; 30 | 31 | if (decibels > 1000) decibels = 1000; 32 | 33 | return std::pow(10.0, decibels / 20.0); 34 | } 35 | 36 | 37 | std::string floatToStringWithTwoDecimalPlaces(float value) { 38 | std::stringstream stream; 39 | stream << std::fixed << std::setprecision(2) << value; 40 | return stream.str(); 41 | } 42 | 43 | 44 | ParameterQuery queryParameter(ParameterNames paramName, const std::string& parameterStringName) { 45 | 46 | static const std::unordered_map paramNameMap = { 47 | {ParameterNames::inGain, { "inGain", "Input Gain", ParameterNames::inGain }}, 48 | {ParameterNames::outGain, { "outGain", "Output Gain", ParameterNames::outGain }}, 49 | {ParameterNames::convexity, { "convexity", "Convexity", ParameterNames::convexity }}, 50 | {ParameterNames::attack, { "attack", "Attack", ParameterNames::attack }}, 51 | {ParameterNames::release, { "release", "Release", ParameterNames::release }}, 52 | {ParameterNames::threshold, { "threshold", "Threshold", ParameterNames::threshold }}, 53 | {ParameterNames::ratio, { "ratio", "Ratio", ParameterNames::ratio }}, 54 | {ParameterNames::channelLink, { "channelLink", "Channel Link", ParameterNames::channelLink }}, 55 | {ParameterNames::feedback, { "feedback", "Feedback", ParameterNames::feedback }}, 56 | {ParameterNames::inertia, { "inertia", "Inertia", ParameterNames::inertia }}, 57 | {ParameterNames::inertiaDecay, { "inertiaDecay", "Inertia Decay",ParameterNames::inertiaDecay }}, 58 | {ParameterNames::sidechain, { "sidechain", "Sidechain", ParameterNames::sidechain }}, 59 | {ParameterNames::oversampling, { "oversampling", "Oversampling", ParameterNames::oversampling }}, 60 | {ParameterNames::ceiling, { "ceiling", "Ceiling", ParameterNames::ceiling }}, 61 | {ParameterNames::variMu, { "variMu", "Vari Mu", ParameterNames::variMu }}, 62 | {ParameterNames::fold, { "fold", "Fold", ParameterNames::fold }} 63 | }; 64 | 65 | if (paramName != ParameterNames::END) { 66 | auto it = paramNameMap.find(paramName); 67 | if (it != paramNameMap.end()) return it->second; 68 | } 69 | 70 | static const std::unordered_map nameToEnumMap = { 71 | {"inGain", ParameterNames::inGain}, 72 | {"outGain", ParameterNames::outGain}, 73 | {"convexity", ParameterNames::convexity}, 74 | {"attack", ParameterNames::attack}, 75 | {"release", ParameterNames::release}, 76 | {"threshold", ParameterNames::threshold}, 77 | {"ratio", ParameterNames::ratio}, 78 | {"channelLink", ParameterNames::channelLink}, 79 | {"feedback", ParameterNames::feedback}, 80 | {"inertia", ParameterNames::inertia}, 81 | {"inertiaDecay", ParameterNames::inertiaDecay}, 82 | {"ceiling", ParameterNames::ceiling}, 83 | {"sidechain", ParameterNames::sidechain}, 84 | {"oversampling", ParameterNames::oversampling}, 85 | {"variMu", ParameterNames::variMu}, 86 | {"fold", ParameterNames::fold} 87 | }; 88 | 89 | auto strIt = nameToEnumMap.find(parameterStringName); 90 | if (strIt != nameToEnumMap.end()) return queryParameter(strIt->second); 91 | 92 | throw std::invalid_argument("Both enum and string queries failed for parameter for queryParameter"); 93 | } 94 | -------------------------------------------------------------------------------- /Source/APCommon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define DEBUG_MODE 0 8 | 9 | 10 | enum class OversamplingOption { 11 | None = 0, 12 | FIR_1x = 1, 13 | IIR_1x = 2, 14 | FIR_2x = 3, 15 | IIR_2x = 4 16 | }; 17 | 18 | 19 | enum class ButtonName { 20 | oversamplingON, 21 | oversamplingOFF, 22 | sidechainInternal, 23 | sidechainExternal, 24 | logo, 25 | meters, 26 | variMu, 27 | none 28 | }; 29 | 30 | 31 | enum class ParameterNames { 32 | inGain = 0, 33 | outGain = 1, 34 | convexity = 2, 35 | attack = 3, 36 | release = 4, 37 | threshold = 5, 38 | ratio = 6, 39 | channelLink = 7, 40 | feedback = 8, 41 | inertia = 9, 42 | inertiaDecay = 10, 43 | sidechain = 11, 44 | oversampling = 12, 45 | ceiling = 13, 46 | variMu = 14, 47 | fold = 15, 48 | END = 16 49 | }; 50 | 51 | 52 | struct ParameterQuery { 53 | std::string id; 54 | std::string label; 55 | ParameterNames parameterEnum; 56 | }; 57 | 58 | 59 | struct TextScreen { 60 | bool isBool; 61 | float value; 62 | bool displayDefaultText; 63 | int timeout; 64 | int defaultTimeout; 65 | std::string parameterName; 66 | std::string suffix; 67 | std::string defaultText; 68 | 69 | TextScreen() 70 | : isBool(false), 71 | value(0.0f), 72 | displayDefaultText(true), 73 | timeout(0), 74 | defaultTimeout(100), 75 | parameterName(""), 76 | suffix(""), 77 | defaultText("Versatile Compressor") {} 78 | }; 79 | 80 | 81 | double linearToExponential(double linearValue, double minValue, double maxValue); 82 | double gainToDecibels(double gain); 83 | double decibelsToGain(double decibels); 84 | std::string floatToStringWithTwoDecimalPlaces(float value); 85 | ParameterQuery queryParameter(ParameterNames paramName, const std::string& parameterStringName = ""); 86 | 87 | class APFont { 88 | public: 89 | static juce::Font getFont() { 90 | 91 | static juce::Font customTypeface = createFont(); 92 | return customTypeface; 93 | } 94 | 95 | static juce::Font getMonoFont() { 96 | 97 | static juce::Font customTypeface = createMonoFont(); 98 | return customTypeface; 99 | } 100 | 101 | private: 102 | static juce::Font createFont() { 103 | 104 | auto typeface = juce::Typeface::createSystemTypefaceFor( 105 | BinaryData::KnockoutFlyweight_otf, BinaryData::KnockoutFlyweight_otfSize); 106 | 107 | if (typeface != nullptr) { 108 | 109 | return juce::Font(juce::FontOptions(typeface)); 110 | } 111 | 112 | return juce::Font(juce::FontOptions(14.0f)); 113 | } 114 | 115 | static juce::Font createMonoFont() { 116 | 117 | auto typeface = juce::Typeface::createSystemTypefaceFor( 118 | BinaryData::JetBrainsMonoLight_otf, BinaryData::JetBrainsMonoLight_otfSize); 119 | 120 | if (typeface != nullptr) { 121 | 122 | return juce::Font(juce::FontOptions(typeface)); 123 | } 124 | 125 | return juce::Font(juce::FontOptions(14.0f)); 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /Source/CircularBuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | class CircularBuffer { 8 | public: 9 | CircularBuffer(size_t size) : size(size), buffer(size), head(0), tail(0), isFull(false) {} 10 | 11 | void add(double value) { 12 | 13 | std::lock_guard lock(mutex); 14 | 15 | buffer[head] = value; 16 | head = (head + 1) % size; 17 | 18 | if (isFull) { 19 | tail = (tail + 1) % size; 20 | } 21 | 22 | isFull = head == tail; 23 | } 24 | 25 | double average() const { 26 | 27 | std::lock_guard lock(mutex); 28 | 29 | if (head == tail && !isFull) { 30 | return 0.0; 31 | } 32 | 33 | double sum = 0.0; 34 | size_t count = 0; 35 | 36 | size_t index = tail; 37 | while (true) { 38 | sum += buffer[index]; 39 | count++; 40 | if (index == head) break; 41 | index = (index + 1) % size; 42 | } 43 | 44 | return sum / count; 45 | } 46 | 47 | private: 48 | size_t size; 49 | std::vector buffer; 50 | size_t head; 51 | size_t tail; 52 | bool isFull; 53 | mutable std::mutex mutex; 54 | }; 55 | -------------------------------------------------------------------------------- /Source/Configuration.cpp: -------------------------------------------------------------------------------- 1 | #include "APCommon.h" 2 | #include "PluginProcessor.h" 3 | #include "PluginEditor.h" 4 | 5 | 6 | juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new APComp(); } 7 | const juce::String APComp::getName() const { return JucePlugin_Name; } 8 | bool APComp::acceptsMidi() const { return false; } 9 | bool APComp::producesMidi() const { return false; } 10 | bool APComp::isMidiEffect() const { return false; } 11 | double APComp::getTailLengthSeconds() const { return 0.0; } 12 | int APComp::getNumPrograms() { return 1; } 13 | int APComp::getCurrentProgram() { return 0; } 14 | void APComp::setCurrentProgram (int index) { } 15 | const juce::String APComp::getProgramName (int index) { return {}; } 16 | void APComp::changeProgramName (int index, const juce::String& newName) {} 17 | bool APComp::hasEditor() const { return true; } 18 | void APComp::releaseResources() {} 19 | bool APComp::isBusesLayoutSupported (const BusesLayout& layouts) const { return true; } 20 | 21 | juce::AudioProcessorEditor* APComp::createEditor() { return new GUI (*this); } 22 | 23 | void APComp::getStateInformation (juce::MemoryBlock& destData) { 24 | 25 | std::unique_ptr xml (apvts.state.createXml()); 26 | copyXmlToBinary (*xml, destData); 27 | } 28 | 29 | void APComp::setStateInformation (const void* data, int sizeInBytes) { 30 | 31 | std::unique_ptr xml (getXmlFromBinary (data, sizeInBytes)); 32 | if (xml != nullptr) 33 | { 34 | if (xml->hasTagName (apvts.state.getType())) 35 | { 36 | apvts.state = juce::ValueTree::fromXml (*xml); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Source/Constants.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | namespace Constants { 5 | constexpr int attackMin = 0.0f; 6 | constexpr int attackMax = 300.0f; 7 | constexpr int releaseMin = 0.0f; 8 | constexpr int releaseMax = 3000.0f; 9 | constexpr int ratioMin = 1.0; 10 | constexpr int ratioMax = 20.0f; 11 | } 12 | -------------------------------------------------------------------------------- /Source/DSP.cpp: -------------------------------------------------------------------------------- 1 | #include "APCommon.h" 2 | #include "PluginProcessor.h" 3 | #include "PluginEditor.h" 4 | #include "Constants.h" 5 | 6 | 7 | void APComp::updateMeters(float *maxValuesForMeters) { 8 | 9 | for (int i = 0; i < meterCount; i++) { 10 | 11 | if (maxValuesForMeters[i] > 100) maxValuesForMeters[i] = 100; 12 | 13 | if (maxValuesForMeters[i] > meterValues[i]) { 14 | meterValues[i] = maxValuesForMeters[i]; 15 | } else { 16 | meterValues[i] = meterValues[i] * meterDecayCoefficient; 17 | } 18 | 19 | meterValuesAtomic[i].store(meterValues[i], std::memory_order_relaxed); 20 | } 21 | } 22 | 23 | void APComp::flushLoopVariables() { 24 | 25 | if (flushDSP.load(std::memory_order_relaxed)) { 26 | for (int channel = 0; channel < 2; channel++) { 27 | slewedSignal[channel] = -200.0; 28 | gainReduction[channel] = 0.0f; 29 | outputSample[channel] = 0.0f; 30 | previousGainReduction[channel] = -200.0; 31 | inertiaVelocity[channel] = 0.0f; 32 | } 33 | 34 | flushDSP.store(false); 35 | } 36 | } 37 | 38 | void APComp::doCompressionDSP(juce::dsp::AudioBlock& mainBlock, 39 | juce::dsp::AudioBlock& sidechainBlock, 40 | size_t oversamplingFactor, 41 | int sampleRate) { 42 | 43 | flushLoopVariables(); 44 | 45 | const float attackValue = linearToExponential(getFloatKnobValue(ParameterNames::attack), 46 | Constants::attackMin, 47 | Constants::attackMax) / 1000; 48 | const float releaseValue = linearToExponential(getFloatKnobValue(ParameterNames::release), 49 | Constants::releaseMin, 50 | Constants::releaseMax) / 1000; 51 | const float ratioValue = linearToExponential(getFloatKnobValue(ParameterNames::ratio), 52 | Constants::ratioMin, 53 | Constants::ratioMax); 54 | 55 | const float inputGainValue = getFloatKnobValue(ParameterNames::inGain); 56 | const float outGainValue = getFloatKnobValue(ParameterNames::outGain); 57 | const float convexityValue = getFloatKnobValue(ParameterNames::convexity); 58 | const float thresholdValue = getFloatKnobValue(ParameterNames::threshold); 59 | const float channelLinkValue = getFloatKnobValue(ParameterNames::channelLink) / 100; 60 | const float feedbackValue = getFloatKnobValue(ParameterNames::feedback); 61 | const float inertiaCoefficientValue = getFloatKnobValue(ParameterNames::inertia); 62 | const float ceiling = getFloatKnobValue(ParameterNames::ceiling); 63 | 64 | float inertiaDecayCoefficient = getFloatKnobValue(ParameterNames::inertiaDecay); 65 | bool sidechainSelected = getBoolKnobValue(ParameterNames::sidechain); 66 | 67 | if (ratioValue == 0) return; 68 | 69 | const double attackCoefficient = std::exp(-1.0 / (sampleRate * attackValue)); 70 | const double releaseCoefficient = std::exp(-1.0 / (sampleRate * releaseValue)); 71 | 72 | float maxValuesForMeters[meterCount] = {0}; 73 | 74 | float* channelData[2]; 75 | float* sidechainChannelData[2]; 76 | 77 | for (int i = 0; i < totalNumInputChannels && i < 2; i++) channelData[i] = mainBlock.getChannelPointer(i); 78 | for (int i = 0; i < totalNumInputChannels - 2 && i < 2; i++) sidechainChannelData[i] = sidechainBlock.getChannelPointer(i); 79 | 80 | inertiaDecayCoefficient = 0.99 + (inertiaDecayCoefficient * 0.01); 81 | 82 | for (int sample = 0; sample < mainBlock.getNumSamples(); ++sample) { 83 | 84 | float inputSample[4]; 85 | double inputSampledb[2]; 86 | inputSampledb[0] = -200.0f; 87 | inputSampledb[1] = -200.0f; 88 | 89 | for (int channel = 0; channel < 2 && channel < totalNumInputChannels; channel++) { 90 | inputSample[channel] = channelData[channel][sample]; 91 | inputSample[channel] *= decibelsToGain(inputGainValue); 92 | inputSampledb[channel] = gainToDecibels(std::abs(inputSample[channel]) + std::abs(outputSample[channel] * feedbackValue)); 93 | outputSample[channel] = 0; 94 | 95 | if (std::isnan(inputSampledb[channel])) inputSampledb[channel] = -200.0f; 96 | 97 | if (sidechainSelected) { 98 | 99 | if (totalNumInputChannels <= channel + 2) inputSampledb[channel] = -200.0f; 100 | else inputSampledb[channel] = gainToDecibels(std::abs(sidechainChannelData 101 | [channel] 102 | [sample / static_cast(std::pow(2, oversamplingFactor))] 103 | )); 104 | } 105 | 106 | if (inputSampledb[channel] < -200) inputSampledb[channel] = -200; 107 | 108 | if (inputSampledb[channel] > 4) { 109 | inputSampledb[channel] = 4.0; // hardcoded 4db feedback path clip. Improve later 110 | feedbackClip.store(true, std::memory_order_relaxed); 111 | } else { 112 | feedbackClip.store(false, std::memory_order_relaxed); 113 | } 114 | 115 | if (inputSampledb[channel] > slewedSignal[channel]) 116 | slewedSignal[channel] = attackCoefficient * (slewedSignal[channel] - inputSampledb[channel]) + inputSampledb[channel]; 117 | else 118 | slewedSignal[channel] = releaseCoefficient * (slewedSignal[channel] - inputSampledb[channel]) + inputSampledb[channel]; 119 | 120 | if (slewedSignal[channel] > thresholdValue) { 121 | double targetLevel = thresholdValue + (slewedSignal[channel] - thresholdValue) / ratioValue; 122 | gainReduction[channel] = slewedSignal[channel] - targetLevel; 123 | gainReduction[channel] = std::pow(gainReduction[channel], convexityValue); 124 | } else { 125 | slewedSignal[channel] = thresholdValue; 126 | gainReduction[channel] = 0; 127 | } 128 | 129 | slewedSignal[channel] = std::clamp(slewedSignal[channel], -200.0, 1000.0); 130 | 131 | double gainReductionDecimal = decibelsToGain(gainReduction[channel]); 132 | 133 | if (inertiaCoefficientValue > 0) { 134 | if (gainReduction[channel] > previousGainReduction[channel]) inertiaVelocity[channel] += inertiaCoefficientValue * gainReductionDecimal * -0.001; 135 | } else { 136 | inertiaVelocity[channel] += inertiaCoefficientValue * gainReductionDecimal * -0.001; 137 | } 138 | 139 | inertiaVelocity[channel] *= inertiaDecayCoefficient; 140 | if (inertiaVelocity[channel] > 100) inertiaVelocity[channel] = 100; 141 | if (inertiaVelocity[channel] < -100) inertiaVelocity[channel] = -100; 142 | 143 | gainReductionDecimal += inertiaVelocity[channel]; 144 | if (gainReductionDecimal > 1000) gainReductionDecimal = 1000; 145 | if (gainReductionDecimal < -1000) gainReductionDecimal = -1000; 146 | 147 | gainReduction[channel] = gainToDecibels(gainReductionDecimal); 148 | previousGainReduction[channel] = gainReduction[channel]; 149 | } 150 | 151 | double maxGainReduction = gainReduction[0]; 152 | if (gainReduction[0] < gainReduction[1]) { 153 | maxGainReduction = gainReduction[1]; 154 | } 155 | 156 | for (int channel = 0; channel < 2 && channel < totalNumInputChannels; channel++) { 157 | 158 | gainReduction[channel] = (maxGainReduction * channelLinkValue) + (gainReduction[channel] * (channelLinkValue - 1) * -1); 159 | 160 | if (sidechainSelected) { 161 | inputSampledb[channel] = gainToDecibels(std::abs(inputSample[channel])); 162 | } 163 | 164 | outputSample[channel] = decibelsToGain(inputSampledb[channel] - gainReduction[channel]) * (inputSample[channel] < 0 ? -1.0f : 1.0f); 165 | 166 | outputSample[channel] /= ceiling; 167 | outputSample[channel] = std::tanh(outputSample[channel]); 168 | outputSample[channel] *= ceiling; 169 | 170 | outputSample[channel] = outputSample[channel] * decibelsToGain(outGainValue); 171 | 172 | if (std::isnan(outputSample[channel])) { 173 | outputSample[channel] = 0.0f; 174 | } 175 | 176 | channelData[channel][sample] = outputSample[channel]; 177 | 178 | if (std::abs(inputSample[channel]) > maxValuesForMeters[channel]) maxValuesForMeters[channel] = std::abs(inputSample[channel]); 179 | if (std::abs(outputSample[channel]) > maxValuesForMeters[channel+2]) maxValuesForMeters[channel+2] = std::abs(outputSample[channel]); 180 | if (gainReduction[channel] > maxValuesForMeters[channel+4]) maxValuesForMeters[channel+4] = gainReduction[channel]; 181 | } 182 | } 183 | 184 | updateMeters(maxValuesForMeters); 185 | } 186 | -------------------------------------------------------------------------------- /Source/KnobLooks.cpp: -------------------------------------------------------------------------------- 1 | #include "KnobLooks.h" 2 | 3 | 4 | KnobLook1::KnobLook1() { 5 | knobImage = juce::ImageFileFormat::loadFrom(BinaryData::knob_png, BinaryData::knob_pngSize); 6 | } 7 | 8 | 9 | KnobLook2::KnobLook2() { 10 | knobImage = juce::ImageFileFormat::loadFrom(BinaryData::knob2_png, BinaryData::knob2_pngSize); 11 | } 12 | 13 | 14 | void KnobLook1::drawRotarySlider(juce::Graphics& g, 15 | int x, int y, int width, int height, 16 | float sliderPosProportional, 17 | float rotaryStartAngle, 18 | float rotaryEndAngle, 19 | juce::Slider& slider) { 20 | 21 | drawKnob(g, knobImage, rotaryEndAngle, rotaryStartAngle, sliderPosProportional, width, x, y); 22 | } 23 | 24 | 25 | void KnobLook2::drawRotarySlider(juce::Graphics& g, 26 | int x, int y, int width, int height, 27 | float sliderPosProportional, 28 | float rotaryStartAngle, 29 | float rotaryEndAngle, 30 | juce::Slider& slider) { 31 | 32 | drawKnob(g, knobImage, rotaryEndAngle, rotaryStartAngle, sliderPosProportional, width, x, y); 33 | } 34 | 35 | 36 | void drawKnob(juce::Graphics &g, 37 | const juce::Image &knobImage, 38 | float rotaryEndAngle, 39 | float rotaryStartAngle, 40 | float sliderPosProportional, 41 | int width, 42 | int &x, 43 | int &y) { 44 | 45 | const float radius = width / 2; 46 | 47 | x += radius; 48 | y += radius; 49 | 50 | const float centreX = x + radius; 51 | const float centreY = y + radius; 52 | const float rx = x - radius; 53 | const float ry = y - radius; 54 | const float angle = rotaryStartAngle + sliderPosProportional * (rotaryEndAngle - rotaryStartAngle); 55 | const float scalingFactor = 0.5; 56 | 57 | if (knobImage.isValid()) { 58 | 59 | g.saveState(); 60 | g.addTransform(juce::AffineTransform::rotation(angle, centreX, centreY).scaled(scalingFactor)); 61 | g.drawImageTransformed(knobImage, juce::AffineTransform::translation(rx, ry), false); 62 | g.restoreState(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Source/KnobLooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "APCommon.h" 4 | 5 | 6 | class KnobLook1 : public juce::LookAndFeel_V4 { 7 | public: 8 | KnobLook1(); 9 | void drawRotarySlider (juce::Graphics& g, 10 | int x, 11 | int y, 12 | int width, 13 | int height, 14 | float sliderPosProportional, 15 | float rotaryStartAngle, 16 | float rotaryEndAngle, 17 | juce::Slider& slider) override; 18 | private: 19 | juce::Image knobImage; 20 | }; 21 | 22 | 23 | class KnobLook2 : public juce::LookAndFeel_V4 { 24 | public: 25 | KnobLook2(); 26 | void drawRotarySlider (juce::Graphics& g, 27 | int x, 28 | int y, 29 | int width, 30 | int height, 31 | float sliderPosProportional, 32 | float rotaryStartAngle, 33 | float rotaryEndAngle, 34 | juce::Slider& slider) override; 35 | private: 36 | juce::Image knobImage; 37 | }; 38 | 39 | 40 | void drawKnob(juce::Graphics &g, 41 | const juce::Image &knobImage, 42 | float rotaryEndAngle, 43 | float rotaryStartAngle, 44 | float sliderPosProportional, 45 | int width, 46 | int &x, 47 | int &y); 48 | -------------------------------------------------------------------------------- /Source/Parameters.cpp: -------------------------------------------------------------------------------- 1 | #include "APCommon.h" 2 | #include "PluginProcessor.h" 3 | #include "Constants.h" 4 | 5 | 6 | std::unique_ptr newFloatParam(ParameterNames paramName, 7 | float minValue, 8 | float maxValue, 9 | float defaultValue) { 10 | 11 | ParameterQuery parameterQuery = queryParameter(paramName); 12 | 13 | return std::make_unique(juce::ParameterID(parameterQuery.id, static_cast(paramName) + 1), 14 | parameterQuery.label, 15 | juce::NormalisableRange(minValue, maxValue), 16 | defaultValue); 17 | } 18 | 19 | 20 | std::unique_ptr newIntParam(ParameterNames paramName, 21 | int minValue, 22 | int maxValue, 23 | int defaultValue) { 24 | 25 | ParameterQuery parameterQuery = queryParameter(paramName); 26 | 27 | 28 | return std::make_unique(juce::ParameterID(parameterQuery.id, static_cast(paramName) + 1), 29 | parameterQuery.id, 30 | minValue, 31 | maxValue, 32 | defaultValue, 33 | parameterQuery.label); 34 | } 35 | 36 | 37 | juce::AudioProcessorValueTreeState::ParameterLayout APComp::createParameterLayout() { 38 | 39 | std::vector> params; 40 | 41 | params.push_back(newFloatParam(ParameterNames::attack, Constants::attackMin, Constants::attackMax, 90.0f )); 42 | params.push_back(newFloatParam(ParameterNames::release, Constants::releaseMin, Constants::releaseMax, 400.0f )); 43 | params.push_back(newFloatParam(ParameterNames::ratio, Constants::ratioMin, Constants::ratioMax, 20.0f )); 44 | 45 | params.push_back(newFloatParam(ParameterNames::inGain, -12.0f, 24.0f, 0.0f )); 46 | params.push_back(newFloatParam(ParameterNames::outGain, -12.0f, 24.0f, 0.0f )); 47 | params.push_back(newFloatParam(ParameterNames::convexity, -2.0f, 2.0f, 1.0f )); 48 | params.push_back(newFloatParam(ParameterNames::threshold, -60.0f, 6.0f, 0.0f )); 49 | params.push_back(newFloatParam(ParameterNames::channelLink, 0.0f, 100.0f, 100.0f )); 50 | params.push_back(newFloatParam(ParameterNames::feedback, 0.0f, 1.0f, 0.0f )); 51 | params.push_back(newFloatParam(ParameterNames::inertia, -1.0f, 0.3f, 0.0f )); 52 | params.push_back(newFloatParam(ParameterNames::inertiaDecay, 0.8f, 0.96f, 0.94f )); 53 | params.push_back(newFloatParam(ParameterNames::ceiling, 0.3f, 3.0f, 1.0f )); 54 | params.push_back(newFloatParam(ParameterNames::fold, 0.0f, 1.0f, 0.0f )); 55 | 56 | params.push_back(newIntParam(ParameterNames::variMu, 0, 1, 0 )); 57 | params.push_back(newIntParam(ParameterNames::sidechain, 0, 1, 0 )); 58 | params.push_back(newIntParam(ParameterNames::oversampling, 0, 1, 1 )); 59 | 60 | return { params.begin(), params.end() }; 61 | } 62 | -------------------------------------------------------------------------------- /Source/PluginEditor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "APCommon.h" 5 | #include "PluginProcessor.h" 6 | #include "PluginEditor.h" 7 | #include "Constants.h" 8 | 9 | 10 | GUI::GUI (APComp& p) 11 | : AudioProcessorEditor (&p), 12 | audioProcessor (p), 13 | knobLook1(), 14 | knobLook2(), 15 | backgroundImage (juce::ImageFileFormat::loadFrom(BinaryData::bg_png, BinaryData::bg_pngSize)), 16 | customTypeface (APFont::getMonoFont()), 17 | inGainSlider(), 18 | outGainSlider(), 19 | convexitySlider(), 20 | attackSlider(), 21 | releaseSlider(), 22 | thresholdSlider(), 23 | ratioSlider(), 24 | channelLinkSlider(), 25 | sidechainSlider(), 26 | variMuSlider(), 27 | foldSlider(), 28 | feedbackSlider(), 29 | inertiaSlider(), 30 | inertiaDecaySlider(), 31 | ceilingSlider(), 32 | inGainAttachment (std::make_unique(audioProcessor.apvts, "inGain", inGainSlider)), 33 | outGainAttachment (std::make_unique(audioProcessor.apvts, "outGain", outGainSlider)), 34 | convexityAttachment (std::make_unique(audioProcessor.apvts, "convexity", convexitySlider)), 35 | attackAttachment (std::make_unique(audioProcessor.apvts, "attack", attackSlider)), 36 | releaseAttachment (std::make_unique(audioProcessor.apvts, "release", releaseSlider)), 37 | thresholdAttachment (std::make_unique(audioProcessor.apvts, "threshold", thresholdSlider)), 38 | ratioAttachment (std::make_unique(audioProcessor.apvts, "ratio", ratioSlider)), 39 | channelLinkAttachment (std::make_unique(audioProcessor.apvts, "channelLink", channelLinkSlider)), 40 | sidechainAttachment (std::make_unique(audioProcessor.apvts, "sidechain", sidechainSlider)), 41 | variMuAttachment (std::make_unique(audioProcessor.apvts, "variMu", variMuSlider)), 42 | foldAttachment (std::make_unique(audioProcessor.apvts, "fold", foldSlider)), 43 | feedbackAttachment (std::make_unique(audioProcessor.apvts, "feedback", feedbackSlider)), 44 | inertiaAttachment (std::make_unique(audioProcessor.apvts, "inertia", inertiaSlider)), 45 | inertiaDecayAttachment (std::make_unique(audioProcessor.apvts, "inertiaDecay", inertiaDecaySlider)), 46 | ceilingAttachment (std::make_unique(audioProcessor.apvts, "ceiling", ceilingSlider)), 47 | oversamplingAttachment (std::make_unique(audioProcessor.apvts, "oversampling", oversamplingSlider)), 48 | metersActive(true), 49 | debugRefreshCountDown(0) { 50 | 51 | for (size_t i = 0; i < sliders.size(); ++i) { 52 | 53 | juce::Slider& slider = sliders[i].second.get(); 54 | const std::string& sliderName = sliders[i].first; 55 | 56 | slider.setSliderStyle(juce::Slider::RotaryVerticalDrag); 57 | slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); 58 | slider.setName(sliderName); 59 | slider.addListener(this); 60 | addAndMakeVisible(slider); 61 | } 62 | 63 | oversamplingSlider.setVisible(false); 64 | sidechainSlider.setVisible(false); 65 | variMuSlider.setVisible(false); 66 | foldSlider.setVisible(false); 67 | 68 | inGainSlider.setLookAndFeel(&knobLook2); 69 | outGainSlider.setLookAndFeel(&knobLook2); 70 | convexitySlider.setLookAndFeel(&knobLook1); 71 | attackSlider.setLookAndFeel(&knobLook1); 72 | releaseSlider.setLookAndFeel(&knobLook1); 73 | thresholdSlider.setLookAndFeel(&knobLook1); 74 | ratioSlider.setLookAndFeel(&knobLook1); 75 | channelLinkSlider.setLookAndFeel(&knobLook1); 76 | feedbackSlider.setLookAndFeel(&knobLook1); 77 | inertiaSlider.setLookAndFeel(&knobLook1); 78 | inertiaDecaySlider.setLookAndFeel(&knobLook1); 79 | ceilingSlider.setLookAndFeel(&knobLook1); 80 | 81 | setSize (680, 450); 82 | 83 | const int refreshRate = 33; 84 | startTimer(refreshRate); 85 | } 86 | 87 | 88 | GUI::~GUI() { 89 | 90 | stopTimer(); 91 | 92 | inGainSlider.setLookAndFeel(nullptr); 93 | outGainSlider.setLookAndFeel(nullptr); 94 | convexitySlider.setLookAndFeel(nullptr); 95 | attackSlider.setLookAndFeel(nullptr); 96 | releaseSlider.setLookAndFeel(nullptr); 97 | thresholdSlider.setLookAndFeel(nullptr); 98 | ratioSlider.setLookAndFeel(nullptr); 99 | channelLinkSlider.setLookAndFeel(nullptr); 100 | feedbackSlider.setLookAndFeel(nullptr); 101 | inertiaSlider.setLookAndFeel(nullptr); 102 | inertiaDecaySlider.setLookAndFeel(nullptr); 103 | ceilingSlider.setLookAndFeel(nullptr); 104 | } 105 | 106 | 107 | void GUI::paintMeters(juce::Graphics &g) { 108 | 109 | const float meterHeight = 237.0f; 110 | const float spacing = 7.6; 111 | 112 | g.setColour(juce::Colours::black); 113 | 114 | if (metersActive) { 115 | 116 | const float meterGrainRedutionMaxDB = 12.0f; 117 | 118 | float meterInputL = audioProcessor.meterValuesAtomic[0].load(std::memory_order_relaxed); 119 | float meterInputR = audioProcessor.meterValuesAtomic[1].load(std::memory_order_relaxed); 120 | float meterOutputL = audioProcessor.meterValuesAtomic[2].load(std::memory_order_relaxed); 121 | float meterOutputR = audioProcessor.meterValuesAtomic[3].load(std::memory_order_relaxed); 122 | float meterGainReductionL = audioProcessor.meterValuesAtomic[4].load(std::memory_order_relaxed); 123 | float meterGainReductionR = audioProcessor.meterValuesAtomic[5].load(std::memory_order_relaxed); 124 | 125 | meterInputL = std::clamp(meterInputL, 0.0f, 1.0f); 126 | meterInputR = std::clamp(meterInputR, 0.0f, 1.0f); 127 | meterOutputL = std::clamp(meterOutputL, 0.0f, 1.0f); 128 | meterOutputR = std::clamp(meterOutputR, 0.0f, 1.0f); 129 | meterGainReductionL = std::clamp(meterGainReductionL, 0.0f, meterGrainRedutionMaxDB); 130 | meterGainReductionR = std::clamp(meterGainReductionR, 0.0f, meterGrainRedutionMaxDB); 131 | 132 | float meterInputLAdjusted = meterHeight - (meterInputL * meterHeight); 133 | float meterInputRAdjusted = meterHeight - (meterInputR * meterHeight); 134 | float meterOutputLAdjusted = meterHeight - (meterOutputL * meterHeight); 135 | float meterOutputRAdjusted = meterHeight - (meterOutputR * meterHeight); 136 | float meterGainReductionLAdjusted = (meterGainReductionL / meterGrainRedutionMaxDB) * meterHeight; 137 | float meterGainReductionRAdjusted = (meterGainReductionR / meterGrainRedutionMaxDB) * meterHeight; 138 | 139 | g.fillRect(metersLeft + spacing * 0, metersTop, spacing, meterInputLAdjusted); 140 | g.fillRect(metersLeft + spacing * 1, metersTop, spacing, meterInputRAdjusted); 141 | 142 | g.fillRect(metersLeft + spacing * 2, 143 | metersTop + meterGainReductionLAdjusted, 144 | spacing, 145 | std::clamp(meterHeight, 0.0f, meterHeight - meterGainReductionLAdjusted)); 146 | g.fillRect(metersLeft + spacing * 3, 147 | metersTop + meterGainReductionRAdjusted, 148 | spacing, 149 | std::clamp(meterHeight, 0.0f, meterHeight - meterGainReductionRAdjusted)); 150 | 151 | g.fillRect(metersLeft + spacing * 4, metersTop, spacing, meterOutputLAdjusted); 152 | g.fillRect(metersLeft + spacing * 5, metersTop, spacing, meterOutputRAdjusted); 153 | 154 | } else { 155 | g.fillRect(metersLeft, metersTop, spacing * 6, meterHeight); 156 | } 157 | 158 | g.setColour(juce::Colours::white.withAlpha(0.7f)); 159 | 160 | const int clipLightRadius = 4; 161 | if (audioProcessor.feedbackClip.load(std::memory_order_relaxed)) g.fillEllipse(550 - clipLightRadius, 162 | 291 - clipLightRadius, 163 | clipLightRadius * 2, 164 | clipLightRadius * 2); 165 | } 166 | 167 | 168 | void GUI::paintBackground(juce::Graphics &g) { 169 | if (backgroundImage.isValid()) { 170 | g.drawImage(backgroundImage, getLocalBounds().toFloat()); 171 | } else { 172 | g.fillAll(juce::Colours::lightgrey); 173 | g.setColour (juce::Colours::black); 174 | g.setFont (24.0f); 175 | g.drawFittedText ("AP Mastering - Advanced Compressor: GUI error", getLocalBounds(), juce::Justification::centredTop, 1); 176 | } 177 | } 178 | 179 | 180 | void GUI::paintButtons(juce::Graphics &g) { 181 | 182 | const int oversamplingX = 411; 183 | const int sidechainX = 589; 184 | const int row1 = 28; 185 | const int row2 = 49; 186 | const int radius = 4; 187 | 188 | if (audioProcessor.getBoolKnobValue(ParameterNames::oversampling)) 189 | g.fillEllipse(oversamplingX - radius, row1 - radius, radius * 2, radius * 2); 190 | else 191 | g.fillEllipse(oversamplingX - radius, row2 - radius, radius * 2, radius * 2); 192 | 193 | if (audioProcessor.getBoolKnobValue(ParameterNames::sidechain)) 194 | g.fillEllipse(sidechainX - radius, row2 - radius, radius * 2, radius * 2); 195 | else 196 | g.fillEllipse(sidechainX - radius, row1 - radius, radius * 2, radius * 2); 197 | } 198 | 199 | 200 | void GUI::paintTextScreen(juce::Graphics &g, std::string &textScreenString) { 201 | 202 | customTypeface.setHeight(20.0f); 203 | g.setFont(customTypeface); 204 | g.setColour (juce::Colours::white.withAlpha(0.6f)); 205 | 206 | g.drawFittedText(textScreenString, 207 | textScreenL, 208 | textScreenT, 209 | textScreenR - textScreenL, 210 | textScreenB - textScreenT, 211 | juce::Justification::centred, 212 | 1); 213 | } 214 | 215 | 216 | std::string GUI::updateTextScreen() { 217 | 218 | std::lock_guard lock(textScreenMutex); 219 | 220 | #if DEBUG_MODE 221 | if (debugRefreshCountDown > 0) { 222 | debugRefreshCountDown--; 223 | } else { 224 | debugRefreshCountDown = 12; 225 | cachedDebugValue = audioProcessor.circularBuffer.average(); 226 | } 227 | std::stringstream stream; 228 | stream << std::fixed << std::setprecision(4) << cachedDebugValue << "ms"; 229 | return stream.str(); 230 | #endif 231 | 232 | if (textScreen.timeout > 0) textScreen.timeout--; 233 | 234 | if (textScreen.timeout < 1) textScreen.displayDefaultText = true; 235 | else textScreen.displayDefaultText = false; 236 | 237 | if (textScreen.displayDefaultText) return textScreen.defaultText; 238 | 239 | std::string textScreenString; 240 | 241 | if (textScreen.isBool) 242 | textScreenString = textScreen.parameterName + 243 | ": " + 244 | (textScreen.value > 0.5f ? "ON" : "OFF"); 245 | else 246 | textScreenString = textScreen.parameterName + 247 | ": " + 248 | floatToStringWithTwoDecimalPlaces(textScreen.value) + 249 | textScreen.suffix; 250 | return textScreenString; 251 | } 252 | 253 | 254 | void GUI::paint (juce::Graphics& g) { 255 | 256 | paintBackground(g); 257 | 258 | paintMeters(g); 259 | 260 | paintButtons(g); 261 | 262 | std::string textScreenString = updateTextScreen(); 263 | 264 | paintTextScreen(g, textScreenString); 265 | } 266 | 267 | 268 | void GUI::resized() { 269 | 270 | const int knobWidth = 54; 271 | const int knobHeight = knobWidth; 272 | const int radius = knobWidth / 2; 273 | 274 | const int IOKnobsX = 642; 275 | const int inputY = 310; 276 | const int outputY = 386; 277 | const int IOknobWidth = 42; 278 | const int IOknobHeight = IOknobWidth; 279 | const int IOradius = IOknobWidth / 2; 280 | 281 | convexitySlider.setBounds (knobColumn1 - radius, knobRow1 - radius, knobWidth, knobHeight); 282 | inertiaSlider.setBounds (knobColumn1 - radius, knobRow2 - radius, knobWidth, knobHeight); 283 | inertiaDecaySlider.setBounds (knobColumn1 - radius, knobRow3 - radius, knobWidth, knobHeight); 284 | 285 | ratioSlider.setBounds (knobColumn2 - radius, knobRow1 - radius, knobWidth, knobHeight); 286 | thresholdSlider.setBounds (knobColumn2 - radius, knobRow2 - radius, knobWidth, knobHeight); 287 | 288 | attackSlider.setBounds (knobColumn3 - radius, knobRow1 - radius, knobWidth, knobHeight); 289 | releaseSlider.setBounds (knobColumn3 - radius, knobRow2 - radius, knobWidth, knobHeight); 290 | 291 | channelLinkSlider.setBounds (knobColumn4 - radius, knobRow1 - radius, knobWidth, knobHeight); 292 | feedbackSlider.setBounds (knobColumn4 - radius, knobRow2 - radius, knobWidth, knobHeight); 293 | ceilingSlider.setBounds (knobColumn4 - radius, knobRow3 - radius, knobWidth, knobHeight); 294 | 295 | inGainSlider.setBounds (IOKnobsX - IOradius, inputY - IOradius, IOknobWidth, IOknobHeight); 296 | outGainSlider.setBounds (IOKnobsX - IOradius, outputY - IOradius, IOknobWidth, IOknobHeight); 297 | } 298 | 299 | 300 | ButtonName GUI::determineButton(const juce::MouseEvent &event) { 301 | 302 | const int row1 = 20; 303 | const int row2 = 40; 304 | const int row3 = 60; 305 | const int oversamplingLeft = 383; 306 | const int oversamplingRight = 423; 307 | const int sidechainLeft = 535; 308 | const int sidechainRight = 605; 309 | const int logoRight = 230; 310 | const int buttonRadius = 20; 311 | 312 | if (event.x > oversamplingLeft && 313 | event.x < oversamplingRight && 314 | event.y > row1 && 315 | event.y < row2) { 316 | 317 | return ButtonName::oversamplingON; 318 | } 319 | 320 | if (event.x > oversamplingLeft && 321 | event.x < oversamplingRight && 322 | event.y > row2 && 323 | event.y < row3) { 324 | 325 | return ButtonName::oversamplingOFF; 326 | } 327 | 328 | if (event.x > sidechainLeft && 329 | event.x < sidechainRight && 330 | event.y > row1 && 331 | event.y < row2) { 332 | 333 | return ButtonName::sidechainInternal; 334 | } 335 | 336 | if (event.x > sidechainLeft && 337 | event.x < sidechainRight && 338 | event.y > row2 && 339 | event.y < row3) { 340 | 341 | return ButtonName::sidechainExternal; 342 | } 343 | 344 | if (event.x > knobColumn2 - buttonRadius && 345 | event.x < knobColumn2 + buttonRadius && 346 | event.y > knobRow3 - buttonRadius && 347 | event.y < knobRow3 + buttonRadius) { 348 | 349 | return ButtonName::variMu; 350 | } 351 | 352 | if (event.x > metersLeft && 353 | event.x < metersRight && 354 | event.y > metersTop && 355 | event.y < metersBottom) { 356 | 357 | return ButtonName::meters; 358 | } 359 | 360 | if (event.x < logoRight && 361 | event.y < row3) { 362 | 363 | return ButtonName::logo; 364 | } 365 | 366 | return ButtonName::none; 367 | } 368 | 369 | 370 | void GUI::mouseDown (const juce::MouseEvent& event) { 371 | 372 | ButtonName button = determineButton(event); 373 | 374 | switch (button) { 375 | 376 | case ButtonName::none: return; 377 | case ButtonName::logo: return; 378 | case ButtonName::meters: { toggleMeters(); return; } 379 | case ButtonName::oversamplingON: { switchOversampling(true); return; } 380 | case ButtonName::oversamplingOFF: { switchOversampling(false); return; } 381 | case ButtonName::sidechainInternal: { switchSidechain(false); return; } 382 | case ButtonName::sidechainExternal: { switchSidechain(true); return; } 383 | 384 | default: return; 385 | } 386 | } 387 | 388 | 389 | void GUI::switchSidechain(bool active) { 390 | 391 | sidechainSlider.setValue(active ? 1 : 0); 392 | } 393 | 394 | 395 | void GUI::toggleVariMu() { 396 | 397 | bool currentValue = audioProcessor.getBoolKnobValue(ParameterNames::variMu); 398 | 399 | if (currentValue) variMuSlider.setValue(false); 400 | else variMuSlider.setValue(true); 401 | } 402 | 403 | 404 | void GUI::switchOversampling(bool active) { 405 | 406 | oversamplingSlider.setValue(active ? 1 : 0); 407 | } 408 | 409 | 410 | void GUI::toggleMeters() { 411 | 412 | metersActive = !metersActive; 413 | } 414 | 415 | 416 | void GUI::timerCallback() { 417 | 418 | repaint(); 419 | } 420 | 421 | 422 | void GUI::sliderValueChanged(juce::Slider* slider) { 423 | 424 | std::lock_guard lock(textScreenMutex); 425 | 426 | textScreen.parameterName = slider->getName().toStdString(); 427 | textScreen.value = slider->getValue(); 428 | 429 | textScreen.isBool = false; 430 | textScreen.suffix = ""; 431 | textScreen.timeout = textScreen.defaultTimeout; 432 | 433 | if (textScreen.parameterName == "oversamplingSlider") { 434 | textScreen.parameterName = "Oversampling"; 435 | textScreen.isBool = true; 436 | return; 437 | } 438 | if (textScreen.parameterName == "variMuSlider") { 439 | textScreen.parameterName = "Vari Mu"; 440 | textScreen.isBool = true; 441 | return; 442 | } 443 | if (textScreen.parameterName == "sidechainSlider") { 444 | textScreen.parameterName = "Ext Side Chain"; 445 | textScreen.isBool = true; 446 | return; 447 | } 448 | if (textScreen.parameterName == "inGainSlider") { 449 | textScreen.parameterName = "Input Gain"; 450 | textScreen.suffix = "db"; 451 | return; 452 | } 453 | if (textScreen.parameterName == "outGainSlider") { 454 | textScreen.parameterName = "Output Gain"; 455 | textScreen.suffix = "db"; 456 | return; 457 | } 458 | if (textScreen.parameterName == "attackSlider") { 459 | textScreen.parameterName = "Attack"; 460 | textScreen.suffix = "ms"; 461 | textScreen.value = linearToExponential(textScreen.value, Constants::attackMin, Constants::attackMax); 462 | return; 463 | } 464 | if (textScreen.parameterName == "releaseSlider") { 465 | textScreen.parameterName = "Release"; 466 | textScreen.suffix = "ms"; 467 | textScreen.value = linearToExponential(textScreen.value, Constants::releaseMin, Constants::releaseMax); 468 | return; 469 | } 470 | if (textScreen.parameterName == "thresholdSlider") { 471 | textScreen.parameterName = "Threshold"; 472 | textScreen.suffix = "db"; 473 | return; 474 | } 475 | if (textScreen.parameterName == "ratioSlider") { 476 | textScreen.parameterName = "Ratio"; 477 | textScreen.value = linearToExponential(textScreen.value, Constants::ratioMin, Constants::ratioMax); 478 | textScreen.suffix = ":1"; 479 | return; 480 | } 481 | if (textScreen.parameterName == "channelLinkSlider"){ 482 | textScreen.parameterName = "Link"; 483 | textScreen.suffix = "%"; 484 | return; 485 | } 486 | if (textScreen.parameterName == "foldSlider") { 487 | textScreen.parameterName = "Fold"; 488 | textScreen.suffix = "%"; 489 | return; 490 | } 491 | if (textScreen.parameterName == "feedbackSlider") { 492 | textScreen.parameterName = "Feedback"; 493 | textScreen.value *= 100; 494 | textScreen.suffix = "%"; 495 | return; 496 | } 497 | if (textScreen.parameterName == "ceilingSlider") { 498 | textScreen.parameterName = "Ceiling"; 499 | textScreen.suffix = ""; 500 | return; 501 | } 502 | if (textScreen.parameterName == "convexitySlider") { 503 | textScreen.parameterName = "Convexity"; 504 | if (textScreen.value < 0.9 || textScreen.value > 1.1) textScreen.suffix = " !"; 505 | else textScreen.suffix = ""; 506 | return; 507 | } 508 | if (textScreen.parameterName == "inertiaSlider") { 509 | textScreen.parameterName = "Inertia"; 510 | textScreen.suffix = ""; 511 | return; 512 | } 513 | if (textScreen.parameterName == "inertiaDecaySlider") { 514 | textScreen.parameterName = "Inertia Decay"; 515 | textScreen.suffix = ""; 516 | return; 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /Source/PluginEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PluginProcessor.h" 4 | #include "KnobLooks.h" 5 | 6 | 7 | class GUI : public juce::AudioProcessorEditor, public juce::Slider::Listener, private juce::Timer { 8 | 9 | public: 10 | 11 | GUI (APComp&); 12 | ~GUI() override; 13 | 14 | void paint (juce::Graphics&) override; 15 | void resized() override; 16 | void timerCallback() override; 17 | void mouseDown (const juce::MouseEvent& event) override; 18 | void sliderValueChanged(juce::Slider* slider) override; 19 | 20 | private: 21 | 22 | APComp& audioProcessor; 23 | 24 | KnobLook1 knobLook1; 25 | KnobLook2 knobLook2; 26 | 27 | juce::Image backgroundImage; 28 | 29 | juce::Font customTypeface; 30 | 31 | juce::Slider inGainSlider; 32 | juce::Slider outGainSlider; 33 | juce::Slider convexitySlider; 34 | juce::Slider attackSlider; 35 | juce::Slider releaseSlider; 36 | juce::Slider thresholdSlider; 37 | juce::Slider ratioSlider; 38 | juce::Slider channelLinkSlider; 39 | juce::Slider sidechainSlider; 40 | juce::Slider variMuSlider; 41 | juce::Slider foldSlider; 42 | juce::Slider feedbackSlider; 43 | juce::Slider inertiaSlider; 44 | juce::Slider inertiaDecaySlider; 45 | juce::Slider ceilingSlider; 46 | juce::Slider oversamplingSlider; 47 | 48 | std::vector>> sliders { 49 | {"inGainSlider", std::ref(inGainSlider)}, 50 | {"outGainSlider", std::ref(outGainSlider)}, 51 | {"convexitySlider", std::ref(convexitySlider)}, 52 | {"attackSlider", std::ref(attackSlider)}, 53 | {"releaseSlider", std::ref(releaseSlider)}, 54 | {"thresholdSlider", std::ref(thresholdSlider)}, 55 | {"ratioSlider", std::ref(ratioSlider)}, 56 | {"channelLinkSlider", std::ref(channelLinkSlider)}, 57 | {"sidechainSlider", std::ref(sidechainSlider)}, 58 | {"variMuSlider", std::ref(variMuSlider)}, 59 | {"foldSlider", std::ref(foldSlider)}, 60 | {"feedbackSlider", std::ref(feedbackSlider)}, 61 | {"inertiaSlider", std::ref(inertiaSlider)}, 62 | {"inertiaDecaySlider", std::ref(inertiaDecaySlider)}, 63 | {"ceilingSlider", std::ref(ceilingSlider)}, 64 | {"oversamplingSlider", std::ref(oversamplingSlider)} 65 | }; 66 | 67 | std::unique_ptr inGainAttachment; 68 | std::unique_ptr outGainAttachment; 69 | std::unique_ptr convexityAttachment; 70 | std::unique_ptr attackAttachment; 71 | std::unique_ptr releaseAttachment; 72 | std::unique_ptr thresholdAttachment; 73 | std::unique_ptr ratioAttachment; 74 | std::unique_ptr channelLinkAttachment; 75 | std::unique_ptr sidechainAttachment; 76 | std::unique_ptr variMuAttachment; 77 | std::unique_ptr foldAttachment; 78 | std::unique_ptr feedbackAttachment; 79 | std::unique_ptr inertiaAttachment; 80 | std::unique_ptr inertiaDecayAttachment; 81 | std::unique_ptr ceilingAttachment; 82 | std::unique_ptr oversamplingAttachment; 83 | 84 | bool metersActive; 85 | TextScreen textScreen; 86 | 87 | const float metersLeft = 619.3f; 88 | const float metersRight = 662.0f; 89 | const float metersTop = 23.0f; 90 | const float metersBottom = 261.0f; 91 | 92 | const int knobRow1 = 242; 93 | const int knobRow2 = 326; 94 | const int knobRow3 = 409; 95 | 96 | const int knobColumn1 = 71; 97 | const int knobColumn2 = 218; 98 | const int knobColumn3 = 368; 99 | const int knobColumn4 = 518; 100 | 101 | const int presetRow1 = 0; 102 | const int presetRow2 = 0; 103 | 104 | const int presetColumn1 = 0; 105 | const int presetColumn2 = 0; 106 | const int presetColumn3 = 0; 107 | const int presetColumn4 = 0; 108 | const int presetColumn5 = 0; 109 | 110 | const int textScreenL = 168; 111 | const int textScreenR = 418; 112 | const int textScreenT = 380; 113 | const int textScreenB = 418; 114 | 115 | void toggleMeters(); 116 | void toggleVariMu(); 117 | void switchOversampling(bool active); 118 | void switchSidechain(bool active); 119 | void paintMeters(juce::Graphics &g); 120 | void paintButtons(juce::Graphics &g); 121 | void paintBackground(juce::Graphics &g); 122 | void paintTextScreen(juce::Graphics &g, std::string &textScreenString); 123 | std::string updateTextScreen(); 124 | ButtonName determineButton(const juce::MouseEvent &event); 125 | 126 | std::mutex textScreenMutex; 127 | 128 | int debugRefreshCountDown; 129 | double cachedDebugValue; 130 | 131 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GUI) 132 | }; 133 | -------------------------------------------------------------------------------- /Source/PluginProcessor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "APCommon.h" 5 | #include "PluginProcessor.h" 6 | 7 | 8 | APComp::APComp() 9 | : AudioProcessor(BusesProperties() 10 | .withInput("Input", juce::AudioChannelSet::quadraphonic(), true) 11 | .withOutput("Output", juce::AudioChannelSet::stereo(), true)), 12 | feedbackClip(false), 13 | meterValuesAtomic(meterCount), 14 | oversamplerReady(false), 15 | oversampledSampleRate(0), 16 | apvts(*this, nullptr, "PARAMETERS", createParameterLayout()), 17 | circularBuffer(100), 18 | meterValues { 0 }, 19 | outputSample { 0, 0 }, 20 | previousGainReduction { -200.0, -200.0 }, 21 | gainReduction { 0, 0 }, 22 | inertiaVelocity { 0, 0 }, 23 | meterDecayCoefficient(0.99f), 24 | totalNumInputChannels(0), 25 | totalNumOutputChannels(0), 26 | slewedSignal { -200.0, -200.0 }, 27 | baseSampleRate(0), 28 | flushDSP(false), 29 | parameterList(static_cast(ParameterNames::END) + 1) { 30 | 31 | for (int i = 0; i < static_cast(ParameterNames::END); ++i) { 32 | 33 | parameterList[i] = static_cast(apvts.getParameter(queryParameter(static_cast(i)).id)); 34 | } 35 | } 36 | 37 | 38 | void APComp::prepareToPlay(double sampleRate, int samplesPerBlock) { 39 | 40 | #if DEBUG_MODE 41 | auto start = std::chrono::high_resolution_clock::now(); 42 | #endif 43 | 44 | baseSampleRate.store(static_cast(sampleRate), std::memory_order_relaxed); 45 | 46 | oversamplerReady.store(false); 47 | 48 | startOversampler(sampleRate, samplesPerBlock); 49 | 50 | flushDSP.store(true, std::memory_order_relaxed); 51 | 52 | #if DEBUG_MODE 53 | auto end = std::chrono::high_resolution_clock::now(); 54 | std::chrono::duration duration = end - start; 55 | std::cout << "prepareToPlay completed in : " << duration.count() << " milliseconds" << std::endl; 56 | #endif 57 | } 58 | 59 | 60 | bool APComp::getBoolKnobValue (ParameterNames parameter) const { 61 | 62 | return parameterList[static_cast(parameter)]->get() > 0.5f ? true : false; 63 | } 64 | 65 | 66 | float APComp::getFloatKnobValue(ParameterNames parameter) const { 67 | 68 | return parameterList[static_cast(parameter)]->get(); 69 | } 70 | 71 | 72 | void APComp::startOversampler(double sampleRate, int samplesPerBlock) { 73 | 74 | oversampler.reset(); 75 | 76 | oversampler = std::make_unique>(2, oversamplingFactor, juce::dsp::Oversampling::filterHalfBandFIREquiripple); 77 | 78 | oversampler->initProcessing(static_cast(samplesPerBlock)); 79 | oversampler->reset(); 80 | 81 | setLatencySamples(oversampler->getLatencyInSamples()); 82 | 83 | oversampledSampleRate = static_cast(sampleRate) * std::pow(2, static_cast(oversamplingFactor)); 84 | 85 | oversamplerReady.store(true); 86 | } 87 | 88 | 89 | void APComp::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { 90 | 91 | juce::ScopedNoDenormals noDenormals; 92 | 93 | #if DEBUG_MODE 94 | startClock(); 95 | #endif 96 | 97 | int sr = baseSampleRate.load(std::memory_order_relaxed); 98 | if (sr < 100) return; 99 | 100 | totalNumInputChannels = getTotalNumInputChannels(); 101 | totalNumOutputChannels = getTotalNumOutputChannels(); 102 | 103 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) buffer.clear(i, 0, buffer.getNumSamples()); 104 | 105 | int overSamplingSelection = static_cast(getFloatKnobValue(ParameterNames::oversampling)); 106 | 107 | juce::dsp::AudioBlock originalBlock(buffer); 108 | juce::dsp::AudioBlock mainBlock; 109 | juce::dsp::AudioBlock sidechainBlock; 110 | 111 | switch (totalNumInputChannels) { 112 | 113 | case 1: 114 | mainBlock = originalBlock.getSingleChannelBlock(0); 115 | sidechainBlock = originalBlock.getSingleChannelBlock(0); 116 | break; 117 | case 2: 118 | mainBlock = originalBlock.getSubsetChannelBlock(0, 2); 119 | sidechainBlock = originalBlock.getSubsetChannelBlock(0, 2); 120 | break; 121 | case 3: 122 | mainBlock = originalBlock.getSubsetChannelBlock(0, 2); 123 | sidechainBlock = originalBlock.getSingleChannelBlock(2); 124 | break; 125 | case 4: 126 | mainBlock = originalBlock.getSubsetChannelBlock(0, 2); 127 | sidechainBlock = originalBlock.getSubsetChannelBlock(2, 2); 128 | break; 129 | default: 130 | return; 131 | } 132 | 133 | if (overSamplingSelection == 0) { 134 | 135 | doCompressionDSP(mainBlock, sidechainBlock, 0, sr); 136 | 137 | #if DEBUG_MODE 138 | stopClock(); 139 | #endif 140 | return; 141 | } 142 | 143 | if (!oversamplerReady.load()) return; 144 | 145 | juce::dsp::AudioBlock oversampledBlock = oversampler->processSamplesUp (mainBlock); 146 | 147 | doCompressionDSP(oversampledBlock, sidechainBlock, oversamplingFactor, oversampledSampleRate); 148 | 149 | oversampler->processSamplesDown (mainBlock); 150 | 151 | #if DEBUG_MODE 152 | stopClock(); 153 | #endif 154 | } 155 | 156 | 157 | void APComp::startClock() { 158 | 159 | startTime = std::chrono::high_resolution_clock::now(); 160 | } 161 | 162 | void APComp::stopClock() { 163 | 164 | auto end = std::chrono::high_resolution_clock::now(); 165 | std::chrono::duration duration = end - startTime; 166 | circularBuffer.add(duration.count()); 167 | } 168 | -------------------------------------------------------------------------------- /Source/PluginProcessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "CircularBuffer.h" 7 | 8 | 9 | class APComp : public juce::AudioProcessor { 10 | 11 | public: 12 | 13 | APComp(); 14 | 15 | void prepareToPlay (double sampleRate, int samplesPerBlock) override; 16 | void releaseResources() override; 17 | #ifndef JucePlugin_PreferredChannelConfigurations 18 | bool isBusesLayoutSupported (const BusesLayout& layouts) const override; 19 | #endif 20 | void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; 21 | juce::AudioProcessorEditor* createEditor() override; 22 | bool hasEditor() const override; 23 | const juce::String getName() const override; 24 | bool acceptsMidi() const override; 25 | bool producesMidi() const override; 26 | bool isMidiEffect() const override; 27 | double getTailLengthSeconds() const override; 28 | int getNumPrograms() override; 29 | int getCurrentProgram() override; 30 | void setCurrentProgram (int index) override; 31 | const juce::String getProgramName (int index) override; 32 | void changeProgramName (int index, const juce::String& newName) override; 33 | void getStateInformation (juce::MemoryBlock& destData) override; 34 | void setStateInformation (const void* data, int sizeInBytes) override; 35 | 36 | bool getBoolKnobValue (ParameterNames parameter) const; 37 | float getFloatKnobValue(ParameterNames parameter) const; 38 | 39 | void startOversampler(double sampleRate, int samplesPerBlock); 40 | 41 | static constexpr int meterCount = 6; 42 | 43 | std::atomic feedbackClip; 44 | std::vector> meterValuesAtomic; 45 | std::atomic oversamplerReady; 46 | 47 | const size_t oversamplingFactor = 2; 48 | int oversampledSampleRate; 49 | 50 | juce::AudioProcessorValueTreeState apvts; 51 | juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); 52 | 53 | CircularBuffer circularBuffer; 54 | 55 | private: 56 | 57 | void updateMeters(float *maxValuesForMeters); 58 | void flushLoopVariables(); 59 | void doCompressionDSP(juce::dsp::AudioBlock& block, 60 | juce::dsp::AudioBlock& sidechainBlock, 61 | size_t oversamplingFactor, 62 | int sampleRate); 63 | void startClock(); 64 | void stopClock(); 65 | 66 | float meterValues[meterCount]; 67 | double outputSample[2]; 68 | double previousGainReduction[2]; 69 | double gainReduction[2]; 70 | float inertiaVelocity[2]; 71 | float meterDecayCoefficient; 72 | int totalNumInputChannels; 73 | int totalNumOutputChannels; 74 | std::vector> parameterCache; 75 | double slewedSignal[2]; 76 | std::atomic baseSampleRate; 77 | std::atomic flushDSP; 78 | 79 | std::chrono::time_point startTime; 80 | 81 | std::vector parameterList; 82 | 83 | std::unique_ptr> oversampler; 84 | 85 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (APComp) 86 | }; 87 | -------------------------------------------------------------------------------- /Source/media/JetBrainsMono-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apmastering/APComp/1f5b12a7a4e40039f25fd111840d781b766a5243/Source/media/JetBrainsMono-Light.otf -------------------------------------------------------------------------------- /Source/media/Knockout-Flyweight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apmastering/APComp/1f5b12a7a4e40039f25fd111840d781b766a5243/Source/media/Knockout-Flyweight.otf -------------------------------------------------------------------------------- /Source/media/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apmastering/APComp/1f5b12a7a4e40039f25fd111840d781b766a5243/Source/media/bg.png -------------------------------------------------------------------------------- /Source/media/knob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apmastering/APComp/1f5b12a7a4e40039f25fd111840d781b766a5243/Source/media/knob.png -------------------------------------------------------------------------------- /Source/media/knob2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apmastering/APComp/1f5b12a7a4e40039f25fd111840d781b766a5243/Source/media/knob2.png -------------------------------------------------------------------------------- /Source/media/knobArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apmastering/APComp/1f5b12a7a4e40039f25fd111840d781b766a5243/Source/media/knobArrow.png -------------------------------------------------------------------------------- /Source/media/knobPointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apmastering/APComp/1f5b12a7a4e40039f25fd111840d781b766a5243/Source/media/knobPointer.png -------------------------------------------------------------------------------- /Versatile Compressor.jucer: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 32 | 33 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | --------------------------------------------------------------------------------