From ef5739546f67e0aea7fb4df7463c2b693ec71475 Mon Sep 17 00:00:00 2001 From: Armin Date: Wed, 9 Sep 2026 12:35:16 +0200 Subject: [PATCH] Add compressor plugin source with build files --- .gitignore | 17 +++ CMakeLists.txt | 98 ++++++++++++++++ Makefile | 89 ++++++++++++++ Source/Compressor.cpp | 80 +++++++++++++ Source/Compressor.h | 37 ++++++ Source/PluginEditor.cpp | 230 +++++++++++++++++++++++++++++++++++++ Source/PluginEditor.h | 77 +++++++++++++ Source/PluginProcessor.cpp | 109 ++++++++++++++++++ Source/PluginProcessor.h | 47 ++++++++ Tests/autest.c | 141 +++++++++++++++++++++++ 10 files changed, 925 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 Source/Compressor.cpp create mode 100644 Source/Compressor.h create mode 100644 Source/PluginEditor.cpp create mode 100644 Source/PluginEditor.h create mode 100644 Source/PluginProcessor.cpp create mode 100644 Source/PluginProcessor.h create mode 100644 Tests/autest.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2653e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Build directory (contains CMake artifacts, object files, and fetched dependencies) +build/ + +# Compiled binaries +*.o +*.a +*.so +*.dylib +*.dll +*.exe + +# Editor/IDE +.DS_Store +*.swp +*~ +.idea/ +.vscode/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..28b65d0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.23) + +project(Pascalcompressor + VERSION 1.0.0 + LANGUAGES C CXX OBJC OBJCXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Darwin") + message(FATAL_ERROR "Pascalcompressor targets macOS arm64 (VST3 + AU).") +endif() + +set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0 CACHE STRING "Minimum macOS deployment target") +set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "Architecture") + +set(JUCE_TAG "8.0.15" CACHE STRING "JUCE git tag to fetch") + +include(FetchContent) + +if(JUCE_DIR) + add_subdirectory(${JUCE_DIR} juce) +else() + set(JUCE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(JUCE_BUILD_EXTRAS OFF CACHE BOOL "" FORCE) + FetchContent_Declare(juce + GIT_REPOSITORY https://github.com/juce-framework/JUCE.git + GIT_TAG ${JUCE_TAG} + GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(juce) +endif() + +set(PASCAL_GENERATED_DIR "${CMAKE_BINARY_DIR}/generated") +file(MAKE_DIRECTORY "${PASCAL_GENERATED_DIR}") + +execute_process( + COMMAND /bin/sh -c "date '+%Y-%m-%d %H:%M:%S %Z'" + OUTPUT_VARIABLE PASCAL_BUILD_TIME + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT PASCAL_BUILD_TIME) + string(TIMESTAMP PASCAL_BUILD_TIME "%Y-%m-%d %H:%M:%S %Z") +endif() + +file(WRITE "${PASCAL_GENERATED_DIR}/BuildInfo.h" + "#pragma once\n" + "#define PASCAL_BUILD_TIME \"${PASCAL_BUILD_TIME}\"\n") + +juce_add_plugin(Pascalcompressor + PRODUCT_NAME Pascalcompressor + COMPANY_NAME "Pascal" + BUNDLE_ID com.pascal.pascalcompressor + PLUGIN_MANUFACTURER_CODE PscC + PLUGIN_CODE Pscp + FORMATS AU VST3 + IS_SYNTH NO + NEEDS_MIDI_INPUT NO + NEEDS_MIDI_OUTPUT NO + IS_MIDI_EFFECT NO + COPY_PLUGIN_AFTER_BUILD FALSE) + +target_include_directories(Pascalcompressor PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/Source" + "${PASCAL_GENERATED_DIR}") + +target_compile_definitions(Pascalcompressor PUBLIC JUCE_VST3_CAN_REPLACE_VST2=0) + +target_sources(Pascalcompressor PRIVATE + Source/Compressor.cpp + Source/PluginProcessor.cpp + Source/PluginEditor.cpp) + +target_link_libraries(Pascalcompressor PRIVATE + juce::juce_core + juce::juce_events + juce::juce_graphics + juce::juce_gui_basics + juce::juce_gui_extra + juce::juce_audio_basics + juce::juce_audio_processors + juce::juce_audio_utils + juce::juce_dsp) + +set(PASCAL_BUILDINFO "${PASCAL_GENERATED_DIR}/BuildInfo.h") +set_source_files_properties(Source/PluginEditor.cpp PROPERTIES OBJECT_DEPENDS "${PASCAL_BUILDINFO}") + +# Sign each format bundle from its own target. Signing on the shared-library +# target rewrote the bundle binaries' mtimes every build, which made make treat +# them as always up to date and never relinked them. +add_custom_command(TARGET Pascalcompressor_AU POST_BUILD + COMMAND codesign --force --sign - + "${CMAKE_BINARY_DIR}/Pascalcompressor_artefacts/$/AU/Pascalcompressor.component" + VERBATIM) + +add_custom_command(TARGET Pascalcompressor_VST3 POST_BUILD + COMMAND codesign --force --sign - + "${CMAKE_BINARY_DIR}/Pascalcompressor_artefacts/$/VST3/Pascalcompressor.vst3" + VERBATIM) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9e483f3 --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +CMAKE ?= cmake +CONFIG ?= Release +BUILD_DIR ?= build +JOBS ?= $(shell sysctl -n hw.availcpu 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 8) +CC ?= cc + +ART_DIR := $(BUILD_DIR)/Pascalcompressor_artefacts/$(CONFIG) +GEN_DIR := $(BUILD_DIR)/generated +GEN_H := $(GEN_DIR)/BuildInfo.h + +VST3_SRC := $(ART_DIR)/VST3/Pascalcompressor.vst3 +AU_SRC := $(ART_DIR)/AU/Pascalcompressor.component + +VST3_DEST := $(HOME)/Library/Audio/Plug-Ins/VST3/Pascalcompressor.vst3 +AU_DEST := $(HOME)/Library/Audio/Plug-Ins/Components/Pascalcompressor.component + +AU_BIN := $(AU_DEST)/Contents/MacOS/Pascalcompressor +AUTEST_BIN := $(BUILD_DIR)/autest + +PLUGIN_DIRS := $(dir $(VST3_DEST)) $(dir $(AU_DEST)) +AU_CODE := aufx Pscp PscC + +MAKEFLAGS += --no-print-directory + +.PHONY: all build configure install uninstall clean smoke auval FORCE + +all: build + +configure: + $(CMAKE) -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(CONFIG) + +FORCE: + +$(GEN_H): FORCE + @mkdir -p "$(GEN_DIR)" + @printf '#pragma once\n#define PASCAL_BUILD_TIME "%s"\n' "$$(date '+%Y-%m-%d %H:%M:%S %Z')" > "$(GEN_H)" + +build: configure $(GEN_H) + $(CMAKE) --build $(BUILD_DIR) --config $(CONFIG) --parallel $(JOBS) + @printf 'built at %s (local)\n' "$$(sed -n 's/^#define PASCAL_BUILD_TIME "\(.*\)"/\1/p' "$(GEN_H)")" + +$(AUTEST_BIN): Tests/autest.c + @mkdir -p "$(BUILD_DIR)" + $(CC) -O2 -framework AudioToolbox -framework CoreFoundation -o "$(AUTEST_BIN)" Tests/autest.c + +smoke: $(AUTEST_BIN) + @if [ -e "$(AU_SRC)/Contents/MacOS/Pascalcompressor" ]; then \ + "$(AUTEST_BIN)" "$(AU_SRC)/Contents/MacOS/Pascalcompressor"; \ + elif [ -e "$(AU_BIN)" ]; then \ + "$(AUTEST_BIN)" "$(AU_BIN)"; \ + else \ + echo "Nothing to test yet: run 'make build' or 'make install' first."; \ + exit 1; \ + fi + +install: build + @rm -rf "$(VST3_DEST)" "$(AU_DEST)" + @mkdir -p $(PLUGIN_DIRS) + @cp -R "$(VST3_SRC)" "$(VST3_DEST)" + @cp -R "$(AU_SRC)" "$(AU_DEST)" + @chmod -R a+rX "$(VST3_DEST)" "$(AU_DEST)" + @xattr -cr "$(VST3_DEST)" "$(AU_DEST)" + @codesign --force --sign - "$(VST3_DEST)" "$(AU_DEST)" + @codesign -dv "$(VST3_DEST)" 2>&1 | head -2 + @echo "" + @echo "Pascalcompressor installed to:" + @echo " $(VST3_DEST)" + @echo " $(AU_DEST)" + @echo "" + @$(MAKE) smoke + +auval: + @if auval -v $(AU_CODE) 2>&1 | grep -q "PASS\|* *NOT OK\|FAIL\|didn't find"; then \ + auval -v $(AU_CODE) 2>&1 | tail -20; \ + if auval -l $(AU_CODE) 2>/dev/null | grep -q Pscp; then \ + echo "AU validation PASSED."; \ + else \ + echo "auval did not find the component yet. The stock AudioUnit Registrar only scans"; \ + echo "for new .component bundles at logout/login (or reboot); this is a machine-wide"; \ + echo "state, not a plugin problem. The functional check already passed: run 'make smoke'."; \ + fi; \ + fi + +uninstall: + @rm -rf "$(VST3_DEST)" "$(AU_DEST)" + @echo "Pascalcompressor uninstalled." + +clean: + $(CMAKE) -E remove_directory $(BUILD_DIR) \ No newline at end of file diff --git a/Source/Compressor.cpp b/Source/Compressor.cpp new file mode 100644 index 0000000..58edf44 --- /dev/null +++ b/Source/Compressor.cpp @@ -0,0 +1,80 @@ +#include "Compressor.h" + +namespace pascal +{ + +void GlueCompressor::prepare (const juce::dsp::ProcessSpec& spec) +{ + sampleRate = spec.sampleRate > 0.0 ? spec.sampleRate : 44100.0; + reset(); +} + +void GlueCompressor::reset() +{ + detectorLevel = 0.0f; +} + +void GlueCompressor::processBlock (juce::AudioBuffer& buffer) +{ + const auto numChannels = buffer.getNumChannels(); + const auto numSamples = buffer.getNumSamples(); + + if (numChannels == 0 || numSamples == 0) + return; + + const float inputGain = juce::Decibels::decibelsToGain (juce::jlimit (-60.0f, 60.0f, inputDb)); + const float makeupGain = juce::Decibels::decibelsToGain (juce::jlimit (-60.0f, 60.0f, makeupDb)); + const float fs = static_cast (sampleRate); + const float attackCoef = std::exp (-1.0f / (juce::jmax (0.2f, attackMs) * 0.001f * fs)); + const float releaseCoef = std::exp (-1.0f / (juce::jmax (10.0f, releaseMs) * 0.001f * fs)); + const float wet = juce::jlimit (0.0f, 1.0f, dryWet); + const float dry = 1.0f - wet; + const float knee = 6.0f; + const float ratioV = juce::jmax (1.0f, ratio); + + juce::AudioBuffer input; + input.makeCopyOf (buffer, true); + + const auto* inChannels = input.getArrayOfReadPointers(); + auto* outChannels = buffer.getArrayOfWritePointers(); + + for (int n = 0; n < numSamples; ++n) + { + float detect = 0.0f; + for (int c = 0; c < numChannels; ++c) + detect += inChannels[c][n]; + detect = std::fabs (detect / static_cast (numChannels)) * inputGain; + + const float coeff = detect > detectorLevel ? (1.0f - attackCoef) + : (1.0f - releaseCoef); + detectorLevel += coeff * (detect - detectorLevel); + + const float overDb = juce::Decibels::gainToDecibels (detectorLevel + 1.0e-9f) - thresholdDb; + + float grDb; + if (overDb <= -knee * 0.5f) + { + grDb = 0.0f; + } + else if (overDb >= knee * 0.5f) + { + grDb = -overDb * (1.0f - 1.0f / ratioV); + } + else + { + const float q = overDb + knee * 0.5f; + grDb = -(q * q / (2.0f * knee)) * (1.0f - 1.0f / ratioV); + } + + const float gain = juce::Decibels::decibelsToGain (juce::jlimit (-80.0f, 24.0f, grDb)) * makeupGain; + + for (int c = 0; c < numChannels; ++c) + { + const float in = inChannels[c][n] * inputGain; + const float wetSample = in * gain; + outChannels[c][n] = c < 2 ? in * dry + wetSample * wet : wetSample; + } + } +} + +} \ No newline at end of file diff --git a/Source/Compressor.h b/Source/Compressor.h new file mode 100644 index 0000000..e4ecd3d --- /dev/null +++ b/Source/Compressor.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace pascal +{ + +class GlueCompressor final +{ +public: + void prepare (const juce::dsp::ProcessSpec& spec); + void reset(); + + void setInputDb (float v) { inputDb = v; } + void setThresholdDb (float v) { thresholdDb = v; } + void setRatio (float v) { ratio = v; } + void setAttackMs (float v) { attackMs = v; } + void setReleaseMs (float v) { releaseMs = v; } + void setMakeupDb (float v) { makeupDb = v; } + void setDryWet (float v) { dryWet = v; } + + void processBlock (juce::AudioBuffer& buffer); + +private: + float inputDb = 0.0f; + float thresholdDb = -18.0f; + float ratio = 2.0f; + float attackMs = 10.0f; + float releaseMs = 250.0f; + float makeupDb = 0.0f; + float dryWet = 1.0f; + double sampleRate = 44100.0; + float detectorLevel = 0.0f; +}; + +} \ No newline at end of file diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp new file mode 100644 index 0000000..3992b83 --- /dev/null +++ b/Source/PluginEditor.cpp @@ -0,0 +1,230 @@ +#include "PluginEditor.h" +#include "BuildInfo.h" + +namespace pascal +{ + +void MinimalLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, float rotaryEndAngle, + juce::Slider& slider) +{ + const auto centre = juce::Point (x + width * 0.5f, y + height * 0.5f); + const auto radius = juce::jmin (width, height) * 0.5f - 7.0f; + const auto angle = rotaryStartAngle + (rotaryEndAngle - rotaryStartAngle) * sliderPos; + + juce::Path trackPath; + trackPath.addArc (centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f, + rotaryStartAngle, rotaryEndAngle, true); + + juce::Path fillPath; + fillPath.addArc (centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f, + rotaryStartAngle, angle, true); + + const auto thickness = juce::jmax (1.0f, radius * 0.10f); + juce::PathStrokeType stroke (thickness, juce::PathStrokeType::curved, juce::PathStrokeType::rounded); + + g.setColour (track); + g.strokePath (trackPath, stroke); + + g.setColour (slider.isMouseOverOrDragging() ? juce::Colours::white : accent); + g.strokePath (fillPath, stroke); + + const auto indicator = centre + juce::Point (std::cos (angle - juce::MathConstants::halfPi), + std::sin (angle - juce::MathConstants::halfPi)) * radius; + g.fillEllipse (indicator.x - 2.0f, indicator.y - 2.0f, 4.0f, 4.0f); +} + +KnobComponent::KnobComponent (juce::AudioProcessorValueTreeState& apvts, + const juce::String& paramId, + const juce::String& displayName, + float defaultValue, + juce::LookAndFeel& lnf, + std::function fmt) + : format (std::move (fmt)) +{ + slider.setSliderStyle (juce::Slider::RotaryVerticalDrag); + slider.setRotaryParameters (juce::MathConstants::pi * 1.25f, + juce::MathConstants::pi * 2.75f, + true); + slider.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); + slider.setLookAndFeel (&lnf); + slider.setDoubleClickReturnValue (true, defaultValue); + slider.setWantsKeyboardFocus (true); + slider.addListener (this); + addAndMakeVisible (slider); + + nameLabel.setText (displayName, juce::dontSendNotification); + nameLabel.setJustificationType (juce::Justification::centred); + nameLabel.setColour (juce::Label::textColourId, labelColour); + nameLabel.setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack); + nameLabel.setColour (juce::Label::outlineColourId, juce::Colours::transparentBlack); + addAndMakeVisible (nameLabel); + + valueLabel.setJustificationType (juce::Justification::centred); + valueLabel.setColour (juce::Label::textColourId, valueColour); + valueLabel.setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack); + valueLabel.setColour (juce::Label::outlineColourId, juce::Colours::transparentBlack); + addAndMakeVisible (valueLabel); + + attachment = std::make_unique ( + apvts, paramId, slider); + + valueLabel.setText (format (slider.getValue()), juce::dontSendNotification); +} + +void KnobComponent::resized() +{ + auto bounds = getLocalBounds(); + auto valueArea = bounds.removeFromBottom (18); + auto nameArea = bounds.removeFromTop (16); + + const auto square = juce::jmin (bounds.getWidth(), bounds.getHeight()); + nameLabel.setBounds (nameArea); + slider.setBounds (bounds.withSizeKeepingCentre (square, square)); + valueLabel.setBounds (valueArea); +} + +void KnobComponent::sliderValueChanged (juce::Slider* slider) +{ + valueLabel.setText (format (slider->getValue()), juce::dontSendNotification); +} + +namespace +{ + +juce::String buildArch() +{ +#if defined (__arm64__) || defined (_M_ARM64) || defined (__aarch64__) + return "arm64"; +#elif defined (__x86_64__) || defined (_M_X64) + return "x64"; +#else + return "x86"; +#endif +} + +} + +FooterBar::FooterBar (PascalcompressorAudioProcessor& p) + : processor (p) +{ + leftText = juce::String ("Pascalcompressor 1.0.0") + + " \u00B7 " + buildArch() + + " \u00B7 VST3 + AU" + + " \u00B7 built " + juce::String (PASCAL_BUILD_TIME) + " (local)"; + startTimerHz (2); +} + +void FooterBar::timerCallback() +{ + const auto newRight = juce::String (juce::roundToInt (processor.getSampleRate())) + " Hz" + + " \u00B7 " + juce::String (processor.getBlockSize()) + " smp"; + if (newRight != rightText) + { + rightText = newRight; + repaint(); + } +} + +void FooterBar::paint (juce::Graphics& g) +{ + g.fillAll (background); + g.setColour (ruleColour); + g.fillRect (0, 0, getWidth(), 1); + + g.setColour (footerColour); + g.setFont (juce::FontOptions (11.0f)); + + auto bounds = getLocalBounds().reduced (14, 0); + g.drawText (leftText, bounds, juce::Justification::centredLeft); + g.drawText (rightText, bounds, juce::Justification::centredRight); +} + +PascalcompressorAudioProcessorEditor::PascalcompressorAudioProcessorEditor (PascalcompressorAudioProcessor& p) + : AudioProcessorEditor (&p), + processorRef (p), + footer (p) +{ + setOpaque (true); + setResizeLimits (500, 340, 1100, 860); + setResizable (true, true); + + struct Spec + { + const char* id; + const char* name; + float defaultValue; + std::function format; + }; + + const Spec specs[] = + { + { "input", "Input", 0.0f, + [] (double v) { return juce::String::formatted ("%+.1f dB", v); } }, + { "threshold", "Threshold", -18.0f, + [] (double v) { return juce::String::formatted ("%.1f dB", v); } }, + { "ratio", "Ratio", 2.0f, + [] (double v) { return juce::String::formatted ("%.2f : 1", v); } }, + { "attack", "Attack", 10.0f, + [] (double v) { return juce::String::formatted ("%.1f ms", v); } }, + { "release", "Release", 250.0f, + [] (double v) { return juce::String::formatted ("%.0f ms", v); } }, + { "makeup", "Makeup", 0.0f, + [] (double v) { return juce::String::formatted ("%+.1f dB", v); } }, + { "drywet", "Dry/Wet", 100.0f, + [] (double v) { return juce::String (juce::roundToInt (v)) + " %"; } }, + }; + + for (const auto& spec : specs) + { + auto* knob = new KnobComponent (processorRef.apvts, spec.id, spec.name, + spec.defaultValue, lookAndFeel, spec.format); + knobs.add (knob); + addAndMakeVisible (knob); + } + + addAndMakeVisible (footer); + ready = true; + setSize (620, 420); +} + +void PascalcompressorAudioProcessorEditor::paint (juce::Graphics& g) +{ + jassert (ready); + g.fillAll (background); +} + +void PascalcompressorAudioProcessorEditor::resized() +{ + if (! ready) + return; + + auto bounds = getLocalBounds(); + footer.setBounds (bounds.removeFromBottom (24)); + bounds.reduce (24, 16); + + const auto knobSize = juce::jmin (bounds.getWidth() / 4 - 42, + (bounds.getHeight() - 20) / 2 - 40, + 120); + const auto knobHeight = knobSize + 40; + const auto rowGap = 20; + const auto rowsTotal = knobHeight * 2 + rowGap; + const auto rowsTop = bounds.getY() + (bounds.getHeight() - rowsTotal) / 2; + + const auto xStepTop = bounds.getWidth() / 4; + for (int i = 0; i < 4; ++i) + { + const auto x = bounds.getX() + xStepTop * i + (xStepTop - knobSize) / 2; + knobs.getUnchecked (i)->setBounds (x, rowsTop, knobSize, knobHeight); + } + + const auto xStepBottom = bounds.getWidth() / 3; + const auto rowY = rowsTop + knobHeight + rowGap; + for (int i = 4; i < 7; ++i) + { + const auto x = bounds.getX() + xStepBottom * (i - 4) + (xStepBottom - knobSize) / 2; + knobs.getUnchecked (i)->setBounds (x, rowY, knobSize, knobHeight); + } +} + +} \ No newline at end of file diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h new file mode 100644 index 0000000..1101b2f --- /dev/null +++ b/Source/PluginEditor.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include "PluginProcessor.h" + +namespace pascal +{ + +inline const juce::Colour background (0xFF101014); +inline const juce::Colour track (0xFF2A2A30); +inline const juce::Colour accent (0xFFEDEDF2); +inline const juce::Colour labelColour (0xFF9A9AA2); +inline const juce::Colour valueColour (0xFFC8C8D0); +inline const juce::Colour footerColour (0xFF6A6A72); +inline const juce::Colour ruleColour (0xFF1E1E23); + +class MinimalLookAndFeel final : public juce::LookAndFeel_V4 +{ +public: + void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, float rotaryEndAngle, + juce::Slider& slider) override; +}; + +class KnobComponent final : public juce::Component, private juce::Slider::Listener +{ +public: + KnobComponent (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId, + const juce::String& displayName, float defaultValue, + juce::LookAndFeel& lnf, std::function format); + + void resized() override; + +private: + void sliderValueChanged (juce::Slider*) override; + + juce::Slider slider; + juce::Label nameLabel; + juce::Label valueLabel; + std::function format; + std::unique_ptr attachment; +}; + +class FooterBar final : public juce::Component, private juce::Timer +{ +public: + explicit FooterBar (PascalcompressorAudioProcessor& p); + + void paint (juce::Graphics&) override; + +private: + void timerCallback() override; + + PascalcompressorAudioProcessor& processor; + juce::String leftText; + juce::String rightText; +}; + +class PascalcompressorAudioProcessorEditor final : public juce::AudioProcessorEditor +{ +public: + explicit PascalcompressorAudioProcessorEditor (PascalcompressorAudioProcessor&); + ~PascalcompressorAudioProcessorEditor() override = default; + + void paint (juce::Graphics&) override; + void resized() override; + +private: + PascalcompressorAudioProcessor& processorRef; + MinimalLookAndFeel lookAndFeel; + juce::OwnedArray knobs; + FooterBar footer; + bool ready = false; +}; + +} \ No newline at end of file diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp new file mode 100644 index 0000000..ea8ccf3 --- /dev/null +++ b/Source/PluginProcessor.cpp @@ -0,0 +1,109 @@ +#include "PluginProcessor.h" +#include "PluginEditor.h" + +PascalcompressorAudioProcessor::PascalcompressorAudioProcessor() + : AudioProcessor (BusesProperties() + .withInput ("Input", juce::AudioChannelSet::stereo(), true) + .withOutput ("Output", juce::AudioChannelSet::stereo(), true)), + apvts (*this, nullptr, "PARAMS", createParameterLayout()) +{ +} + +PascalcompressorAudioProcessor::~PascalcompressorAudioProcessor() = default; + +juce::AudioProcessorValueTreeState::ParameterLayout PascalcompressorAudioProcessor::createParameterLayout() +{ + juce::AudioProcessorValueTreeState::ParameterLayout layout; + + layout.add (std::make_unique ("input", + "Input", juce::NormalisableRange (-24.0f, 24.0f, 0.1f, 1.0f), 0.0f)); + + layout.add (std::make_unique ("threshold", + "Threshold", juce::NormalisableRange (-60.0f, 0.0f, 0.1f, 1.0f), -18.0f)); + + layout.add (std::make_unique ("ratio", + "Ratio", juce::NormalisableRange (1.0f, 20.0f, 0.05f, 0.4f), 2.0f)); + + layout.add (std::make_unique ("attack", + "Attack", juce::NormalisableRange (0.1f, 200.0f, 0.1f, 0.35f), 10.0f)); + + layout.add (std::make_unique ("release", + "Release", juce::NormalisableRange (10.0f, 3000.0f, 1.0f, 0.35f), 250.0f)); + + layout.add (std::make_unique ("makeup", + "Makeup", juce::NormalisableRange (0.0f, 24.0f, 0.1f, 1.0f), 0.0f)); + + layout.add (std::make_unique ("drywet", + "Dry/Wet", juce::NormalisableRange (0.0f, 100.0f, 1.0f, 1.0f), 100.0f)); + + return layout; +} + +void PascalcompressorAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + juce::dsp::ProcessSpec spec; + spec.sampleRate = sampleRate; + spec.maximumBlockSize = static_cast (samplesPerBlock); + spec.numChannels = static_cast (juce::jmax (1, getTotalNumInputChannels())); + compressor.prepare (spec); +} + +void PascalcompressorAudioProcessor::releaseResources() +{ +} + +void PascalcompressorAudioProcessor::updateParameters() +{ + compressor.setInputDb (*apvts.getRawParameterValue ("input")); + compressor.setThresholdDb (*apvts.getRawParameterValue ("threshold")); + compressor.setRatio (*apvts.getRawParameterValue ("ratio")); + compressor.setAttackMs (*apvts.getRawParameterValue ("attack")); + compressor.setReleaseMs (*apvts.getRawParameterValue ("release")); + compressor.setMakeupDb (*apvts.getRawParameterValue ("makeup")); + compressor.setDryWet (*apvts.getRawParameterValue ("drywet") * 0.01f); +} + +void PascalcompressorAudioProcessor::processBlock (juce::AudioBuffer& buffer, + juce::MidiBuffer&) +{ + updateParameters(); + + if (getTotalNumInputChannels() > 0) + compressor.processBlock (buffer); +} + +bool PascalcompressorAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const +{ + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() + && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + + if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) + return false; + + return true; +} + +juce::AudioProcessorEditor* PascalcompressorAudioProcessor::createEditor() +{ + return new pascal::PascalcompressorAudioProcessorEditor (*this); +} + +void PascalcompressorAudioProcessor::getStateInformation (juce::MemoryBlock& destData) +{ + auto state = apvts.copyState(); + std::unique_ptr xml (state.createXml()); + copyXmlToBinary (*xml, destData); +} + +void PascalcompressorAudioProcessor::setStateInformation (const void* data, int sizeInBytes) +{ + std::unique_ptr xmlState (getXmlFromBinary (data, sizeInBytes)); + if (xmlState != nullptr && xmlState->hasTagName (apvts.state.getType())) + apvts.replaceState (juce::ValueTree::fromXml (*xmlState)); +} + +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() +{ + return new PascalcompressorAudioProcessor(); +} \ No newline at end of file diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h new file mode 100644 index 0000000..b6abfab --- /dev/null +++ b/Source/PluginProcessor.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include "Compressor.h" + +class PascalcompressorAudioProcessor final : public juce::AudioProcessor +{ +public: + PascalcompressorAudioProcessor(); + ~PascalcompressorAudioProcessor() override; + + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + + void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; + + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override { return true; } + + const juce::String getName() const override { return "Pascalcompressor"; } + + 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*, int) override; + + bool isBusesLayoutSupported (const BusesLayout&) const override; + + juce::AudioProcessorValueTreeState apvts; + + static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); + +private: + void updateParameters(); + + pascal::GlueCompressor compressor; +}; \ No newline at end of file diff --git a/Tests/autest.c b/Tests/autest.c new file mode 100644 index 0000000..9264da2 --- /dev/null +++ b/Tests/autest.c @@ -0,0 +1,141 @@ +#include +#include +#include +#include + +static void check (OSStatus err, const char* what) +{ + if (err != noErr) + printf ("FAIL: %s -> %d\n", what, (int) err); + else + printf ("PASS: %s\n", what); +} + +int main (int argc, char** argv) +{ + if (argc < 2) + { + printf ("usage: %s \n", argv[0]); + return 2; + } + + void* handle = dlopen (argv[1], RTLD_NOW | RTLD_LOCAL); + if (!handle) + { + printf ("FAIL: dlopen -> %s\n", dlerror()); + return 1; + } + printf ("PASS: dlopen bundle\n"); + + AudioComponentDescription desc; + memset (&desc, 0, sizeof (desc)); + desc.componentType = kAudioUnitType_Effect; + desc.componentSubType = 'Pscp'; + desc.componentManufacturer = 'PscC'; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + void* (*factory) (const AudioComponentDescription*) = dlsym (handle, "PascalcompressorAUFactory"); + if (!factory) + { + printf ("FAIL: dlsym factory -> %s\n", dlerror()); + return 1; + } + printf ("PASS: dlsym factory\n"); + + AudioComponent comp = AudioComponentRegister (&desc, + CFSTR ("Pascal: Pascalcompressor"), + 0x00010000, + (AudioComponentFactoryFunction) factory); + if (!comp) + { + printf ("FAIL: AudioComponentRegister\n"); + return 1; + } + printf ("PASS: AudioComponentRegister\n"); + + AudioUnit au = 0; + check (AudioComponentInstanceNew (comp, &au), "AudioComponentInstanceNew"); + if (!au) return 1; + printf (" instance %p\n", (void*) au); + + AudioUnitParameterInfo info; + memset (&info, 0, sizeof (info)); + UInt32 infoSz = sizeof (info); + for (UInt32 pid = 0; pid < 7; ++pid) + { + if (AudioUnitGetProperty (au, kAudioUnitProperty_ParameterInfo, + kAudioUnitScope_Global, pid, &info, &infoSz) == noErr) + printf (" parameter %u: default %.3f name='%s'\n", (unsigned) pid, + (double) info.defaultValue, + info.cfNameString ? CFStringGetCStringPtr (info.cfNameString, kCFStringEncodingUTF8) : "?"); + } + + AudioStreamBasicDescription fmt; + memset (&fmt, 0, sizeof (fmt)); + fmt.mSampleRate = 44100.0; + fmt.mFormatID = kAudioFormatLinearPCM; + fmt.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved; + fmt.mBytesPerPacket = 4; + fmt.mFramesPerPacket = 1; + fmt.mBytesPerFrame = 4; + fmt.mChannelsPerFrame = 2; + fmt.mBitsPerChannel = 32; + + check (AudioUnitSetProperty (au, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, &fmt, sizeof (fmt)), + "Set input format"); + check (AudioUnitSetProperty (au, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, 0, &fmt, sizeof (fmt)), + "Set output format"); + + check (AudioUnitInitialize (au), "AudioUnitInitialize"); + + AudioUnitParameter p; + memset (&p, 0, sizeof (p)); + p.mAudioUnit = au; + for (int i = 0; i < 7; ++i) + { + p.mParameterID = (AudioUnitParameterID) i; + p.mScope = kAudioUnitScope_Global; + p.mElement = 0; + AudioUnitParameterValue value = 0.0f; + OSStatus err = AudioUnitGetParameter (au, p.mParameterID, p.mScope, p.mElement, &value); + if (err == noErr) + printf (" parameter %d = %.3f\n", i, (double) value); + } + + AudioUnitParameterValue setTo = 0.5f; + check (AudioUnitSetParameter (au, 1, kAudioUnitScope_Global, 0, setTo, 0), + "Set threshold parameter"); + + const int numFrames = 512; + AudioBufferList abl; + abl.mNumberBuffers = 2; + for (int c = 0; c < 2; ++c) + { + abl.mBuffers[c].mNumberChannels = 1; + abl.mBuffers[c].mDataByteSize = numFrames * sizeof (float); + abl.mBuffers[c].mData = malloc (abl.mBuffers[c].mDataByteSize); + memset (abl.mBuffers[c].mData, 0, abl.mBuffers[c].mDataByteSize); + } + + AudioTimeStamp ts; + memset (&ts, 0, sizeof (ts)); + ts.mSampleTime = 0.0; + ts.mFlags = kAudioTimeStampSampleTimeValid; + + check (AudioUnitRender (au, NULL, &ts, 0, numFrames, &abl), "AudioUnitRender (silence)"); + + float* ch0 = (float*) abl.mBuffers[0].mData; + float peak = 0.0f; + for (int n = 0; n < numFrames; ++n) + peak = peak > ch0[n] ? peak : ch0[n]; + printf (" rendered peak: %.6f\n", (double) peak); + + for (int c = 0; c < 2; ++c) free (abl.mBuffers[c].mData); + + check (AudioComponentInstanceDispose (au), "AudioComponentInstanceDispose"); + dlclose (handle); + return 0; +} \ No newline at end of file