ValueTree order

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:

  1. Track view, which shows the tracks horizontally on screen.

  2. Clip view, which shows the tracks vertically on screen.

  3. 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:

  • Classes A, B and C are updated in random order when Undo/Redo happens.
  • Class C might be updated before A and B, which would be bad.
  • Realtime playback needs A, B and C to be in total sync before they can be used.

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 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.

1 Like

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?

1 Like

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.

3 Likes

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.

1 Like

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.

1 Like