Last active
February 16, 2025 21:25
-
-
Save yairchu/0a70e553af6176b2f6af to your computer and use it in GitHub Desktop.
AudioProcessorUndoAttachment for JUCE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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