This page is extracted from the full Doxygen documentation, which is why the links don't work. To see the full documentation, download and unpack the source package, ensure you have Doxygen and Graphviz installed, and typemake doc
.
Chorale mainly consists of a bunch of implementations of the abstract class mediadb::Database, which does what it says on the tin: it represents a database of media files. There are derived classes which encapsulate a collection of local files, a networked UPnP A/V MediaServer, a networked Rio Receiver server, or a networked Empeg music player.
There are also a bunch of things that can use a mediadb::Database once you've got one: you can serve it out again over a different protocol, synchronise it with a different storage device, or queue up tracks from it (on any audio device, local or remote) and listen to them. You can even merge together a number of different databases, and treat them as a single one. Effectively, because of careful choice of abstractions, Chorale acts as a giant crossbar switch between sources of media and consumers of media:
Notable things in the code
- Non-recursive make. There's a famous white-paper, "Recursive
make
Considered Harmful", which did a good job of convincing people that traditional automake-style Makefiles were a bad idea, but was short on detail of what to do instead. Chorale uses autoconf but not automake; the makefiles all include each other rather than recursively invoking each other. This has the effect that one singlemake
invocation gets to see the entire dependency graph of the system, and so can make better decisions. For instance, using the-j
option tomake
keeps multi-core or multi-CPU machines much more effectively-used, asmake
can start new jobs from any part of the project at any time.
- Unit tests and coverage tests. The
tests
Makefile target runs all the unit tests relevant to the directory where it's invoked (for the top-level directory, that's all unit tests). Code coverage of the unit tests is also displayed each time. The unit tests have proper dependencies, and are only re-tested when something has changed.
- Missing configure dependencies. The configure script checks for all required libraries, then prints a list of all the ones it can't find. This is so much less annoying than the standard practice of bailing out as soon as one is missing; it saves having to do several rounds of re-configuring.
- Autogenerated UPnP APIs. A UPnP XML service description (an SCPD) is very much like an IDL. So, rather than manually write XML building or parsing, all the boilerplate code, for both client and server, is generated directly from the XML file (straight from upnp.org) using XSLT. Client code (or unit tests) accesses the same API whether it's being run against a local "loopback" implementation, or over the wire. For each SCPD, say AVTransport2.xml, we generate: a base class upnp::AVTransport2 (and a stubbed-out implementation, all of whose member functions return ENOSYS); a client class upnp::AVTransport2Client, which packages up the arguments and makes a SOAP call; and a server class upnp::AVTransport2Server, which unpackages a SOAP request and calls the actual implementation of the AVTransport service (which is itself derived from upnp::AVTransport2).
- DSEL for specifying XML parsers. Too cunning to describe here; see XML parsing in C++.
- Inter-library dependencies. Coupling between modules is a source of undesirable complexity in software systems. One tool that is used in Chorale to keep an eye on that, is an automatically-generated graph of library dependencies (
make libdeps.png
at the top level). It represents the concrete architecture of the system, to be compared with the abstract architecture in the above graph:
- Porting and Portability. Portable code can end up being a maze of
ifdef
's; the aim in Chorale has been to reduce uses ofifdef
to either just one or two lines of code, or entire files (so that either way it's easy for the reader to determine what's going on). For instance, in libutil/file.h, the APIs with two implementations are kept in two different C++ namespaces, util::posix and util::win32, one of which is namespace-aliased to util::fileapi, with using-declarations to draw them from util::fileapi into the common util namespace where everyone uses them. That way, only the one-line namespace-alias need be insideifdef
. (An earlier design had "using namespace win32", but the present design (a) ensures that there's a single place with a complete list of all the portable APIs, and (b) discourages writers of portable code from using the non-portable APIs in namespaces win32 and posix.)
- Optimisation and -fwhole-program. When configured with --enable-final, the entire Chorale daemon is compiled as a single C++ translation unit (by automatically figuring out which Chorale library objects are used, and then include'ing all their sources instead). This reduces the size of the final binary by about 20% on both ia32 and amd64, at the expense of vast memory usage during compilation.
- Unicode is the One True God, and UTF-8 is His prophet. Native Win32 applications use UTF-16 to interface to the filesystem (there's an alternative non-Unicode "ANSI" API, but that leaves some files with Unicode names inaccessible or indistinguishable). In order to avoid having two parallel implementations of large swathes of functionality, Chorale includes a translation layer that lets all the rest of the code assume that filenames are always UTF-8 everywhere.