AMaMP
Audio Mixing and Manipulation Project
SourceForge.net Logo
 
 
 

The Core :: A Guided Tour
This document takes a high level overview of the internals of the AMaMP core. If you're planning to join in the fun of developing it or just want to have a play with the internals, this would be a good place to start reading.

Basic Program Flow
Files: main.c
Execution starts in the main function which is located in the file main.c. The function starts by doing some initialization tasks, then it attempts to open the instruction file. Provided this succeeds, the parser is invoked. The parser looks at the input file and generates an amamp_job structure based upon what it finds. Once the parser has run, we do some preparation work relating to Input/Output, which is explained later on in a discussion of the I/O layer. With everything ready, the engine is then called. The engine sits in a loop, calls into the I/O layer to read inputs, mixes the data and write it to outputs. Once in a while, it calls the IPC message handler. IPC is the mechanism by which the core and an application using it can communicate. The IPC message handler reads messages from STDIN, parses them and does message dispatch (calling the appropriate message handlers). If the engine terminates naturally (no errors or a stop message is not received) then outputs are finalized and memory is freed.

Structures
Files: amamp_structures.h
Any structures that are used throughout the AMaMP core are defined in amamp_structures.h.

  • amamp_job contains data related to the mix process as a whole, including pointers to the heads of the inputs, outputs and placements list, buffer sizes, data from the Global chunk of the instruction file and runtime context information that we may need to pass around. Things stored in amamp_job could be stored as global variables, but this approach is neater and means embedding AMaMP in a multi-threading environment to do multiple mixes at once will be easier (not that the core currently makes any promises about being thread-safe).
  • amamp_input contains data related to a particular input. It has a place for individual I/O modules to hang data off at runtime, but aside from that attempts to stay as generic as possible. These are stored in a linked list, so this structure also has a pointer to the next input.
  • amamp_output contains data related to a particular output. This is kept as generic as possible. These are stored in a linked list, so this structure also has a pointer to the next output.
  • amamp_placement contains data related to particular placement, including data on where to source the input from. It has a place for individual I/O modules to hang runtime data off at runtime to help ensure this structure remains as generic as possible. There is a pointer to an amamp_placement_output structure. These form a linked list of pointers to amamp_output structures, enabling a placement to be routed to multiple outputs.
  • amamp_message is used to store an IPC message. Has a pointer to a head of a linked list of parameters, which are stored in the amamp_message_param structure. Functions used to manipulate these are stored in amamp_ipc.c, and these should be used as far as is possible to work with this data structure.
  • amamp_io_param is used by I/O modules to pass around key/value style parameters. Functions to manipulate these are stored in amamp_io.c, and these should be used as far as is possible to work with this data structure.

Errors, Warnings And Debug Messages
Files: amamp_error.*
amamp_error.c provides 3 functions that should always be used when issuing an error, a warning or a debug message. The first parameter for any of these is the name of the module, which may be one of parser, io, ipc, engine, effect or dll. Parameters after this are concatenated to form the message, and should never include newline characters. The final parameter should be a NULL or 0.

  • amamp_error is used to issue an error message and immediately terminate the core. This should be used when a condition arises which means the core cannot continue.
  • amamp_warning is used to issue warning messages that alert the listening program to a potential problem, but something that the core can work around are continue.
  • amamp_debug is used to issue debug messages. Calls to this should always be between preprocessor directives so building with or without debug messages is possible.

The Parser
Files: amamp_parser.*
The parser is a hefty chunk of C code that reads an input file and creates an amamp_job structure along with linked lists of inputs, outputs and placements. The code isn't particularly clever or exciting, but here's a quick overview of the steps it goes through.

  • Tokenizing - First the input file is broken up into a linked list of tokens (e.g. the line "Placement myplac {" is broken into the tokens "Placement", "myplac", "{" and a newline character). This is also the stage where comments get stripped out.
  • Parser - The parsers loops through the list of tokens and builds up a list of inputs, outputs and placements. The order of inputs and outputs is unimportant, so they are just tacked onto the end of the existing lists (or maybe the start). The placements one need to be sorted by start position, and this is done at parse time.
  • Resolver - After parsing, the placements know the identifiers of the inputs and outputs they use. The resolver takes these, looks them up in the list of inputs and outputs and stores pointers to the matching structures.

The AMaMP I/O Layer
Files: amamp_io*.*, config.h
The AMaMP I/O layer is based around function pointers and I/O modules. There are two types of I/O modules: those that are compiled into the core (which will always be accessed by a FileInput, StreamInput, FileOutput or StreamOutput chunk) and those which are loaded dynamically (which will always be accessed by an Input or Output chunk). At the time of writing, only the first type is available.

All I/O layer modules are expected to provide a specific set of functions with the given prototypes. The naming convention for internal I/O modules that are in the main source tree is amamp_io_name.c and amamp_io_name.h. Functions will be named as amamp_io_name_function.

In config.h each internal module is assigned a numerical ID. Names of input/output modules are translated to these IDs by the parser. As well as data, the amamp_input and amamp_output structures also store a set of function pointers. After the parser has run, we loop through the list of inputs and outputs and call amamp_io_input_deref and amamp_io_output_deref (respectively) on them. This looks at the type of input or output and sets the function pointers to point at the functions in the appropriate I/O module. This means that from this point onwards calling into the I/O layer (from the engine) is a straight function call - we only have the one-off cost of doing the dereferencing rather than having to do it every time later on.

Looking forward, this approach is perfectly suited to external I/O modules; the only difference is that we'll be finding the function pointers by looking up symbols in a dynamically linked library.

The Engine
Files: amamp_engine.*
The engine has one function that does the heavy work (named amamp_mix_start) and two others that are called to prepare all of the outputs and later finalize them. amamp_prepare_outputs loops through all of the outputs and for each of them allocates an output buffer and calls output_open. amamp_finalize_outputs is similar, but this time calls output_close and frees the buffers.

Essentially, amamp_mix_start is just a big loop. Every iteration it mixes the set of samples at a particular sample position. However, there's a little more to it than that.

A list of active placements is maintained. When the current sample position matches the sample offset for a placement, the placement is put in the active list. When all data from that placement has been mixed into the output (either when we've read all data from the input or reached the end of trimmed section - we calculate this beforehand) it is removed from the active list.

In each iteration of the main mix loop, we go through the active list, get the appropriate sample data from each placement's input buffer and add it to the current position in the output buffers of all the outputs in it's output list. We also have to keep check of when we've read all of the data from an input so we can call into the I/O layer to get more data.

Once we have filled write buffers, we call into the I/O layer for each output, asking it to write the data in its buffer and clear it.

The only other major task performed by the engine is to call the IPC message listening function every so often, so IPC messages are checked.

IPC
Files: amamp_ipc*.*
The AMaMP IPC mechanism uses pipes. There are documents that talk about the intricacies of this elsewhere, so I won't dwell on it much - in a nutshell, applications using the core send messages by writing to a pipe attached to the core's standard in, and receive messages by reading from a pipe attached to the core's standard out.

In amamp_ipc.c is a function named amamp_ipc_listen. When called, this function looks to see if any messages have been sent. If they have, it reads them into a buffer, then parses each one from that buffer into an amamp_message structure. For each message, amamp_ipc_dispatch is called. This function looks at the message type and passes it to the appropriate handler. For example, core messages are handled by passing the message onto amamp_ipc_core_dispatch, located in amamp_ipc_core.c. I/O type messages are handled by looking up the appropriate input or output structure and calling that I/O module's message handler.

If a request is handled successfully, amamp_ipc_request_confirmation should be called, passing the id of the message. If a request is invalid, amamp_ipc_request_failed should be called. See the existing code base for examples.

Dynamic Library Loading Abstraction Layer
Files: amamp_dll.*
AMaMP employs DLLs for its effects and I/O modules. These are handled differently on different platforms. As dynamic library loading will be needed in multiple places, AMaMP has an abstraction layer for DLL related tasks which is implemented in amamp_dll.c. It exports amamp_dll_load to load a DLL, amamp_dll_close to close a DLL and amamp_dll_findsymbol for symbol lookup.

The only additional quirk here is that we need to know the path of the module we want to load, and the function amamp_dll_setpath is called from main at the start of AMaMP's execution to work out this path. At the time of writing the full details of this had not been hashed out.

Build System
Files: Configure.pl, config.h
AMaMP needs to build on a range of platforms, so a Configure script is required. The configuration system for AMaMP is written in Perl, since Perl is available as a standard component of many operating systems and available on virtually all other operating systems we'd wish to compile on. Configure.pl pokes and prods a system, tries to work stuff out and generates a makefile and the file config.h. If there is a conditional compilement statement checking something in AMaMP, config.h is most probably where it is set.