Professional Documents
Culture Documents
Using JUCE ValueTrees and Modern C++ To Build Large Scale Applications PDF
Using JUCE ValueTrees and Modern C++ To Build Large Scale Applications PDF
1
Notes
● Slides are numbered
● Slides/examples will be on GitHub
● Code style tweaked for slide presentation
2
1. Introduction
3
What’s it all about? Waveform
4
Why ValueTrees?
● 4 years ago restructured Tracktion to
use ValueTrees
● Refactored ½ million line code base
● Now ~¼ million lines
5
Tracktion Development Over Time
(ValueTree Refactor)
6
Intro
● Overview of ValueTree related classes
○ var, Identifier , ValueTree, Value
● In-depth look at ValueTree
○ Callbacks, lifetime/reference counting, thread safety
○ Custom data storage, serialisation, undo/redo
● Using ValueTrees as application data models
● Building type-safe object lists
7
2. Explanation of var, ValueTree, and
Value classes
8
juce::var
var v (3.141);
● Variant type DBG("Type: " << getVarType (v));
DBG("Value: " << static_cast<double> (v));
● Analogous to a Javascript var DBG("Convert to a String: " << v.toString());
DBG("Type: " << getVarType (v)); // Still a double
● Can be used to store:
○ Primitive types (int, int64, bool, double) v = "Hello World!";
DBG("Type: " << getVarType (v));
○ juce types (String, MemoryBlock)
○ juce::Arrays of vars Array<var> </> Output:
Type: double
○ juce::ReferenceCountedObjects Value: 3.141
○ Callable juce::DynamicObjects Convert to a String: 3.1410000000000000142
Type: double
● Can be serialised to JSON Type: String
9
juce::Identifier
namespace IDs
● juce::Identifier is like an enum {
#define DECLARE_ID(name) const juce::Identifier name (#name);
● Actually a globally pooled String
DECLARE_ID (TREE)
● Comparisons between Identifiers are DECLARE_ID (pi)
pointer comparisons so extremely quick #undef DECLARE_ID
}
● Creating an Identifier can be slow (O(log
DBG(IDs::TREE.toString());
n) where n is the number of existing DBG(IDs::pi.toString());
Strings in the pool) const Identifier localTree ("TREE");
const String treeString ("TREE");
● The best way to create them is statically const Identifier treeFromString (treeString);
so they are created at app startup if (IDs::TREE == localTree && localTree == treeFromString)
DBG("All equivalent");
● Use a global set of Identifiers and a else
DBG("Not the same");
macro to create (use the same name as
the ID for readability and avoid spelling </> Output:
TREE
mistakes) pi
All equivalent
10
juce::ValueTree - Introduction
ValueTree v (IDs::TREE);
● juce::ValueTree is analogous to XML DBG(v.toXmlString());
● Its structure is the same (without text ValueTree clip (IDs::CLIP);
clip.setProperty (IDs::start, 42.0, nullptr);
elements) clip.setProperty (IDs::length, 10.0, nullptr);
v.addChild (clip, -1, nullptr);
● Imagine if you could have an DBG(v.toXmlString());
XmlElement but listen to when
</> Output:
attributes are changed and children are <TREE/>
added/removed/moved. This is <TREE>
<CLIP start="42" length="10"/>
juce::ValueTree! </TREE>
● Terminology:
○ attribute -> property
○ tag -> type
11
juce::ValueTree - Reference Counting
ValueTree v1 (IDs::TREE);
● ValueTrees are actually lightweight DBG ("1. Ref count v1: " << v1.getReferenceCount());
wrappers around an internal reference ValueTree v2 (v1);
DBG ("2. Ref count v1: " << v1.getReferenceCount());
counted object called a SharedObject DBG ("3. Ref count v2: " << v2.getReferenceCount());
● Copying a ValueTree simply increments
ValueTree v3 (v1.createCopy());
the reference count of the SharedObject DBG("4. Ref count v3: " << v3.getReferenceCount());
● This means it’s cheap to store and copy
ValueTrees as they point to shared data </> Outputs:
1. Ref count v1: 1
● You can create a unique copy using 2. Ref count v1: 2
3. Ref count v2: 2
ValueTree::createCopy 4. Ref count v3: 1
12
juce::ValueTree - Callbacks (1)
13
juce::ValueTree - Callbacks (2)
ValueTree::setProperty
SharedObject::setProperty
NamedValueSet::setProperty
SharedObject.valueTreesWithListeners
ValueTree.listeners
ValueTree::Listener::valueTreePropertyChanged
ValueTree::Listener::valueTreePropertyChanged
ValueTree.listeners
ValueTree::Listener::valueTreePropertyChanged 14
juce::ValueTree - Callbacks (3)
struct Widget : public ValueTree::Listener
● When you register as a listener, the {
Widget (ValueTree v) : state (v)
ValueTree holds a pointer to the {
state.addListener (this);
listener, not the SharedObject }
● This means it’s usually best to take a ValueTree state;
copy of the ValueTree and register with void valueTreePropertyChanged (ValueTree&, const Identifier&) override {}
void valueTreeChildAdded (ValueTree&, ValueTree&) override {}
that void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override {}
● ✎ Callbacks iterate upwards in the tree void
void
valueTreeChildOrderChanged (ValueTree&, int, int) override {}
valueTreeParentChanged (ValueTree&) override {}
};
hierarchy so listeners to parent nodes
will receive property change callbacks
for deeply nested trees
● Make sure to design your tree structure
accordingly and check types/properties
in each of your callbacks
15
juce::ValueTree - Callbacks (4)
● ✎ Don’t rely on the order of callbacks
i.e. don’t assume one callback will have
happened before another for the same
property
● Use an AsyncUpdater to ensure all
callbacks have happened and your
concrete objects have the correct state
before using them
16
juce::ValueTree - Undo/redo
UndoManager um;
● Because all changes to a ValueTree are ValueTree v (IDs::CLIP);
v.setProperty (IDs::start, 0.0, nullptr);
either property changes, child v.setProperty (IDs::length, 42.0, nullptr);
DBG (v.toXmlString());
add/remove or child moved actions,
um.beginNewTransaction();
they can very easily be undone/redone v.setProperty (IDs::length, 10.0, nullptr);
DBG (v.toXmlString());
simply by supplying a
um.undo();
juce::UndoManager to the actions DBG("Undoing:");
DBG (v.toXmlString());
● If all your ‘views’ simply respond to the
state of the tree and are updated when </> Output:
it changes, everything will always stay in <CLIP start="0" length="42"/>
<CLIP start="0" length="10"/>
sync Undoing:
<CLIP start="0" length="42"/>
17
juce::Value (1)
Value v1 (42);
● juce::Value has similarities to Value v2 (3.141);
juce::ValueTree but wraps a single DBG("v1: " << v1.toString());
DBG("v2: " << v2.toString());
juce::var
v2.referTo (v1);
● Internally it has a ValueSource which by DBG("v2: " << v2.toString());
default acts as a reference counted var
</> Output:
● You can listen for changes to the single v1: 42
v2: 3.1410000000000000142
var v2: 42
● ✎Value callbacks are asynchronous*
● Useful for attaching to UI
18
juce::Value (2)
struct SyncronousValueSource : public Value::ValueSource
● You can also create a custom {
SyncronousValueSource() = default;
ValueSource which you can either use to
SyncronousValueSource (const var& initialValue)
get synchronous change messages : value (initialValue)
{
● Or wrap custom data as a var }
● You can create Values from ValueTree var getValue() const override
{
properties return value;
}
● This creates a custom ValueSource that
void setValue (const var& newValue) override
is a ValueTree::Listener {
if (! newValue.equalsWithSameType (value))
{
value = newValue;
sendChangeMessage (true);
}
}
private:
var value;
};
19
juce::Value (3)
struct Transport : public ChangeBroadcaster Transport transport;
{ Value transportValue (Value (new TransportValueSource
Transport() = default; (transport)));
void start() { isPlaying = true; sendSynchronousChangeMessage(); } DBG("playing: " << (int) transport.isPlaying);
void stop() { isPlaying = false; sendSynchronousChangeMessage(); } DBG("value: " << (int) transportValue.getValue());
Transport& transport;
};
20
struct Transport : public ChangeBroadcaster Transport transport;
{ Value transportValue (Value (new TransportValueSource (transport)));
Transport() = default; DBG("playing: " << (int) transport.isPlaying);
void start() { isPlaying = true; sendSynchronousChangeMessage(); } DBG("value: " << (int) transportValue.getValue());
void stop() { isPlaying = false; sendSynchronousChangeMessage(); }
DBG("\nSTARTING");
bool isPlaying = false; transport.start();
}; DBG("playing: " << (int) transport.isPlaying);
DBG("value: " << (int) transportValue.getValue());
struct TransportValueSource : public Value::ValueSource,
private ChangeListener DBG("\nSETTING VALUE: 0");
{ transportValue.setValue (false);
TransportValueSource (Transport& t) : transport (t) DBG("playing: " << (int) transport.isPlaying);
{ DBG("value: " << (int) transportValue.getValue());
transport.addChangeListener (this);
}
Transport& transport; 21
};
Custom Data Storage
template<>
● You can store custom data by converting struct VariantConverter<Image>
{
to/from Strings or MemoryBlocks static Image fromVar (const var& v)
{
● Since it may be slow to do these if (auto* mb = v.getBinaryData())
return ImageFileFormat::loadFrom (mb->getData(), mb->getSize());
conversions you might want to use a
return {};
CachedValue }
● By specialising var conversions, you only static var toVar (const Image& i)
{
do the String conversion when the data MemoryBlock mb;
actually changes {
MemoryOutputStream os (mb, false);
● Some data is external to your model and
if (! JPEGImageFormat().writeImageToStream (i, os))
will require flushing to the tree before return {};
}
serialisation e.g. AudioProcessor state
return std::move (mb);
}
};
22
Use CachedValue<> to Wrap Properties as Objects
● Allows “property” type get/set methods
● Simple primitives are easy:
CachedValue<float> start;
start.referTo (v, IDs::start, um);
23
Use CachedValue<> to Wrap Properties as Objects (2)
template<>
● More complex objects can be stored as struct VariantConverter<Image>
{
var wrapped Strings or static Image fromVar (const var& v)
{
MemoryBlocks if (auto* mb = v.getBinaryData())
return ImageFileFormat::loadFrom (mb->getData(), mb->getSize());
● Specialise VarientConverter<> to do
return {};
this transparently }
CachedValue<Image> image; static var toVar (const Image& i)
image.referTo (v, IDs::image, um); {
MemoryBlock mb;
image = Image::loadFrom (imageFile);
{
MemoryOutputStream os (mb, false);
24
Serialisation
ValueTree v (IDs::TREE);
● juce::ValueTree can be easily serialised v.setProperty (IDs::pi, double_Pi, nullptr);
DBG("Type before: " << getVarType (v[IDs::pi]));
to either XML, a binary format or a
std::unique_ptr<XmlElement> xml (v.createXml());
compressed binary format DBG("Type after XML: " << getVarType (ValueTree::fromXml
○ XML is plain text so can be debugged (*xml)[IDs::pi]));
25
Thread Safety (1)
● juce::var is not thread-safe
● juce::Value is not thread-safe
● juce::ValueTree is not thread-safe
● juce::CachedValue<> is not thread-safe*
● (Data race on a POD type which could be
argued is a benign data-race)
● Which is still bad!
● Make sure all ValueTree interactions
happen on the message thread!
Otherwise your callbacks will happen
from the calling thread and cause havoc
26
Thread Safety (2)
● Use message posting, AsyncUpdater etc. to enforce this
● This should ensure all callbacks happen on the message thread
● Take copies of the data and make that thread safe either with atomics or locks
● CachedValue<AtomicWrapper<Type>>
27
ValueTrees - Top 5 Things to Know
28
1. ValueTree are Like XML
<EDIT appVersion="Waveform 8.1.8" mediaId="e31b7/1d17644"
● juce::ValueTree is analogous to XML creationTime="1402484148538" fps="24" timecodeFormat="beats">
<VIEWSTATE viewleft="-1" viewright="194.9998083000000122"/>
● Its structure is the same (without text <TRACK mediaId="e2b7f/23eff3ab" name="melody"
height="34.214285714285715301"
elements) colour="ffff4d4c">
<CLIP mediaId="e31b7/d45f454" name="xxxx" type="midi"
● Imagine if you could have an source="e2b7f/23eff3c4"
start="59.076864000000000487"
XmlElement but listen to when length="29.076893999999999352"
offset="0" colour="ffffab00"/>
attributes are changed and children are <CLIP mediaId="e31b7/30291232" name="melodyoutro"
type="midi" source="e2b7f/23eff3c4"
added/removed/moved. This is start="147.69216000000000122"
length="29.538432000000000244"
juce::ValueTree ! offset="0" colour="ffffab00"/>
</TRACK>
● Terminology: </EDIT>
○ attribute -> property
○ tag -> type
29
2. ValueTrees are Wrappers around a SharedObject
● ValueTrees are lightweight objects that manage a reference-counted
SharedObject
● The SharedObject holds a NamedValueSet of the properties
● ValueTree provides an interface to the SharedObject
● It is cheap to copy around ValueTrees
SharedObject
[NamedValueSet]
30
3. Listeners are Stored with the ValueTree
struct Widget : public ValueTree::Listener
● When you register as a listener, the {
Widget (ValueTree v)
ValueTree holds a pointer to the : state (v)
{
listener, not the SharedObject state.addListener (this);
}
● This means it’s usually best to take a
ValueTree state;
copy of the ValueTree and register
void valueTreePropertyChanged (ValueTree&, const Identifier&) override {}
with that void valueTreeChildAdded (ValueTree&, ValueTree&) override {}
void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override {}
void valueTreeChildOrderChanged (ValueTree&, int, int) override {}
void valueTreeParentChanged (ValueTree&) override {}
};
31
3. Listeners are Stored with the ValueTree (2)
ValueTree::setProperty
SharedObject::setProperty
NamedValueSet::set
var =
SharedObject.valueTreesWithListeners
ValueTree.listeners
ValueTree::Listener::valueTreePropertyChanged
ValueTree::Listener::valueTreePropertyChanged
ValueTree.listeners
ValueTree::Listener::valueTreePropertyChanged 32
3. Listeners are Stored with the ValueTree (3)
ValueTree::setProperty
SharedObject
ValueTree.listeners
ValueTree::Listener::valueTreePropertyChanged
ValueTree::Listener::valueTreePropertyChanged
ValueTree.listeners
ValueTree::Listener::valueTreePropertyChanged
33
4. Callbacks are Synchronous and Called for all Children
struct Widget : public ValueTree::Listener,
● Callbacks are synchronous so don’t do private AsyncUpdater
{
anything time consuming Widget (ValueTree v) : state (v)
{
● Use AsyncUpdater to combine actions state.addListener (this);
}
from property changes
ValueTree state;
34
5. Serialise Non-primitive Data to Strings
struct TimestretchOptions
● Combine multiple, related properties {
TimestretchOptions() = default;
into a single property TimestretchOptions (const String& s)
{
● Serialise to/from a String auto tokens = StringArray::fromTokens (s, "|", "");
stereoMS = tokens[0].getIntValue() != 0;
● Think about efficiency here, most syncroniseTimePitch = tokens[1].getIntValue() != 0;
preserveFormants = tokens[2].getIntValue() != 0;
properties won’t be changing very often. envelopeOrder = tokens[3].getIntValue();
}
For ones that do, you may need a
String toString() const
slightly different paradigm {
StringArray s;
s.add (stereoMS ? "1" : "0");
TimestretchOptions options; s.add (syncroniseTimePitch ? "1" : "0");
options.syncroniseTimePitch = true; s.add (preserveFormants ? "1" : "0");
options.preserveFormants = true; s.add (String (envelopeOrder));
options.envelopeOrder = 128; return s.joinIntoString ("|");
}
ValueTree v (IDs::CLIP);
v.setProperty (IDs::timestretchOptions, options.toString(), nullptr); bool stereoMS = false, syncroniseTimePitch = false,
DBG(TimestretchOptions (v[IDs::timestretchOptions].toString()).toString()); preserveFormants = false;
int envelopeOrder = 64;
};
</> Output:
0|1|1|128
35
3. Using ValueTrees as the
model of an application
36
Trees as classes
<EDIT appVersion="Waveform 8.1.8" mediaId="e31b7/1d17644"
● ValueTrees naturally fit the creationTime="1402484148538" fps="24" timecodeFormat="beats">
<VIEWSTATE viewleft="-1" viewright="194.9998083000000122"/>
hierarchical nature of a lot of apps <TRACK mediaId="e2b7f/23eff3ab" name="melody"
height="34.214285714285715301"
● Tree types are analogous to class names colour="ffff4d4c">
<CLIP mediaId="e31b7/d45f454" name="xxxx" type="midi"
● Properties are analogous to member source="e2b7f/23eff3c4"
start="59.076864000000000487"
variables length="29.076893999999999352"
offset="0" colour="ffffab00"/>
<CLIP mediaId="e31b7/30291232" name="melodyoutro"
type="midi" source="e2b7f/23eff3c4"
start="147.69216000000000122"
length="29.538432000000000244"
offset="0" colour="ffffab00"/>
</TRACK>
</EDIT>
37
Trees and MVC
● Using the callback mechanism of
ValueTree you can build objects
● In an MVC type paradigm, the
ValueTree takes care of the data
model and controller aspects
● You can then write multiple ‘views’ to
represent this data model letting the
callbacks notify you when things have
changed
● Making changes to the tree results in the
various views updating themselves to
reflect the new state of the tree
● You can have multiple ‘views’ if required
38
LIVE DEMO - 1_ValueTreeDemo
● ~750 lines of code
○ Tree view of data
○ Selected property panel
○ Colour picker
○ Drag and drop
○ Undo/redo
○ Serialisation
● ~250 UI code
○ Drawing
○ Property panel
● ~250 reusable library/helper methods
○ Saving ValueTree to/from a file
○ juce::Component helpers
● ~250 of ValueTree interaction
○ Creation
○ Validation 39
○ Get/set properties
Undo/redo
● When mutating a ValueTree, simply supply an UndoManager to make
that action undoable
● v.setProperty (IDs::start, 42.0, &undoManager);
● ValueTree clip (IDs::CLIP);
v.addChild (clip, -1, &undoManager);
40
4. Scaling Usage to Large Apps
41
Typed Objects
struct Clip
● Interacting directly with a ValueTree is {
Clip (const ValueTree& v) : state (v)
quick but there are pitfalls: {
○ No type checking jassert (v.hasType (IDs::CLIP));
}
○ No verification/bounds checking
double getStart() const
○ Requires hidden knowledge about the {
return state[IDs::start];
data properties and tree structure }
● We like: void setStart (double time)
○ Abstractions {
jassert (time >= 0.0);
○ Type-safety state.setProperty (IDs::start, jmax (0.0, time), nullptr);
}
○ Efficiency
● You can build wrappers around Colour getColour() const
{
ValueTrees to enforce all of this }
return Colour::fromString (state[IDs::colour].toString());
42
Reduce Boilerplate with CachedValues
struct Clip
● Use CachedValue<> to remove {
Clip (const ValueTree& v)
accessor/mutator methods : state (v)
{
● Easily add undo/redo by supplying an jassert (v.hasType (IDs::CLIP));
start.referTo (state, IDs::start, nullptr);
UndoManager (3rd argument) colour.referTo (state, IDs::colour, nullptr);
}
● Still type-safe
ValueTree state;
● We’ve lost range checking! CachedValue<double> start;
CachedValue<Colour> colour;
};
template<>
struct VariantConverter<Colour> ValueTree clipState (IDs::CLIP);
{ Clip c (clipState);
static Colour fromVar (const var& v) c.start = -1.0;
{ c.colour = Colours::red;
return Colour::fromString (v.toString()); DBG(c.state.toXmlString());
}
</> Output:
static var toVar (const Colour& c) <CLIP start="-1.0" colour="ffff0000" />
{
return c.toString();
}
};
43
Add Verification with Wrapper Classes
template<typename Type, typename Constrainer>
struct ConstrainerWrapper
{
ConstrainerWrapper() = default;
template<typename OtherType>
ConstrainerWrapper (const OtherType& other)
{
value = Constrainer::constrain (other);
}
bool operator== (const ConstrainerWrapper& other) const noexcept { return value == other.value; }
bool operator!= (const ConstrainerWrapper& other) const noexcept { return value != other.value; }
44
Add Verification with Wrapper Classes (2)
struct StartTimeConstrainer ValueTree clipState (IDs::CLIP);
{ Clip c (clipState);
static double constrain (const double& v)
{ c.start = 0.0;
return Range<double> (0.0, 42.0).clipValue (v); DBG("start: " << c.start.get());
}
}; c.start = 10.0;
DBG("start: " << c.start.get());
45
Add Verification with Wrapper Classes (3)
template<typename Type, int StartValue, int EndValue>
struct RangeConstrainer
{
static Type constrain (const Type& v)
{
const Type start = static_cast<Type> (StartValue);
const Type end = static_cast<Type> (EndValue);
46
Add Verification with Wrapper Classes (4)
struct NumberConstrainer struct LetterConstrainer
{ {
static String constrain (const String& v) static String constrain (const String& v)
{ {
return v.retainCharacters ("0123456789"); MemoryOutputStream os;
} auto p = v.getCharPointer();
};
do
{
if (p.isLetter())
os << String::charToString (*p);
}
while (p.getAndAdvance());
return os.toString();
}
};
ValueTree c (IDs::CLIP);
CachedValue<ConstrainerWrapper<String, LetterConstrainer>> name;
name.referTo (c, IDs::name, nullptr);
name = "He110 W0r1d";
DBG("name: " << name.get());
</> Output:
name: HeWrd
47
CachedValue<> Wrapper Classes Summary
● It’s possible to use templates to specify
constraints on data held in
CachedValue<>
● Alternatively you could create a version
of CachedValue<> that takes a
std::function<double (double)>
to constrain the contents
48
Lists of Objects - ValueTreeObjectList<>
● Although it’s possible to have all of your
classes derive from
ValueTree::Listener , often you’re
managing lists of objects
● We have developed ValueTreeObjectList
to take care of this
● Easier to deal with rather than iterating child
trees
○ Easier to iterate
○ More efficient
○ Enforced verification and validation
○ Type-safety
● Avoids lists of mixed types
49
Lists of Objects - ValueTreeObjectList<> (2)
ValueTree track (IDs::TRACK); ValueTree track (IDs::TRACK);
track.addChild (ValueTree (IDs::CLIP), -1, nullptr); track.addChild (ValueTree (IDs::CLIP), -1, nullptr);
track.addChild (ValueTree (IDs::CLIP), -1, nullptr); track.addChild (ValueTree (IDs::CLIP), -1, nullptr);
50
Lists of Objects - ValueTreeObjectList<> (3)
template<typename ObjectType>
● Listens to a single ValueTree (parent) class ValueTreeObjectList : public juce::ValueTree::Listener
● When a child of a specific type is added {
public:
there is a virtual method to create an ValueTreeObjectList (const juce::ValueTree& parentTree);
virtual ~ValueTreeObjectList();
object of that type
// Call in the sub-class when being created
● When a child is removed, there is a void rebuildObjects();
virtual method to destroy the object // Call in the sub-class when being destroyed
void freeObjects();
referring to that tree
//===============================================================
● If the child order is changed, the virtual bool isSuitableType (const juce::ValueTree&) const = 0;
managed objects are re-ordered to virtual ObjectType* createNewObject (const juce::ValueTree&) = 0;
virtual void deleteObject (ObjectType*) = 0;
follow
virtual void newObjectAdded (ObjectType*) = 0;
● Object “ValueTree state ” is used as a virtual void objectRemoved (ObjectType*) = 0;
virtual void objectOrderChanged() = 0;
unique identifier
juce::Array<ObjectType*> objects;
● You now have a simple array of objects //...
to utilise that are tied to the tree
51
Lists of Objects - ValueTreeObjectList<> (4)
struct ClipList : public drow::ValueTreeObjectList<Clip> ValueTree track (IDs::TRACK);
{ track.addChild (ValueTree (IDs::CLIP), -1, nullptr);
ClipList (const ValueTree& v) track.addChild (ValueTree (IDs::CLIP), -1, nullptr);
: drow::ValueTreeObjectList<Clip> (v)
{ ClipList clipList (track);
rebuildObjects(); DBG("Num Clips: " << clipList.objects.size());
}
// Add another CLIP child
~ClipList() track.addChild (ValueTree (IDs::CLIP), -1, nullptr);
{ DBG("Num Clips: " << clipList.objects.size());
freeObjects();
} // Call some methods
for (auto c : clipList.objects)
bool isSuitableType (const ValueTree& v) const override c->setColour (Colours::blue);
{
return v.hasType (IDs::CLIP); DBG(track.toXmlString());
}
52
LIVE_DEMO - 2_MultipleViews (Simple Tracks and Clips)
● ~200 lines new app code
53
Thread Safety
struct ClipList : private drow::ValueTreeObjectList<Clip>
{
● ValueTreeObjectList<> is not ClipList (const ValueTree& v)
: drow::ValueTreeObjectList<Clip> (v)
thread safe {
rebuildObjects();
● If you need to access objects from other }
private:
//...
needs to set the ValueTree property AtomicWrapper& operator= (const AtomicWrapper& other) noexcept
{
● This combined with the previous value.store (other.value);
return *this;
ValueTreeObjectList techniques }
enable you to have thread safe reads bool operator== (const AtomicWrapper& other) const noexcept
{
from ValueTree managed objects return value.load() == other.value.load();
}
57
Future Thoughts
● ValueTrees can be kept in sync using a ValueTreeSynchroniser
○ Can keep apps on different devices in sync e.g. desktop/tablet
○ Leads to interesting ideas about remote clients such as cloud...
● Performance improvements
○ Copy-on-write memory blocks?
58
Questions?
Presentation/notes available on
github:
https://github.com/drowaudio/
presentations
59