Skip to content

Integration Quickstart Guide

Kai Blaschke edited this page Feb 17, 2022 · 6 revisions

Integration Quickstart Guide

projectM is released as either a shared or static library and provides a C-based API for all exposed functionality. This means that developers can use projectM in any application or with any programming language that supports calling native C functions and provide a proper OpenGL rendering context, including the platform-specific drawing surface.

The quickstart guide explains how to get started with projectM in a C/C++ application. If you need to use it in other languages, the best approach would be writing a small C-to-your-language layer, which will be very similar to using it directly in a C application. Since there are many different programming languages out there, we cannot cover them all

No operating system specific details like creating an OpenGL context are handled in this guide. libprojectM and its API are generally platform-agnostic and require the application to provide the rendering environment.

Adding libprojectM to your application

The recommended way to integrate projectM into your application is by building libprojectM as a static or shared library and use the public C API (by including projectM.h) to instantiate and control it. This is the recommended way to use projectM. While the exposed functionality is limited to the public API libprojectM provides, you can easily update to recent releases without effort. If libprojectM is linked as a shared library, updating is as easy as replacing the library file - you don't even need to recompile your application.

If you need more control over projectM's internals, for example if you're building an advanced preset editor for which you require access to projectM's internal parser or rendering code, you can also directly integrate the source files into your application. Be aware that this comes with a more involved process of updating to newer libprojectM releases.

Also be aware that either statically linking libprojectM or directly including the sources into your codebase requires your application to be released under the GPL or LGPL licenses.

This integration guide will only cover integration via the official C API and linking the static or shared library.

Build the library

First, download and build libprojectM and install the build results in a location where you can access them easily.

Add the libraries and include dirs

Using CMake

The recommended way to use libprojectM is by using CMake to build your application. In your CMakeLists.txt, you need to find the libprojectM package and then link the correct library target to your executable or library:

# After the project() command.
# In the case projectM is optional in your build, leave out the REQUIRED.
# If you need a specific libprojectM version, you can specify it after the package name,
find_package(libprojectM REQUIRED)

add_executable(MyApp
        main.cpp
        )

# Replace libprojectM::shared with libprojectM::static to link against the static library instead
target_link_libraries(MyApp
        PRIVATE
        libprojectM::shared
        )

That's all. If CMake finds libprojectM, it'll link the correct library and also add any required include dirs and compiler flags to your application's build configuration. To have CMake find your copy of libprojectM, you might need to add it via CMAKE_PREFIX_PATH:

cmake -S /path/to/source \
      -B /path/to/build \
      -GNinja \
      -DCMAKE_PREFIX_PATH=/path/to/libprojectM

If libprojectM is installed in a standard path your toolchain is configured to look into, you won't need it.

Using a different build system

If you can't or don't want to use CMake, you need to gather the library and include dirs on your own and add them manually to your project configuration.

On Linux, libprojectM will also create a libprojectM.pc file for use with pkgconfig, which will be the tool of choice for autotools-based builds for example.

Other build systems might provide libprojectM packages in the future, but the projectM developers are not planning on creating or maintinaing those.

Call projectM in your code

One word about memory allocation

Before we start using the API, it is important to know how the API works in regard to memory allocation. The C API is technically a C wrapper around C++ code, to while you call C functions, they internally execute as C++ code in the library. In addition to that, the library, if loaded as a shared/dynamic library, might have a different heap area than your host application on some platforms.

To make sure all allocated memory is properly being disposed of after use, it must be freed in the same context where it has been allocated: if your application reserved memory, it has to release it. If libprojectM reserved memory, it must be released in the library code.

Currently, this only applies to either strings or the settings structure. As a rule of thumb, here is a quick reference:

  • If a pointer (char* or projectm_settings*) is returned by an API function, always use the appropriate projectm_free_xxx() function if you're done using the data.
  • If your code passes any self-allocated pointer to the API, it is safe to free the allocated memory immediately after the call. Do not use the projectm_free_xxx() functions on these pointers.
  • It is safe to pass temporary pointers to any API call, e.g. using std::string::c_str() in an argument.
  • You can use projectm_alloc_string to allocate memory for strings, but you must then use projectm_free_string to free it after use.
  • If you call projectm_free_settings, all non-null char*members must have been allocated using projectm_alloc_string.
  • Any data pointers passed to callbacks are only valid until the callback returns. If you need the data afterwards,.make a copy. Your code must not call free or delete on the passed data pointers inside the callback.

Create a new projectM instance

Now that you have your project files configured properly, you can immediately start using projectM. As stated above, this guide assumes your application already takes care of creating a proper OpenGL rendering context. This must be done before calling any projectM functions.

First, include the API header and create a new projectM instance:

#include <libprojectM/projectM.h>

/* In your setup code: */

/* Create a projectM instance with default settings */
projectm_handle projectMHandle = projectm_create("", PROJECTM_FLAG_NONE);
if (!projectMHandle)
{
    /* Something went wrong. */
}

The opaque handle returned by projectm_create() identifies your instance and must be used as the first parameter to all API calls that have an instance parameter.

It is safe to include the header from both C and C++ files. It wraps extern "C" around declarations automatically.

Make sure to clean up the instance

Now is a good time to make sure the newly created instance is deleted after your application is done using it:

/* In your shutdown code: */
projectm_destroy(projectMHandle);
projectMHandle = NULL;

If you make sure to set projectMHandle to NULL after destroying it, any further calls to projectm_destroy() will simply be a no-op: it is safe to pass a null pointer.

Note: It is not safe to pass NULL or already-destroyed instance handles in instance to any other API call! Make sure your code doesn't do that.

Set the canvas size

Once your rendering context is ready and the dimensions of the target surface are known, you must provide the size to projectM once after initializing, and again every time the surface was invalidated or changed size, e.g. after a windows was resized, minimized and restored:

/* Initialize these with your current surface dimensions. */
size_t renderWidth = 800;
size_t renderHeight = 600;

projectm_set_window_size(projectMHandle, renderWidth, renderHeight);

For performance reasons, make sure to only call projectm_set_window_size() if really needed, as this will currently cause a full recreation of projectM's internal rendering pipeline, including shader compilation.

Render a frame

projectM is now ready to start rendering frames. This needs to be done in your application's rendering loop:

projectm_render_frame(projectMHandle);
/* Swap buffers here */

libprojectM does not have any FPS limiting capabilities. This means you can render frames as fast as projectM can draw them. In end-user applications, this might not be a good thing as it will fully utilize one or more CPU cores while the display possibly cannot display frames at the same speed. By enabling VSync for buffer swaps, it will automatically limit FPS to the refresh rate. You might further consider FPS limiting to a certain target framerate to safe resources on the user's device.

Supplying audio data

With the above setup, the application will only render the default idle preset (the wandering "M" logo), but not do anything fancy. To make it react to some audio playing, the application must pass audio sample data into the library.

Where the application sources the audio data is up to the actual implementation, e.g. capturing external audio via some system API, directly decoding an audio file or using data from an underlying player application.

The API currently supports a few different data formats. All functions start with projectm_pcm_add_, followed by the sample data type, the number of channels and the data structure type accepted to pass in the actual data. For best performance and visuals, it is recommended to always use the projectm_pcm_add_float_2ch_data() function. It requires your data to be in this format:

  • 32 bit floating-point samples
  • 2 channels (LR)
  • A simple data pointer, pointing to the first data byte
  • The number of samples

The actual sample frequency is not part of the interface, but projectM is optimized for 44.1 kHz, same as Milkdrop. It will also work with other sample frequencies, but the beat detection and presets drawing spectrum data might not behave as expected. Using 48 kHz is fine though as the difference is minimal.

The number of samples is the count of a full complement of data for all channels. This means if sample_count is 1, then the data must at least contain 16 bytes or 2 floats (General formula: numBytes = sizeof(sample data type) * channels).

Ideally, if the application is gathering audio data in a separate thread than the renderer, it should not pass audio data only while projectm_render_frame is running. libprojectM does not use mutexes internally to prevent mutual access to the audio buffer. If there are race conditions, it won't cause crashes though, but may negatively impact the rendered visuals.

With all that said, let's say your application has a wrapper function that gets properly formatted audio data as a basic byte (unsigned char*) buffer:

/* audio_data is passed in float/2ch format */
void add_audio_to_projectm(const unsigned char* audio_data, unsigned int length)
{
    projectm_pcm_add_float_2ch_data(projectMHandle, (const float*)audio_data, length / sozeof(float) / 2 /* channels */);
}

Now projectM should start reacting to the audio.

Audio data caveats

The application does not need to care about how much data is stored inside projectM's internal buffer. If more data is added than projectM can consume on each frame, it is implemented as a ring buffer and will simply be overwritten. Each frame only renders the last added audio samples available in the buffer. This in turn means that if audio data is added only sporadically in large batches, multiple frames will use the same audio data for rendering, effectively "freezing" waveforms on screen.

Depending on the frequency and amount of audio data the application gets on each update (e.g. in callbacks from an external audio driver), it might be necessary to spread out adding the data over multiple frames instead of adding it all at once directly in the callback. Knowing the sample frequency and actual frame rendering time, the application can calculate the number of new audio samples to pass to projectM before each frame. If audio data comes in faster than frames are rendered, only the last few samples of the available audio data need to be passed. The application can query projectM's internal sample buffer size with the projectm_pcm_get_max_samples() function for optimization.

In addition to all that, the application should make sure not to get too much behind the actually played audio, as it might introduce visible lag between what the user hears and what projectM renders. As a rule of thumb, latencies over 35ms will potentially be noticeable by the user.

What next?

Now that the application is able to render visualizations with projectM, it should also take care of configuring additional features like adding presets, properly setting the different options projectM supports and handling user input as required.

A good further reading is the API reference and looking at the available members in the projectm_settings structure.

projectM also supports scanning a single directory recursively for presets, so your application doesn't need to do that. For proper playlist management, rating support and faster loading times it is recommended to fill the preset playlist using projectm_add_preset_url(). Future API extensions may add additional functionality for bulk playlist management.

Clone this wiki locally