Lets say that I have three ValueTrees: A, B and C.
Whenever changes happen due to UndoManager (undo/redo), is there a way to force/ensure that one of them always gets updated first before the others?
Lets say that I have three ValueTrees: A, B and C.
Whenever changes happen due to UndoManager (undo/redo), is there a way to force/ensure that one of them always gets updated first before the others?
If the value trees are part of the same shared source: I say no. And even if you get it to work by testing and passing the references around in a certain order, an important aspect of this observer design pattern is, that you donât rely on the order of listeners being called. Being independent of that enforces very strong separation and modularisation of your code (which is a good trait to have).
If you share a bit more about your context, maybe we can give an advise on how to avoid (or transitively enforce) a certain order.
I have music tracks in my application and I have three different places where they can be accessed in different ways by the software or the user:
Track view, which shows the tracks horizontally on screen.
Clip view, which shows the tracks vertically on screen.
Channel Manager, which must be aware of both of the above viewâs tracks so it can call their methods in realtime.
Each of the above places must have the exact same amount of tracks, as their tracks are associated with each other.
So all three of those should always have matching tracks in them, but each of them have different data. Channel Manager needs to have pointers to tracks both of the views have, so it can call their methods in realtime playback.
The idea is that user can drag and drop and do all kinds of changes in Track view and Clip view, but all of those three components must be in sync which tracks are alive and which have been deleted/moved already.
The actual issue might arise if the user has moved/created/deleted tracks in either Track or Clip view and then uses Undo/Redo. If the Channel Manager gets updated first by the UndoManager, it cannot get the new updated tracks from Track and Clip views for playback, since they still might have old tracks lists in them.
Basically, you can control the update order by setting up listeners on ValueTree A. When A changes due to undo/redo actions, trigger updates for B and C right after. This way, A always updates first. Itâs a manual setup, but pretty effective.
When A changes due to undo/redo actions, trigger updates for B and C right after.
It is not allowed to modify (= perform undoable actions) ValueTree in ValueTree::Listener callbacks during undo/redo operation. But a timer could be triggered to update other ValueTreeâs.
I did some thinking and came up with a fairly simple solution.
So the core issues are basically the following:
Solution:
Those changes should make A, B and C modular, fairly independent of each other. Also it should ensure they have the exact same amount of tracks, in the same order AFTER the update has happened.
The mutex then ensures the update happens fully before the updated data can be used in any way.
I think that might actually work.
I think this is your core problem. The design choice you went through sounds pretty solid! But is there any way you can get rid of this dependency? What does C have to do to A and B? Why canât this go through the same valuetree with A and B listening and C altering the value?
This is what AsyncUpdater or any of these types of classes can be used for. By moving the realtime engines update into the next message cycle, you know the UI is fully refresh. But I think itâs also question worthy, why real-time stuff needs an up to date UI.
Could it be just one tree, and in the track view you simply maintain a list in which order or which tracks at all are selected to be displayed?
Or make the order a sort property in the treeâŠ
That would solve the redundancy removing the need to synchronise those three trees.
Just to clarify: C does not have a GUI. Only A and B have a GUI.
A, B and C all have their own unique internal data.
Think for example Ableton Live with itâs timeline and clip launcher views (A and B) and C would be the thing that reads data from A and B in realtime playback and does all sorts of things to it afterwards.
Itâs up to A and B to decide what kind of data they have inside them, where they get that data and if they want to pre-render/cache them in any way. C just request that audio/MIDI data in realtime when new audio/MIDI needs to be rendered.
So what currently happens is this (a simplified example) :
juce::AudioAppComponent::getNextAudioBlock() uses C in realtime to render song audio and MIDI data.
C uses A and B, by calling one of their methods to render/copy their audio/MIDI data into a buffer:
for (int channel_index = 0; channel_index < channel_count; channel_index++)
{
A->copyEventsToBuffer(channel_index, playhead_info, output_buffer_a[channel_index]);
B->copyEventsToBuffer(channel_index, playhead_info, output_buffer_b[channel_index]);
}
The before mentioned ValueTree would be used to hold a track list and hopefully all their data (after refactoring). A, B and C use the exact same instance of ValueTree, which they can access and listen to its changes. But only the modules themselves know how to decipher and use their own data in that tree. So theyâre ultimately responsible for delivering the rendered audio/MIDI data to C whenever C requests it from A and B.
The ValueTree will contain X amount of children, which are the actual tracks. If anyone, anywhere, adds a new child to the ValueTree root node, then A, B and C get instantly notified and theyâll update their internal track list accordingly. For A and B this means that theyâll show new tracks in their GUIs.
A, B and C will most probably add their own data to the child nodes whenever new tracks are created. So the final hierarchy of ValueTrees would look something like this:
See my latest answer with picture of the intended ValueTree hierarchy.
It sounds like you have three views sharing the same state, so why are you trying to duplicate it everywhere and then synchronize whatâs shared instead of just sharing it?
Iâm not sure I understood your question, but hereâs one answer which hopefully answers your question:
The ValueTrees are not duplicated. They are the exact same instance of ValueTree. Thatâs how ValueTrees work: when you give a ValueTree to X amount of places, they all point to the exact same instance of the tree.
So I have not written anywhere in the code ValueTree::createCopy(). I only give the ValueTree as a parameter to all the places which need it, which makes all of them point to the exact same instance of data.
My gut says the issue is your architecture. Something feels coupled wrong (due to expectations of ValueTree callbacks, which you cannot control very well). You either need to centralize access to the data through a single piece of code that monitors the valuetree and updates the other code that needs this data, or properly design your code to work independantly with the shared data. I donât understand the actual use case, so I canât say which is the âmore correctâ approach, but if I were having this problem, I would be thinking about the solutions that I mentioned.
Representations of the ValueTree, such as UI or audio (referred to as Views), should operate independently of each other. Ideally, they should be unaware of each otherâs existence and should not be dependent on the order of notifications for changes in the ValueTree.
I often read discussions here about people looking for more control over the ValueTree, proposing features to skip callbacks to specific listeners or suggesting methods to alter the order of listener callbacks. In most cases, the root of the issue lies in the software design rather than the ValueTree API.
In the rare instance that you find yourself needing more control, consider implementing a listener system layered on top of the ValueTree. This can provide more control. I occasionally write wrappers aorund ValueTree that abstract ValueTree callbacks and expose meaningful std functions, but my motivation is to establish an interface that simplifies the state management of my codebase, not to ensure a specific order of ValueTree callbacks or âfixâ bad software design decisions.
The easiest way to explain the use-case scenario is Ableton Live type of software:
You have Timeline view of the song and a Clip Launcher view of the song. Each have exact same amount of tracks, with their internal representation of their own data. Then thereâs the player routine, which can switch each tracks playback between those two sources of tracks. Also user is able to drag & drop those tracks in one view and the other view reflects those changes in its own track order.
My current implementation (the A, B, C module example) was originally doing it so, that every meaningful change (such as creating/deleting/moving tracks) gets done through the module C, to ensure everything stays in perfect sync. But with ValueTrees this becomes not so good idea anymore, especially when I want to get the benefits it offers (undo/redo, easy load/save of applicationâs state, changes in one place automatically get done elsewhere also)
This is, what I donât understand. Here is, how it is working for me:
I have one module (completely separated): the audio engine. It uses absolutely no UI, no ValueTree (because not thread safe). There are two main ways of interaction: 1) an outer design layer which has setters to alter behaviour of the engine. Those can be called from any thread and are guaranteed to not mess with the audio execution. 2) a set of auto allocated resources that are feed with data from audio processing (for example for wave-forms). Each point of interest (for wave-form) has a unique ID, that makes it easy to identify. The audio thread has a shared_ptr to the resource for updating the audio material, the UI parts may (or may not) also acquire a shared_ptr and read the audio data. (It is note worthy, that this resource object managing the audio data feed by the engine and read by the UI is responsible for thread-safety).
This bit of code, does not exist in my projects. The audio engine populates resources with calculation results (always), the UI bits may or may not read from those resources, if they are interested.
Your description does not really explain why the updates have to be in a particular order, or change what I have suggested. You have given callback ordering as a requirement that is not met by your current architecture, and the suggested routes are valid.
For me, I would prefer to the fix where clients of the ValueTree can update in any order, because they rely only on the data of the ValueTree, not the state of each other. This feels like the code is well abstracted from the data. If there is some other level of synchronization that A, B, and C need, I would deal with this seperate from the callback ordering. Something that queues the execution of the synchronization code after the callbacks (MM::callAsync, AsyncUpdater, Timer, etc) could be used for this.
But, having said this, I still lack the understanding of the requirement, and I wonder if the requirement itself is the issue. ie. does the requirement expose the underlying architecutural issue?
Iâm not 100% sure, but we might be talking about two different things here. I believe the thing confusing people here is that some of the modules in my examples have their own UI code in addition to the code which generates their own output.
My idea is to have modules A and B, which module C uses to get the final playback data.
Modules A and B are fully responsible for handling whatever has to be done, so they can deliver their data to module C. This includes having a GUI for the end user if needed, which module C doesnât need to know anything about. The only requirement is that those modules must deliver their data in standardized final format, in realtime, when module C needs it.
This way modules A and B can have their own unique ways of handing things and donât need to have mutually compatible data / data structures. Only when module C asks them to deliver data from playback location time X seconds to Y seconds, they convert part of their internal data into âfinal formatâ for module C to use.
So it is important that modules A and B can have whatever arbitrary data structures and formats they want to have, which none of the other modules need to know anything about. Especially module C which handles the final playback of the data which the other modules generate.
So it would be impractical to put such a player routine into module C, which would understand module A and B internal data so well that it could generate the final playback data from it all by itself.
My third post in this topic mentioned how I could refactor the system so that it would not be dependent on the update order anymore:
Solution:
- Ensure that A, B and C all use the exact same algorithm to create their own track list, based on ValueTree updates (listener).
- Make class C access A and B by index, instead of keeping pointers to their internal tracks.
- Protect Undo/Redo keypress with a mutex, which ensures that realtime playback wonât be able to access A, B or C if theyâre being updated.
Those changes should make A, B and C modular, fairly independent of each other. Also it should ensure they have the exact same amount of tracks, in the same order AFTER the update has happened.
The mutex then ensures the update happens fully before the updated data can be used in any way.
Iâve now done some refactoring according to the above plan and the update order doesnât seem to be an issue anymore. I havenât implemented the Undo/Redo just yet, but the code seems that it should be able to handle it correctly now.