free-audio / clap

Audio Plugin API
https://cleveraudio.org/
MIT License
1.76k stars 97 forks source link

plugin undo/redo extension #223

Closed Trinitou closed 1 year ago

Trinitou commented 1 year ago

The plugin could provide some access to its undo/redo steps/actions so that the host could integrate those into its own undo/redo history.

E.g. the u-he plugins already have internal undo/redo buttons but they can't be triggered by pressing the undo shortcuts inside Bitwig Studio.

There are a few things I'm not completely sure about:

defiantnerd commented 1 year ago

Just to add some thoughts:

Should parameter or preset changes be tracked by the host or by the plugin? Definitely the plugin. In complex plugins the host can only track the exposed parameters. The plugin itself could notify the host about a possible "new" undo step.

Should the plugin provide all individual undo steps themselves? The plugin could be given the context of a setstate call (see https://github.com/free-audio/clap/blob/main/include/clap/ext/draft/state-context.h)

If so, should they provide a string with a description for the undo step? If so, should there be rules for the formulation of such a string? I would find this reasonable - so in a history this could be displayed: "FancyPlugin: Filter edited"

Or should the plugin just offer simple undo/redo trigger actions to the host? + maybe bools that indicate if an undo/redo step is available

Notification callbacks - host can decide if it wants to call getstate(context: UNDOSTEP)

baconpaul commented 1 year ago

The way we did undo in surge is to have a stack of objects with payloads and types. It would be easy to add ids to them and share those with the host. I don’t think the blob should go to the host just the description and an id. So the plugin can say “add undoable event 765 called filter to 400” and the host can say “undo event 765”

the problem is redo thoigh. Innsurge each undo projects the opposite item onto the redo stack. So you need an advertise redo also

plus we need to know if the host can call undos and redos out of stack order or not. Surge is pretty un resilient to that for instance but can unwinds up to point x

defiantnerd commented 1 year ago

That's why the host should store a (lightweight) memento for it. he can then jump between the states and recall them. Assume you've delivered 3 mementos to the host and the user presses UNDO twice, the plugin gets setState(...) with "CONTEXT_UNDO". As soon as the user edits something, the plugin would again notify the host about the edit. The host then can discard the two "future" mementos and replace them by a new one. This way, it is easily implementable for the host and without further implementation needed in the plugin.

Just stay away from ideas like multi-path undo/redo....

baconpaul commented 1 year ago

yeah we have an undo and redo stack and you can shift between the tops but that's it.

defiantnerd commented 1 year ago

yeah we have an undo and redo stack and you can shift between the tops but that's it.

This UNDO/REDO would live completely in the host, I think.

baconpaul commented 1 year ago

yeah we have an undo and redo stack and you can shift between the tops but that's it.

This UNDO/REDO would live completely in the host, I think.

Well I’m not going to remove my undo! Our users in live and logic will still want it. Realistically I think we need to plan for cooperation between the okugin and the host here

abique commented 1 year ago

I think this is a very difficult topic. It can easily fall into over engineering or become "too simple". Also this implies a parallel structure structure of the undo stack between the host and plugin which is really tricky and requires deep understanding of all the surrounding issues for both the host and plugin.

It is possible that exposing the plugin's undo stack to the host is not desirable, both from a workflow or a technical perspective.

sagamusix commented 1 year ago

Some more thoughts here: https://github.com/free-audio/clap/discussions/161

Trinitou commented 1 year ago

I think for the workflow it could be very useful and powerful. As a user, I never understand that e.g. when I insert a plugin, change some things in the plugin and then undo in the host later it will immediately remove the plugin completely. Also when changing something in a plugin in between host undo steps, the host undo will completely step over those plugin undo steps. So all in all I think it definitely would be worth the effort.

At the same time I don't feel experienced enough to immediately pull out a clever solution for everybody out of the hat. 🐇 I mean I could try a proposal for an extension if I find the time but there are probably better people for this. But if I don't find the time in near future and nobody else wants to propose an extension (?), I will close this issue.

At least we have some rough requirements flying around here and there (in #161, thank you for the hint, @sagamusix! 😉 )

sadko4u commented 1 year ago

Why this feature shouldn't be considered as a part of state extension? I suppose, the host can handle several state changes and manage them in both UNDO and REDO directions since the plugin notifies the host about it's state change. If you need to have UNDO/REDO in the UI of the plugin, it is a probably the responsibility of the plugin. I don't think you'll need the UNDO/REDO feature stack while the UI of the plugin is closed.

baconpaul commented 1 year ago

Undo and redo messages can be much much smaller than the entire state. And you can have lots more undoable events than state saves. So it is definitely a different “thing” than load/save state I think!

I view the undo/redo problem really as a plugin being able to introduce actions into the host undo stack and then apply them if done in reverse order. But that’s just my experience writing undo/redo in a plugin where we do that! (It’s also what vcv rack does internalky also; you make micro undo objects not full state saves)

baconpaul commented 1 year ago

(Oh and we actually save our stack on the plugin not the editor so that the stack can survive open/close operations which destroy the editor, at the cost of not supporting many-to-one editor to instance patterns; but that’s an implementation choice. One our users like though!)

Trinitou commented 1 year ago

Ok, some more questions

defiantnerd commented 1 year ago

I am with @sadko4u here. It could be definitely part of the state extension. There is already a draft providing context:

https://github.com/free-audio/clap/blob/main/include/clap/ext/draft/state-context.h

There are several points that support this idea and the integration of UNDO/REDO in the host:

I know that this has been discussed for years and there is no satisfying result yet, but I think this needs to be started on the plugin side, so a host programmer can develop ux concepts to use this feature.

The good thing is, when the mechanism has a good design, it is easy to implement this in all plugins.

sadko4u commented 1 year ago

When we talk about the micro-changes, it is a good reason to look into how the RDBMS are organized relative to the transaction replay and rollback. The plugin can emit it's own POD containing two sections: the UNDO and REDO section which can be put into some UNDO/REDO chain by the host. When moving by this chain forward (REDO) or backward (UNDO), we can issue some apply method and pass this POD and UNDO/REDO indicator. So:

Looks like a pretty simple but robust design.

baconpaul commented 1 year ago

Yes that’s exactly the design many plugs use internally. Being able to advertise that to the host would be wonderful

defiantnerd commented 1 year ago

Is state generation that expensive in plugins? In case of the micro-changes it is probably better to be a separate extension that also could signal to the host to get the next undo state.

The undo pattern with mementos is even more simple, but perhaps too expensive? Thinking of plugins using additional wave files or so.

baconpaul commented 1 year ago

Changing a single slider in surge generates about a 70 byte memento. A state save can be many 10s of kilobytes or more and involves xml generation and binary streaming, so yes,

Trinitou commented 1 year ago

What should the plugin-internal undo/redo controls do when plugin undo steps were embedded into the host undo steps? Here a quick demonstration with u-he Bazille inside Bitwig Studio: plugin-internal undo

For the host controls I think it's clear what they should do but for the plugin ones? Should they put the plugin back to only its own previous state (which might be complicated) or should it just signal the host to perform the undo (move back to the last host undo step)? plugin-internal undo question (circles represent undo states, from oldest (top) to newest (bottom) )

defiantnerd commented 1 year ago

I guess it is the same state, but the host can't either follow that or will loose it's redo step since it is an edit on the plugin side.

abique commented 1 year ago

This seems stale, shall I close?

Trinitou commented 1 year ago

This seems stale, shall I close?

Couldn't find the time yet. I close it for you. ;-) Hopefully we can come back to this later as some other people seem to be interested in it as well.