Ecasound documentation - Programmer's guide
Table of Contents
1: PrefaceThis document describes how ecasound library works, how to use it, how to extend and add features to it and so on. Before reading this document, you should first look at other available documentation (especially ecasound users's guide).
Unlike most web pages, this document really is under construction. :)
2: General guidelines
2.1: Design and programming2.1.1: Open and generic design
Over the years ecasound's core design has been revised many times. After rewriting some code sections hundreds of times, you start to appreciate genericity. :) Although specific use-cases are used for testing new ideas, they are just design aids.
Ecasound is written in C++ (as specified in 1997 ANSI/ISO C++ standard). Because C++ language itself doesn't force you to follow OO-principles, I often use Eiffel language as a reference when designing classes and routines.
This OO-feature deserves to be mentioned separately. Whenever possible, I always try to hide the actual data representation. This allows you to make local implementation changes without affecting other parts of the code base. One thing I've especially tried to avoid is excessive use of pointer magic.
Design by contract means that when you write a new routine, in addition to the actual code, you also describe routine's behaviour as accurately as possible.
Routine must specify all requirements and assumptions. If the caller violates this specification, routine is not responsible for the error. This means that routine mustn't check argument validity. This must be done by the caller.
Routine should also specify, what conditions are true when returning to the caller. By doing this, routine ensures that it works correctly and calling routine knows what has been done.
Ideally, these conditions prove that the routine works correctly. The benefits of this approach should be clear. When you call a well-defined routine, a) you know what parameter values it accepts, b) you know what it does and c) if errors occur, it's easier to pinpoint the faulty routine. In practice this is done by using comments and pre/postconditions. As C++ doesn't directly support pre/postconditions, I've simulated them using the class DEFINITION_BY_CONTRACT from kvutils package and with standard assert() calls.
I try to make a clear distinction between routines that have side-effects (=methods, processors, modifiers; routines that change object's state) and const routines (=functions, observers).
Sanity checks are done only to prevent crashes. All effects and operators happily accept "insane" parameters. For instance you can give -100.0% to the amplifier effect. This of course results in inverted sample data. I think this a reasonable approach. After all, ecasound is supposed to be a tool for creative work and experimenting. It's not meant for e-commerce. ;)
Two specific things worth mentioning: First, the standard UNIX-style error handling, where functions performing actions return an integer value, is not used in ecasound. As described in the above section Routine side effects, all routines are either modifiers or observers, not both. So when using ecasound APIs, you first perform an action (modifying function), and then afterwards check what happened (using an observer function).
C++ exceptions are used in ecasound. Exception based error handling has its problems, but in some cases it is clearly the best option. Using exceptions for anything other than pure exception handling is to be avoided at all cost. And when exceptions are used, their use must be specified in function prototypes. This is important, as clients need to know what exceptions can be thrown. All in all, use of exceptions should be carefully planned.
A list of specific cases where exceptions are used follows:
2.2: Coding style2.2.1: General guide lines
Variable names are all lower case and words are separated with underscores (int very_long_variable_name_with_underscores). Class data members are marked with _rep postfix. Data members which are pointers are marked with _repp. Index-style short variable names (n, m, etc.) are only used in local scopes. Enum types have capitalized names (Some_enum).
2.3: Physical level organizationEcasound libraries and applications are divided into distribution packages, directories and file groups.
As an example, ecasound and qtecasound are distributed as separate packages. This decision has been made because a) they are clearly independent, b) they have different external dependencies, and c) they address different target uses.
It's convenient to organize larger sets of source code into separate directories. For instance in ecasound, libecasound and ecatools are in two separate directories.
Although files are divided in directories and subdirectories, there's still a need to logically group a set of source files based on their use and role in the overall design. As the use of C++ namespaces is very limited in ecasound (to avoid portability problems), filename prefixes are used for grouping files. Here's a short list of commonly used prefixes.
You should note that these are just recommendations - there are no strict rules on how files should be named.
2.4: Documentation styleJavadoc-style class documentation is the preferred style. Class members can be documented either when they are declared (header files), or when they are defined. Especially when specifying complicated interfaces, it's better to put documentation in the definition files. This way the header files remain compact and serve better as a reference.
Here's a few general documentation guide lines:
2.5: VersioningAll ecasound releases have a distinct version number. To emphasize the difference between stable and development releases, stable versions have an even, and devel versions have an odd minor version number. In addition, devel versions are marked with 'devXX'. For instance, you could have development releases 0.1dev1, 0.1dev2 and 0.1dev3. When the development reaches a stable status, 0.2 is released. After this, a new development series will start (0.3dev1, and so on).
Separation between development and stable releases is very important, because it affects library versioning. The idea is that all library interfaces are versioned separately with libtool. If during development changes are made to the public interfaces, a new interface version is created. The new interface version won't be frozen until the next stable version. After this, no changes to the interface (affects both at binary and source level) are allowed. If these changes must be made, a new interface version must be created. It's important to note that interface compatibility is not guaranteed between development releases. So if you are linking apps against development versions of ecasound libraries, you must be prepared for interface changes.
All changes in the public interfaces are documented in library specific ChangeLog files. These files are usually located in the top-level source directory.
3: How ecasound works?
3.1: Common use-casesHere's some common cases how ecasound can be used.
One input is processed and then written to one output. This includes effect processing, normal sample playback, format conversions, etc.
Multiple inputs are mixed into one output.
There's at least one realtime input and one realtime output. Signal is sampled from the realtime input, processed and written to the realtime output.
One input is processed and written to one or more outputs.
The most common situation is that there are two separate chains. First one consists of realtime input routed to a non-realtime output. This is the recording chain. The other one is the monitor chain and it consists of one or more non-realtime inputs routed to a realtime output. You could also route your realtime input to the monitoring chain, but this is not recommended because of severe timing problems. To synchronize these two separate chains, ecasound uses a special multitrack mode (which should be enabled automatically).
Just like multirack recording. The only difference is that realtime input and output are externally connected.
3.2: Signal flowThis is simple. A group of inputs is routed to a group of chains. Audio data is processed in the chains and afterwards routed to a group of outputs. Using internal loop devices, it' also possible to route signals from one chain to another. It's also possible to assign inputs and outputs to multiple chains.
3.3: Control flow3.3.1: Passive operation
When ecasound is run in passive mode, the program flow is simple. A ECA_SESSION object is created with suitable parameters, it is passed to a ECA_PROCESSOR object and that is all. Processing is started with the exec() member function of ECA_PROCESSOR. After processing is finished, exec() returns to the caller.
Another way to do passive processing is to create a ECA_CONTROL object and use it to to access and modify the ECA_SESSION object before passing it to ECA_PROCESSOR.
In interactive mode, everything is done using the interface provided by ECA_CONTROL. This is when things get complex:
ECA_SESSION object can contain many ECA_CHAINSETUP objects, but only one of them can be active. On the other hand it is possible that there are no chainsetups. If this is the case, about the only thing you can do is to add a new chainsetup.
One chainsetup at a time can be selected. In this state the chainsetup can be edited using the methods provided by ECA_CONTROL. Only one setup at a time can be connected to the engine (ie. actual use). Trying to connect an invalid chainsetups will fail. A valid chainsetup has at least one input-output pair connected to the same chain.
3.4: Class descriptions
The primary source for class documentation is header files. A browsable version of header documentation is at www.wakkanet.fi/~kaiv/ecasound/Documentation/doxygen_pages.html Anyway, let's look at the some central classes.
Object maps are a central repositories for commonly used objects. When object is registered to a map, a regular expression is attached to it. When object map receives a request for a new object, it goes through all registered regular expressions, and returns an object attached to the matching expression. Object maps can also provide a list of all registered objects.
This system may sound a bit complex, but in practise it is quite simple and makes a lot of things more easier. For instance, when adding new object types to the library, you only have to add a call which registers the new object; no need to modify any other part of the library. It also makes it possible to add new types at runtime. For instance, an application linked against libecasound might add its own custom object types on startup. All parts of libecasound can use the custom objects, although they are not part of library itself.
All objects defined in libecasound are registered in the file eca-static-object-maps.cpp.
4: Using ecasound from other programs
4.1: Console mode ecasound - [all languages]This is the easiest way to take advantage of ecasound features in your own programs. You can fork ecasound, pipe commands to ecasound's interactive mode or you can create chainsetup (.ecs) files and load them to ecasound. You'll be able to do practically anything. The only real problem is getting information from ecasound. You'll have to parse ecasound's ascii output if you want to do this. To make this a bit easier, ecasound offers the dump-* commands. These print configuration and status info as formatted text strings (easier to parse than normal output).
4.2: Ecasound Control Interface - [C++, C, Python]
Idea behind Ecasound Control Interface (ECI) is to take a subset of functionality provided by libecasound, write a simple API for it, and port it to various languages. At the moment, at least C++, C and Python implementations of the ECI API are available and part of the main ecasound distribution. ECI is heavily based on ecasound's interactive mode (EIAM), and the services it provides.
Specific tasks ECI is aimed at:
4.3: Libecasound's ECA_CONTROL class - [C++]By linking your program to libecasound, you can use the ECA_CONTROL class for controlling ecasound. This is a large interface class that offers routines for controlling all ecasound features. It's easy to use while still powerful. Best examples are the utils in ecatools directory (most of them are just a couple screenfuls of code). Also, qtecasound and ecawave heavily use ECA_CONTROL. Here's a few lines of example code:
--cut-- ECA_SESSION esession; ECA_CONTROL ctrl (&esession); ctrl.new_chainsetup("default"); [... other setup routines ] ctrl.start(); // starts processing in another thread (doesn't block) --cut--
If you don't want to use threads, you can do as above, but use ECA_PROCESSOR directly to do the actual processing. In other words, you only use ECA_CONTROL for setting up a ECA_SESSION object. This way the processing is done without additional threads. Here's a short sample:
--cut-- ECA_SESSION esession; ECA_CONTROL ctrl (&esession); ctrl.add_chainsetup("default"); [... other setup routines ] ECA_PROCESSOR p (&esession); p.exec(); // blocks until processing is finished --cut--
4.4: Ecasound classes as building blocks - [C++]And of course, you can also use individual ecasound classes directly. This means more control, but it also means more work. Here's another short sample:
--cut-- - create a SAMPLE_BUFFER object for storing the samples - read samples with an audio I/O object - for example WAVEFILE - process sample data with some effect class - for example EFFECT_LOWPASS - maybe change the filter frequency with EFFECT_LOWPASS::set_parameter(1, new_value) - write samples with an audio I/O object - OSSDEVICE, WAVEFILE, etc. --cut--
5: Adding new features and components?
5.1: Things to remember when writing new C++ classes5.1.1: Copy constructor and assignment operator
Always take a moment to check your copy constructor and the assign operation (=operation()). Basicly you have three alternatives:
5.2: Audio objectsTo implement a new audio object type, you must first select which top-level class to derive from. Usually this is either AUDIO_IO (the top-level class), AUDIO_IO_BUFFERED (a more low level interface) or AUDIO_IO_DEVICE (realtime devices).
The second step is to implement the various virtual functions declared in the parent classes. These functions can be divided into four categories: 1) attributes (describes the object and its capabilities), 2) configuration (routines used for setting up the object), 3) main functionality (open, close, input, output, etc) and 4) runtime information (status info).
Adding the new object to ecasound is much like adding a new effect (see the next section). Basicly you just add it to the makefiles and then register it to the appropriate object map (see below).
5.3: Effects and other chain operatorsWrite a new class that inherits from CHAIN_OPERATOR or any of its successors. Implement the necessary routines (init, set/get_parameter, process and a default constructor) and add your source files to libecasound's makefiles. Then all that's left to do is to add your effect to libecasound/eca-static-object-maps.cpp, register_default_objects(). Now the new effect can be used just like any other ecasound effect (parameters control, effect presets, etc).
Another way to add effects to ecasound is to write them as LADSPA plugins. The API is well documented and there's plenty of example code available. See www.ladspa.org for more information.
5.4: Differences between audio objects and chain operatorsDesign-wise, audio objects and effects (chain operators) aren't that far away from each other. Many audio apps don't separate these concepts at all (for instance most UG based synthesizers). In ecasound though, there are some differences:
A good example of the similarity between the two types are LADSPA oscillator plugins. Although they are effects, you can easily use them as audio inputs by specifying:
"ecasound -i null -o /dev/dsp -el:sine_fcac,440,1"
5.5: LADSPA pluginsEcasound supports LADSPA-effect plugins (Linux Audio Developer's Simple Plugin API). See LAD mailing list web site for more info about LADSPA. Other useful sites are LADSPA home page and LADSPA documentation.