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