JUCE makes my new year happy - a case study


#1

Thanks for all the support!

I managed to come up with a very solid beta of the program I’ve been working on a couple of days ago and testing is so far very positive. (We still don’t have the Windows build side yet, the last time we did that it worked identically once we got past the usual compile/link issues, crossed fingers!)

I hope I don’t give the impression of someone beset by problems - this project has actually gone extremely well but there’s an awful lot of stuff in it and so not everything worked right the first time.

However, it came out extremely well, and I attribute a lot of that to the virtues of Juce.

Reliability and support

The code has so far been extremely reliable. Perhaps you saw the various funkinesses I described in previous posts here - as I’d anticipated, each and every one of them was due to some gross error on my part.

At least on the Mac side, I’d describe the threading model as “very consistent” - that’s a big deal when you’re writing multi-threaded code. I have about a dozen threads going, I haven’t even adjusted the priorities, and the program is fine. It uses quite a lot of CPU when it’s ripping things from a CD but when it gets into a quiescent, cached state the CPU is quite low.

I generally assume that Windows versions of things work well because there are more Windows users, so this is good (Windows beta here we come!)

And, of course, Jules’ level support is extremely high.

Fast and small.

I work quite hard to make my code fast and small but I am often disappointed when in the end the thing that I wrote starts up, runs or shuts down or slowly due to some other part I have no control over.

This application bounces into life and shuts down just as fast (I have had recurrent issues in shutdown due to various contentions and race-conditions but I seem to have gotten past them).

It runs very fast. It’s so fast that I see no difference between the debug and optimized builds (and yet I know from my benchmarks that parts of it are many times faster) - because I don’t ever seem to be waiting on the CPU. Now, I haven’t tested this on a little machine yet but it really seems light.

And the optimized build is less than 3MB, compressed. And my code base is not small - I have about 20k lines of handwritten C++ code, 30k lines of generated code (Google protocol buffers, see below), and easily 100k lines of code in third-party libraries (although I’ll bet less than half of that actually gets linked in).

Features

Again, really hard to beat, at least in the areas of digital audio, UI and to a lesser extent graphics, where I’m working.

Protocol buffers and other libraries

I used quite a lot of other libraries in the course of this project, which worked fairly harmoniously.

Probably the most central decision was the use of Google’s protocol buffer format for transfer data structures. Certainly the fact that I was very familiar with protos was a key point in my decision, but I wasn’t going to do it initially, but use XML and Juce things only.

I did not do this, I used Google protobuffers, and I continue to be extremely glad of that decision every day. :smiley: It’s not that there’s anything wrong with XML (well - there are a lot of things wrong with XML I suppose but that’s another story), but that the advantages of having a data description language are huge, and protos translate directly into C++ data structures with fast copy and access.

In a nutshell, protos have all the advantages of Plain Old Data, because you can pass them around by value and store them in containers, but have a huge amount more functionality, particularly the ability to quickly serialize and de-serialize into either a human-readable form, or a tightly compressed-wire format.

And they’re also extensible - not meaning that you can inherit from them (in fact, that’s almost always a bad idea, you should treat them as plain old data) but that there is a simple, systematic for your data structures to grow and change over time while still always being able to read your old files, or even have old versions of your application read your new data.

Here’s a random proto file from my code:[code]package rec.util.file;

message VolumeFile {
enum Type {
NONE = 0;
CD = 1;
MUSIC = 2;
USER = 3;
VOLUME = 4;
};

enum Status {
ONLINE = 1;
OFFLINE = 2;
DISK_OPEN = 3;
WRITEABLE_DISK = 4;
NO_DISK = 5;
UNKNOWN = 6;
};

optional Type type = 1 [default = NONE];
optional string name = 2;
repeated string path = 3;
optional Status status = 4 [default = ONLINE];
// I could have used other protos within this one as well…
};[/code]
and this automatically generates a C++ class called VolumeFile in namespace rec::util::file that has methods like Type type(), bool has_type(), set_type(Type), or (for repeated fields) methods like std::string path(int), int path_size(), and the like - but also reflection if you need it, at no cost if you don’t.

Always On™

The program has a unique architecture that you might find entertaining.

There is no Save button, and no database. Everything comes as a single .exe or .app with no external assets. Every operation is indefinitely undoable (well… :smiley: I haven’t really hooked that up but we have the undo queue sitting there waiting), there will be a separate navigation and operation undo queue and you’ll be able to undo just commands on one item, and the current state immediately saved to disk. Almost every operation that’s time-consuming and doesn’t take too much space (like audio thumbnails or CDDB requests) is cached to disk.

From the programmer’s standpoint, getting and setting data is very easy, and it’s very easy to create even a quite complex control that’s simply attached to a piece of persistent data, and works no matter when in your code you drop it (i.e., you don’t have to hook it up, or only have to hook it up once if you want to change it…)

Again, it’s all protos. I have persistent protos that live in memory and correspond to actual disk files that are named after the data type. Your controls or processes can listen to these protos, and get updates when they change, or get a snapshot at any time.

You can also send atomic editing operations to the protos. You don’t edit the data by holding some lock on it and then actually performing an edit, instead you prepare a data structure that contains that edit and then sending it to an edit queue…

…except of course I have sprinkled a ton of semantic sugar on it so that your code ends up looking like:

[code]// All this code is completely thread-safe, with the asterisk that it’s a bad idea
// to start new data sources during the shutdown procedure.

// Gets a consistent snapshot of the current top-level/“global” instance of VolumeFile.
VolumeFile file = persist::get();

// A snapshot of TimeStretch for a specific file.
TimeStretch stretch = persist::get(file);

// persist::Data lets you do everything that’s not snapshots.
persist::Data* data = persist::data(file_);
TimeStretch s2 = data->get(); // Same as persist::get().

// Edit the persistent data.
TimeStretch stretch = makeMyStretch();
data->set(stretch); // This operation is undoable, and goes off in a separate thread.
if (data->get() != stretch) {} // No guarantee that it’s updated yet!

// For a larger data structure, you can just change one part of it,
// all undoably, all done in a separate thread.
data->set(“time_stretch”, 1.2);
data->clear(); // Clear the whole thing.
data->clear(“time_stretch”); // Clear just one part to its default value.

// Listeners and Broadcasters in my code are generic and so there’s one per proto class.
data->addListener(this); // Unlike in Juce, you’re automatically unlistened in your destructor.

// Listeners will eventually receive an update with the current value of the data in another thread.
// Right now, everyone hears these updates, but it hasn’t been an issue.
data->requestUpdate();[/code]