Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ cmake_minimum_required(VERSION 3.22)
# cmake --build build --config Release
# ============================================================================

project(Anamorph VERSION 0.6.2 LANGUAGES C CXX)
project(Anamorph VERSION 0.6.3 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down
88 changes: 44 additions & 44 deletions src/PluginEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -586,12 +586,13 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess
lastFrameTime = t;
stepMeterReveal (dt);
stepMicroAnims (dt);
stepScaleAnim (dt);
tooltips.step ((float) dt); // tooltip fade-out (#4)
});
}

AnamorphAudioProcessorEditor::~AnamorphAudioProcessorEditor()
{
tooltips.animOn = false; // no fade stepping during teardown (#4)
stopTimer();
openGLContext.detach();
channelModeBox.setLookAndFeel (nullptr);
Expand Down Expand Up @@ -698,13 +699,39 @@ void AnamorphAudioProcessorEditor::applyScopePersist()
void AnamorphAudioProcessorEditor::updateAlgoControls()
{
const int algo = algorithmBox.getSelectedItemIndex();

// The bottom-right Widen slot hosts a DIFFERENT knob per algorithm (Haas
// Delay <-> Velvet Density). When the algorithm swaps which knob lives there,
// hand the OUTGOING knob's visual position to the INCOMING one so the slot's
// knob sweeps continuously to its new value instead of teleporting (#8). The
// Chorus layout has two knobs in that area and is deliberately left to their
// own separate vpos registers (#8).
auto getVp = [] (juce::Slider& s)
{
return (float) (double) s.getProperties().getWithDefault (
"vpos", (double) s.valueToProportionOfLength (s.getValue()));
};
float oldBR = -1.0f;
if (algo != brPrevAlgo && brPrevAlgo >= 0)
{
if (haasDelayK.isVisible()) oldBR = getVp (haasDelayK); // leaving Haas
else if (velvetK.isVisible()) oldBR = getVp (velvetK); // leaving Velvet
}

haasDelayK.setVisible (algo == 0); haasDelayL.setVisible (algo == 0);
velvetK.setVisible (algo == 1); velvetL.setVisible (algo == 1);
chorusRateK.setVisible (algo == 2); chorusRateL.setVisible (algo == 2);
chorusDepthK.setVisible (algo == 2);chorusDepthL.setVisible (algo == 2);
haasSideBox.setVisible (algo == 0);
dimModeBox.setVisible (algo == 3);

if (oldBR >= 0.0f)
{
if (algo == 0) haasDelayK.getProperties().set ("vpos", (double) oldBR); // -> Haas Delay
else if (algo == 1) velvetK.getProperties().set ("vpos", (double) oldBR); // -> Velvet Density
}
brPrevAlgo = algo;

// Caption over the side/voicing combo (#9): one intuitive word per algorithm.
algoOptLabel.setVisible (algo == 0 || algo == 3);
algoOptLabel.setText (algo == 0 ? "FOCUS" : algo == 3 ? "STYLE" : juce::String(), // #13
Expand Down Expand Up @@ -820,6 +847,7 @@ void AnamorphAudioProcessorEditor::timerCallback()
updateMsLabels();

uiAnimOn = animToggle.getToggleState(); // micro-anims follow the Settings switch (F3)
tooltips.animOn = uiAnimOn; // tooltip fade follows the same switch (#4)

// Whole-window scale: follow the parameter (Settings combo, state recall, F4).
if (uiScaleBox.getSelectedItemIndex() != lastScaleIdx)
Expand Down Expand Up @@ -980,26 +1008,18 @@ void AnamorphAudioProcessorEditor::stepMicroAnims (double dt)
|| (bool) props.getWithDefault ("dragging", false);
actT = interacting ? 1.0f : 0.0f;

// Rotary knobs ease their pointer/arc toward the live value, so a
// preset or A/B switch SWEEPS the knob instead of teleporting it (#5).
// While the user turns it, snap so the knob stays 1:1 (no lag). vpos is
// ALWAYS kept current (even at rest) so there's a real "from" position
// to ease out of the instant the value jumps.
const auto st = s->getSliderStyle();
const bool rotary = st == juce::Slider::RotaryVerticalDrag
|| st == juce::Slider::RotaryHorizontalDrag
|| st == juce::Slider::RotaryHorizontalVerticalDrag
|| st == juce::Slider::Rotary;
if (rotary)
{
const float realPos = (float) s->valueToProportionOfLength (s->getValue());
const float curr = (float) (double) props.getWithDefault (vpos, (double) realPos);
float vp = (interacting || ! uiAnimOn) ? realPos
: curr + (realPos - curr) * rPos;
if (std::abs (vp - realPos) < 0.0015f) vp = realPos;
if (std::abs (vp - curr) > 0.0004f) c->repaint();
props.set (vpos, vp);
}
// ROTARY knobs AND LINEAR sliders ease their drawn position toward the
// live value, so a preset / A-B switch SWEEPS them instead of teleporting
// (#5/#7). While the user turns the control, snap so it stays 1:1 (no
// lag). vpos is kept current at rest so there's a real "from" position to
// ease out of the instant the value jumps.
const float realPos = (float) s->valueToProportionOfLength (s->getValue());
const float curr = (float) (double) props.getWithDefault (vpos, (double) realPos);
float vp = (interacting || ! uiAnimOn) ? realPos
: curr + (realPos - curr) * rPos;
if (std::abs (vp - realPos) < 0.0015f) vp = realPos;
if (std::abs (vp - curr) > 0.0004f) c->repaint();
props.set (vpos, vp);
}
else if (auto* t = dynamic_cast<juce::ToggleButton*> (c))
onT = t->getToggleState() ? 1.0f : 0.0f;
Expand All @@ -1014,36 +1034,16 @@ void AnamorphAudioProcessorEditor::stepMicroAnims (double dt)
// Whole-window scale (F4): the layout stays at its logical 940x720 and a plain
// transform scales the editor; every control is vector-drawn, so the result is
// crisp at any step and the composition cannot drift. The wrapper resizes the
// host window from the transformed bounds automatically.
// host window from the transformed bounds automatically. Applied as a single
// instant step -- a resize doesn't want an animation (#3).
void AnamorphAudioProcessorEditor::applyUiScale()
{
static constexpr float scales[] = { 0.75f, 0.85f, 1.0f, 1.25f, 1.5f };
const int idx = juce::jlimit (0, 4, uiScaleBox.getSelectedItemIndex());
if (idx == lastScaleIdx) return;
lastScaleIdx = idx;
uiScaleTarget = scales[idx];

// With animations off, snap. Otherwise stepScaleAnim eases the transform over
// a few frames so the window GROWS/SHRINKS smoothly in BOTH axes at once,
// which masks the host's two-step width-then-height resize -- the L-shape
// flash the single jump produced (#2).
if (! uiAnimOn || lastFrameTime <= 0.0) // also snap at construction (no vblank yet)
{
uiScaleCurrent = uiScaleTarget;
setTransform (juce::AffineTransform::scale (uiScaleCurrent));
if (openGLContext.isAttached()) openGLContext.triggerRepaint();
}
}

void AnamorphAudioProcessorEditor::stepScaleAnim (double dt)
{
if (std::abs (uiScaleCurrent - uiScaleTarget) < 0.0005f) return; // idle

uiScaleCurrent += (uiScaleTarget - uiScaleCurrent) * (1.0f - (float) std::pow (0.30, dt * 24.0));
if (std::abs (uiScaleCurrent - uiScaleTarget) < 0.003f)
uiScaleCurrent = uiScaleTarget;

setTransform (juce::AffineTransform::scale (uiScaleCurrent));
setTransform (juce::AffineTransform::scale (scales[idx]));
if (openGLContext.isAttached())
openGLContext.triggerRepaint();
}
Expand Down
34 changes: 29 additions & 5 deletions src/PluginEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor,
void paint (juce::Graphics& g) override { g.fillAll (juce::Colour (0x66090b0e)); }
};

// Tooltip window with a weak, fast non-linear fade-out so hints don't blink
// out abruptly (#4). JUCE hides a tooltip by calling setVisible(false); we
// intercept that and ramp the window alpha down over ~90 ms instead, stepped
// from the editor's vblank. Off (instant hide) when UI Animations is off.
struct FadingTooltip : public juce::TooltipWindow
{
FadingTooltip() : juce::TooltipWindow (nullptr, 600) {}
bool animOn = true;
bool fading = false;
float a = 1.0f;
void setVisible (bool shouldShow) override
{
if (shouldShow) { fading = false; a = 1.0f; setAlpha (1.0f); juce::TooltipWindow::setVisible (true); }
else if (! animOn) { juce::TooltipWindow::setVisible (false); }
else if (isVisible() && ! fading) { fading = true; a = 1.0f; } // begin fade, stay up
// (already fading: ignore; step() will hide it)
}
void step (float dt)
{
if (! fading) return;
a -= dt / 0.09f; // ~90 ms
if (a <= 0.0f) { fading = false; a = 1.0f; setAlpha (1.0f); juce::TooltipWindow::setVisible (false); }
else setAlpha (a * a); // ease-out (non-linear) tail
}
};

// A/B control: shows "A / B" with the active letter bright, the other dim,
// a single click toggles (FabFilter-style). Wrapped in a racetrack/stadium
// frame with a micro-gradient + edge glow to match the design language (#6).
Expand All @@ -69,7 +95,6 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor,
void layoutScopeArea(); // scope + meter block; re-run per frame during the reveal (#6)
void stepMeterReveal (double dt); // vsync-driven meter reveal animation (#6/#3)
void stepMicroAnims (double dt); // eased hover/press/toggle micro-animations (F3)
void stepScaleAnim (double dt); // smooth window-scale grow/shrink (#13/#2)
void registerAnimated (juce::Component&);
void mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails&) override; // Persist scroll reveal (#1)
void applyUiScale(); // whole-window XS..XL transform scale (F4)
Expand All @@ -95,7 +120,7 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor,
anamorph::gui::CompactComboLookAndFeel compactCombo; // smaller list for Input combos (#12)
anamorph::gui::SimpleComboLookAndFeel simpleCombo; // bigger text for Simple-mode Widen combos (#17)
juce::OpenGLContext openGLContext;
juce::TooltipWindow tooltips { nullptr, 600 };
FadingTooltip tooltips; // fade-out tooltip window (#4)

// Centrepiece + meters
std::unique_ptr<anamorph::gui::Vectorscope> scope;
Expand Down Expand Up @@ -207,9 +232,8 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor,
// properties the LookAndFeel blends with; repaints fire only while moving.
juce::Array<juce::Component*> animated;
bool uiAnimOn = true;
int lastScaleIdx = -1; // selected UI-scale step (F4)
float uiScaleCurrent = 1.0f; // displayed scale, eased toward the target (#2)
float uiScaleTarget = 1.0f;
int lastScaleIdx = -1; // applied UI-scale step (F4)
int brPrevAlgo = -1; // last Widen algorithm seen, for the bottom-right knob sweep (#8)

// Single fixed window for both modes: toggling Advanced relays out the
// content in place, so the host never resizes us and nothing flickers (#20).
Expand Down
12 changes: 10 additions & 2 deletions src/dsp/Correlation.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,16 @@ class CorrelationMeter
float bal = sum > 1.0e-12f ? (rrSlow - llSlow) / sum : 0.0f;
bal = bal < -1.0f ? -1.0f : (bal > 1.0f ? 1.0f : bal);
balance.store (bal, std::memory_order_relaxed);

// Fast mean-square energy so the GUI can tell "playing" from "silent" and
// glide the pointers back to centre when the input stops (#1/#2).
energy.store (llFast + rrFast, std::memory_order_relaxed);
}

float getFast() const noexcept { return fast.load (std::memory_order_relaxed); }
float getSlow() const noexcept { return slow.load (std::memory_order_relaxed); }
float getBalance() const noexcept { return balance.load (std::memory_order_relaxed); }
float getEnergy() const noexcept { return energy.load (std::memory_order_relaxed); }

private:
static float coeffFor (double sr, double ms) noexcept
Expand All @@ -82,9 +87,12 @@ class CorrelationMeter
float lrFast = 0, llFast = 0, rrFast = 0;
float lrSlow = 0, llSlow = 0, rrSlow = 0;

std::atomic<float> fast { 1.0f };
std::atomic<float> slow { 1.0f };
// Correlation has no meaning without signal: idle / silent reads as 0
// (decorrelated), not +1, so the meter sits centred at rest (#1).
std::atomic<float> fast { 0.0f };
std::atomic<float> slow { 0.0f };
std::atomic<float> balance { 0.0f };
std::atomic<float> energy { 0.0f };
};

} // namespace anamorph
12 changes: 10 additions & 2 deletions src/gui/CorrelationMeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ StereoMeter::~StereoMeter() { stopTimer(); }

void StereoMeter::timerCallback()
{
const float target = (type == Type::Balance) ? source.getBalance() : source.getSlow();
value += 0.165f * (target - value); // same ballistics as 0.3 @ 30 Hz, time-corrected
// When the input goes silent (or before any audio at all), both the phase
// correlation and the L/R balance lose all meaning, so the pointer glides
// gently back to centre (0) rather than holding at an extreme or jumping to
// +1 on open (#1/#2). Active motion stays snappy; the return-to-centre is a
// slow ~700 ms damped release so it reads as relaxing, never a snap.
const bool silent = source.getEnergy() < 6.0e-9f; // ~ -82 dBFS (sum of L^2 + R^2)
const float target = silent ? 0.0f
: (type == Type::Balance ? source.getBalance() : source.getSlow());
const float rate = silent ? 0.030f : 0.165f;
value += rate * (target - value);
repaint();
}

Expand Down
Loading
Loading