Unlike most web pages, this document really is under construction. :)
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:
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).
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.
Here's a few general documentation guide lines:
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.1.1: Simple non-interactive processing
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.
3.1.3: Realtime effect processing
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).
3.1.6: Recycling a signal through external devices
Just like multirack recording. The only difference is that realtime input and output are externally connected.
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.
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.
ECA_PROCESSOR
ECA_PROCESSOR is the actual processing engine. It is initialized with
a pointer to a ECA_SESSION object, which has all information needed at
runtime. Processing is started with the exec() member function and
after that, ECA_PROCESSOR runs on its own. If the interactive mode is
enabled in ECA_SESSION, ECA_PROCESSOR can be controlled using the
ECA_CONTROL class. It offers a safe way to control ecasound.
Another way to communicate with ECA_PROCESSOR is to access the
ECA_SESSION object directly.
ECA_SESSION
ECA_SESSION represents the data used by ecasound. A session contains
all ECA_CHAINSETUP objects and general runtime settings (iactive-mode,
debug-level, etc). Only one ECA_CHAINSETUP can be active at a time. To
make it easier to control how threads access ECA_SESSION, only
ECA_PROCESSOR and ECA_CONTROL classes have direct access to
ECA_SESSION data and functions. Other classes can only use const
members of ECA_SESSION.
ECA_CONTROL
ECA_CONTROL represents the whole public interface offered by
ecasound library. It also has a simple command interpreter
(interactive-mode) that can used for controlling ecasound.
SAMPLEBUFFER
Basic unit for representing sample data. The data type used to
represent a single sample, valid value range, channel count, global
sampling rate and system endianess are all specified in
"samplebuffer.h" and "sample_specs.h".
DEBUG
Virtual interface class for ecasound's debugging subsystem. Ecasound
engine sends all debug messages to an instance of this class. The actual
implementation can be done in many ways. For example in the console mode
user-interface of ecasound, TEXTDEBUG class implements the DEBUG
interface. It sends all messages that have a suitable debug
level to the standard output stream. On the other hand, in qtecasound
DEBUG is implemented using a Qt widget. New DEBUG implementations can
be registered at runtime with the attach_debug_object() call
(declared in eca-debug.h).
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.
ECA_OBJECT
A virtual base class that represents one object. All classes deriving
from ECA_OBJECT can be registered to object maps.
ECA_OBJECT_MAP
This is the basic object map implementation. It offers map services
for ECA_OBJECT objects.
ECA_PRESET_MAP
Special class that inherits from ECA_OBJECT_MAP. This class is used
for mapping instances of class PRESET.
ECA_AUDIO_OBJECT_MAP
A convenience class for mapping AUDIO_IO objects. Handles the casting
between ECA_OBJECT<->AUDIO_IO.
ECA_CHAIN_OPERATOR_MAP
A convenience class for mapping CHAIN_OPERATOR objects. Handles the casting
between ECA_OBJECT<->CHAIN_OPERATOR.
ECA_CONTROLLER_MAP
A convenience class for mapping GENERIC_CONTROLLER objects. Handles the casting
between ECA_OBJECT<->GENERIC_CONTROLLER.
ECA_LADSPA_PLUGIN_MAP
A convenience class for mapping EFFECT_LADSPA objects. Handles the casting
between ECA_OBJECT<->EFFECT_LADSPA.
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:
--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--
--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--
Always take a moment to check your copy constructor and the assign operation (=operation()). Basicly you have three alternatives:
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).
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.
Input/output:
Audio format:
Control:
Position:
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"