mirror of
https://codeberg.org/armin/terion.git
synced 2026-10-11 16:41:53 +02:00
Add mono toggle, wider scale dropdown, dynamic UI scaling
This commit is contained in:
parent
9ee92e515c
commit
d15e61b96f
6 changed files with 90 additions and 30 deletions
|
|
@ -4,12 +4,17 @@ project(Terion VERSION 1.0.0 LANGUAGES C CXX)
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
# Locate JUCE: prefer an explicit TERION_JUCE_DIR, then ~/JUCE, then the
|
# Locate JUCE: prefer an explicit TERION_JUCE_DIR, then ~/JUCE,
|
||||||
# vendored copy at third_party/JUCE (populated by `make juce`).
|
# ~/.local/git/juce, ~/.local/git/JUCE, then the vendored copy at
|
||||||
|
# third_party/JUCE (populated by `make juce`).
|
||||||
if(TERION_JUCE_DIR)
|
if(TERION_JUCE_DIR)
|
||||||
set(JUCE_DIR_SELECTED "${TERION_JUCE_DIR}")
|
set(JUCE_DIR_SELECTED "${TERION_JUCE_DIR}")
|
||||||
elseif(EXISTS "$ENV{HOME}/JUCE/CMakeLists.txt")
|
elseif(EXISTS "$ENV{HOME}/JUCE/CMakeLists.txt")
|
||||||
set(JUCE_DIR_SELECTED "$ENV{HOME}/JUCE")
|
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")
|
elseif(EXISTS "${CMAKE_SOURCE_DIR}/third_party/JUCE/CMakeLists.txt")
|
||||||
set(JUCE_DIR_SELECTED "${CMAKE_SOURCE_DIR}/third_party/JUCE")
|
set(JUCE_DIR_SELECTED "${CMAKE_SOURCE_DIR}/third_party/JUCE")
|
||||||
else()
|
else()
|
||||||
|
|
|
||||||
14
Makefile
14
Makefile
|
|
@ -4,6 +4,8 @@
|
||||||
BUILD_DIR ?= build
|
BUILD_DIR ?= build
|
||||||
LOCAL_JUCE ?= third_party/JUCE
|
LOCAL_JUCE ?= third_party/JUCE
|
||||||
HOME_JUCE := $(HOME)/JUCE
|
HOME_JUCE := $(HOME)/JUCE
|
||||||
|
XDG_JUCE_LOWER := $(HOME)/.local/git/juce
|
||||||
|
XDG_JUCE_UPPER := $(HOME)/.local/git/JUCE
|
||||||
CMAKE ?= cmake
|
CMAKE ?= cmake
|
||||||
CONFIG ?= Release
|
CONFIG ?= Release
|
||||||
JUCE_REPO ?= https://github.com/juce-framework/JUCE.git
|
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
|
VST3_DIR := $(HOME)/Library/Audio/Plug-Ins/VST3/Terion.vst3
|
||||||
AU_DIR := $(HOME)/Library/Audio/Plug-Ins/Components/Terion.component
|
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)),)
|
ifeq ($(strip $(JUCE_DIR)),)
|
||||||
ifneq ($(wildcard $(HOME_JUCE)/CMakeLists.txt),)
|
ifneq ($(wildcard $(HOME_JUCE)/CMakeLists.txt),)
|
||||||
JUCE_DIR := $(HOME_JUCE)
|
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
|
else
|
||||||
JUCE_DIR := $(LOCAL_JUCE)
|
JUCE_DIR := $(LOCAL_JUCE)
|
||||||
endif
|
endif
|
||||||
|
|
@ -28,7 +35,8 @@ endif
|
||||||
all: configure
|
all: configure
|
||||||
$(CMAKE) --build $(BUILD_DIR) --config $(CONFIG) -j$(JOBS)
|
$(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:
|
juce:
|
||||||
@if [ ! -f "$(JUCE_DIR)/CMakeLists.txt" ]; then \
|
@if [ ! -f "$(JUCE_DIR)/CMakeLists.txt" ]; then \
|
||||||
if [ ! -d "$(dir $(JUCE_DIR))" ]; then mkdir -p "$(dir $(JUCE_DIR))"; fi; \
|
if [ ! -d "$(dir $(JUCE_DIR))" ]; then mkdir -p "$(dir $(JUCE_DIR))"; fi; \
|
||||||
|
|
@ -94,7 +102,7 @@ deepclean: clean
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " all configure + build"
|
@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 " configure run CMake configure against $(BUILD_DIR)"
|
||||||
@echo " build compile the configured tree"
|
@echo " build compile the configured tree"
|
||||||
@echo " sign codesign both installed plugin bundles (ad-hoc by default)"
|
@echo " sign codesign both installed plugin bundles (ad-hoc by default)"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
{
|
{
|
||||||
setSize(kBaseWidth, kBaseHeight);
|
setSize(kBaseWidth, kBaseHeight);
|
||||||
setResizable(true, true);
|
setResizable(true, true);
|
||||||
setResizeLimits(kBaseWidth, kBaseHeight, 1200, 1200);
|
|
||||||
|
|
||||||
addAndMakeVisible(scope);
|
addAndMakeVisible(scope);
|
||||||
|
|
||||||
|
|
@ -47,6 +46,14 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
freezeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
|
freezeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
|
||||||
processorRef.apvts, "freeze", freezeButton);
|
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<juce::AudioProcessorValueTreeState::ButtonAttachment>(
|
||||||
|
processorRef.apvts, "mono", monoButton);
|
||||||
|
|
||||||
// Scale dropdown
|
// Scale dropdown
|
||||||
addAndMakeVisible(scaleBox);
|
addAndMakeVisible(scaleBox);
|
||||||
scaleBox.addItem("100%", 1);
|
scaleBox.addItem("100%", 1);
|
||||||
|
|
@ -58,6 +65,10 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
scaleBox.onChange = [this] { scaleChanged(); };
|
scaleBox.onChange = [this] { scaleChanged(); };
|
||||||
scaleChanged();
|
scaleChanged();
|
||||||
|
|
||||||
|
// After any scale change, recompute the resize limits.
|
||||||
|
const int maxDim = static_cast<int>(kBaseWidth * kMaxScale);
|
||||||
|
setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim);
|
||||||
|
|
||||||
// Knob labels (separate from knobs to ensure correct z-order)
|
// Knob labels (separate from knobs to ensure correct z-order)
|
||||||
auto setupLabel = [this](juce::Label& label, const juce::String& text)
|
auto setupLabel = [this](juce::Label& label, const juce::String& text)
|
||||||
{
|
{
|
||||||
|
|
@ -89,7 +100,8 @@ void TerionProcessorEditor::scaleChanged()
|
||||||
default: currentScale = 1.25f; break;
|
default: currentScale = 1.25f; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTransform(juce::AffineTransform::scale(currentScale));
|
const int maxDim = static_cast<int>(kBaseWidth * kMaxScale);
|
||||||
|
setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim);
|
||||||
setSize(static_cast<int>(kBaseWidth * currentScale),
|
setSize(static_cast<int>(kBaseWidth * currentScale),
|
||||||
static_cast<int>(kBaseHeight * currentScale));
|
static_cast<int>(kBaseHeight * currentScale));
|
||||||
}
|
}
|
||||||
|
|
@ -98,49 +110,62 @@ void TerionProcessorEditor::paint(juce::Graphics& g)
|
||||||
{
|
{
|
||||||
g.fillAll(juce::Colour(0xff0a0a0f));
|
g.fillAll(juce::Colour(0xff0a0a0f));
|
||||||
|
|
||||||
|
const float s = currentScale;
|
||||||
|
const int footerH = static_cast<int>(kFooterHeight * s);
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
auto bounds = getLocalBounds();
|
auto bounds = getLocalBounds();
|
||||||
auto footer = bounds.removeFromBottom(kFooterHeight);
|
auto footer = bounds.removeFromBottom(footerH);
|
||||||
|
|
||||||
g.setColour(juce::Colour(0xff15151e));
|
g.setColour(juce::Colour(0xff15151e));
|
||||||
g.fillRect(footer);
|
g.fillRect(footer);
|
||||||
|
|
||||||
g.setColour(juce::Colour(0xff555566));
|
g.setColour(juce::Colour(0xff555566));
|
||||||
g.setFont(9.0f);
|
g.setFont(juce::Font(juce::FontOptions(9.0f * s)));
|
||||||
|
|
||||||
juce::String footerText = "Terion "
|
juce::String footerText = "Terion "
|
||||||
+ juce::String(TERION_GIT_HASH)
|
+ juce::String(TERION_GIT_HASH)
|
||||||
+ " | " + juce::String(TERION_BUILD_TIME)
|
+ " | " + juce::String(TERION_BUILD_TIME)
|
||||||
+ " | " + juce::String(TERION_GIT_LOCAL_COMMITS) + " local";
|
+ " | " + juce::String(TERION_GIT_LOCAL_COMMITS) + " local";
|
||||||
|
|
||||||
g.drawFittedText(footerText, footer.reduced(10, 0),
|
g.drawFittedText(footerText, footer.reduced(static_cast<int>(10.0f * s), 0),
|
||||||
juce::Justification::centredLeft, 1);
|
juce::Justification::centredLeft, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerionProcessorEditor::resized()
|
void TerionProcessorEditor::resized()
|
||||||
{
|
{
|
||||||
auto bounds = getLocalBounds();
|
const float s = currentScale;
|
||||||
auto footer = bounds.removeFromBottom(kFooterHeight);
|
const int footerH = static_cast<int>(kFooterHeight * s);
|
||||||
|
const int knobs = static_cast<int>(kKnobSize * s);
|
||||||
|
const int margin = static_cast<int>(kKnobMargin * s);
|
||||||
|
const int scaleW = static_cast<int>(76.0f * s);
|
||||||
|
const int monoW = static_cast<int>(72.0f * s);
|
||||||
|
|
||||||
// Footer: scale dropdown on the right
|
auto bounds = getLocalBounds();
|
||||||
scaleBox.setBounds(footer.getWidth() - 60, 1, 56, kFooterHeight - 2);
|
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 fills the remaining area
|
||||||
scope.setBounds(bounds);
|
scope.setBounds(bounds);
|
||||||
|
|
||||||
// Controls layout: top-right corner, horizontal row
|
// Controls layout: top-right corner, horizontal row
|
||||||
const int startX = bounds.getWidth() - (4 * (kKnobSize + kKnobMargin) + kKnobSize + kKnobMargin);
|
const int startX = bounds.getWidth() - (4 * (knobs + margin) + knobs + margin);
|
||||||
const int startY = 10;
|
const int startY = static_cast<int>(10.0f * s);
|
||||||
|
|
||||||
fadeKnob.setBounds(startX, startY, kKnobSize, kKnobSize);
|
fadeKnob.setBounds(startX, startY, knobs, knobs);
|
||||||
sensitivityKnob.setBounds(startX + (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize);
|
sensitivityKnob.setBounds(startX + (knobs + margin), startY, knobs, knobs);
|
||||||
dotSizeKnob.setBounds(startX + 2 * (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize);
|
dotSizeKnob.setBounds(startX + 2 * (knobs + margin), startY, knobs, knobs);
|
||||||
smoothingKnob.setBounds(startX + 3 * (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize);
|
smoothingKnob.setBounds(startX + 3 * (knobs + margin), startY, knobs, knobs);
|
||||||
freezeButton.setBounds(startX + 4 * (kKnobSize + kKnobMargin), startY + 10, kKnobSize, kKnobSize - 20);
|
freezeButton.setBounds(startX + 4 * (knobs + margin), startY + static_cast<int>(10.0f * s),
|
||||||
|
knobs, knobs - static_cast<int>(20.0f * s));
|
||||||
|
|
||||||
const int labelY = startY + kKnobSize - 6;
|
const int labelY = startY + knobs - static_cast<int>(6.0f * s);
|
||||||
fadeLabel.setBounds(fadeKnob.getX(), labelY, kKnobSize, 12);
|
const int labelH = static_cast<int>(12.0f * s);
|
||||||
sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, kKnobSize, 12);
|
fadeLabel.setBounds(fadeKnob.getX(), labelY, knobs, labelH);
|
||||||
dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, kKnobSize, 12);
|
sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, knobs, labelH);
|
||||||
smoothingLabel.setBounds(smoothingKnob.getX(), labelY, kKnobSize, 12);
|
dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, knobs, labelH);
|
||||||
|
smoothingLabel.setBounds(smoothingKnob.getX(), labelY, knobs, labelH);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ private:
|
||||||
ArcKnob dotSizeKnob;
|
ArcKnob dotSizeKnob;
|
||||||
ArcKnob smoothingKnob;
|
ArcKnob smoothingKnob;
|
||||||
juce::ToggleButton freezeButton;
|
juce::ToggleButton freezeButton;
|
||||||
|
juce::ToggleButton monoButton;
|
||||||
juce::ComboBox scaleBox;
|
juce::ComboBox scaleBox;
|
||||||
juce::Label fadeLabel, sensitivityLabel, dotSizeLabel, smoothingLabel;
|
juce::Label fadeLabel, sensitivityLabel, dotSizeLabel, smoothingLabel;
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ private:
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> dotSizeAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> dotSizeAttachment;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> smoothingAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> smoothingAttachment;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> freezeAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> freezeAttachment;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> monoAttachment;
|
||||||
|
|
||||||
float currentScale = 1.0f;
|
float currentScale = 1.0f;
|
||||||
static constexpr int kBaseWidth = 420;
|
static constexpr int kBaseWidth = 420;
|
||||||
|
|
@ -39,6 +41,7 @@ private:
|
||||||
static constexpr int kFooterHeight = 28;
|
static constexpr int kFooterHeight = 28;
|
||||||
static constexpr int kKnobSize = 54;
|
static constexpr int kKnobSize = 54;
|
||||||
static constexpr int kKnobMargin = 6;
|
static constexpr int kKnobMargin = 6;
|
||||||
|
static constexpr float kMaxScale = 3.0f;
|
||||||
|
|
||||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TerionProcessorEditor)
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TerionProcessorEditor)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ TerionProcessor::TerionProcessor()
|
||||||
std::make_unique<juce::AudioParameterBool>(
|
std::make_unique<juce::AudioParameterBool>(
|
||||||
juce::ParameterID{"freeze", 5},
|
juce::ParameterID{"freeze", 5},
|
||||||
"Freeze",
|
"Freeze",
|
||||||
|
false),
|
||||||
|
std::make_unique<juce::AudioParameterBool>(
|
||||||
|
juce::ParameterID{"mono", 6},
|
||||||
|
"Mono",
|
||||||
false)
|
false)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
|
@ -46,8 +50,10 @@ void TerionProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiB
|
||||||
juce::ScopedNoDenormals noDenormals;
|
juce::ScopedNoDenormals noDenormals;
|
||||||
|
|
||||||
const int numSamples = buffer.getNumSamples();
|
const int numSamples = buffer.getNumSamples();
|
||||||
const float* left = buffer.getReadPointer(0);
|
float* leftOut = buffer.getWritePointer(0);
|
||||||
const float* right = buffer.getNumChannels() > 1 ? buffer.getReadPointer(1) : left;
|
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 fadeTime = apvts.getRawParameterValue("fadeTime")->load();
|
||||||
const float agePerSample = 1.0f / (fadeTime * static_cast<float>(getSampleRate()));
|
const float agePerSample = 1.0f / (fadeTime * static_cast<float>(getSampleRate()));
|
||||||
|
|
@ -55,6 +61,18 @@ void TerionProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiB
|
||||||
const float sensGain = juce::Decibels::decibelsToGain(apvts.getRawParameterValue("sensitivity")->load());
|
const float sensGain = juce::Decibels::decibelsToGain(apvts.getRawParameterValue("sensitivity")->load());
|
||||||
const float smoothCoef = apvts.getRawParameterValue("smoothing")->load();
|
const float smoothCoef = apvts.getRawParameterValue("smoothing")->load();
|
||||||
const float frozen = apvts.getRawParameterValue("freeze")->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
|
// Reset metrics for this block
|
||||||
sumMid = 0.0f;
|
sumMid = 0.0f;
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,8 @@ public:
|
||||||
sensitivity = 2,
|
sensitivity = 2,
|
||||||
dotSize = 3,
|
dotSize = 3,
|
||||||
smoothing = 4,
|
smoothing = 4,
|
||||||
freeze = 5
|
freeze = 5,
|
||||||
|
mono = 6
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue