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.3 LANGUAGES C CXX)
project(Anamorph VERSION 0.6.4 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down
32 changes: 22 additions & 10 deletions src/PluginEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess
addAndMakeVisible (titleButton);

abControl.getActive = [this] { return processor.abActiveSlot(); };
abControl.onToggle = [this] { processor.abSwitchTo (processor.abActiveSlot() == 0 ? 1 : 0); repaint(); };
abControl.onToggle = [this] { processor.abSwitchTo (processor.abActiveSlot() == 0 ? 1 : 0); knobSweepTime = 0.45; refreshPresetDisplay(); repaint(); };
abControl.setTooltip ("A/B Compare"); // #17 (no period)
addAndMakeVisible (abControl);
copyButton.onClick = [this] { processor.abCopyToOther(); };
Expand All @@ -282,8 +282,8 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess
presetPrev.setTooltip ("Previous preset");
presetNext.setTooltip ("Next preset");
presetName.setTooltip ("Presets"); // short, no period (#12)
presetPrev.onClick = [this] { processor.getPresets().step (-1); refreshPresetDisplay(); };
presetNext.onClick = [this] { processor.getPresets().step (+1); refreshPresetDisplay(); };
presetPrev.onClick = [this] { processor.getPresets().step (-1); knobSweepTime = 0.45; refreshPresetDisplay(); };
presetNext.onClick = [this] { processor.getPresets().step (+1); knobSweepTime = 0.45; refreshPresetDisplay(); };
presetName.onClick = [this] { showPresetMenu(); };
addAndMakeVisible (presetPrev);
addAndMakeVisible (presetNext);
Expand Down Expand Up @@ -326,8 +326,8 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess
redoButton.setComponentID ("icon");
undoButton.setTooltip ("Undo");
redoButton.setTooltip ("Redo");
undoButton.onClick = [this] { processor.undo(); };
redoButton.onClick = [this] { processor.redo(); };
undoButton.onClick = [this] { processor.undo(); knobSweepTime = 0.45; refreshPresetDisplay(); };
redoButton.onClick = [this] { processor.redo(); knobSweepTime = 0.45; refreshPresetDisplay(); };
addAndMakeVisible (undoButton);
addAndMakeVisible (redoButton);

Expand All @@ -343,7 +343,7 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess

// --- WIDEN module (the Simple-mode core) ---
setupCombo (algorithmBox, pid::algorithm, "The stereo-widening algorithm."); // #4
algorithmBox.onChange = [this] { updateAlgoControls(); resized(); };
algorithmBox.onChange = [this] { knobSweepTime = 0.45; updateAlgoControls(); resized(); };
algorithmLabel.setText ("WIDEN", juce::dontSendNotification);
algorithmLabel.setJustificationType (juce::Justification::centredLeft);
algorithmLabel.setColour (juce::Label::textColourId, colours::textDim);
Expand Down Expand Up @@ -552,6 +552,11 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess
animToggle.setTooltip (tidyTip ("Smooth micro-animations on hovers, presses and switches")); // no F3 ref (#4)
settingsBackdrop.addAndMakeVisible (animToggle);
buttonAtts.add (new ButtonAttachment (processor.getAPVTS(), pid::uiAnimations, animToggle));
// Adopt the new state IMMEDIATELY on click (not on the next 24 Hz tick): so
// turning it ON makes uiAnimOn true before the next frame and the toggle's own
// slide plays, while turning it OFF makes it false first so the toggle snaps --
// the user feels the on/off difference on the switch itself (#1).
animToggle.onClick = [this] { uiAnimOn = animToggle.getToggleState(); };

// Initial cached view-state from the (recalled) parameters.
advanced = advancedToggle.getToggleState();
Expand Down Expand Up @@ -586,13 +591,11 @@ AnamorphAudioProcessorEditor::AnamorphAudioProcessorEditor (AnamorphAudioProcess
lastFrameTime = t;
stepMeterReveal (dt);
stepMicroAnims (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 @@ -847,7 +850,6 @@ 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 @@ -976,6 +978,12 @@ void AnamorphAudioProcessorEditor::stepMicroAnims (double dt)
const float rOn = uiAnimOn ? 1.0f - std::exp (-(float) dt / 0.055f) : 1.0f;
const float rPos = uiAnimOn ? 1.0f - std::exp (-(float) dt / 0.090f) : 1.0f;

// Knob/slider position only EASES while a sweep window is open (preset / A-B /
// undo / algorithm change); outside it, a value jump from the scroll wheel or
// host automation snaps instantly so it never lags or misleads (#3).
if (knobSweepTime > 0.0) knobSweepTime -= dt;
const bool sweeping = uiAnimOn && knobSweepTime > 0.0;

for (auto* c : animated)
{
auto& props = c->getProperties();
Expand Down Expand Up @@ -1015,7 +1023,7 @@ void AnamorphAudioProcessorEditor::stepMicroAnims (double dt)
// 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
float vp = (interacting || ! sweeping) ? realPos
: curr + (realPos - curr) * rPos;
if (std::abs (vp - realPos) < 0.0015f) vp = realPos;
if (std::abs (vp - curr) > 0.0004f) c->repaint();
Expand Down Expand Up @@ -1136,6 +1144,7 @@ void AnamorphAudioProcessorEditor::showPresetMenu()
if (r == 10001) { showSavePreset (true); return; }
if (r == 10002) { showLoadPreset(); return; }
processor.getPresets().load (r - 1);
knobSweepTime = 0.45; // sweep the knobs to the preset (#3)
refreshPresetDisplay();
});
}
Expand All @@ -1152,7 +1161,10 @@ void AnamorphAudioProcessorEditor::showLoadPreset()
{
const auto file = fc.getResult();
if (file.existsAsFile() && processor.getPresets().loadFile (file))
{
knobSweepTime = 0.45; // sweep the knobs to the preset (#3)
refreshPresetDisplay();
}
});
}

Expand Down
32 changes: 5 additions & 27 deletions src/PluginEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,6 @@ 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 Down Expand Up @@ -120,7 +94,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;
FadingTooltip tooltips; // fade-out tooltip window (#4)
juce::TooltipWindow tooltips { nullptr, 600 };

// Centrepiece + meters
std::unique_ptr<anamorph::gui::Vectorscope> scope;
Expand Down Expand Up @@ -234,6 +208,10 @@ class AnamorphAudioProcessorEditor : public juce::AudioProcessorEditor,
bool uiAnimOn = true;
int lastScaleIdx = -1; // applied UI-scale step (F4)
int brPrevAlgo = -1; // last Widen algorithm seen, for the bottom-right knob sweep (#8)
// Knobs/sliders only EASE to a new value during this short window, which is
// opened by a preset / A-B / undo / algorithm change; a scroll-wheel or host
// automation edit leaves it closed, so those snap and never mislead (#3).
double knobSweepTime = 0.0;

// 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
124 changes: 90 additions & 34 deletions src/PluginProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,25 @@ juce::String AnamorphAudioProcessor::soundSignature() const

void AnamorphAudioProcessor::syncCommitted()
{
committedState = apvts.copyState();
committed = currentStateSet();
committedSig = soundSignature();
lastPolledSig = committedSig;
}

// A full snapshot: parameters PLUS the live preset name + clean baseline (#6).
AnamorphAudioProcessor::StateSet AnamorphAudioProcessor::currentStateSet()
{
return { apvts.copyState(), presets.currentName(), presets.baseline() };
}

// Restore a state set: parameters (keeping the shared view params) AND the
// preset metadata, so the name + dirty-star reappear exactly as stored (#6).
void AnamorphAudioProcessor::applyStateSet (const StateSet& s)
{
applyStatePreservingView (s.params);
presets.setMeta (s.name, s.baseline);
}

void AnamorphAudioProcessor::applyStatePreservingView (const juce::ValueTree& target)
{
// Restore a snapshot but keep the CURRENT shared view/Settings params (#10/#13).
Expand All @@ -161,13 +175,14 @@ void AnamorphAudioProcessor::pollUndoCoalesce()
{
const auto sig = soundSignature();
// Commit only once a sound edit has SETTLED (signature stable for a tick),
// folding a whole knob gesture into a single undo step.
// folding a whole knob gesture into a single undo step. The pushed entry is
// the PREVIOUS state set (its own name + baseline), so undo restores them (#6).
if (sig != committedSig && sig == lastPolledSig)
{
abUndo[abActive].undo.push_back (committedState);
abUndo[abActive].undo.push_back (committed);
if (abUndo[abActive].undo.size() > 128) abUndo[abActive].undo.erase (abUndo[abActive].undo.begin());
abUndo[abActive].redo.clear();
committedState = apvts.copyState();
committed = currentStateSet(); // captures the NOW-current preset name/baseline
committedSig = sig;
}
lastPolledSig = sig;
Expand All @@ -177,60 +192,67 @@ void AnamorphAudioProcessor::undo()
{
auto& st = abUndo[abActive];
if (st.undo.empty()) return;
st.redo.push_back (apvts.copyState());
auto target = st.undo.back(); st.undo.pop_back();
applyStatePreservingView (target);
syncCommitted();
st.redo.push_back (currentStateSet());
committed = st.undo.back(); st.undo.pop_back();
applyStateSet (committed);
committedSig = soundSignature();
lastPolledSig = committedSig;
}

void AnamorphAudioProcessor::redo()
{
auto& st = abUndo[abActive];
if (st.redo.empty()) return;
st.undo.push_back (apvts.copyState());
auto target = st.redo.back(); st.redo.pop_back();
applyStatePreservingView (target);
syncCommitted();
st.undo.push_back (currentStateSet());
committed = st.redo.back(); st.redo.pop_back();
applyStateSet (committed);
committedSig = soundSignature();
lastPolledSig = committedSig;
}

// ----------------------------------------------------------------------------
// A/B compare
// ----------------------------------------------------------------------------
void AnamorphAudioProcessor::abEnsureInit()
{
if (! abSlotA.isValid()) abSlotA = apvts.copyState();
if (! abSlotB.isValid()) abSlotB = abSlotA.createCopy();
if (! abSlot[0].isValid()) abSlot[0] = currentStateSet();
if (! abSlot[1].isValid())
{
abSlot[1] = abSlot[0];
abSlot[1].params = abSlot[0].params.createCopy(); // independent tree
}
}

void AnamorphAudioProcessor::abApplySlot (int slot)
{
// The "view" + "settings" params live in a SINGLE shared store: they are not
// part of A/B and never swap (feedback #13 / #15). Same list as undo/presets.
applyStatePreservingView (slot == 1 ? abSlotB : abSlotA);
// Read the WHOLE target state set: params (keeping the shared view params) AND
// its preset name + dirty baseline, so switching shows that slot's own name,
// never the previous slot's (#6). View/Settings params never swap (#13/#15).
applyStateSet (abSlot[slot]);
}

void AnamorphAudioProcessor::abSwitchTo (int slot)
{
abEnsureInit();
if (slot == abActive) return;
(abActive == 1 ? abSlotB : abSlotA) = apvts.copyState(); // store edits in the old slot
abMatchGain[abActive] = engine.getMatchGainDb(); // remember this slot's match (#23)
abSlot[abActive] = currentStateSet(); // store the whole state set in the old slot
abMatchGain[abActive] = engine.getMatchGainDb(); // remember this slot's match (#23)
abActive = slot;
abApplySlot (slot);
engine.injectMatchGainDb (abMatchGain[slot]); // restore the new slot's match (#23)
syncCommitted(); // the switch itself isn't undoable (#11)
engine.injectMatchGainDb (abMatchGain[slot]); // restore the new slot's match (#23)
syncCommitted(); // the switch itself isn't undoable (#11)
}

void AnamorphAudioProcessor::abCopyToOther()
{
abEnsureInit();
(abActive == 1 ? abSlotB : abSlotA) = apvts.copyState();
abSlot[abActive] = currentStateSet();
const int other = abActive == 1 ? 0 : 1;
// Record the target slot's pre-copy state so undoing on that slot reverts the
// Copy without disturbing the active slot's history (#12).
abUndo[other].undo.push_back ((other == 1 ? abSlotB : abSlotA).createCopy());
abUndo[other].undo.push_back (abSlot[other]);
abUndo[other].redo.clear();
(other == 1 ? abSlotB : abSlotA) = apvts.copyState().createCopy();
abSlot[other] = currentStateSet(); // overwrite the other slot with the FULL state set (#6)
}

// ----------------------------------------------------------------------------
Expand All @@ -243,12 +265,18 @@ void AnamorphAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
abEnsureInit();
juce::ValueTree root ("AnamorphRoot");
root.setProperty ("presetName", presets.currentName(), nullptr); // remembered across sessions (F2)
root.setProperty ("presetName", presets.currentName(), nullptr); // remembered across sessions (F2)
root.setProperty ("presetBaseline", presets.baseline(), nullptr); // so the dirty-star survives reload (#6)
root.appendChild (apvts.copyState(), nullptr);
juce::ValueTree ab ("AB");
ab.setProperty ("active", abActive, nullptr);
ab.setProperty ("slotA", abSlotA.toXmlString(), nullptr);
ab.setProperty ("slotB", abSlotB.toXmlString(), nullptr);
// Each slot carries its params AND its preset name + baseline (#6).
ab.setProperty ("slotAParams", abSlot[0].params.toXmlString(), nullptr);
ab.setProperty ("slotAName", abSlot[0].name, nullptr);
ab.setProperty ("slotABase", abSlot[0].baseline, nullptr);
ab.setProperty ("slotBParams", abSlot[1].params.toXmlString(), nullptr);
ab.setProperty ("slotBName", abSlot[1].name, nullptr);
ab.setProperty ("slotBBase", abSlot[1].baseline, nullptr);
root.appendChild (ab, nullptr);

if (auto xml = root.createXml())
Expand All @@ -261,31 +289,59 @@ void AnamorphAudioProcessor::setStateInformation (const void* data, int sizeInBy
if (xml == nullptr) return;

auto root = juce::ValueTree::fromXml (*xml);
juce::String restoredName, restoredBaseline;
bool haveBaseline = false;
if (root.hasType ("AnamorphRoot"))
{
auto params = root.getChildWithName (apvts.state.getType());
if (params.isValid()) apvts.replaceState (params.createCopy());

restoredName = root.getProperty ("presetName").toString();
if (root.hasProperty ("presetBaseline"))
{
restoredBaseline = root.getProperty ("presetBaseline").toString();
haveBaseline = true;
}

auto ab = root.getChildWithName ("AB");
if (ab.isValid())
{
abActive = (int) ab.getProperty ("active", 0);
if (auto a = juce::parseXML (ab.getProperty ("slotA").toString())) abSlotA = juce::ValueTree::fromXml (*a);
if (auto b = juce::parseXML (ab.getProperty ("slotB").toString())) abSlotB = juce::ValueTree::fromXml (*b);
auto readSlot = [&ab] (StateSet& dst, const char* pk, const char* nk, const char* bk,
const char* legacyKey)
{
if (ab.hasProperty (pk))
{
if (auto x = juce::parseXML (ab.getProperty (pk).toString()))
dst.params = juce::ValueTree::fromXml (*x);
dst.name = ab.getProperty (nk).toString();
dst.baseline = ab.getProperty (bk).toString();
}
else if (ab.hasProperty (legacyKey)) // pre-0.6.4 slots: params only
{
if (auto x = juce::parseXML (ab.getProperty (legacyKey).toString()))
dst.params = juce::ValueTree::fromXml (*x);
}
};
readSlot (abSlot[0], "slotAParams", "slotAName", "slotABase", "slotA");
readSlot (abSlot[1], "slotBParams", "slotBName", "slotBBase", "slotB");
}
}
else if (xml->hasTagName (apvts.state.getType())) // backward-compat (v0.2)
{
apvts.replaceState (juce::ValueTree::fromXml (*xml));
}

// Fresh session: clear undo history and re-baseline.
// Fresh session: clear undo history.
abUndo[0] = {}; abUndo[1] = {};
syncCommitted();

// Adopt the remembered preset name (clean baseline = the restored state).
presets.adoptRestoredState (root.hasType ("AnamorphRoot")
? root.getProperty ("presetName").toString() : juce::String());
// Adopt the remembered preset name + baseline so the dirty-star is reproduced
// (#6); fall back to a clean baseline at the restored state when absent.
if (haveBaseline) presets.setMeta (restoredName.isNotEmpty() ? restoredName : presets.currentName(),
restoredBaseline);
else presets.adoptRestoredState (restoredName);

syncCommitted();
}

// ----------------------------------------------------------------------------
Expand Down
Loading
Loading