diff --git a/exports/ysfx.txt b/exports/ysfx.txt index ec0adfc..d0b7855 100644 --- a/exports/ysfx.txt +++ b/exports/ysfx.txt @@ -84,6 +84,9 @@ ysfx_load_state ysfx_save_state ysfx_state_free ysfx_state_dup +ysfx_load_serialized_state +ysfx_preset_free +ysfx_preset_from_state ysfx_get_bank_path ysfx_load_bank ysfx_save_bank diff --git a/include/ysfx.h b/include/ysfx.h index 871917f..04ed824 100644 --- a/include/ysfx.h +++ b/include/ysfx.h @@ -377,6 +377,8 @@ YSFX_API ysfx_state_t *ysfx_save_state(ysfx_t *fx); YSFX_API void ysfx_state_free(ysfx_state_t *state); // duplicate a state object YSFX_API ysfx_state_t *ysfx_state_dup(ysfx_state_t *state); +// load only serialized state +YSFX_API bool ysfx_load_serialized_state(ysfx_t *fx, ysfx_state_t *state); typedef struct ysfx_preset_s { // name of the preset @@ -387,6 +389,11 @@ typedef struct ysfx_preset_s { ysfx_state_t *state; } ysfx_preset_t; +// release a preset object +YSFX_API void ysfx_preset_free(ysfx_preset_t *preset); +// Create a preset from a state blob and string. Note that the state blob is _not_ copied. +YSFX_API ysfx_preset_t *ysfx_preset_from_state(const char* value, ysfx_state_t* state); + typedef struct ysfx_bank_s { // name of the bank char *name; @@ -612,6 +619,7 @@ YSFX_DEFINE_AUTO_PTR(ysfx_u, ysfx_t, ysfx_free); YSFX_DEFINE_AUTO_PTR(ysfx_state_u, ysfx_state_t, ysfx_state_free); YSFX_DEFINE_AUTO_PTR(ysfx_bank_u, ysfx_bank_t, ysfx_bank_free); YSFX_DEFINE_AUTO_PTR(ysfx_menu_u, ysfx_menu_t, ysfx_menu_free); +YSFX_DEFINE_AUTO_PTR(ysfx_preset_u, ysfx_preset_t, ysfx_preset_free); #define YSFX_DEFINE_SHARED_PTR(sptr, styp, freefn) \ struct sptr##_deleter { \ diff --git a/plugin/editor.cpp b/plugin/editor.cpp index f3a10ee..7fd52a2 100644 --- a/plugin/editor.cpp +++ b/plugin/editor.cpp @@ -103,6 +103,8 @@ struct YsfxEditor::Impl { std::unique_ptr m_btnPresetOpts; std::unique_ptr m_btnSwitchEditor; std::unique_ptr m_btnReload; + std::unique_ptr m_btnUndo; + std::unique_ptr m_btnRedo; std::unique_ptr m_btnGfxScaling; std::unique_ptr m_lblFilePath; @@ -899,6 +901,10 @@ void YsfxEditor::Impl::createUI() m_self->addAndMakeVisible(*m_btnRecentFiles); m_btnReload.reset(new juce::TextButton(TRANS("Reload"))); m_self->addAndMakeVisible(*m_btnReload); + m_btnUndo.reset(new juce::TextButton(TRANS("R"))); + m_self->addAndMakeVisible(*m_btnUndo); + m_btnRedo.reset(new juce::TextButton(TRANS("U"))); + m_self->addAndMakeVisible(*m_btnRedo); m_btnEditCode.reset(new juce::TextButton(TRANS("Edit"))); m_self->addAndMakeVisible(*m_btnEditCode); m_btnGfxScaling.reset(new juce::TextButton(TRANS("x1"))); @@ -972,6 +978,11 @@ void YsfxEditor::Impl::connectUI() this->m_self ); }; + m_btnUndo->onClick = [this] { + this->m_proc->popUndoState(); + }; + //m_btnRedo + m_btnGfxScaling->onClick = [this] { if (m_graphicsView) { float newScaling = (m_graphicsView->getScaling() + 0.5f); @@ -1097,6 +1108,10 @@ void YsfxEditor::Impl::relayoutUI() m_btnReload->setBounds(temp.removeFromLeft(buttonWidth)); temp.removeFromLeft(spacing); m_btnReload->setVisible(true); + + m_btnUndo->setBounds(temp.removeFromLeft(25)); + temp.removeFromLeft(spacing); + m_btnUndo->setVisible(true); } else { m_btnReload->setVisible(false); } diff --git a/plugin/processor.cpp b/plugin/processor.cpp index 133e169..8ca3d1e 100644 --- a/plugin/processor.cpp +++ b/plugin/processor.cpp @@ -29,6 +29,7 @@ #include #include #include +#include struct YsfxProcessor::Impl : public juce::AudioProcessorListener { YsfxProcessor *m_self = nullptr; @@ -60,6 +61,9 @@ struct YsfxProcessor::Impl : public juce::AudioProcessorListener { void loadNewPreset(const ysfx_preset_t &preset); void resetPresetInfo(); + void pushUndoState(); + void popUndoState(); + //========================================================================== struct LoadRequest : public std::enable_shared_from_this { juce::String filePath; @@ -83,10 +87,14 @@ struct YsfxProcessor::Impl : public juce::AudioProcessorListener { LoadRequest::Ptr m_loadRequest; PresetRequest::Ptr m_presetRequest; + bool m_wantPopUndo{false}; bool m_wantUndoPoint{false}; ysfx::sync_bitset64 m_sliderParamsToNotify[ysfx_max_slider_groups]; ysfx::sync_bitset64 m_sliderParamsTouching[ysfx_max_slider_groups]; bool m_updateParamNames{false}; + + std::deque m_undoStack; + int m_undoPosition{-1}; //========================================================================== class SliderNotificationUpdater : public juce::AsyncUpdater { @@ -321,6 +329,12 @@ void YsfxProcessor::loadJsfxPreset(YsfxInfo::Ptr info, ysfx_bank_shared bank, ui } } +void YsfxProcessor::popUndoState() +{ + m_impl->m_wantPopUndo = true; + m_impl->m_background->wakeUp(); +} + bool YsfxProcessor::presetExists(const char* presetName) { auto sourceBank = m_impl->m_bank; @@ -985,6 +999,47 @@ void YsfxProcessor::Impl::loadNewPreset(const ysfx_preset_t &preset) m_background->wakeUp(); } +void YsfxProcessor::Impl::pushUndoState() +{ + ysfx_state_t* state; + { + AudioProcessorSuspender sus(*m_self); + sus.lockCallbacks(); + ysfx_t *fx = m_fx.get(); + state = ysfx_save_state(fx); + } + + ysfx_preset_u preset; + preset.reset(ysfx_preset_from_state(m_currentPresetInfo->m_lastChosenPreset.toStdString().c_str(), state)); + + // We add a new undo state -> Invalidate everything after our current position + if (m_undoPosition > -1) { + for (int ix = m_undoPosition + 1; ix < static_cast(m_undoStack.size()); ++ix) m_undoStack.pop_back(); + } + + m_undoStack.emplace_back(std::move(preset)); + m_undoPosition = static_cast(m_undoStack.size()) - 1; + + if (m_undoStack.size() > 100) { + m_undoStack.pop_front(); + } +} + +void YsfxProcessor::Impl::popUndoState() +{ + AudioProcessorSuspender sus{*m_self}; + sus.lockCallbacks(); + + if (m_undoPosition < 0) return; // Nothing to undo + + ysfx_t *fx = m_fx.get(); + auto& preset = m_undoStack[static_cast(m_undoPosition)]; + ysfx_load_serialized_state(fx, preset->state); + m_background->wakeUp(); + + m_undoPosition -= 1; +} + void YsfxProcessor::Impl::resetPresetInfo() { YsfxCurrentPresetInfo::Ptr presetInfo{new YsfxCurrentPresetInfo()}; @@ -1085,9 +1140,15 @@ void YsfxProcessor::Impl::Background::run() if (m_impl->m_wantUndoPoint) { m_impl->m_wantUndoPoint = false; + m_impl->pushUndoState(); Impl::ManualUndoPointUpdater *undoPointUpdater = m_impl->m_manualUndoPointUpdater.get(); undoPointUpdater->triggerAsyncUpdate(); } + + if (m_impl->m_wantPopUndo) { + m_impl->popUndoState(); + m_impl->m_wantPopUndo = false; + } } } diff --git a/plugin/processor.h b/plugin/processor.h index 8f43d7e..3311d70 100644 --- a/plugin/processor.h +++ b/plugin/processor.h @@ -34,6 +34,7 @@ class YsfxProcessor : public juce::AudioProcessor { YsfxParameter *getYsfxParameter(int sliderIndex); void loadJsfxFile(const juce::String &filePath, ysfx_state_t *initialState, bool async, bool preserveState); void loadJsfxPreset(YsfxInfo::Ptr info, ysfx_bank_shared bank, uint32_t index, PresetLoadMode load, bool async); + void popUndoState(); bool presetExists(const char *preset_name); void reloadBank(); void savePreset(const char* preset_name, ysfx_state_t *preset); diff --git a/sources/ysfx.cpp b/sources/ysfx.cpp index 8e604ce..2012212 100644 --- a/sources/ysfx.cpp +++ b/sources/ysfx.cpp @@ -1591,6 +1591,27 @@ bool ysfx_load_state(ysfx_t *fx, ysfx_state_t *state) return true; } +bool ysfx_load_serialized_state(ysfx_t *fx, ysfx_state_t *state) +{ + if (!fx->code.compiled) + return false; + + // restore the serialization + std::string buffer((char *)state->data, state->data_size); + + // invoke @serialize + { + std::unique_lock lock; + ysfx_serializer_t *serializer = static_cast(ysfx_get_file(fx, 0, lock)); + assert(serializer); + serializer->begin(false, buffer); + lock.unlock(); + ysfx_serialize(fx); + lock.lock(); + serializer->end(); + } +} + ysfx_state_t *ysfx_save_state(ysfx_t *fx) { if (!fx->code.compiled) diff --git a/sources/ysfx_preset.cpp b/sources/ysfx_preset.cpp index 9c3b761..98b10f8 100644 --- a/sources/ysfx_preset.cpp +++ b/sources/ysfx_preset.cpp @@ -33,7 +33,6 @@ #include "WDL/lineparse.h" -static void ysfx_preset_clear(ysfx_preset_t *preset); static ysfx_bank_t *ysfx_load_bank_from_rpl_text(const std::string &text); static void ysfx_parse_preset_from_rpl_blob(ysfx_preset_t *preset, const char *name, const std::vector &data); @@ -75,15 +74,17 @@ void ysfx_bank_free(ysfx_bank_t *bank) if (ysfx_preset_t *presets = bank->presets) { uint32_t count = bank->preset_count; for (uint32_t i = 0; i < count; ++i) - ysfx_preset_clear(&presets[i]); + ysfx_preset_free(&presets[i]); delete[] presets; } delete bank; } -static void ysfx_preset_clear(ysfx_preset_t *preset) +void ysfx_preset_free(ysfx_preset_t *preset) { + if (!preset) return; + delete[] preset->name; preset->name = nullptr; @@ -94,6 +95,15 @@ static void ysfx_preset_clear(ysfx_preset_t *preset) preset->state = nullptr; } +ysfx_preset_t *ysfx_preset_from_state(const char* value, ysfx_state_t* state) { + ysfx_preset_t *preset = new ysfx_preset_t{}; + preset->name = ysfx::strdup_using_new(value); + preset->blob_name = ysfx::strdup_using_new(value); + preset->state = state; + + return preset; +} + static ysfx_bank_t *ysfx_load_bank_from_rpl_text(const std::string &text) { LineParser parser; @@ -106,7 +116,7 @@ static ysfx_bank_t *ysfx_load_bank_from_rpl_text(const std::string &text) auto list_cleanup = ysfx::defer([&preset_list]() { for (ysfx_preset_t &pst : preset_list) - ysfx_preset_clear(&pst); + ysfx_preset_free(&pst); }); ///