diff --git a/CMakeLists.txt b/CMakeLists.txt index 70081e5..cb10356 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,12 +4,17 @@ project(Terion VERSION 1.0.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Locate JUCE: prefer an explicit TERION_JUCE_DIR, then ~/JUCE, then the -# vendored copy at third_party/JUCE (populated by `make juce`). +# Locate JUCE: prefer an explicit TERION_JUCE_DIR, then ~/JUCE, +# ~/.local/git/juce, ~/.local/git/JUCE, then the vendored copy at +# third_party/JUCE (populated by `make juce`). if(TERION_JUCE_DIR) set(JUCE_DIR_SELECTED "${TERION_JUCE_DIR}") elseif(EXISTS "$ENV{HOME}/JUCE/CMakeLists.txt") set(JUCE_DIR_SELECTED "$ENV{HOME}/JUCE") +elseif(EXISTS "$ENV{HOME}/.local/git/juce/CMakeLists.txt") + set(JUCE_DIR_SELECTED "$ENV{HOME}/.local/git/juce") +elseif(EXISTS "$ENV{HOME}/.local/git/JUCE/CMakeLists.txt") + set(JUCE_DIR_SELECTED "$ENV{HOME}/.local/git/JUCE") elseif(EXISTS "${CMAKE_SOURCE_DIR}/third_party/JUCE/CMakeLists.txt") set(JUCE_DIR_SELECTED "${CMAKE_SOURCE_DIR}/third_party/JUCE") else() diff --git a/Makefile b/Makefile index 62342b3..aad2224 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,8 @@ BUILD_DIR ?= build LOCAL_JUCE ?= third_party/JUCE HOME_JUCE := $(HOME)/JUCE +XDG_JUCE_LOWER := $(HOME)/.local/git/juce +XDG_JUCE_UPPER := $(HOME)/.local/git/JUCE CMAKE ?= cmake CONFIG ?= Release JUCE_REPO ?= https://github.com/juce-framework/JUCE.git @@ -14,10 +16,15 @@ CODESIGN_ID ?= VST3_DIR := $(HOME)/Library/Audio/Plug-Ins/VST3/Terion.vst3 AU_DIR := $(HOME)/Library/Audio/Plug-Ins/Components/Terion.component -# Prefer ~/JUCE if present, otherwise use the vendored copy in the repo. +# Prefer a local JUCE checkout (~/JUCE, then ~/.local/git/{juce,JUCE}) if +# present, otherwise use the vendored copy in the repo. ifeq ($(strip $(JUCE_DIR)),) ifneq ($(wildcard $(HOME_JUCE)/CMakeLists.txt),) JUCE_DIR := $(HOME_JUCE) + else ifneq ($(wildcard $(XDG_JUCE_LOWER)/CMakeLists.txt),) + JUCE_DIR := $(XDG_JUCE_LOWER) + else ifneq ($(wildcard $(XDG_JUCE_UPPER)/CMakeLists.txt),) + JUCE_DIR := $(XDG_JUCE_UPPER) else JUCE_DIR := $(LOCAL_JUCE) endif @@ -28,7 +35,8 @@ endif all: configure $(CMAKE) --build $(BUILD_DIR) --config $(CONFIG) -j$(JOBS) -# Ensure a JUCE source tree is available: use ~/JUCE, else clone into third_party/. +# Ensure a JUCE source tree is available: use a local checkout +# (~/JUCE or ~/.local/git/{juce,JUCE}), else clone into third_party/. juce: @if [ ! -f "$(JUCE_DIR)/CMakeLists.txt" ]; then \ if [ ! -d "$(dir $(JUCE_DIR))" ]; then mkdir -p "$(dir $(JUCE_DIR))"; fi; \ @@ -94,7 +102,7 @@ deepclean: clean help: @echo "Targets:" @echo " all configure + build" - @echo " juce clone JUCE into third_party/JUCE if missing (else use ~/JUCE)" + @echo " juce clone JUCE into third_party/JUCE if missing (else use ~/JUCE or ~/.local/git/{juce,JUCE})" @echo " configure run CMake configure against $(BUILD_DIR)" @echo " build compile the configured tree" @echo " sign codesign both installed plugin bundles (ad-hoc by default)" diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index db52d29..32f05c5 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -11,7 +11,6 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p) { setSize(kBaseWidth, kBaseHeight); setResizable(true, true); - setResizeLimits(kBaseWidth, kBaseHeight, 1200, 1200); addAndMakeVisible(scope); @@ -47,6 +46,14 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p) freezeAttachment = std::make_unique( processorRef.apvts, "freeze", freezeButton); + // Mono button + addAndMakeVisible(monoButton); + monoButton.setButtonText("MONO"); + monoButton.setColour(juce::ToggleButton::textColourId, juce::Colour(0xff888899)); + monoButton.setColour(juce::ToggleButton::tickColourId, juce::Colour(0xff7ecfcf)); + monoAttachment = std::make_unique( + processorRef.apvts, "mono", monoButton); + // Scale dropdown addAndMakeVisible(scaleBox); scaleBox.addItem("100%", 1); @@ -58,6 +65,10 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p) scaleBox.onChange = [this] { scaleChanged(); }; scaleChanged(); + // After any scale change, recompute the resize limits. + const int maxDim = static_cast(kBaseWidth * kMaxScale); + setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim); + // Knob labels (separate from knobs to ensure correct z-order) auto setupLabel = [this](juce::Label& label, const juce::String& text) { @@ -89,7 +100,8 @@ void TerionProcessorEditor::scaleChanged() default: currentScale = 1.25f; break; } - setTransform(juce::AffineTransform::scale(currentScale)); + const int maxDim = static_cast(kBaseWidth * kMaxScale); + setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim); setSize(static_cast(kBaseWidth * currentScale), static_cast(kBaseHeight * currentScale)); } @@ -98,49 +110,62 @@ void TerionProcessorEditor::paint(juce::Graphics& g) { g.fillAll(juce::Colour(0xff0a0a0f)); + const float s = currentScale; + const int footerH = static_cast(kFooterHeight * s); + // Footer auto bounds = getLocalBounds(); - auto footer = bounds.removeFromBottom(kFooterHeight); + auto footer = bounds.removeFromBottom(footerH); g.setColour(juce::Colour(0xff15151e)); g.fillRect(footer); g.setColour(juce::Colour(0xff555566)); - g.setFont(9.0f); + g.setFont(juce::Font(juce::FontOptions(9.0f * s))); juce::String footerText = "Terion " + juce::String(TERION_GIT_HASH) + " | " + juce::String(TERION_BUILD_TIME) + " | " + juce::String(TERION_GIT_LOCAL_COMMITS) + " local"; - g.drawFittedText(footerText, footer.reduced(10, 0), + g.drawFittedText(footerText, footer.reduced(static_cast(10.0f * s), 0), juce::Justification::centredLeft, 1); } void TerionProcessorEditor::resized() { - auto bounds = getLocalBounds(); - auto footer = bounds.removeFromBottom(kFooterHeight); + const float s = currentScale; + const int footerH = static_cast(kFooterHeight * s); + const int knobs = static_cast(kKnobSize * s); + const int margin = static_cast(kKnobMargin * s); + const int scaleW = static_cast(76.0f * s); + const int monoW = static_cast(72.0f * s); - // Footer: scale dropdown on the right - scaleBox.setBounds(footer.getWidth() - 60, 1, 56, kFooterHeight - 2); + auto bounds = getLocalBounds(); + auto footer = bounds.removeFromBottom(footerH); + + // Footer: scale dropdown + mono toggle on the right + scaleBox.setBounds(footer.getWidth() - scaleW, 1, scaleW, footerH - 2); + monoButton.setBounds(footer.getWidth() - scaleW - monoW, 1, monoW, footerH - 2); // Scope fills the remaining area scope.setBounds(bounds); // Controls layout: top-right corner, horizontal row - const int startX = bounds.getWidth() - (4 * (kKnobSize + kKnobMargin) + kKnobSize + kKnobMargin); - const int startY = 10; + const int startX = bounds.getWidth() - (4 * (knobs + margin) + knobs + margin); + const int startY = static_cast(10.0f * s); - fadeKnob.setBounds(startX, startY, kKnobSize, kKnobSize); - sensitivityKnob.setBounds(startX + (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize); - dotSizeKnob.setBounds(startX + 2 * (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize); - smoothingKnob.setBounds(startX + 3 * (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize); - freezeButton.setBounds(startX + 4 * (kKnobSize + kKnobMargin), startY + 10, kKnobSize, kKnobSize - 20); + fadeKnob.setBounds(startX, startY, knobs, knobs); + sensitivityKnob.setBounds(startX + (knobs + margin), startY, knobs, knobs); + dotSizeKnob.setBounds(startX + 2 * (knobs + margin), startY, knobs, knobs); + smoothingKnob.setBounds(startX + 3 * (knobs + margin), startY, knobs, knobs); + freezeButton.setBounds(startX + 4 * (knobs + margin), startY + static_cast(10.0f * s), + knobs, knobs - static_cast(20.0f * s)); - const int labelY = startY + kKnobSize - 6; - fadeLabel.setBounds(fadeKnob.getX(), labelY, kKnobSize, 12); - sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, kKnobSize, 12); - dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, kKnobSize, 12); - smoothingLabel.setBounds(smoothingKnob.getX(), labelY, kKnobSize, 12); + const int labelY = startY + knobs - static_cast(6.0f * s); + const int labelH = static_cast(12.0f * s); + fadeLabel.setBounds(fadeKnob.getX(), labelY, knobs, labelH); + sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, knobs, labelH); + dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, knobs, labelH); + smoothingLabel.setBounds(smoothingKnob.getX(), labelY, knobs, labelH); } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 93be438..e618a99 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -24,6 +24,7 @@ private: ArcKnob dotSizeKnob; ArcKnob smoothingKnob; juce::ToggleButton freezeButton; + juce::ToggleButton monoButton; juce::ComboBox scaleBox; juce::Label fadeLabel, sensitivityLabel, dotSizeLabel, smoothingLabel; @@ -32,6 +33,7 @@ private: std::unique_ptr dotSizeAttachment; std::unique_ptr smoothingAttachment; std::unique_ptr freezeAttachment; + std::unique_ptr monoAttachment; float currentScale = 1.0f; static constexpr int kBaseWidth = 420; @@ -39,6 +41,7 @@ private: static constexpr int kFooterHeight = 28; static constexpr int kKnobSize = 54; static constexpr int kKnobMargin = 6; + static constexpr float kMaxScale = 3.0f; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TerionProcessorEditor) }; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index e26f834..8039c99 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -29,6 +29,10 @@ TerionProcessor::TerionProcessor() std::make_unique( juce::ParameterID{"freeze", 5}, "Freeze", + false), + std::make_unique( + juce::ParameterID{"mono", 6}, + "Mono", false) }) { @@ -46,8 +50,10 @@ void TerionProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiB juce::ScopedNoDenormals noDenormals; const int numSamples = buffer.getNumSamples(); - const float* left = buffer.getReadPointer(0); - const float* right = buffer.getNumChannels() > 1 ? buffer.getReadPointer(1) : left; + float* leftOut = buffer.getWritePointer(0); + float* rightOut = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : leftOut; + const float* left = leftOut; + const float* right = rightOut; const float fadeTime = apvts.getRawParameterValue("fadeTime")->load(); const float agePerSample = 1.0f / (fadeTime * static_cast(getSampleRate())); @@ -55,6 +61,18 @@ void TerionProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiB const float sensGain = juce::Decibels::decibelsToGain(apvts.getRawParameterValue("sensitivity")->load()); const float smoothCoef = apvts.getRawParameterValue("smoothing")->load(); const float frozen = apvts.getRawParameterValue("freeze")->load(); + const float mono = apvts.getRawParameterValue("mono")->load(); + + // Collapse to mono: sum L/R and duplicate into both channels + if (mono > 0.5f && buffer.getNumChannels() > 1) + { + for (int i = 0; i < numSamples; ++i) + { + float m = (left[i] + right[i]) * 0.5f; + leftOut[i] = m; + rightOut[i] = m; + } + } // Reset metrics for this block sumMid = 0.0f; diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index beaabcb..1097123 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -67,7 +67,8 @@ public: sensitivity = 2, dotSize = 3, smoothing = 4, - freeze = 5 + freeze = 5, + mono = 6 }; private: