Skip to content

Instantly share code, notes, and snippets.

@yairchu
Last active March 31, 2024 05:05
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yairchu/0a70e553af6176b2f6af to your computer and use it in GitHub Desktop.
Save yairchu/0a70e553af6176b2f6af to your computer and use it in GitHub Desktop.
AudioProcessorUndoAttachment for JUCE
#include "AudioProcessorUndoAttachment.h"
using namespace juce;
class AudioProcessorUndoAttachment::ChangeAction : public UndoableAction
{
public:
ChangeAction (AudioProcessor*);
bool perform() override;
bool undo() override;
UndoableAction* createCoalescedAction (UndoableAction*) override;
int getSizeInUnits() override;
// A change can include several parameters, due to multi-touch or coalescing.
struct ParamVal
{
int parameterIndex;
float value;
};
bool beforeContains (int parameterIndex);
void setAfter (const ParamVal&);
bool isSameParams (const ChangeAction&) const;
Array<ParamVal> before, after;
private:
static float* lookup (const Array<ParamVal>&, int parameterIndex);
void setValues (const Array<ParamVal>&);
AudioProcessor* processor;
};
AudioProcessorUndoAttachment::ChangeAction::ChangeAction (AudioProcessor* p)
: processor (p)
{
}
bool AudioProcessorUndoAttachment::ChangeAction::perform()
{
setValues (after);
return true;
}
bool AudioProcessorUndoAttachment::ChangeAction::undo()
{
setValues (before);
return true;
}
UndoableAction* AudioProcessorUndoAttachment::ChangeAction::createCoalescedAction (UndoableAction *nextAction)
{
ChangeAction* const next = dynamic_cast<ChangeAction*> (nextAction);
if (next == nullptr)
{
// Next action has different type. Cannot merge.
return nullptr;
}
if (next->processor != processor)
{
// Can merge actions working on different audio processors.
return nullptr;
}
ChangeAction* result = new ChangeAction (processor);
result->before = before;
for (int i = 0; i < next->before.size(); ++i)
{
const ParamVal cur = next->before[i];
if (! beforeContains (cur.parameterIndex))
result->before.add (cur);
}
result->after = after;
for (int i = 0; i < next->after.size(); ++i)
result->setAfter (next->after[i]);
return result;
}
int AudioProcessorUndoAttachment::ChangeAction::getSizeInUnits()
{
const int estimatedAllocOverhead = 3 * 32;
return estimatedAllocOverhead + (int) sizeof (*this) + sizeof (ParamVal) * (before.size() + after.size());
}
void AudioProcessorUndoAttachment::ChangeAction::setValues (const Array<ParamVal>& values)
{
for (int i = 0; i < values.size(); ++i)
processor->setParameter (values[i].parameterIndex, values[i].value);
}
bool AudioProcessorUndoAttachment::ChangeAction::beforeContains (int parameterIndex)
{
return lookup (before, parameterIndex) != nullptr;
}
void AudioProcessorUndoAttachment::ChangeAction::setAfter (const ParamVal& paramVal)
{
float* storedVal = lookup (after, paramVal.parameterIndex);
if (storedVal == nullptr)
after.add (paramVal);
else
*storedVal = paramVal.value;
}
float* AudioProcessorUndoAttachment::ChangeAction::lookup (const Array<ParamVal>& arr, int parameterIndex)
{
for (int i = 0; i < arr.size(); ++i)
{
ParamVal& cur = arr.getReference (i);
if (cur.parameterIndex == parameterIndex)
return &cur.value;
}
return nullptr;
}
bool AudioProcessorUndoAttachment::ChangeAction::isSameParams (const ChangeAction& other) const
{
if (before.size() != other.before.size())
return false;
for (int i = 0; i < other.before.size(); ++i)
{
const int paramIdx = other.before[i].parameterIndex;
int k;
for (k = 0; k < before.size(); ++k)
{
if (before[k].parameterIndex == paramIdx)
break;
}
if (k == before.size())
return false;
}
return true;
}
AudioProcessorUndoAttachment::AudioProcessorUndoAttachment (AudioProcessor* p, UndoManager* um)
: processor (p), undoManager (um)
{
processor->addListener (this);
}
AudioProcessorUndoAttachment::~AudioProcessorUndoAttachment()
{
processor->removeListener (this);
}
void AudioProcessorUndoAttachment::audioProcessorParameterChangeGestureBegin (AudioProcessor* p, int parameterIndex)
{
jassert (p == processor); (void) p;
if (curChange.get() == nullptr)
curChange = new ChangeAction (processor);
if (curChange->beforeContains (parameterIndex))
{
// This change already recorded this parameter;
return;
}
curChange->before.add ({parameterIndex, processor->getParameter (parameterIndex)});
}
void AudioProcessorUndoAttachment::audioProcessorParameterChangeGestureEnd (AudioProcessor* p, int parameterIndex)
{
jassert (p == processor); (void) p;
if (curChange.get() == nullptr)
{
// We must have got attached after a gesture has already started.
return;
}
if (!curChange->beforeContains (parameterIndex))
{
// We got attached after the begin gesture for this parameter started.
return;
}
curChange->setAfter ({parameterIndex, processor->getParameter (parameterIndex)});
if (curChange->before.size() == curChange->after.size())
{
// The change is complete (same number of parameters before and after).
if (shouldBeginNewTransaction())
undoManager->beginNewTransaction();
undoManager->perform (curChange.release());
}
}
bool AudioProcessorUndoAttachment::shouldBeginNewTransaction() const
{
const RelativeTime tooLongTime = RelativeTime::seconds (0.5);
if (Time::getCurrentTime() - undoManager->getTimeOfUndoTransaction() >= tooLongTime)
return true;
Array<const UndoableAction*> actions;
undoManager->getActionsInCurrentTransaction (actions);
if (actions.isEmpty())
return false;
for (int i = 0; i < actions.size(); ++i)
{
const ChangeAction* const cur = dynamic_cast<const ChangeAction*> (actions[i]);
if (cur == nullptr)
return true;
if (! curChange->isSameParams (*cur))
return true;
}
return false;
}
#ifndef AUDIOPROCESSORUNDOATTACHMENT_H_INCLUDED
#define AUDIOPROCESSORUNDOATTACHMENT_H_INCLUDED
#include "JuceHeader.h"
// AudioProcessorUndoAttachment connects an AudioProcessor with an UndoManager.
//
// This relies on the plugin UI properly reporting parameter change gestures
// (some plugin hosts rely on it for properly recording automation anyhow).
//
// Do not use this if you use an AudioProcessorValueTreeState and supply it an UndoManager to use,
// because that will result in duplicate actions.
class AudioProcessorUndoAttachment : private juce::AudioProcessorListener
{
public:
AudioProcessorUndoAttachment (juce::AudioProcessor*, juce::UndoManager*);
~AudioProcessorUndoAttachment();
private:
class ChangeAction;
void audioProcessorParameterChangeGestureBegin (juce::AudioProcessor*, int) override;
void audioProcessorParameterChangeGestureEnd (juce::AudioProcessor*, int) override;
void audioProcessorParameterChanged (juce::AudioProcessor*, int, float) override {}
void audioProcessorChanged (juce::AudioProcessor*) override {}
bool shouldBeginNewTransaction() const;
juce::AudioProcessor* processor;
juce::UndoManager* undoManager;
juce::ScopedPointer<ChangeAction> curChange;
};
#endif // AUDIOPROCESSORUNDOATTACHMENT_H_INCLUDED
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment