Google


Ecasound documentation - Programmer's guide


Kai Vehmanen

26092001

Table of Contents

1: Preface

2: General guidelines

2.1: Design and programming

2.1.1: Open and generic design
2.1.2: Object-orientation
2.1.3: Data hiding
2.1.4: Design by contract
2.1.5: Routine side effects
2.1.6: Sanity checks
2.1.7: Error handling
2.1.8: Exceptions

2.2: Coding style

2.2.1: General guide lines
2.2.2: Package specific

2.3: Physical level organization

2.3.1: Distribution packages
2.3.2: Directories
2.3.3: File groups

2.4: Documentation style

2.5: Versioning

3: How ecasound works?

3.1: Common use-cases

3.1.1: Simple non-interactive processing
3.1.2: Multitrack mixing
3.1.3: Realtime effect processing
3.1.4: One-track recording
3.1.5: Multitrack recording
3.1.6: Recycling a signal through external devices

3.2: Signal flow

3.3: Control flow

3.3.1: Passive operation
3.3.2: Interactive operation

3.4: Class descriptions

3.4.1: Core
3.4.2: Data objects
3.4.3: Object maps

4: Using ecasound from other programs

4.1: Console mode ecasound - [all languages]

4.2: Ecasound Control Interface - [C++, C, Python]

4.3: Libecasound's ECA_CONTROL class - [C++]

4.4: Ecasound classes as building blocks - [C++]

5: Adding new features and components?

5.1: Things to remember when writing new C++ classes

5.1.1: Copy constructor and assignment operator

5.2: Audio objects

5.3: Effects and other chain operators

5.4: Differences between audio objects and chain operators

5.5: LADSPA plugins



1: Preface

This 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 programming

2.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.

2.1.2: Object-orientation

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.

2.1.3: Data hiding

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.

2.1.4: Design by contract

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.

2.1.5: Routine side effects

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).

2.1.6: Sanity checks

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. ;)

2.1.7: Error handling

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).

2.1.8: Exceptions

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:

AUDIO_IO - open()
This method is used for initializing external connections (opening files or devices, loading shared libraries, opening IPC connections). It's impossible to know in advance what might happen. In many cases it is also useful to get more verbose information about the problem that caused open() to fail. Throwing an exception is an excellent way to achieve this.

ECA_CHAINSETUP - enable()

ECA_CHAINSETUP - load_from_, save() and save_to_

ECA_SESSION - constructor

2.2: Coding style

2.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.2.2: Package specific

libecasound, ecasound, ecatools, libkvutils
Class names are all in upper case and words separated with underscores (class ECA_CONTROL_BASE). This a standard style in Eiffel programming.

libqtecasound, qtecasound, ecawave
Qt-style is used when naming classes (class QELevelMeter), otherwise same as above.

2.3: Physical level organization

Ecasound libraries and applications are divided into distribution packages, directories and file groups.

2.3.1: Distribution packages

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.

2.3.2: Directories

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.

2.3.3: File groups

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.

audioio*.{cpp,h}
Audio device and file input/output.

audiofx*.{cpp,h}
Audio effects and other DSP-related code.

audiogate*.{cpp,h}
Gate operators.

eca-*.{cpp,h}
Core functionality.

midi-*.{cpp,h}
MIDI input/output devices, handlers and controller code.

osc-*.{cpp,h}
Oscillator and other controller sources.

qe*.{cpp,h}
Generic prefix for files utilizing both Qt and ecasound libraries.

samplebuffer-*.{cpp,h}
Routines and helper functions for processing audio data buffers.

You should note that these are just recommendations - there are no strict rules on how files should be named.

2.4: Documentation style

Javadoc-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:

Use of 3rd person
"Writes samples to memory." instead of "Write samples to memory."

Sentences start with a verb
"Writes samples to memory." instead of "Samples are written to memory."

This instead of the
"Get controllers connected to this effect." instead of "Get controllers connected to the effect.

2.5: Versioning

All 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-cases

Here's some common cases how ecasound can be used.

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.

3.1.2: Multitrack mixing

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.

3.1.4: One-track recording

One input is processed and written to one or more outputs.

3.1.5: Multitrack recording

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.

3.2: Signal flow

This 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 flow

3.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.

3.3.2: Interactive operation

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.

ECA_CHAINSETUP can be...

not selected
- can't be accessed from ECA_PROCESSOR

selected, invalid
- can be edited (files and devices are not opened)

selected, valid
- can be connected (files and devices are not opened)

connected
- ready for processing (files and devices are opened before connecting)

ECA_PROCESSOR status is one of...

not_ready
- ECA_SESSION object is not ready for processing or ECA_PROCESSOR hasn't been created

running
- processing

stopped
- processing hasn't been started or it has been stopped before completion

finished
- processing has been completed

error
- an error has occured during prosessing

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.

3.4.1: Core

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.

3.4.2: Data objects

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).

3.4.3: Object maps

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.

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:

  • 1. automating (scripting in its traditional sense)
  • 2. frontends (generic / specialized)
  • 3. sound services to other apps
  • 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++ classes

    5.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:

  • Trust the automatically created default definitons. If you don't have any pointers as data members, this isn't necessarily a bad choice at all. At least the compiler remembers to copy all members!

  • If you have pointers to objects as class data members, you should write definitions for both the copy-constructor and the assign operation.

  • If you are lazy, just declare the two functions as null functions, and put them in _private_ access scope. At least this way nobody will use the functions by accident!
  • 5.2: Audio objects

    To 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 operators

    Write 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 operators

    Design-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:

    Input/output:

  • audio objects can be opened for reading writing or read&write
  • effects are modeless
  • audio objects read from, or write to a buffer
  • effects get a buffer which they operate (in-place processing)
  • Audio format:

  • audio objects have a distinct audio format (sample rate, bits, channels)
  • effects should be capable of accepting audio data in any format (this is usually easy as ecasound converts all input data to its internal format)
  • Control:

  • audio objects can be opened, closed, prepared, started and stopped
  • effects don't have a running state
  • Position:

  • audio objects have length and position attributes
  • effects just process buffers and don't know about their 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"
    
    

    5.5: LADSPA plugins

    Ecasound 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.