General architecture / Basic types and classes¶
Essentia’s main purpose is to serve as a library of signal-processing blocks. As such, it is intended to provide as many pre-written algorithms as possible, while trying to be as little intrusive as possible. Hence the general design is very simple and easy to understand.
Each processing block is called an Algorithm, and has 3 differents types of attributes:
Inputs
Outputs
Parameters
Every algorithm can have any number of each of these (zero included). For instance, a “Centroid” algorithm will have 1 input (an array), 1 output (the value of the centroid) and 1 parameter (the range of the centroid).
Basically, that is all you need to grasp what an Algorithm is in Essentia.
The general workflow is the following:
you instantiate (create) an algorithm
you configure it using the desired parameters
you feed it some input(s), and get back the output(s)
repeat 2. and/or 3. as much as desired
In the next sections, we will delve a bit more into the details.
Algorithms¶
You already know that an Algorithm has inputs, outputs and parameters. To be more
precise, an essentia::Algorithm
actually is a subclass of essentia::Configurable
,
which is the part that takes care of the parameters, and it adds the inputs and
outputs on top of it.
An algorithm is required to be able to perform a certain number of things (on top of what being a Configurable requires, see Configurables)
you can get access to its inputs/outputs given their names
you can
compute()
the result(s), that is, apply the specific algorithm to the inputs you have previously set, and get back the corresponding resultsif the algorithm maintains a state, you can
reset()
it.
Algorithms are stored in an essentia::AlgorithmFactory
, which contains information
about them and also knows how to instantiate them, using the
essentia::AlgorithmFactory::create(const std::string&)
method.
Source documentation: standard::Algorithm and streaming::Algorithm
Inputs / outputs¶
Inputs and outputs can be of any type in Essentia, and you can even create algorithms
in Essentia that use your own types. They are named, and in order to be recognized by
the algorithm they pertain to, they need to be declared explicitly using the
essentia::Algorithm::declareInput()
and essentia::Algorithm::declareOutput()
methods.
It is highly recommended (although not mandatory) to declare them in the constructor of your class (see Writing a new Algorithm for more details on how to write your own Algorithm).
Inputs and Outputs do not store data at all, they just point to it. So before calling
compute()
on an Algorithm, you need to make sure that its inputs/outputs point
to a correct place. To do this, you need to tell the input/output which variable
it should read/write in, by using the set()
method.
For instance, to call a Centroid algorithm, you would do the following:
std::vector<Real> myArray; // the input array
Real myCentroid; // the variable containing the resulting centroid
// point the input/output of the algorithm to their respective variable
centroidAlgo->input("array").set(myArray);
centroidAlgo->output("centroid").set(myCentroid);
// only now can you call compute()
centroidAlgo->compute();
Configurables¶
The Configurable
class is the base class for the Algorithm
. A Configurable
instance is
a named object that can maintain a fixed set of parameters, and which you can reconfigure any
number of times. To be able to instantiate a Configurable
, you need to implement the
essentia::Configurable::declareParameters()
method, which will declare all the
parameters that your Configurable object can take. If you later try to configure it
with a parameter that wasn’t declared in the declareParameters()
method, it will fail.
You can access the current value of a parameter by calling the
essentia::Configurable::parameter(const std::string& name)
method and passing it the
name of the parameter.
To (re)configure a Configurable, you need to call the configure(const ParameterMap& pmap)
method.
This will check whether the parameters are acceptable, set them, and call the configure()
method,
which you should have redefined if you want your object to do some specific action when being configured.
Source documentation: Configurable
Parameters¶
A Parameter is a variant type, meaning that it can basically represent any type of data. For instance, at the moment of this writing, Parameters can represent strings, integers, floating point numbers, booleans, vectors of strings or reals. More type conversions can be added if necessary.
This is especially useful in C++ as it is a statically-typed language, but we want to allow different types of data for configuring an algorithm. In Python, the point of having variant types is moot, thanks to the dynamic typing.
Here is a small example of creating / retrieving the values of some parameters:
Parameter param1(23);
int param1_int = param1.toInt();
std::vector<Real> v; // v is empty
v.push_back(1.2); // v = [ 1.2 ]
v.push_back(2.3); // v = [ 1.2, 2.3 ]
Parameter param2(v);
std::vector<Real> param2_vector = param2.toVectorReal();
// conversions between types are allowed as long as they make sense
Parameter param3(117); // constructed from an integer
Real p3 = param3.toReal(); // works because an integer is also a float
Another closely related class to Parameter
is the ParameterMap
, which is just
a map from std::string
(the name of the parameter) to Parameter
(its value).
It represents a set of Parameters, and is mostly used in the call to the
Configurable::configure(const ParameterMap& pmap)
method.
Source documentation: Parameter and ParameterMap
Pool¶
A Pool
is a thread-safe structure that is used to store values. It could be thought of as a cache.
Basically, during processing you generate lots of values which you want to post process
afterwards, and in that case, a Pool
is the perfect candidate for a storage mechanism.
The pool stores these values using a std::string
as identifier, which can be
dot (‘.’) separated to indicate namespaces. For instance, the following are all valid
names: filename
, lowlevel.centroid
, highlevel.genre.value
,
highlevel.genre.rock.probability
, …
There are 2 ways to store values in a pool: you can either add()
, or set()
them.
When you add a value, it gets appended to the list of values with the same name, when you
set it, you replace the value which was previously stored with this name (or create it).
To retrieve those values, you need to call the value()
function, which is templated by
the type of the value.
For instance, you might want to store all the values of the per-frame energy, and compute the mean at the end to have an idea of the average energy for a track.
You could do it this way:
Pool pool;
while (moreFrames) {
// compute energy here
pool.add("lowlevel.energy", energyValue);
}
const vector<Real>& allEnergyValues = pool.value<vector<Real> >("lowlevel.energy");
Real averageEnergy = mean(allEnergyValues);
pool.set("highlevel.average_energy", averageEnergy);
cout << "The average energy is: " << pool.value<Real>("highlevel.average_energy");
Note that although you feed the pool with a Real
value for the energy, the call to
Pool::value()
will return a std::vector<Real>
, because it will return all
the values that you gave it. Even if you only added one value into the pool, a call
to value()
will return a vector, of size 1 in that case.
On the other hand, if you used set()
, the value returned is of the same type.
Source documentation: Pool
Logging framework¶
Logging in C++¶
Essentia provides you with a logging framework that is meant to be both efficient and easy to use. It tries to learn from other logging frameworks and has 4 logging levels that can be activated/deactivated independently at runtime:
Error
Warning
Info
Debug
Furthermore, the debug level is itself subdivided into different debugging modules, defined in the DebuggingModule enumeration which you can find in the debugging.h file.
The way to debug is to use the following macros:
E_ERROR("This is an error message!");
E_WARNING("This is a warning message...");
E_INFO("And this is an info message.");
E_DEBUG(EMemory, "This is a debug message relating to the memory usage");
E_DEBUG(EAlgorithm, "And this one is a debug message relating to an algorithm");
You can also log more than just a string using the stream operator, as you would do
in a C++ std::ostream
:
E_INFO("You can log ints, such as " << 42 << " floats, as in " << 3.14 << " and " <<
"pretty much anything that you can send in a std::ostream");
To activate/deactivate the debugging modules at runtime, use the functions:
setDebugLevel(EAll); // EAll is a special value that contains all modules
unsetDebugLevel(EMemory | EConnectors); // modules are bitmasks
To change the logging levels at runtime (all levels are activated by default), use:
essentia::warningLevelActive = false; // deactivate warnings
essentia::infoLevelActive = false; // deactivate info
essentia::errorLevelActive = true; // activate error level
Note that when a logging module is deactivated, the cost on runtime is minimal
(i.e., you only really pay for logging when you use it). If you wish to completely
turn off logging, this can be done at compile time by setting the DEBUGGING_ENABLED
variable in the config.h file to 0
. Note that in this case, it will not be
possible to activate any logging at runtime at all, the advantage being that you
pay absolutely nothing for logging, so don’t hesitate to (ab)use logging in your
algorithms because of fear of losing efficiency.
Advanced logging¶
Sometimes, you wish to only activate logging for a certain period of time, to avoid being overwhelmed by too many messages. For example, if your algorithm fails at some point in an audio file, you might want to only log some info for the 5 previous calls to the generator (frames of the audioloader, usually).
This is done by specifying a list of time ranges (in number of calls to the generator) and modules that should be activated during this range. For example:
DebuggingSchedule s = { {0, INT_MAX, ENetwork}, // always active
{500, INT_MAX, EAlgorithm }, // from time index 500 until the end
{782, 782, EScheduler} }; // only for time index 782
scheduleDebug(s, ARRAY_SIZE(s));
This will run a network and activate the ENetwork level always, the EAlgorithm from the 500th call to the generator until the end, and the EScheduler level only on the 782th call to the generator.
Logging in Python¶
In Python, you would usually log things using the standard logging
module, but
Essentia also gives you access to its C++ logger in order to ensure that
the output is not mangled between the 2 logging frameworks:
essentia.log.info("This is how you use Essentia's logger in Python")
essentia.log.debug(essentia.EAlgorithm, 'Debugging modules are also available')
essentia.log.infoActive = True # activate the info level
essentia.log.debugLevels += essentia.EAll # activate all debug modules
essentia.log.debugLevels -= essentia.EExecution # deactivate the ``Execution`` one
essentia.log.warningActive = False # deactivate the warning level